diff options
author | Ben Bridle <bridle.benjamin@gmail.com> | 2025-05-29 12:18:16 +1200 |
---|---|---|
committer | Ben Bridle <bridle.benjamin@gmail.com> | 2025-05-29 12:18:28 +1200 |
commit | ff99a5df4c5a3265d215afa4b937fbb95a55b96c (patch) | |
tree | eb299429a57233881c47eb86622498eba841f03e /src/lib.rs | |
download | bedrock-asm-ff99a5df4c5a3265d215afa4b937fbb95a55b96c.zip |
Initial commit
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d45d449 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,230 @@ +#![feature(path_add_extension)] + +mod formats; +mod types; +mod stages; +pub use formats::*; +pub use types::*; +pub use stages::*; + +use assembler::*; +use log::*; +use switchboard::*; + +use std::io::Read; +use std::io::Write; + + +pub const RETURN_MODE: u8 = 0x80; +pub const WIDE_MODE: u8 = 0x40; +pub const IMMEDIATE_MODE: u8 = 0x20; + + +pub fn assemble(mut args: Switchboard, invocation: &str) { + 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("no-truncate"); + + args.named("format").default("raw"); + args.named("dry-run").short('n'); + args.named("tree"); + args.named("with-symbols"); + args.named("help").short('h'); + args.raise_errors(); + + if args.get("help").as_bool() { + print_help(invocation); + std::process::exit(0); + } + + 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 no_truncate = args.get("no-truncate").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 program = match generate_bytecode(&semantic) { + Ok(program) => program, + Err(errors) => { + report_bytecode_errors(&errors, &merged_source); + std::process::exit(1); + } + }; + + let AssembledProgram { mut bytecode, symbols } = program; + + 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 !no_truncate { + // Remove null bytes from end of bytecode. + while let Some(0) = bytecode.last() { + bytecode.pop(); + } + let difference = length - bytecode.len(); + if difference > 0 { + info!("Truncated program to {length} bytes (saved {difference} bytes)"); + } + } + + 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::Raw => 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); +} + + +fn print_help(invocation: &str) { + eprintln!("\ +Usage: {invocation} [source] [destination] + +Convert Bedrock source code into an assembled Bedrock program. + +Usage: + To assemble a Bedrock program from a source file and write to an output + file, run `br-asm [source] [destination]`, where [source] is the path + of the source file and [destination] is the path to write to. + + If [destination] is omitted, the assembled program will be written to + standard output. If [source] is omitted, the program source code will + be read from standard input. + +Environment variables: + 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: + [source] Bedrock source code file to assemble. + [destination] Destination path for assembler output. + +Switches: + --dry-run (-n) Assemble and show errors only, don't write any output + --extension=<ext> File extension to identify source files (default is 'brc') + --format=<fmt> Output format to use for assembled program (default is 'raw') + --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 + --no-truncate Don't remove trailing zero-bytes from the assembled program + --tree Show a tree diagram 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 +"); +} |