diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bin/br.rs | 461 | ||||
-rw-r--r-- | src/bin/br/asm.rs | 117 | ||||
-rw-r--r-- | src/bin/br/main.rs | 101 | ||||
-rw-r--r-- | src/bin/br/run.rs | 230 |
4 files changed, 448 insertions, 461 deletions
diff --git a/src/bin/br.rs b/src/bin/br.rs deleted file mode 100644 index d7bde60..0000000 --- a/src/bin/br.rs +++ /dev/null @@ -1,461 +0,0 @@ -use bedrock_asm::*; -use bedrock_pc::*; -use phosphor::*; - -use log::{info, fatal}; -use switchboard::{Switchboard, SwitchQuery}; - -use std::cmp::max; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; -use std::process::exit; - - -fn print_help() -> ! { - println!("\ -Usage: br [source] - br asm [source] [destination] [extension] - -Integrated Bedrock assembler and emulator. - -Usage: - To load a Bedrock program from a file, run `br <path>`, where - <path> is the path to 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. - -Arguments: - [program] Path to a Bedrock program to run - -Switches: - --verbose, (-v) Print additional debug information - --version Print the assembler version and exit - --help (-h) Prints help - --debug, (-d) Show debug information while the program is running - --fullscreen (-f) Start the program in fullscreen mode (toggle with F11) - --size=<dim> (-s) Set the initial window size in the format <width>x<height> - --zoom=<scale> (-z) Set the pixel size for the screen (change with F5/F6) - --palette=<pal> Set a debug colour palette in the format <rgb>,... (toggle with F2) - --show-cursor (-c) Show the operating system cursor over the window - --decode-stdin (-i) Decode standard input - --encode-stdout (-o) Encode standard output - -Arguments (asm mode): - [source] Bedrock source code file to assemble. - [destination] Destination path for assembler output. - [extension] File extension to identify source files. - -Switches (asm mode): - --no-libs Don't include libraries or resolve references. - --no-project-libs Don't include project libraries - --no-env-libs Don't include environment libraries. - --tree Show the resolved source file heirarchy - --check Assemble the program without saving any output - --resolve Only return resolved source code. - --symbols Generate debug symbols with file extension '.br.sym' -"); - std::process::exit(0); -} - - -fn print_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); -} - - -fn main() { - let mut args = Switchboard::from_env(); - if args.named("help").short('h').as_bool() { - print_help(); - } - if args.named("version").as_bool() { - print_version(); - } - if args.named("verbose").short('v').as_bool() { - log::set_log_level(log::LogLevel::Info); - } - match args.peek() { - Some("run") => { args.pop(); main_run(args) }, - Some("asm") => { args.pop(); main_asm(args) }, - _ => main_run(args), - } -} - -fn main_run(mut args: Switchboard) { - let source = args.positional("source").as_path_opt(); - let debug = args.named("debug").short('d').as_bool(); - let fullscreen = args.named("fullscreen").short('f').as_bool(); - let scale = max(1, args.named("zoom").short('z').default("1").quick("3").as_u32()); - let dimensions = match args.named("size").short('s').as_string_opt() { - Some(string) => parse_dimensions(&string).unwrap_or_else(|| - fatal!("Invalid dimensions string {string:?}")), - None => DEFAULT_SCREEN_SIZE / (scale as u16), - }; - let debug_palette = match args.named("palette").as_string_opt() { - Some(string) => match parse_palette(&string) { - Some(palette) => Some(palette), - None => fatal!("Invalid palette string {string:?}"), - }, - None => None, - }; - let show_cursor = args.named("show-cursor").short('c').as_bool(); - let decode_stdin = args.named("decode-stdin").short('i').as_bool(); - let encode_stdout = args.named("encode-stdout").short('o').as_bool(); - - let Bytecode { bytes: bytecode, path } = load_bytecode(source.as_ref()); - let symbols_path = path.as_ref().map(|p| { - let mut path = p.to_path_buf(); - path.set_extension("br.sym"); - path - }); - - let metadata = parse_metadata(&bytecode); - if metadata.is_none() { - info!("Could not read program metadata"); - } - - let config = EmulatorConfig { - dimensions, - fullscreen, - scale, - debug_palette, - show_cursor, - initial_transmission: None, - decode_stdin, - encode_stdout, - symbols_path, - }; - let phosphor = Phosphor::new(); - - if phosphor.is_ok() && dimensions.area_usize() != 0 { - info!("Starting graphical emulator"); - let mut phosphor = phosphor.unwrap(); - let cursor = match config.show_cursor { - true => Some(CursorIcon::Default), - false => None, - }; - - let mut graphical = GraphicalEmulator::new(&config, 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), - cursor, - icon: None, - program: Box::new(graphical), - }; - - phosphor.create_window(window); - phosphor.run().unwrap(); - } - } else { - info!("Starting headless emulator"); - let mut headless = HeadlessEmulator::new(&config, debug); - headless.load_program(&bytecode); - headless.run(debug); - }; - - std::process::exit(0); -} - -fn load_bytecode(path: Option<&PathBuf>) -> Bytecode { - // TODO: Etch file location into bytecode as per metadata. - if let Some(path) = path { - if let Ok(bytecode) = load_bytecode_from_file(path) { - let length = bytecode.bytes.len(); - let path = bytecode.path(); - info!("Loaded program from {path:?} ({length} bytes)"); - return bytecode; - } else if let Some(bytecode) = load_bytecode_from_bedrock_path(path) { - let length = bytecode.bytes.len(); - let path = bytecode.path(); - info!("Loaded program from {path:?} ({length} bytes)"); - return bytecode; - } else { - fatal!("Could not read program from {path:?}"); - } - } else { - info!("Reading program from standard input..."); - if let Ok(bytecode) = load_bytecode_from_stdin() { - let length = bytecode.bytes.len(); - info!("Loaded program from standard input ({length} bytes)"); - return bytecode; - } else { - fatal!("Could not read program from standard input"); - } - } -} - -/// Attempt to load bytecode from a directory in the BEDROCK_PATH environment variable. -fn load_bytecode_from_bedrock_path(path: &Path) -> Option<Bytecode> { - 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); - info!("Attempting to load program from {base_path:?}"); - if let Ok(bytecode) = load_bytecode_from_file(&base_path) { - return Some(bytecode); - } - if path.extension().is_some() { continue; } - base_path.set_extension("br"); - info!("Attempting to load program from {base_path:?}"); - if let Ok(bytecode) = load_bytecode_from_file(&base_path) { - return Some(bytecode); - } - } - } - return None; -} - -/// Attempt to load bytecode from a file path. -fn load_bytecode_from_file(path: &Path) -> Result<Bytecode, std::io::Error> { - // Canonicalize paths so that symbolic links to program files resolve to - // the real program directory, which could contain a symbols file. - let path = match path.canonicalize() { - Ok(canonical) => canonical, - Err(_) => path.to_path_buf(), - }; - load_bytecode_from_readable_source(std::fs::File::open(&path)?, Some(&path)) -} - -/// Attempt to load bytecode from standard input. -fn load_bytecode_from_stdin() -> Result<Bytecode, std::io::Error> { - load_bytecode_from_readable_source(std::io::stdin(), None) -} - -/// Attempt to load bytecode from a source that implements std::io::Read. -fn load_bytecode_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<Bytecode, std::io::Error> { - let mut bytes = Vec::<u8>::new(); - source.take(65536).read_to_end(&mut bytes)?; - return Ok(Bytecode { bytes, path: path.map(|p| p.to_path_buf()) }); -} - -struct Bytecode { - bytes: Vec<u8>, - path: Option<PathBuf>, -} - -impl Bytecode { - fn path(&self) -> String { - match &self.path { - Some(path) => path.as_os_str().to_string_lossy().to_string(), - None => String::from("<unknown>"), - } - } -} - - -fn main_asm(mut args: Switchboard) { - let source = args.positional("source").as_path_opt(); - let destination = args.positional("source").as_path_opt(); - let extension = args.positional("extension").default("brc").as_string(); - - let no_libs = args.named("no-libs").as_bool(); - let no_project_libs = args.named("no-project-libs").as_bool(); - let no_environment_libs = args.named("no-env-libs").as_bool(); - - let print_tree = args.named("tree").as_bool(); - let dry_run = args.named("check").as_bool(); - let only_resolve = args.named("resolve").as_bool(); - let export_symbols = args.named("symbols").as_bool(); - - // ----------------------------------------------------------------------- - // RESOLVE syntactic symbols - let source_path = source.clone().map(|p| { - p.canonicalize().unwrap_or(p) - }); - - let mut resolver = if let Some(path) = &source_path { - match SourceUnit::from_path(&path, &extension) { - Ok(source_unit) => SymbolResolver::from_source_unit(source_unit), - Err(err) => { - match err { - ParseError::InvalidExtension => fatal!( - "File {path:?} has invalid extension, must be '.{extension}'"), - ParseError::NotFound => fatal!( - "File {path:?} was not found"), - ParseError::InvalidUtf8 => fatal!( - "File {path:?} does not contain valid UTF-8 text"), - ParseError::NotReadable => fatal!( - "File {path:?} is not readable"), - ParseError::IsADirectory => fatal!( - "File {path:?} is a directory"), - ParseError::Unknown => fatal!( - "Unknown error while attempting to read from {path:?}") - }; - } - } - } else { - let mut source_code = String::new(); - info!("Reading program source from standard input"); - if let Err(err) = std::io::stdin().read_to_string(&mut source_code) { - fatal!("Could not read from standard input\n{err:?}"); - } - 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) = &source_path { - if !no_libs && !no_project_libs { - let project_library = gather_project_libraries(path, &extension); - resolver.add_library_units(project_library); - } - } - // Load environment libraries. - if !no_libs && !no_environment_libs { - for env_library in gather_environment_libraries(&extension) { - resolver.add_library_units(env_library); - } - } - resolver.resolve(); - - // ----------------------------------------------------------------------- - // PRINT information, generate merged source code - if print_tree { - print_source_tree(&resolver); - } - if print_resolver_errors(&resolver) { - std::process::exit(1); - }; - let merged_source = match resolver.get_merged_source_code() { - Ok(merged_source) => merged_source, - Err(ids) => { - print_cyclic_source_units(&ids, &resolver); - std::process::exit(1); - }, - }; - if only_resolve && !dry_run { - write_bytes_and_exit(merged_source.as_bytes(), destination.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); - - if export_symbols && !dry_run { - if let Some(path) = &destination { - let mut symbols_path = path.to_path_buf(); - symbols_path.set_extension("br.sym"); - let symbols = generate_symbols_file(&semantic_tokens); - if let Err(err) = std::fs::write(&symbols_path, symbols) { - info!("Could not write to symbols path {symbols_path:?}"); - eprintln!("{err:?}"); - } else { - info!("Saved debug symbols to {symbols_path:?}"); - } - } - } - - let length = bytecode.len(); - let percentage = (length as f32 / 65536.0 * 100.0).round() as u16; - info!("Assembled program in {length} bytes ({percentage}% of maximum)"); - - if !dry_run { - write_bytes_and_exit(&bytecode, destination.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) { - fatal!("Could not write to path {:?}\n{err:?}", path.as_ref()); - } - } else { - if let Err(err) = std::io::stdout().write_all(bytes) { - fatal!("Could not write to standard output\n{err:?}"); - } - } - exit(0); -} - - -fn parse_dimensions(string: &str) -> Option<ScreenDimensions> { - if string.trim().to_lowercase() == "none" { - return Some(ScreenDimensions::ZERO); - } - let (w_str, h_str) = string.trim().split_once('x')?; - Some( ScreenDimensions { - width: w_str.parse().ok()?, - height: h_str.parse().ok()?, - } ) -} - -fn parse_palette(string: &str) -> Option<[Colour; 16]> { - fn decode_ascii_hex_digit(ascii: u8) -> Option<u8> { - match ascii { - b'0'..=b'9' => Some(ascii - b'0'), - b'a'..=b'f' => Some(ascii - b'a' + 10), - b'A'..=b'F' => Some(ascii - b'A' + 10), - _ => { None } - } - } - let mut c = Vec::new(); - for token in string.split(',') { - let mut bytes = token.bytes(); - if bytes.len() != 3 { return None; } - 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())?; - c.push(Colour::from_rgb(r*17, g*17, b*17)); - } - Some(match c.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 None, - }) -} diff --git a/src/bin/br/asm.rs b/src/bin/br/asm.rs new file mode 100644 index 0000000..cb8c35a --- /dev/null +++ b/src/bin/br/asm.rs @@ -0,0 +1,117 @@ +use crate::*; + +use bedrock_asm::*; +use log::{info, fatal}; + +use std::io::{Read, Write}; + + +pub fn main(mut args: Switchboard) { + let source_path = args.positional("source").as_path_opt().map( + |p| p.canonicalize().unwrap_or(p)); + let destination_path = args.positional("source").as_path_opt(); + let extension = args.positional("extension").default("brc").as_string(); + + let no_libs = args.named("no-libs").as_bool(); + let no_project_libs = args.named("no-project-libs").as_bool(); + let no_environment_libs = args.named("no-env-libs").as_bool(); + + let print_tree = args.named("tree").as_bool(); + let dry_run = args.named("dry-run").short('n').as_bool(); + let only_resolve = args.named("resolve").as_bool(); + let export_symbols = args.named("symbols").as_bool(); + + // ----------------------------------------------------------------------- + + let mut compiler = if let Some(path) = &source_path { + info!("Reading program source from {path:?}"); + Compiler::from_path(path).unwrap_or_else(|err| match err { + FileError::InvalidExtension => fatal!( + "File {path:?} has invalid extension, must be '.{extension}'"), + FileError::NotFound => fatal!( + "File {path:?} was not found"), + FileError::InvalidUtf8 => fatal!( + "File {path:?} does not contain valid UTF-8 text"), + FileError::NotReadable => fatal!( + "File {path:?} is not readable"), + FileError::IsADirectory => fatal!( + "File {path:?} is a directory"), + FileError::Unknown => fatal!( + "Unknown error while attempting to read from {path:?}") + }) + } else { + let mut source_code = String::new(); + info!("Reading program source from standard input"); + if let Err(err) = std::io::stdin().read_to_string(&mut source_code) { + fatal!("Could not read from standard input\n{err:?}"); + } + Compiler::from_string(source_code, "<standard input>") + }; + if compiler.error().is_some() && !no_libs && !no_project_libs { + compiler.include_libs_from_parent(&extension); + } + if compiler.error().is_some() && !no_libs && !no_environment_libs { + compiler.include_libs_from_path_variable("BEDROCK_LIBS", &extension); + } + + if print_tree { + compiler.resolver.hierarchy().report() + } + if let Some(error) = compiler.error() { + error.report(); + std::process::exit(1); + } + let merged_source = compiler.get_compiled_source().unwrap_or_else( + |error| { error.report(); std::process::exit(1); } + ); + if only_resolve && !dry_run { + write_bytes_and_exit(merged_source.as_bytes(), destination_path.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); + + if export_symbols && !dry_run { + if let Some(path) = &destination_path { + let mut symbols_path = path.to_path_buf(); + symbols_path.set_extension("br.sym"); + let symbols = generate_symbols_file(&semantic_tokens); + match std::fs::write(&symbols_path, symbols) { + Ok(_) => info!("Saved debug symbols to {symbols_path:?}"), + Err(err) => info!("Could not write symbols to {symbols_path:?}\n{err:?}"), + } + } + } + + let length = bytecode.len(); + let percentage = (length as f32 / 65536.0 * 100.0).round() as u16; + info!("Assembled program in {length} bytes ({percentage}% of maximum)"); + + if !dry_run { + write_bytes_and_exit(&bytecode, destination_path.as_ref()); + } +} + + +fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! { + match path { + Some(path) => match std::fs::write(path, bytes) { + Ok(_) => info!("Wrote output to {:?}", path.as_ref()), + Err(err) => fatal!("Could not write to {:?}\n{err:?}", path.as_ref()), + } + None => match std::io::stdout().write_all(bytes) { + Ok(_) => info!("Wrote output to standard output"), + Err(err) => fatal!("Could not write to standard output\n{err:?}"), + } + } + std::process::exit(0); +} diff --git a/src/bin/br/main.rs b/src/bin/br/main.rs new file mode 100644 index 0000000..4df8c67 --- /dev/null +++ b/src/bin/br/main.rs @@ -0,0 +1,101 @@ +mod asm; +mod run; + +use switchboard::{Switchboard, SwitchQuery}; + + +fn main() { + let mut args = Switchboard::from_env(); + if args.named("help").short('h').as_bool() { + print_help(); + } + if args.named("version").as_bool() { + print_version(); + } + if args.named("verbose").short('v').as_bool() { + log::set_log_level(log::LogLevel::Info); + } + match args.peek() { + Some("run") => { args.pop(); run::main(args) }, + Some("asm") => { args.pop(); asm::main(args) }, + _ => run::main(args), + } +} + + +fn print_help() -> ! { + println!("\ +Usage: br [source] + br asm [source] [destination] [extension] + +Integrated Bedrock assembler and emulator. + +Usage: + To load a Bedrock program from a file, run `br <path>`, where + <path> is the path to 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. + +Arguments: + [program] Path to a Bedrock program to run + +Switches: + --verbose, (-v) Print additional debug information + --version Print the assembler version and exit + --help (-h) Prints help + --debug, (-d) Show debug information while the program is running + --fullscreen (-f) Start the program in fullscreen mode (toggle with F11) + --size=<dim> (-s) Set the initial window size in the format <width>x<height> + --zoom=<scale> (-z) Set the pixel size for the screen (change with F5/F6) + --palette=<pal> Set a debug colour palette in the format <rgb>,... (toggle with F2) + --show-cursor (-c) Show the operating system cursor over the window + --decode-stdin (-i) Decode standard input + --encode-stdout (-o) Encode standard output + +Arguments (asm mode): + [source] Bedrock source code file to assemble. + [destination] Destination path for assembler output. + [extension] File extension to identify source files. + +Switches (asm mode): + --no-libs Don't include libraries or resolve references. + --no-project-libs Don't include project libraries + --no-env-libs Don't include environment libraries. + --tree Show the resolved source file heirarchy + --check Assemble the program without saving any output + --resolve Only return resolved source code. + --symbols Generate debug symbols with file extension '.br.sym' +"); + std::process::exit(0); +} + + +fn print_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); +} diff --git a/src/bin/br/run.rs b/src/bin/br/run.rs new file mode 100644 index 0000000..728ebda --- /dev/null +++ b/src/bin/br/run.rs @@ -0,0 +1,230 @@ +use log::*; +use crate::*; + +use bedrock_pc::*; +use phosphor::*; + +use std::cmp::max; +use std::io::Read; +use std::path::{Path, PathBuf}; + + +pub fn main(mut args: Switchboard) { + let source = args.positional("source").as_path_opt(); + let debug = args.named("debug").short('d').as_bool(); + let fullscreen = args.named("fullscreen").short('f').as_bool(); + let scale = max(1, args.named("zoom").short('z').default("1").quick("3").as_u32()); + let dimensions = match args.named("size").short('s').as_string_opt() { + Some(string) => parse_dimensions(&string).unwrap_or_else(|| + fatal!("Invalid dimensions string {string:?}")), + None => DEFAULT_SCREEN_SIZE / (scale as u16), + }; + let debug_palette = match args.named("palette").as_string_opt() { + Some(string) => match parse_palette(&string) { + Some(palette) => Some(palette), + None => fatal!("Invalid palette string {string:?}"), + }, + None => None, + }; + let show_cursor = args.named("show-cursor").short('c').as_bool(); + let decode_stdin = args.named("decode-stdin").short('i').as_bool(); + let encode_stdout = args.named("encode-stdout").short('o').as_bool(); + + let Bytecode { bytes: bytecode, path } = load_bytecode(source.as_ref()); + let symbols_path = path.as_ref().map(|p| { + let mut path = p.to_path_buf(); + path.set_extension("br.sym"); + path + }); + + let metadata = parse_metadata(&bytecode); + if metadata.is_none() { + info!("Could not read program metadata"); + } + + let config = EmulatorConfig { + dimensions, + fullscreen, + scale, + debug_palette, + show_cursor, + initial_transmission: None, + decode_stdin, + encode_stdout, + symbols_path, + }; + let phosphor = Phosphor::new(); + + if phosphor.is_ok() && dimensions.area_usize() != 0 { + info!("Starting graphical emulator"); + let mut phosphor = phosphor.unwrap(); + let cursor = match config.show_cursor { + true => Some(CursorIcon::Default), + false => None, + }; + + let mut graphical = GraphicalEmulator::new(&config, 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), + cursor, + icon: None, + program: Box::new(graphical), + }; + + phosphor.create_window(window); + phosphor.run().unwrap(); + } + } else { + info!("Starting headless emulator"); + let mut headless = HeadlessEmulator::new(&config, debug); + headless.load_program(&bytecode); + headless.run(debug); + }; +} + +fn load_bytecode(path: Option<&PathBuf>) -> Bytecode { + // TODO: Etch file location into bytecode as per metadata. + if let Some(path) = path { + if let Ok(bytecode) = load_bytecode_from_file(path) { + let length = bytecode.bytes.len(); + let path = bytecode.path(); + info!("Loaded program from {path:?} ({length} bytes)"); + return bytecode; + } else if let Some(bytecode) = load_bytecode_from_bedrock_path(path) { + let length = bytecode.bytes.len(); + let path = bytecode.path(); + info!("Loaded program from {path:?} ({length} bytes)"); + return bytecode; + } else { + fatal!("Could not read program from {path:?}"); + } + } else { + info!("Reading program from standard input..."); + if let Ok(bytecode) = load_bytecode_from_stdin() { + let length = bytecode.bytes.len(); + info!("Loaded program from standard input ({length} bytes)"); + return bytecode; + } else { + fatal!("Could not read program from standard input"); + } + } +} + +/// Attempt to load bytecode from a directory in the BEDROCK_PATH environment variable. +fn load_bytecode_from_bedrock_path(path: &Path) -> Option<Bytecode> { + 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); + info!("Attempting to load program from {base_path:?}"); + if let Ok(bytecode) = load_bytecode_from_file(&base_path) { + return Some(bytecode); + } + if path.extension().is_some() { continue; } + base_path.set_extension("br"); + info!("Attempting to load program from {base_path:?}"); + if let Ok(bytecode) = load_bytecode_from_file(&base_path) { + return Some(bytecode); + } + } + } + return None; +} + +/// Attempt to load bytecode from a file path. +fn load_bytecode_from_file(path: &Path) -> Result<Bytecode, std::io::Error> { + // Canonicalize paths so that symbolic links to program files resolve to + // the real program directory, which could contain a symbols file. + let path = match path.canonicalize() { + Ok(canonical) => canonical, + Err(_) => path.to_path_buf(), + }; + load_bytecode_from_readable_source(std::fs::File::open(&path)?, Some(&path)) +} + +/// Attempt to load bytecode from standard input. +fn load_bytecode_from_stdin() -> Result<Bytecode, std::io::Error> { + load_bytecode_from_readable_source(std::io::stdin(), None) +} + +/// Attempt to load bytecode from a source that implements std::io::Read. +fn load_bytecode_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<Bytecode, std::io::Error> { + let mut bytes = Vec::<u8>::new(); + source.take(65536).read_to_end(&mut bytes)?; + return Ok(Bytecode { bytes, path: path.map(|p| p.to_path_buf()) }); +} + +struct Bytecode { + bytes: Vec<u8>, + path: Option<PathBuf>, +} + +impl Bytecode { + fn path(&self) -> String { + match &self.path { + Some(path) => path.as_os_str().to_string_lossy().to_string(), + None => String::from("<unknown>"), + } + } +} + + + + +fn parse_dimensions(string: &str) -> Option<ScreenDimensions> { + if string.trim().to_lowercase() == "none" { + return Some(ScreenDimensions::ZERO); + } + let (w_str, h_str) = string.trim().split_once('x')?; + Some( ScreenDimensions { + width: w_str.parse().ok()?, + height: h_str.parse().ok()?, + } ) +} + +fn parse_palette(string: &str) -> Option<[Colour; 16]> { + fn decode_ascii_hex_digit(ascii: u8) -> Option<u8> { + match ascii { + b'0'..=b'9' => Some(ascii - b'0'), + b'a'..=b'f' => Some(ascii - b'a' + 10), + b'A'..=b'F' => Some(ascii - b'A' + 10), + _ => { None } + } + } + let mut c = Vec::new(); + for token in string.split(',') { + let mut bytes = token.bytes(); + if bytes.len() != 3 { return None; } + 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())?; + c.push(Colour::from_rgb(r*17, g*17, b*17)); + } + Some(match c.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 None, + }) +} |