summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/br.rs461
-rw-r--r--src/bin/br/asm.rs152
-rw-r--r--src/bin/br/formats/clang.rs10
-rw-r--r--src/bin/br/formats/mod.rs24
-rw-r--r--src/bin/br/main.rs111
-rw-r--r--src/bin/br/run.rs186
6 files changed, 483 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..f4620cf
--- /dev/null
+++ b/src/bin/br/asm.rs
@@ -0,0 +1,152 @@
+use crate::formats::*;
+
+use bedrock_asm::*;
+use log::{info, fatal};
+use switchboard::*;
+
+use std::io::{Read, Write};
+use std::path::Path;
+
+
+pub fn main(mut args: Switchboard) {
+ args.positional("source");
+ args.positional("destination");
+ args.named("extension").default("brc");
+
+ args.named("no-libs");
+ args.named("no-project-libs");
+ args.named("no-env-libs");
+
+ args.named("format").default("binary");
+ args.named("dry-run").short('n');
+ args.named("tree");
+ args.named("with-symbols");
+ args.raise_errors();
+
+ let source_path = args.get("source").as_path_opt().map(
+ |p| p.canonicalize().unwrap_or_else(|e| fatal!("{p:?}: {e:?}")));
+ let destination_path = args.get("destination").as_path_opt();
+ let extension = args.get("extension").as_string();
+ let opt_extension = Some(extension.as_str());
+
+ let no_libs = args.get("no-libs").as_bool();
+ let no_project_libs = args.get("no-project-libs").as_bool();
+ let no_env_libs = args.get("no-env-libs").as_bool();
+
+ let format = Format::from_str(args.get("format").as_str());
+ let dry_run = args.get("dry-run").as_bool();
+ let print_tree = args.get("tree").as_bool();
+ let export_symbols = args.get("with-symbols").as_bool();
+
+ // -----------------------------------------------------------------------
+
+ let mut compiler = new_compiler();
+
+ if let Some(path) = &source_path {
+ info!("Reading program source from {path:?}");
+ compiler.root_from_path(path).unwrap_or_else(|err| fatal!("{err:?}: {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.root_from_string(source_code, "<standard input>")
+ };
+ if compiler.error().is_some() && !no_libs && !no_project_libs {
+ compiler.include_libs_from_parent(opt_extension);
+ }
+ if compiler.error().is_some() && !no_libs && !no_env_libs {
+ compiler.include_libs_from_path_variable("BEDROCK_LIBS", opt_extension);
+ }
+
+ if print_tree {
+ compiler.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 !dry_run && format == Format::Source {
+ write_bytes_and_exit(merged_source.as_bytes(), destination_path.as_ref());
+ }
+
+ // -----------------------------------------------------------------------
+
+ let path = Some("<merged source>");
+ let syntactic = match parse_syntactic(&merged_source, path) {
+ Ok(tokens) => tokens,
+ Err(errors) => {
+ report_syntactic_errors(&errors, &merged_source);
+ std::process::exit(1);
+ }
+ };
+
+ let semantic = match parse_semantic(syntactic) {
+ Ok(tokens) => tokens,
+ Err(errors) => {
+ report_semantic_errors(&errors, &merged_source);
+ std::process::exit(1);
+ }
+ };
+
+ let AssembledProgram { mut bytecode, symbols } = generate_bytecode(&semantic);
+ // Remove null bytes from end of bytecode.
+ while let Some(0) = bytecode.last() {
+ bytecode.pop();
+ }
+
+ 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 {
+ if export_symbols {
+ if let Some(path) = &destination_path {
+ let mut symbols_path = path.to_path_buf();
+ symbols_path.add_extension("sym");
+ let mut symbols_string = String::new();
+ for symbol in &symbols {
+ let address = &symbol.address;
+ let name = &symbol.name;
+ let location = &symbol.source.location();
+ symbols_string.push_str(&format!(
+ "{address:04x} {name} {location}\n"
+ ));
+ }
+ match std::fs::write(&symbols_path, symbols_string) {
+ Ok(_) => info!("Saved symbols to {symbols_path:?}"),
+ Err(err) => info!("Could not write symbols to {symbols_path:?}\n{err:?}"),
+ }
+ }
+ }
+
+ let bytes = match format {
+ Format::Binary => bytecode,
+ Format::Clang => format_clang(&bytecode),
+ Format::Source => unreachable!("Source output is handled before full assembly"),
+ };
+ write_bytes_and_exit(&bytes, 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/formats/clang.rs b/src/bin/br/formats/clang.rs
new file mode 100644
index 0000000..30d80de
--- /dev/null
+++ b/src/bin/br/formats/clang.rs
@@ -0,0 +1,10 @@
+pub fn format_clang(bytecode: &[u8]) -> Vec<u8> {
+ let mut output = String::new();
+ for chunk in bytecode.chunks(16) {
+ for byte in chunk {
+ output.push_str(&format!("0x{byte:02x}, "));
+ }
+ output.push('\n');
+ }
+ return output.into_bytes();
+}
diff --git a/src/bin/br/formats/mod.rs b/src/bin/br/formats/mod.rs
new file mode 100644
index 0000000..b159b16
--- /dev/null
+++ b/src/bin/br/formats/mod.rs
@@ -0,0 +1,24 @@
+mod clang;
+
+pub use clang::*;
+
+use log::fatal;
+
+
+#[derive(Clone, Copy, PartialEq)]
+pub enum Format {
+ Binary,
+ Source,
+ Clang,
+}
+
+impl Format {
+ pub fn from_str(string: &str) -> Self {
+ match string {
+ "binary" => Self::Binary,
+ "source" => Self::Source,
+ "c" => Self::Clang,
+ _ => fatal!("Unknown format '{string}', expected 'binary', 'c', or 'source'"),
+ }
+ }
+}
diff --git a/src/bin/br/main.rs b/src/bin/br/main.rs
new file mode 100644
index 0000000..52c53b0
--- /dev/null
+++ b/src/bin/br/main.rs
@@ -0,0 +1,111 @@
+#![feature(path_add_extension)]
+
+mod asm;
+mod run;
+mod formats;
+
+use switchboard::*;
+
+
+fn main() {
+ let mut args = Switchboard::from_env();
+ args.named("help").short('h');
+ args.named("version");
+ args.named("verbose").short('v');
+
+ if args.get("help").as_bool() {
+ print_help();
+ }
+ if args.get("version").as_bool() {
+ print_version();
+ }
+ if args.get("verbose").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() -> ! {
+ eprintln!("\
+Usage: br [source]
+ br asm [source] [destination]
+
+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:
+ --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)
+ --show-cursor (-c) Show the operating system cursor over the window
+ --palette=<pal> Set a debug colour palette in the format <rgb>,... (toggle with F2)
+ --debug, (-d) Show debug information while the program is running
+ --decode-stdin (-i) Decode transmissions on standard input from text lines.
+ --encode-stdout (-o) Encode transmissions on standard output as text lines.
+ --help (-h) Print this help information
+ --verbose, (-v) Print additional information
+ --version Print the program version and exit
+
+Arguments (asm mode):
+ [source] Bedrock source code file to assemble.
+ [destination] Destination path for assembler output.
+
+Switches (asm mode):
+ --dry-run (-n) Assemble and show errors only, don't write any output
+ --extension File extension to identify source files (default is 'brc')
+ --format=<fmt> Output format to use for assembled program (default is 'binary')
+ --no-project-libs Don't search for libraries in the source parent folder
+ --no-env-libs Don't search for libraries in the BEDROCK_LIBS path variable
+ --no-libs Combination of --no-project-libs and --no-env-libs
+ --tree Display a tree visualisation of all included library files
+ --with-symbols Also generate debug symbols file with extension '.sym'
+ --help (-h) Print this help information
+ --verbose, (-v) Print additional information
+ --version Print the program version and exit
+");
+ 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..86fdd43
--- /dev/null
+++ b/src/bin/br/run.rs
@@ -0,0 +1,186 @@
+use log::*;
+use crate::*;
+
+use bedrock_pc::*;
+use phosphor::*;
+
+use std::cmp::{min, max};
+use std::num::NonZeroU32;
+
+
+pub fn main(mut args: Switchboard) {
+ args.positional("source");
+
+ args.named("debug").short('d');
+ args.named("mode").default("dynamic");
+
+ args.named("palette");
+ args.named("fullscreen").short('f');
+ args.named("show-cursor").short('c');
+ args.named("zoom").short('z').quick("3").default("1");
+ args.named("size").short('s');
+
+ args.named("decode-stdin").short('i');
+ args.named("encode-stdout").short('o');
+ args.raise_errors();
+
+ let source = args.get("source").as_path_opt();
+ let debug = args.get("debug").as_bool();
+ let mode = Mode::from_str(args.get("mode").as_str());
+ let override_palette = args.get("palette").as_str_opt().map(parse_palette);
+ let fullscreen = args.get("fullscreen").as_bool();
+ let show_cursor = args.get("show-cursor").as_bool();
+ let zoom_raw = min(10, max(1, args.get("zoom").as_u32()));
+ let zoom = unsafe { NonZeroU32::new_unchecked(zoom_raw) };
+ let initial_dimensions = match args.get("size").as_str_opt() {
+ Some(string) => parse_dimensions(string),
+ None => DEFAULT_SCREEN_SIZE / (zoom_raw as u16),
+ };
+ let decode_stdin = args.get("decode-stdin").as_bool();
+ let encode_stdout = args.get("encode-stdout").as_bool();
+
+ // -----------------------------------------------------------------------
+
+ let Program { bytecode, path } = load_program(source.as_ref());
+
+ let symbols_path = path.as_ref().map(|p| {
+ let mut path = p.to_path_buf();
+ path.add_extension("sym");
+ path
+ });
+
+ let metadata = parse_metadata(&bytecode);
+ if metadata.is_none() {
+ info!("Could not read program metadata");
+ }
+
+ let config = EmulatorConfig {
+ initial_dimensions,
+ fullscreen,
+ zoom,
+ override_palette,
+ show_cursor,
+
+ decode_stdin,
+ encode_stdout,
+
+ symbols_path,
+ metadata,
+ };
+
+ if let Ok(phosphor) = Phosphor::new() {
+ match mode {
+ Mode::Dynamic => {
+ info!("Starting dynamic emulator");
+ let mut emulator = DynamicEmulator::new(&config, debug);
+ emulator.load_program(&bytecode);
+ emulator.run();
+ info!("Upgrading dynamic emulator to graphical emulator");
+ let emulator = emulator.to_graphical(config);
+ emulator.run(phosphor);
+ }
+ Mode::Graphical => {
+ info!("Starting graphical emulator");
+ let emulator = GraphicalEmulator::new(config, debug);
+ emulator.run(phosphor);
+ }
+ Mode::Headless => {
+ info!("Starting headless emulator");
+ let mut emulator = HeadlessEmulator::new(&config, debug);
+ emulator.run();
+ }
+ }
+ } else {
+ match mode {
+ Mode::Dynamic => {
+ info!("Could not start graphical event loop");
+ info!("Starting headless emulator");
+ let mut emulator = HeadlessEmulator::new(&config, debug);
+ emulator.run();
+ }
+ Mode::Graphical => {
+ fatal!("Could not start graphical event loop");
+ }
+ Mode::Headless => {
+ info!("Starting headless emulator");
+ let mut emulator = HeadlessEmulator::new(&config, debug);
+ emulator.run();
+ }
+ }
+ }
+}
+
+
+fn parse_dimensions(string: &str) -> ScreenDimensions {
+ fn parse_inner(string: &str) -> Option<ScreenDimensions> {
+ let (w_str, h_str) = string.trim().split_once('x')?;
+ Some( ScreenDimensions {
+ width: w_str.parse().ok()?,
+ height: h_str.parse().ok()?,
+ } )
+ }
+ let dimensions = parse_inner(string).unwrap_or_else(|| {
+ fatal!("Invalid dimensions string '{string}'");
+ });
+ if dimensions.is_zero() {
+ fatal!("Screen dimensions must be greater than zero");
+ }
+ return dimensions;
+}
+
+fn parse_palette(string: &str) -> [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 }
+ }
+ }
+ fn parse_inner(string: &str) -> Option<[Colour; 16]> {
+ 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,
+ })
+ }
+ parse_inner(string).unwrap_or_else(|| {
+ fatal!("Invalid palette string '{string}'");
+ })
+}
+
+
+#[derive(Copy, Clone, PartialEq)]
+enum Mode {
+ Graphical,
+ Headless,
+ Dynamic,
+}
+
+impl Mode {
+ pub fn from_str(string: &str) -> Self {
+ match string {
+ "g" | "graphical" => Self::Graphical,
+ "h" | "headless" => Self::Headless,
+ "d" | "dynamic" => Self::Dynamic,
+ _ => fatal!("Invalid mode string '{string}'"),
+ }
+ }
+}