use bedrock_asm::*; use bedrock_pc::*; use phosphor::*; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::process::exit; const NORMAL: &str = "\x1b[0m"; const BOLD: &str = "\x1b[1m"; const WHITE: &str = "\x1b[37m"; const RED: &str = "\x1b[31m"; const BLUE: &str = "\x1b[34m"; static mut VERBOSE: bool = false; macro_rules! verbose { ($($tokens:tt)*) => { if unsafe { VERBOSE } { eprint!("{BOLD}{BLUE}[INFO]{NORMAL}: "); eprint!($($tokens)*); eprintln!("{NORMAL}"); } }; } macro_rules! error { ($($tokens:tt)*) => {{ eprint!("{BOLD}{RED}[ERROR]{WHITE}: "); eprint!($($tokens)*); eprintln!("{NORMAL}"); }}; } 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) { let program_path = args.program.as_ref().map(|p| p.as_path()); let Bytecode { bytes: bytecode, path } = load_bytecode(program_path); 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() { verbose!("Could not read program metadata"); } let mut config = EmulatorConfig { dimensions: ScreenDimensions::ZERO, fullscreen: args.fullscreen, scale: args.scale(), debug_palette: args.palette(), show_cursor: args.show_cursor, initial_transmission: None, decode_stdin: args.decode_stdin, encode_stdout: args.encode_stdout, symbols_path, }; let phosphor = Phosphor::new(); if phosphor.is_ok() && args.dimensions().is_some() { verbose!("Starting graphical emulator"); let mut phosphor = phosphor.unwrap(); config.dimensions = args.dimensions().unwrap(); let cursor = match config.show_cursor { true => Some(CursorIcon::Default), false => None, }; let mut graphical = GraphicalEmulator::new(&config, args.debug, unsafe {VERBOSE}); 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 { verbose!("Starting headless emulator"); let mut headless = HeadlessEmulator::new(&config, args.debug, unsafe {VERBOSE}); headless.load_program(&bytecode); headless.run(args.debug); }; std::process::exit(0); } fn load_bytecode(path: Option<&Path>) -> Bytecode { // TODO: Etch file location into bytecode. if let Some(path) = path { if let Ok(bytecode) = load_bytecode_from_file(path) { let length = bytecode.bytes.len(); let path = bytecode.path(); verbose!("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(); verbose!("Loaded program from {path:?} ({length} bytes)"); return bytecode; } else { error!("Could not read program from {path:?}, exiting"); exit(1); } } else { verbose!("Reading program from standard input..."); if let Ok(bytecode) = load_bytecode_from_stdin() { let length = bytecode.bytes.len(); verbose!("Loaded program from standard input ({length} bytes)"); return bytecode; } else { error!("Could not read program from standard input, exiting"); exit(1); } } } /// Attempt to load bytecode from a directory in the BEDROCK_PATH environment variable. fn load_bytecode_from_bedrock_path(path: &Path) -> Option { 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); } 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); } } } return None; } /// Attempt to load bytecode from a file path. fn load_bytecode_from_file(path: &Path) -> Result { // 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 { 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 { let mut bytes = Vec::::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, path: Option, } impl Bytecode { fn path(&self) -> String { match &self.path { Some(path) => path.as_os_str().to_string_lossy().to_string(), None => String::from(""), } } } fn main_asm(args: Asm) { // ----------------------------------------------------------------------- // RESOLVE syntactic symbols let ext = args.ext.unwrap_or(String::from("brc")); let source_path = args.source.clone().map(|p| { p.canonicalize().unwrap_or(p) }); let mut resolver = if let Some(path) = &source_path { 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:?}") }; exit(1); } } } 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) { error!("Could not read from standard input"); eprintln!("{err:?}"); exit(1); } let path = ""; 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 !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 = match resolver.get_merged_source_code() { Ok(merged_source) => merged_source, Err(ids) => { print_cyclic_source_units(&ids, &resolver); std::process::exit(1); }, }; 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(""); 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 args.symbols && !args.check { if let Some(path) = &args.output { 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) { verbose!("Could not write to symbols path {symbols_path:?}"); eprintln!("{err:?}"); } else { verbose!("Saved debug symbols to {symbols_path:?}"); } } } 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>(bytes: &[u8], path: Option<&P>) -> ! { if let Some(path) = path { if let Err(err) = std::fs::write(path, bytes) { error!("Could not write to path {:?}", path.as_ref()); eprintln!("{err:?}"); exit(1); } } else { if let Err(err) = std::io::stdout().write_all(bytes) { error!("Could not write to standard output"); eprintln!("{err:?}"); exit(1); } } 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 x 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 ,... (toggle with F2) optional --palette colours: ParsedPalette /// Show the operating system cursor over the window optional --show-cursor /// Decode standard input optional -i, --decode-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 /// Generate debug symbols with file extension '.br.sym' optional --symbols } } } impl Run { pub fn dimensions(&self) -> Option> { let size = match &self.screen { Some(parsed) => geometry::Dimensions::new(parsed.width, parsed.height), None => DEFAULT_SCREEN_SIZE / (self.scale() as u16), }; 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 { 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 { fn decode_ascii_hex_digit(ascii: u8) -> Result { 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") } }