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) {
if args.debug {
unsafe { VERBOSE = true; }
}
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);
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);
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<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);
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<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(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 = "<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 !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("<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 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<P: AsRef<Path>>(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 <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
/// 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<geometry::Dimensions<u16>> {
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<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")
}
}