summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
authorBen Bridle <bridle.benjamin@gmail.com>2024-10-28 20:25:01 +1300
committerBen Bridle <bridle.benjamin@gmail.com>2024-10-28 20:29:12 +1300
commit1a830a3d1b9d99653322d5ae49ea8165de7ed9d0 (patch)
tree798e77b6fcf2438b1c2538a67efe856a2f7cb979 /src/bin
parent03c4b069e1806af256730639cefdae115b24401a (diff)
downloadbedrock-pc-1.0.0-alpha1.zip
Rewrite emulatorv1.0.0-alpha1
This is a complete rewrite and restructure of the entire emulator project, as part of the effort in locking down the Bedrock specification and in creating much better tooling for creating and using Bedrock programs. This commit adds a command-line argument scheme, an embedded assembler, a headless emulator for use in non-graphical environments, deferred window creation for programs that do not access the screen device, and new versions of phosphor and bedrock-core. The new version of phosphor supports multi-window programs, which will make it possible to implement program forking in the system device later on, and the new version of bedrock-core implements the final core specification.
Diffstat (limited to 'src/bin')
-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")
+ }
+}