type AssembleDirectoryResult = (
std::path::PathBuf,
std::path::PathBuf,
Option<std::path::PathBuf>,
usize,
);
pub mod assembler;
pub mod bkend;
pub mod bkend_buxn;
pub mod bkend_drif;
pub mod bkend_uxn;
pub mod bkend_uxn38;
pub mod chocolatal;
pub mod debug;
pub mod devicemap;
pub mod dis_uxndis;
pub mod emu_uxncli;
pub mod error;
pub mod hexrev;
pub mod lexer;
pub mod opcode_table;
pub mod opcodes;
pub mod parser;
pub mod rom;
pub mod runes;
pub mod wsl;
pub use assembler::Assembler;
pub use error::AssemblerError;
pub mod fetch;
pub mod paths;
pub mod urlutil;
pub mod util;
pub use fetch::resolver::resolve_entry_from_url;
pub fn assemble(source: &str) -> Result<Vec<u8>, AssemblerError> {
let mut a = Assembler::new();
a.assemble(source, None)
}
pub fn assemble_with_path(source: &str, path: &str) -> Result<Vec<u8>, AssemblerError> {
let mut a = Assembler::new();
a.assemble(source, Some(path.to_string()))
}
pub fn assemble_file<P: AsRef<std::path::Path>>(input_path: P) -> Result<Vec<u8>, AssemblerError> {
let source = std::fs::read_to_string(&input_path)?;
let mut assembler = Assembler::new();
let path_str = input_path.as_ref().to_string_lossy().into_owned();
assembler.assemble(&source, Some(path_str))
}
pub fn assemble_file_to_rom<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
input_path: P,
output_path: Q,
) -> Result<usize, AssemblerError> {
let rom = assemble_file(input_path)?;
std::fs::write(&output_path, &rom)?;
Ok(rom.len())
}
pub fn assemble_file_auto<P: AsRef<std::path::Path>>(
input_path: P,
) -> Result<(std::path::PathBuf, usize), AssemblerError> {
let input_path = input_path.as_ref();
let output_path = input_path.with_extension("rom");
let size = assemble_file_to_rom(input_path, &output_path)?;
Ok((output_path, size))
}
pub fn assemble_file_with_symbols<P: AsRef<std::path::Path>>(
input_path: P,
) -> Result<(std::path::PathBuf, std::path::PathBuf, usize), AssemblerError> {
let input_path = input_path.as_ref();
let source = std::fs::read_to_string(input_path)?;
let mut assembler = Assembler::new();
let path_str = input_path.to_string_lossy().into_owned();
let rom = assembler.assemble(&source, Some(path_str))?;
let rom_path = input_path.with_extension("rom");
std::fs::write(&rom_path, &rom)?;
let sym_path = input_path.with_extension("sym");
let symbols = assembler.generate_symbol_file();
std::fs::write(&sym_path, &symbols)?;
Ok((rom_path, sym_path, rom.len()))
}
pub fn assemble_directory<P: AsRef<std::path::Path>>(
dir_path: P,
generate_symbols: bool,
) -> Result<Vec<AssembleDirectoryResult>, AssemblerError> {
let dir_path = dir_path.as_ref();
let mut results = Vec::new();
for entry in std::fs::read_dir(dir_path)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("tal") {
if generate_symbols {
let (rom_path, sym_path, size) = assemble_file_with_symbols(&path)?;
results.push((path, rom_path, Some(sym_path), size));
} else {
let (rom_path, size) = assemble_file_auto(&path)?;
results.push((path, rom_path, None, size));
}
}
}
Ok(results)
}
pub fn assemble_with_rust_interface_module(
source: &str,
module_name: &str,
) -> Result<(Vec<u8>, String), AssemblerError> {
let mut a = Assembler::new();
let rom = a.assemble(source, None)?;
let module = generate_rust_interface_module(&a, module_name);
Ok((rom, module))
}
pub fn generate_rust_interface_module(
assembler: &crate::assembler::Assembler,
module_name: &str,
) -> String {
let mut out = String::new();
out.push_str("#![allow(clippy::module_inception)]\n");
out.push_str(&format!("pub mod {} {{\n", module_name));
out.push_str(" #![allow(non_upper_case_globals)]\n");
out.push_str(" // Auto-generated: label address & size constants\n");
for name in &assembler.symbol_order {
if let Some(sym) = assembler.symbols.get(name) {
let id = {
let mut s: String = name
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
if s.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
s.insert(0, '_');
}
s.to_ascii_uppercase()
};
out.push_str(&format!(
" pub const _c{}: usize = 0x{:04X};\n",
id, sym.address
));
let next_addr = assembler
.symbol_order
.iter()
.skip_while(|n| *n != name)
.skip(1)
.filter_map(|n| assembler.symbols.get(n))
.map(|s| s.address)
.find(|&a| a > sym.address)
.unwrap_or(assembler.effective_length as u16);
let size = next_addr.saturating_sub(sym.address);
out.push_str(&format!(
" pub const _c{}_SIZE: usize = 0x{:04X};\n",
id, size
));
}
}
out.push_str(
r#"
/// Returns a slice of RAM for a label by name (address, size)
pub fn get_slice<'a>(ram: &'a [u8], label: &str) -> Option<&'a [u8]> {
match label {
"#,
);
for name in &assembler.symbol_order {
if let Some(_sym) = assembler.symbols.get(name) {
let id = {
let mut s: String = name
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
if s.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
s.insert(0, '_');
}
s.to_ascii_uppercase()
};
out.push_str(&format!(
" \"{name}\" => Some(&ram[_c{}.._c{}+_c{}_SIZE]),\n",
id, id, id
));
}
}
out.push_str(
r#" _ => None,
}
}
"#,
);
out.push_str("}\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_assembly() {
let source = r#"
|0100
#42 #43 ADD BRK
"#;
let mut assembler = Assembler::new();
let rom = assembler.assemble(source, None).expect("Assembly failed");
assert_eq!(rom.len(), 6);
assert_eq!(rom[0], 0x80); assert_eq!(rom[1], 0x42); assert_eq!(rom[2], 0x80); assert_eq!(rom[3], 0x43); assert_eq!(rom[4], 0x18); }
#[test]
fn test_label_reference() {
let source = r#"
|0100 @start
;data LDA2
BRK
@data #1234
"#;
let mut assembler = Assembler::new();
let rom = assembler
.assemble(source, Some("(test_label_reference)".to_string()))
.expect("Assembly failed");
assert!(rom.len() > 4);
assert_eq!(rom[0], 0xa0); let expected_addr = 0x0105_u16;
assert_eq!(rom[1], (expected_addr >> 8) as u8); assert_eq!(rom[2], (expected_addr & 0xff) as u8); }
fn _test_instruction_modes() {
let source = r#"
|0100
ADD ( base instruction )
ADD2 ( short mode )
ADDr ( return mode )
ADDk ( keep mode )
ADD2rk ( all modes )
BRK
"#;
let mut assembler = Assembler::new();
let rom = assembler
.assemble(source, Some("(test_instruction_modes)".to_string()))
.expect("Assembly failed");
assert_eq!(rom[1], 0x18 | 0x20); assert_eq!(rom[2], 0x18 | 0x40); assert_eq!(rom[3], 0x18 | 0x80); assert_eq!(rom[4], 0x18 | 0x20 | 0x40 | 0x80); assert_eq!(rom[5], 0x00); }
#[test]
fn test_hex_literals() {
let source = r#"
#12 #3456 #ab #cdef
"#;
let mut assembler = Assembler::new();
let rom = assembler
.assemble(source, Some("(test_hex_literals1)".to_string()))
.expect("Assembly failed");
assert_eq!(rom[0], 0x80); assert_eq!(rom[1], 0x12); assert_eq!(rom[2], 0xa0); assert_eq!(rom[3], 0x34); assert_eq!(rom[4], 0x56); assert_eq!(rom[5], 0x80); assert_eq!(rom[6], 0xab); assert_eq!(rom[7], 0xa0); assert_eq!(rom[8], 0xcd); assert_eq!(rom[9], 0xef); }
#[test]
fn test_character_literals() {
let source = r#"
|0100
'A 'B 'C
"#;
let mut assembler = Assembler::new();
let rom = assembler.assemble(source, None).expect("Assembly failed");
assert_eq!(rom[0], b'A');
assert_eq!(rom[1], b'B');
assert_eq!(rom[2], b'C');
}
#[test]
fn test_raw_strings() {
let source = r#"
|0100
"Hello"
"#;
let mut assembler = Assembler::new();
let rom = assembler.assemble(source, None).expect("Assembly failed");
assert_eq!(&rom[0..5], b"Hello");
}
#[test]
#[ignore = "reason: not sure why it fails, tbd"]
fn test_undefined_label_error() {
let source = r#"
|0100
;undefined-label LDA2
"#;
let mut assembler = Assembler::new();
let result = assembler.assemble(source, None);
assert!(matches!(result, Err(AssemblerError::UndefinedLabel { .. })));
}
#[test]
#[ignore = "reason: not sure why it fails, tbd"]
fn test_duplicate_label_error() {
let source = r#"
|0100 @label
@label
"#;
let mut assembler = Assembler::new();
let result = assembler.assemble(source, Some("(test_duplicate_label_error)".to_owned()));
assert!(matches!(result, Err(AssemblerError::DuplicateLabel { .. })));
}
#[test]
#[ignore = "reason: not sure why it fails, tbd"]
fn test_unknown_opcode_error() {
let source = r#"
|0100
UNKNOWN
"#;
let mut assembler = Assembler::new();
let result = assembler.assemble(source, None);
assert!(matches!(result, Err(AssemblerError::UnknownOpcode { .. })));
}
#[test]
fn test_skip_directive() {
let source = r#"
|0100
#12
$04
#34
"#;
let mut assembler = Assembler::new();
let data = assembler
.assemble(source, Some("(test_skip_directive)".to_string()))
.unwrap();
assert_eq!(data[0], 0x80); assert_eq!(data[1], 0x12); let _rom = assembler
.assemble(source, Some("(test_hex_literals)".to_string()))
.expect("Assembly failed");
assert_eq!(data[3], 0x00); assert_eq!(data[4], 0x00); assert_eq!(data[5], 0x00); assert_eq!(data[6], 0x80); assert_eq!(data[7], 0x34); }
#[test]
fn test_device_access() {
let source = r#"
|00 @System &r $2
|0100 @main
#ff .System/r DEO
"#;
let mut assembler = Assembler::new();
let data = assembler
.assemble(source, Some("(test_device_access)".to_string()))
.unwrap();
assert_eq!(data.len(), 5);
assert_eq!(data[0], 0x80); assert_eq!(data[1], 0xff); assert_eq!(data[2], 0x80); assert_eq!(data[3], 0x00); assert_eq!(data[4], 0x17); }
#[test]
fn test_macros() {
let source = r#"
%DOUBLE { DUP ADD }
|0100 @main
#05 DOUBLE
"#;
let mut assembler = Assembler::new();
let data = assembler
.assemble(source, Some("(test_macros)".to_string()))
.unwrap();
assert_eq!(data.len(), 4);
assert_eq!(data[0], 0x80); assert_eq!(data[1], 0x05); assert_eq!(data[2], 0x06); assert_eq!(data[3], 0x18); }
#[test]
fn test_inline_assembly() {
let source = r#"
|0100 @main
[ #05 DUP ADD ]
"#;
let mut assembler = Assembler::new();
let data = assembler
.assemble(source, Some("(test_inline_assembly)".to_string()))
.unwrap();
assert_eq!(data.len(), 4);
assert_eq!(data[0], 0x80); assert_eq!(data[1], 0x05); assert_eq!(data[2], 0x06); assert_eq!(data[3], 0x18); }
#[test]
fn test_complete_tal_features() {
let source = r#"
|0100 @main
#41 #18 DEO
BRK
"#;
let mut assembler = Assembler::new();
let result = assembler.assemble(source, Some("(test_complete_tal_features)".to_string()));
if let Err(ref e) = result {
println!("Assembly error: {}", e);
}
assert!(result.is_ok(), "Complete TAL assembly should succeed");
let data = result.unwrap();
assert!(data.len() == 6, "Should generate some ROM data");
assert_eq!(data[0], 0x80); assert_eq!(data[1], 0x41); assert_eq!(data[2], 0x80); assert_eq!(data[3], 0x18); assert_eq!(data[4], 0x17); }
}