summaryrefslogtreecommitdiff
path: root/src/bin/br.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/br.rs')
-rw-r--r--src/bin/br.rs448
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")
+ }
+}