diff options
Diffstat (limited to 'src/bin/br.rs')
-rw-r--r-- | src/bin/br.rs | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/src/bin/br.rs b/src/bin/br.rs new file mode 100644 index 0000000..b4cf19d --- /dev/null +++ b/src/bin/br.rs @@ -0,0 +1,448 @@ +use bedrock_asm::*; +use bedrock_pc::*; +use phosphor::*; + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + + +static mut VERBOSE: bool = false; + +macro_rules! verbose { + ($($tokens:tt)*) => { if unsafe { VERBOSE } { + eprint!("[INFO] "); eprintln!($($tokens)*); + } }; +} +macro_rules! error { + ($($tokens:tt)*) => {{ + eprint!("[ERROR] "); eprintln!($($tokens)*); std::process::exit(1); + }}; +} + + +fn main() { + let args = Arguments::from_env_or_exit(); + if args.version { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + eprintln!("{name} v{version}"); + eprintln!("Written by Ben Bridle."); + std::process::exit(0); + } + if args.verbose { + unsafe { VERBOSE = true; } + } + match args.subcommand { + ArgumentsCmd::Run(run) => main_run(run), + ArgumentsCmd::Asm(asm) => main_asm(asm), + } +} + +fn main_run(args: Run) { + if args.debug { + unsafe { VERBOSE = true; } + } + + let bytecode = load_bytecode(args.program.as_ref().map(|p| p.as_path())); + let metadata = parse_metadata(&bytecode); + if metadata.is_none() { + verbose!("Could not read program metadata"); + } + // TODO: Load and parse symbols file into debug state, use nearest symbol + // path when debugging. + + let config = EmulatorConfig { + dimensions: args.dimensions().unwrap_or(ScreenDimensions::ZERO), + fullscreen: args.fullscreen, + scale: args.scale(), + debug_palette: args.palette(), + initial_transmission: None, + encode_stdin: args.encode_stdin, + encode_stdout: args.encode_stdout, + }; + let phosphor = Phosphor::new(); + + if phosphor.is_ok() && args.dimensions().is_some() { + verbose!("Starting graphical emulator"); + let mut phosphor = phosphor.unwrap(); + let mut graphical = GraphicalEmulator::new(&config, args.debug); + graphical.load_program(&bytecode); + if let EmulatorSignal::Promote = graphical.run() { + let program_name = match &metadata { + Some(metadata) => match &metadata.name { + Some(name) => name.to_string(), + None => String::from("Bedrock"), + } + None => String::from("Bedrock"), + }; + let window = WindowBuilder { + dimensions: Some(graphical.dimensions()), + size_bounds: Some(graphical.size_bounds()), + fullscreen: graphical.fullscreen, + scale: graphical.scale, + title: Some(program_name), + icon: None, + cursor: None, + program: Box::new(graphical), + }; + + phosphor.create_window(window); + phosphor.run().unwrap(); + } + } else { + verbose!("Starting headless emulator"); + let mut headless = HeadlessEmulator::new(&config, args.debug); + headless.load_program(&bytecode); + headless.run(args.debug); + }; + + std::process::exit(0); +} + +fn load_bytecode(path: Option<&Path>) -> Vec<u8> { + // TODO: Etch file location into bytecode. + if let Some(path) = path { + if let Ok(bytecode) = load_bytecode_from_file(path) { + verbose!("Loaded program from {path:?} ({} bytes)", bytecode.len()); + return bytecode; + } else if let Some((bytecode, path)) = load_bytecode_from_bedrock_path(path) { + verbose!("Loaded program from {path:?} ({} bytes)", bytecode.len()); + return bytecode; + } else { + eprintln!("Could not read program from {path:?}, exiting"); + std::process::exit(1); + } + } else { + verbose!("Reading program from standard input..."); + if let Ok(bytecode) = load_bytecode_from_stdin() { + verbose!("Loaded program from standard input ({} bytes)", bytecode.len()); + return bytecode; + } else { + eprintln!("Could not read program from standard input, exiting"); + std::process::exit(1); + } + } +} + +/// Attempt to load bytecode from a file path. +fn load_bytecode_from_file(path: &Path) -> Result<Vec<u8>, std::io::Error> { + load_bytecode_from_readable_source(std::fs::File::open(path)?) +} + +/// Attempt to load bytecode from a directory in the BEDROCK_PATH environment variable. +fn load_bytecode_from_bedrock_path(path: &Path) -> Option<(Vec<u8>, PathBuf)> { + if path.is_relative() && path.components().count() == 1 { + for base_path in std::env::var("BEDROCK_PATH").ok()?.split(':') { + let mut base_path = PathBuf::from(base_path); + if !base_path.is_absolute() { continue; } + base_path.push(path); + verbose!("Attempting to load program from {base_path:?}"); + if let Ok(bytecode) = load_bytecode_from_file(&base_path) { + return Some((bytecode, base_path)); + } + if path.extension().is_some() { continue; } + base_path.set_extension("br"); + verbose!("Attempting to load program from {base_path:?}"); + if let Ok(bytecode) = load_bytecode_from_file(&base_path) { + return Some((bytecode, base_path)); + } + } + } + return None; +} + +/// Attempt to load bytecode from standard input. +fn load_bytecode_from_stdin() -> Result<Vec<u8>, std::io::Error> { + load_bytecode_from_readable_source(std::io::stdin()) +} + +/// Attempt to load bytecode from a source that implements std::io::Read. +fn load_bytecode_from_readable_source(source: impl Read) -> Result<Vec<u8>, std::io::Error> { + let mut bytecode = Vec::<u8>::new(); + source.take(65536).read_to_end(&mut bytecode)?; + return Ok(bytecode); +} + + +fn main_asm(args: Asm) { + // ----------------------------------------------------------------------- + // RESOLVE syntactic symbols + let ext = args.ext.unwrap_or(String::from("brc")); + let mut resolver = if let Some(path) = &args.source { + match SourceUnit::from_path(&path, &ext) { + Ok(source_unit) => SymbolResolver::from_source_unit(source_unit), + Err(err) => match err { + ParseError::InvalidExtension => error!( + "File {path:?} has invalid extension, must be '.{ext}'"), + ParseError::NotFound => error!( + "File {path:?} was not found"), + ParseError::InvalidUtf8 => error!( + "File {path:?} does not contain valid UTF-8 text"), + ParseError::NotReadable => error!( + "File {path:?} is not readable"), + ParseError::IsADirectory => error!( + "File {path:?} is a directory"), + ParseError::Unknown => error!( + "Unknown error while attempting to read from {path:?}") + } + } + } else { + let mut source_code = String::new(); + verbose!("Reading program source from standard input"); + if let Err(err) = std::io::stdin().read_to_string(&mut source_code) { + eprintln!("Could not read from standard input, exiting."); + eprintln!("({err:?})"); + std::process::exit(1); + } + let path = "<standard input>"; + let source_unit = SourceUnit::from_source_code(source_code, path); + SymbolResolver::from_source_unit(source_unit) + }; + // Load project libraries. + if let Some(path) = &args.source { + if !args.no_libs && !args.no_project_libs { + let project_library = gather_project_libraries(path, &ext); + resolver.add_library_units(project_library); + } + } + // Load environment libraries. + if !args.no_libs && !args.no_env_libs { + for env_library in gather_environment_libraries(&ext) { + resolver.add_library_units(env_library); + } + } + resolver.resolve(); + + // ----------------------------------------------------------------------- + // PRINT information, generate merged source code + if args.tree { + print_source_tree(&resolver); + } + if print_resolver_errors(&resolver) { + std::process::exit(1); + }; + let merged_source = resolver.get_merged_source_code(); + if args.resolve && !args.check { + write_bytes_and_exit(merged_source.as_bytes(), args.output.as_ref()); + } + + // ----------------------------------------------------------------------- + // PARSE semantic tokens from merged source code + let path = Some("<merged source>"); + let mut semantic_tokens = generate_semantic_tokens(&merged_source, path); + if print_semantic_errors(&semantic_tokens, &merged_source) { + std::process::exit(1); + }; + + // ----------------------------------------------------------------------- + // GENERATE symbols file and bytecode + let bytecode = generate_bytecode(&mut semantic_tokens); + // let symbols = generate_symbols_file(&semantic_tokens); + + let length = bytecode.len(); + let percentage = (length as f32 / 65536.0 * 100.0).round() as u16; + verbose!("Assembled program in {length} bytes ({percentage}% of maximum)."); + + if !args.check { + write_bytes_and_exit(&bytecode, args.output.as_ref()); + } +} + +fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! { + if let Some(path) = path { + if let Err(err) = std::fs::write(path, bytes) { + eprintln!("Could not write to path {:?}, exiting.", path.as_ref()); + eprintln!("({err:?})"); + std::process::exit(1); + } + } else { + if let Err(err) = std::io::stdout().write_all(bytes) { + eprintln!("Could not write to standard output, exiting."); + eprintln!("({err:?})"); + std::process::exit(1); + } + } + std::process::exit(0); +} + + +xflags::xflags! { + /// Integrated Bedrock assembler and emulator. + /// + /// The arguments and options shown are for running a Bedrock program. + /// To see the documentation for the assembler, run `br asm --help` + /// + /// Usage: + /// To load a Bedrock program from a file, run `br [path]`, where + /// [path] is the path of an assembled Bedrock program. + /// + /// To load a Bedrock program from piped input, run `[command] | br`, + /// where [command] is a command that generates Bedrock bytecode. + /// + /// To assemble a Bedrock program from a source file and write to an + /// output file, run `br asm [source] [output]`, where [source] is the + /// path of the source file and [output] is the path to write to. + /// + /// To assemble and run a Bedrock program without saving to a file, + /// run `br asm [source] | br`, where [source] is the path of the + /// source file. + /// + /// Environment variables: + /// BEDROCK_PATH + /// A list of colon-separated paths which will be searched to find + /// a Bedrock program when the program doesn't exist in the current + /// directory. This allows the user to run frequently-used programs + /// from any directory. + /// BEDROCK_LIBS + /// A list of colon-separated paths which will be searched to find + /// Bedrock source code files to use as libraries when assembling a + /// Bedrock program. If a library file resolves an unresolved symbol + /// in the program being assembled, the library file will be merged + /// into the program. + cmd arguments { + /// Print additional debug information + optional -v, --verbose + /// Print the assembler version and exit + optional --version + + /// Run a Bedrock program (this is the default command) + default cmd run { + /// Path to a Bedrock program to run + optional program: PathBuf + + /// Show debug information while the program is running + optional -d, --debug + /// Start the program in fullscreen mode (toggle with F11) + optional -f, --fullscreen + /// Set the initial window size in the format <width>x<height> + optional -s, --screen size: ParsedDimensions + /// Set the pixel size for the screen (change with F5/F6) + optional -z, --zoom factor: u32 + /// Set a debug colour palette in the format <rgb>,... (toggle with F2) + optional --palette colours: ParsedPalette + /// Encode standard input + optional -i, --encode-stdin + /// Encode standard output + optional -o, --encode-stdout + } + + /// Assemble a Bedrock program from source code + cmd asm { + /// Bedrock source code file to assemble. + optional source: PathBuf + /// Destination path for assembler output. + optional output: PathBuf + /// File extension to identify source files. + optional ext: String + + /// Don't include libraries or resolve references. + optional --no-libs + /// Don't include project libraries + optional --no-project-libs + /// Don't include environment libraries. + optional --no-env-libs + + /// Show the resolved source file heirarchy + optional --tree + /// Assemble the program without saving any output + optional --check + /// Only return resolved source code. + optional --resolve + } + } +} + + +impl Run { + pub fn dimensions(&self) -> Option<geometry::Dimensions<u16>> { + let size = match &self.screen { + Some(parsed) => geometry::Dimensions::new(parsed.width, parsed.height), + None => DEFAULT_SCREEN_SIZE, + }; + match size.is_zero() { + true => None, + false => Some(size), + } + } + + pub fn scale(&self) -> u32 { + std::cmp::max(1, self.zoom.unwrap_or(1)) + } + + pub fn palette(&self) -> Option<[Colour; 16]> { + self.palette.as_ref().map(|p| p.palette) + } +} + + +#[derive(Debug)] +struct ParsedDimensions { width: u16, height: u16 } +impl std::str::FromStr for ParsedDimensions { + type Err = ParsedDimensionsError; + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s == "none" { + Ok( Self { width: 0, height: 0 } ) + } else { + let (w_str, h_str) = s.split_once('x').ok_or(ParsedDimensionsError)?; + Ok( Self { + width: u16::from_str(w_str).or(Err(ParsedDimensionsError))?, + height: u16::from_str(h_str).or(Err(ParsedDimensionsError))?, + } ) + } + } +} + +pub struct ParsedDimensionsError; +impl std::fmt::Display for ParsedDimensionsError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + f.write_str("try giving a value like 800x600") + } +} + + +#[derive(Debug)] +pub struct ParsedPalette { palette: [Colour; 16] } +impl std::str::FromStr for ParsedPalette { + type Err = ParsedPaletteError; + fn from_str(s: &str) -> Result<Self, Self::Err> { + fn decode_ascii_hex_digit(ascii: u8) -> Result<u8, ParsedPaletteError> { + match ascii { + b'0'..=b'9' => Ok(ascii - b'0'), + b'a'..=b'f' => Ok(ascii - b'a' + 10), + b'A'..=b'F' => Ok(ascii - b'A' + 10), + _ => { Err(ParsedPaletteError)} + } + } + let mut colours = Vec::new(); + for token in s.split(',') { + let mut bytes = token.bytes(); + if bytes.len() != 3 { return Err(ParsedPaletteError); } + let r = decode_ascii_hex_digit(bytes.next().unwrap())?; + let g = decode_ascii_hex_digit(bytes.next().unwrap())?; + let b = decode_ascii_hex_digit(bytes.next().unwrap())?; + colours.push(Colour::from_rgb(r*17, g*17, b*17)); + } + let c = &colours; + let palette = match colours.len() { + 2 => [ c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1], + c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1] ], + 3 => [ c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2], + c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2] ], + 4 => [ c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3], + c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3] ], + 8 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7] ], + 16 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[8], c[9], c[10], c[11], c[12], c[13], c[14], c[15] ], + _ => return Err(ParsedPaletteError), + }; + Ok( Self { palette }) + } +} + +pub struct ParsedPaletteError; +impl std::fmt::Display for ParsedPaletteError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + f.write_str("try giving a value like 000,fff,880,808") + } +} |