diff --git a/Cargo.lock b/Cargo.lock index 2a2575d..edb74ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,108 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "logos" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6b6e02facda28ca5fb8dbe4b152496ba3b1bd5a4b40bb2b1b2d8ad74e0f39b" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32eb6b5f26efacd015b000bfc562186472cd9b34bdba3f6b264e2a052676d10" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax", + "syn", +] + +[[package]] +name = "logos-derive" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5d0c5463c911ef55624739fc353238b4e310f0144be1f875dc42fec6bfd5ec" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "syn" +version = "2.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "um" version = "0.1.0" dependencies = [ + "logos", "smallvec", ] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" diff --git a/Cargo.toml b/Cargo.toml index 541be56..7ad577b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,16 @@ name = "um" version = "0.1.0" edition = "2021" +default-run = "um" +rust-version = "1.74.1" [dependencies] smallvec = { version = "1.13.2" } +logos = { version = "0.14.2" } [features] default = [] -timing = [] + +[profile.release] +lto = "fat" +codegen-units = 1 diff --git a/README.md b/README.md index 37b7359..ad0c19e 100644 --- a/README.md +++ b/README.md @@ -6,5 +6,5 @@ An implementation of the UM-32 "Universal Machine" as described by the [Cult of Run the benchmark: ```sh -; cargo run --release --features timing -- files/sandmark.umz +; cargo run --release -- files/sandmark.umz ``` diff --git a/files/cat.asm b/files/cat.asm new file mode 100644 index 0000000..c25454a --- /dev/null +++ b/files/cat.asm @@ -0,0 +1,33 @@ +; +; cat.asm +; +; Read from stdin and echo to stdout. +; +main: + ; set r2 to 0xffffffff + nand r2 + + ; setup branches + adr r6, output + adr r5, loop + +loop: + ; read stdin, r1 will contain 0xffffffff if we've reached EOF. + in r1 + + ; set r3 to 0 if r2 == r1 + nand r3, r2, r1 + + ; setup branch + adr r4, end + ; overwrite r4 with $output iff r3 == 0. + mov r4, r6, r3 + jmp [r0, r4] + +output: + ; write to stdout + out r1 + jmp [r0, r5] + +end: + halt diff --git a/files/hello-world.asm b/files/hello-world.asm new file mode 100644 index 0000000..fedfac6 --- /dev/null +++ b/files/hello-world.asm @@ -0,0 +1,24 @@ +; +; hello-world.asm +; +; Prints "Hello, world!" to the stdout. +; +message: + .wstr "Hello, world!\n" + + adr r1, message + adr r4, loop + mov r3, 1 +loop: + ldr r2, [r0, r1] + adr r6, next + adr r7, end + mov r7, r6, r2 + jmp [r0, r7] +next: + out r2 + add r1, r3 + jmp [r0, r4] + +end: + halt diff --git a/src/asm.rs b/src/asm.rs new file mode 100644 index 0000000..dba95fb --- /dev/null +++ b/src/asm.rs @@ -0,0 +1,335 @@ +mod lexer; +mod parse; + +use crate::{Platter, Register}; +use lexer::Token; +use parse::{Instruction, Node, NodeType, PragmaType}; +use std::collections::HashMap; + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +enum Section { + Text, + Data, +} + +pub fn assemble<'s>(source: &'s str) -> Vec { + let parsed = parse::parse("", source).unwrap(); + + let mut sections: HashMap>> = HashMap::new(); + let mut offsets: HashMap = HashMap::new(); + let mut label_locations: HashMap<&'s str, (Section, usize)> = HashMap::new(); + for node in parsed.nodes().iter() { + match node.entity { + NodeType::Pragma(_) => { + let loc = *offsets + .entry(Section::Data) + .and_modify(|loc| *loc += node.size()) + .or_default(); + + sections + .entry(Section::Data) + .and_modify(|section| section.push(node)) + .or_insert(vec![node]); + + for label in &node.labels { + label_locations.insert(label, (Section::Data, loc)); + } + } + NodeType::Instruction(_) => { + let loc = *offsets + .entry(Section::Text) + .and_modify(|loc| *loc += node.size()) + .or_default(); + + sections + .entry(Section::Text) + .and_modify(|section| section.push(node)) + .or_insert(vec![node]); + + for label in &node.labels { + label_locations.insert(label, (Section::Text, loc)); + } + } + _ => {} + } + } + + let text = sections.remove(&Section::Text).unwrap(); + let data_offset = text.len(); + + let mut program = vec![]; + for node in text.into_iter() { + let NodeType::Instruction(instruction) = &node.entity else { + panic!("invalid node in .text section"); + }; + + let encoded = match instruction { + Instruction::ConditionalMove { + destination, + source, + condition, + } => encode_standard(0x00, destination, source, condition), + Instruction::Load { + destination, + address, + } => { + let parse::Location { block, offset } = address; + encode_standard(0x01, destination, block, offset) + } + Instruction::Store { source, address } => { + let parse::Location { block, offset } = address; + encode_standard(0x02, block, offset, source) + } + Instruction::Add { destination, a, b } => encode_standard(0x03, destination, a, b), + Instruction::AddAssign { destination, a } => { + encode_standard(0x03, destination, destination, a) + } + Instruction::AddSelf { destination } => { + encode_standard(0x03, destination, destination, destination) + } + Instruction::Mul { destination, a, b } => encode_standard(0x04, destination, a, b), + Instruction::MulAssign { destination, a } => { + encode_standard(0x04, destination, destination, a) + } + Instruction::MulSelf { destination } => { + encode_standard(0x04, destination, destination, destination) + } + Instruction::Div { destination, a, b } => encode_standard(0x05, destination, a, b), + Instruction::DivAssign { destination, a } => { + encode_standard(0x05, destination, destination, a) + } + Instruction::DivSelf { destination } => { + encode_standard(0x05, destination, destination, destination) + } + Instruction::Nand { destination, a, b } => encode_standard(0x06, destination, a, b), + Instruction::NandAssign { destination, a } => { + encode_standard(0x06, destination, destination, a) + } + Instruction::NandSelf { destination } => { + encode_standard(0x06, destination, destination, destination) + } + Instruction::Halt => encode_standard( + 0x07, + &Default::default(), + &Default::default(), + &Default::default(), + ), + Instruction::Alloc { + destination, + length, + } => encode_standard(0x08, &Register::default(), destination, length), + Instruction::Free { block } => { + encode_standard(0x09, &Register::default(), &Register::default(), block) + } + Instruction::Out { source } => { + encode_standard(0x0a, &Default::default(), &Default::default(), source) + } + Instruction::In { destination } => { + encode_standard(0x0b, &Default::default(), &Default::default(), destination) + } + Instruction::Jmp { location } => { + let parse::Location { block, offset } = location; + encode_standard(0x0c, &Register::default(), block, offset) + } + Instruction::Address { + destination, + reference, + } => { + // lookup reference + let Some((section, offset)) = label_locations.get(reference.label) else { + panic!("failed to resolve {}", reference.label); + }; + + let value = match section { + Section::Text => *offset, + Section::Data => data_offset + *offset, + }; + + 0xd0000000 | destination.encode_a_ortho() | encode_literal(value as Platter) + } + Instruction::LiteralMove { + destination, + literal, + } => 0xd0000000 | destination.encode_a_ortho() | encode_literal(*literal), + }; + + program.push(encoded); + } + + if let Some(data) = sections.remove(&Section::Data) { + for node in data.into_iter() { + let NodeType::Pragma(pragma) = &node.entity else { + panic!("invalid node in .data section. {node:?}"); + }; + + let encoded = match &pragma.payload { + PragmaType::WideString { value } => { + for byte in value.as_bytes() { + program.push(*byte as Platter); + } + Some(0) // terminating byte. + } + PragmaType::U32 { value } => Some(*value), + }; + + if let Some(encoded) = encoded { + program.push(encoded); + } + } + } + + program +} + +fn encode_literal(value: Platter) -> Platter { + const LITERAL_MAX: Platter = 0x1ffffff; + assert!(value <= LITERAL_MAX, "literal value exceeds available bits. value: {value} (0x{value:x}), max: {LITERAL_MAX} (0x{LITERAL_MAX:x})"); + value as Platter +} + +fn encode_standard(op: Platter, a: &Register, b: &Register, c: &Register) -> Platter { + (op << 28) | a.encode_a() | b.encode_b() | c.encode_c() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Operation, Register::*}; + + #[test] + fn wide_str() { + // Embed a wide string and get a reference to it. + let program = assemble( + r#" + adr r0, msg + msg: .wstr "Hello" + "#, + ); + + let ops = crate::decode_ops(&program); + assert_eq!(ops[0], Operation::Orthography { a: R0, value: 1 }); + + let mut platters = program.into_iter().skip(1); + assert_eq!(platters.next(), Some('H' as Platter)); + assert_eq!(platters.next(), Some('e' as Platter)); + assert_eq!(platters.next(), Some('l' as Platter)); + assert_eq!(platters.next(), Some('l' as Platter)); + assert_eq!(platters.next(), Some('o' as Platter)); + assert_eq!(platters.next(), Some(0)); + assert_eq!(platters.next(), None); + } + + #[test] + fn addresses() { + let program = assemble( + r#" + halt + start: + ldr r2, [r0, r1] + str r2, [r0, r1] + adr r3, start + halt + "#, + ); + + let mut ops = crate::decode_ops(&program).into_iter(); + + assert_eq!(ops.next(), Some(Operation::Halt)); + assert_eq!( + ops.next(), + Some(Operation::ArrayIndex { + a: R2, + b: R0, + c: R1 + }) + ); + assert_eq!( + ops.next(), + Some(Operation::ArrayAmendment { + a: R0, + b: R1, + c: R2 + }) + ); + assert_eq!(ops.next(), Some(Operation::Orthography { a: R3, value: 1 })); + assert_eq!(ops.next(), Some(Operation::Halt)); + assert_eq!(ops.next(), None); + } + + #[test] + fn load_store() { + let state = crate::Um::new(assemble( + r#" + adr r1, loc + ldr r2, [r0, r1] + mov r3, 56 + str r3, [r0, r1] + halt + loc:.u32 42 + "#, + )) + .run(); + assert_eq!(state.registers[R2], 42); + assert_eq!(state.memory[0][5], 56); + } + + #[test] + fn addition() { + let state = crate::Um::new(assemble( + r#" + mov r0, 42 + mov r1, 64 + mov r2, 8192 + + add r3, r0, r1 ; r3 = r0 + r1 = 106 + add r1, r2 ; r1 = r1 + r2 = 8256 + add r0 ; r0 = r0 + r0 = 84 + + halt + "#, + )) + .run(); + + assert_eq!(state.registers[R0], 84); + assert_eq!(state.registers[R1], 8256); + assert_eq!(state.registers[R2], 8192); + assert_eq!(state.registers[R3], 106); + } + + #[test] + fn alloc() { + let state = crate::Um::new(assemble( + r#" + ; Allocate 1000 bytes. + mov r0, 1000 + alloc r1, r0 + halt + "#, + )) + .run(); + assert_eq!(state.registers[R0], 1000); + assert_ne!(state.registers[R1], 0); + assert_eq!(state.memory[state.registers[R1] as usize].len(), 1000); + } + + #[test] + fn free() { + let state = crate::Um::new(assemble( + r#" + ; Allocate 1000 bytes. + mov r0, 1000 + alloc r1, r0 + free r1 + halt + "#, + )) + .run(); + assert_eq!(state.registers[R0], 1000); + assert_ne!(state.registers[R1], 0); + assert_eq!( + state.memory[state.registers[R1] as usize].len(), + 0, + "memory not free'd" + ); + } +} diff --git a/src/asm/lexer.rs b/src/asm/lexer.rs new file mode 100644 index 0000000..f5ada2f --- /dev/null +++ b/src/asm/lexer.rs @@ -0,0 +1,127 @@ +use crate::{Platter, Register}; +use logos::{Lexer, Logos}; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Extras { + pub line: usize, +} + +#[derive(Logos, Debug, PartialEq)] +#[logos(skip r"[ \t\f,]+", extras = Extras)] +pub enum Token<'source> { + #[token("\n", lex_newline)] + Newline, + + #[regex("[a-zA-Z]+[a-zA-Z0-9_]*:", lex_label)] + Label(&'source str), + + #[regex("[a-zA-Z_]+[a-zA-Z0-9_]*", |lexer| lexer.slice())] + Ident(&'source str), + + #[regex(r#"\.([a-zA-Z0-9]+)"#, |lexer| &lexer.slice()[1..])] + Pragma(&'source str), + + #[token("[")] + AddressOpen, + + #[token("]")] + AddressClose, + + #[regex("r[0-7]", lex_register, priority = 10)] + Register(Register), + + #[token("#")] + Pound, + + #[token("+")] + Plus, + + #[token("-")] + Minus, + + #[token(".")] + Here, + + #[regex(r#"(0x[a-fA-F0-9]+)|([0-9]+)"#, lex_number)] + Number(Platter), + + #[token("\"", lex_string_literal)] + String(&'source str), + + #[token(";", lex_comment)] + Comment(&'source str), +} + +fn lex_newline<'source>(lexer: &mut Lexer<'source, Token<'source>>) { + lexer.extras.line += 1; +} + +fn lex_label<'source>(lex: &mut Lexer<'source, Token<'source>>) -> &'source str { + let slice = lex.slice(); + &slice[..slice.len() - 1] +} + +fn lex_number<'source>(lex: &mut Lexer<'source, Token<'source>>) -> Platter { + let slice = &lex.slice(); + if slice.starts_with("0x") { + Platter::from_str_radix(slice.trim_start_matches("0x"), 16).unwrap() + } else { + slice.parse().unwrap() + } +} + +fn lex_string_literal<'source>(lexer: &mut Lexer<'source, Token<'source>>) -> &'source str { + let remainder = lexer.remainder(); + + let mut in_escape = false; + let mut complete = false; + let mut final_index = 0; + for (index, character) in remainder.char_indices() { + if complete { + lexer.bump(index); + return &remainder[..final_index]; + } + + if character == '\\' { + in_escape = true; + continue; + } + + if character == '"' && in_escape { + continue; + } + + if character == '"' && !in_escape { + complete = true; + final_index = index; + continue; + } + + in_escape = false; + } + + lexer.bump(remainder.len()); + remainder +} + +fn lex_register<'source>(lex: &mut Lexer<'source, Token<'source>>) -> Register { + let slice = lex.slice(); + let index = slice[1..] + .parse() + .expect("regex for register tokens should make the infallible"); + + Register::from_u8(index) +} + +fn lex_comment<'source>(lex: &mut Lexer<'source, Token<'source>>) -> &'source str { + let remainder = lex.remainder(); + for (position, c) in remainder.char_indices() { + if c == '\n' { + lex.bump(position); + return &remainder[..position]; + } + } + + lex.bump(remainder.len()); + remainder +} diff --git a/src/asm/parse.rs b/src/asm/parse.rs new file mode 100644 index 0000000..c0a2de3 --- /dev/null +++ b/src/asm/parse.rs @@ -0,0 +1,696 @@ +use super::Token; +use crate::{Platter, Register}; +use logos::{Logos, Source}; +use std::{borrow::Cow, collections::HashMap, iter::Peekable, ops::Range}; + +pub fn parse(_unit: impl std::fmt::Display, source: &str) -> Result { + Parser::new(source).parse() +} + +#[derive(Debug)] +pub enum NodeType<'s> { + Pragma(Pragma<'s>), + Instruction(Instruction<'s>), + Comment(#[allow(unused)] &'s str), +} + +impl NodeType<'_> { + pub fn size(&self) -> usize { + match self { + Self::Pragma(pragma) => match &pragma.payload { + PragmaType::U32 { .. } => 1, + PragmaType::WideString { value } => value.len() + 1, + }, + // Instructions are always one platter. + Self::Instruction(_) => 1, + Self::Comment(_) => 0, + } + } +} + +#[derive(Debug)] +pub struct Node<'s> { + pub labels: Vec<&'s str>, + pub entity: NodeType<'s>, + #[allow(unused)] + pub span: Range, +} + +impl Node<'_> { + /// Compute encoded size of the node in platters. + #[inline] + pub fn size(&self) -> usize { + self.entity.size() + } +} + +#[derive(Debug)] +pub struct ParsedProgram<'s> { + #[allow(unused)] + pub source: &'s str, + nodes: Vec>, +} + +impl<'s> ParsedProgram<'s> { + pub fn nodes(&self) -> &[Node<'s>] { + &self.nodes + } +} + +#[derive(Debug, Default)] +pub struct Parser<'s> { + source: &'s str, + labels: HashMap<&'s str, Range>, + active_labels: Vec<&'s str>, +} + +impl<'s> Parser<'s> { + fn new(source: &'s str) -> Self { + Self { + source, + ..Default::default() + } + } + + fn parse(mut self) -> Result, Error> { + let mut lexer = Token::lexer(self.source); + let mut spanned = vec![]; + while let Some(res) = lexer.next() { + match res { + Ok(token) => { + spanned.push((token, lexer.span())); + } + Err(error) => Err(Error::new(format!("lex: {error:?}"), &lexer.span()))?, + } + } + + let mut nodes = vec![]; + let mut tokens = spanned.into_iter().peekable(); + while let Some((token, span)) = tokens.peek() { + let node = match token { + Token::Label(_) => { + self.consume_label(&mut tokens)?; + continue; + } + Token::Pragma(_) => self.consume_pragma(&mut tokens)?, + Token::Ident(_) => self.consume_instruction(&mut tokens)?, + Token::Comment(comment) => { + let node = Node { + labels: vec![], + entity: NodeType::Comment(comment), + span: span.clone(), + }; + tokens.next(); + node + } + Token::Newline => { + tokens.next(); + continue; + } + _ => Err(Error::new(format!("unexpected token {token:?}"), span))?, + }; + + nodes.push(node); + } + + Ok(ParsedProgram { + source: self.source, + nodes, + }) + } + + /// Consumes a label from the token stream. + fn consume_label(&mut self, tokens: &mut I) -> Result<(), Error> + where + I: Iterator, Range)>, + { + let Some((Token::Label(label_ident), span)) = tokens.next() else { + unreachable!("consume_label called on non-label token"); + }; + + // Add the label to the set of observed labels. + let label_span = self + .labels + .entry(label_ident) + .or_insert_with(|| span.clone()); + + // If the span of the current token is not equal to + // `label_span`, then we have already seen label with the + // same identifier. + if label_span != &span { + return Err(Error::new( + format!("duplicate label '{label_ident}', original label span: {label_span:?}"), + &span, + )); + } + + self.active_labels.push(label_ident); + Ok(()) + } + + fn consume_pragma(&mut self, tokens: &mut Peekable) -> Result, Error> + where + I: Iterator, Range)>, + { + assert!( + matches!(tokens.peek(), Some((Token::Pragma(_), _))), + "consume_pragma called on non-pragma token" + ); + + let labels = std::mem::take(&mut self.active_labels); + let (pragma, span) = Pragma::consume(tokens)?; + + Ok(Node { + labels, + entity: NodeType::Pragma(pragma), + span, + }) + } + + fn consume_instruction(&mut self, tokens: &mut Peekable) -> Result, Error> + where + I: Iterator, Range)>, + { + assert!( + matches!(tokens.peek(), Some((Token::Ident(_), _))), + "consume_instruction called on non-ident token" + ); + + let labels = std::mem::take(&mut self.active_labels); + let (instr, span) = Instruction::consume(tokens)?; + Ok(Node { + labels, + entity: NodeType::Instruction(instr), + span, + }) + } +} + +/// An error encountered during parsing. +#[derive(Debug)] +#[allow(unused)] +pub struct Error(pub String, pub Range); + +impl Error { + fn new(message: impl ToString, span: &Range) -> Self { + Self(message.to_string(), span.clone()) + } + + fn eof() -> Self { + Self("unexpected eof".into(), 0..0) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for Error {} + +#[derive(Debug, Default)] +pub struct Location { + pub block: Register, + pub offset: Register, +} + +impl Location { + pub fn consume<'s, I>(tokens: &mut Peekable) -> Result<(Self, Range), Error> + where + I: Iterator, Range)>, + { + // Require a '[' token. + let start_span = match tokens.next() { + Some((Token::AddressOpen, span)) => span, + Some((_, span)) => Err(Error::new("expected an address opening bracket", &span))?, + _ => Err(Error::eof())?, + }; + + let (block, _) = consume_register(tokens)?; + let (offset, _) = consume_register(tokens)?; + + // Require a ']' token. + let end_span = match tokens.next() { + Some((Token::AddressClose, span)) => span, + Some((_, span)) => Err(Error::new("expected an address closing bracket", &span))?, + _ => Err(Error::eof())?, + }; + + Ok((Self { block, offset }, merge_spans(&start_span, &end_span))) + } +} + +#[derive(Debug)] +pub struct Expr<'s> { + pub label: &'s str, +} + +#[derive(Debug)] +pub enum PragmaType<'s> { + U32 { value: u32 }, + WideString { value: Cow<'s, str> }, +} + +#[derive(Debug)] +pub struct Pragma<'s> { + #[allow(unused)] + relocatable: bool, + pub payload: PragmaType<'s>, +} + +impl<'s> Pragma<'s> { + pub fn consume(tokens: &mut Peekable) -> Result<(Self, Range), Error> + where + I: Iterator, Range)>, + { + let relocatable = true; + let token = tokens.next().ok_or(Error::eof())?; + match token { + (Token::Pragma("u32"), start_span) => { + let (value, end_span) = consume_number(tokens)?; + Ok(( + Self { + relocatable, + payload: PragmaType::U32 { value }, + }, + merge_spans(&start_span, &end_span), + )) + } + (Token::Pragma("wstr"), start_span) => { + let (value, end_span) = consume_string(tokens)?; + Ok(( + Self { + relocatable, + payload: PragmaType::WideString { value }, + }, + merge_spans(&start_span, &end_span), + )) + } + (Token::Pragma(command), span) => Err(Error::new( + format!("unknown pragma command {command}"), + &span, + ))?, + (_, span) => Err(Error::new("unexpected token", &span))?, + } + } +} + +#[derive(Debug)] +pub enum Instruction<'s> { + /// Operation #0. + ConditionalMove { + destination: Register, + source: Register, + condition: Register, + }, + /// Operation #13. + Address { + destination: Register, + reference: Expr<'s>, + }, + /// Operation #13. + LiteralMove { + destination: Register, + literal: Platter, + }, + Load { + destination: Register, + address: Location, + }, + Store { + source: Register, + address: Location, + }, + Add { + destination: Register, + a: Register, + b: Register, + }, + AddAssign { + destination: Register, + a: Register, + }, + AddSelf { + destination: Register, + }, + Mul { + destination: Register, + a: Register, + b: Register, + }, + MulAssign { + destination: Register, + a: Register, + }, + MulSelf { + destination: Register, + }, + Div { + destination: Register, + a: Register, + b: Register, + }, + DivAssign { + destination: Register, + a: Register, + }, + DivSelf { + destination: Register, + }, + Nand { + destination: Register, + a: Register, + b: Register, + }, + NandAssign { + destination: Register, + a: Register, + }, + NandSelf { + destination: Register, + }, + Halt, + Alloc { + destination: Register, + length: Register, + }, + Free { + block: Register, + }, + Out { + source: Register, + }, + In { + destination: Register, + }, + Jmp { + location: Location, + }, +} + +impl<'s> Instruction<'s> { + pub fn consume(tokens: &mut Peekable) -> Result<(Self, Range), Error> + where + I: Iterator, Range)>, + { + let ident = tokens.next().unwrap(); + match ident { + (Token::Ident("halt"), span) => Ok((Self::Halt, span)), + (Token::Ident("adr"), start_span) => { + let (destination, _) = consume_register(tokens)?; + let (identifier, end_span) = consume_ident(tokens)?; + Ok(( + Self::Address { + destination, + reference: Expr { label: identifier }, + }, + merge_spans(&start_span, &end_span), + )) + } + (Token::Ident("mov"), start_span) => { + let (destination, _) = consume_register(tokens)?; + if peek_register(tokens)?.is_some() { + let (source, _) = consume_register(tokens)?; + let (condition, end_span) = consume_register(tokens)?; + Ok(( + Self::ConditionalMove { + destination, + source, + condition, + }, + merge_spans(&start_span, &end_span), + )) + } else { + let (literal, end_span) = consume_number(tokens)?; + Ok(( + Self::LiteralMove { + destination, + literal, + }, + merge_spans(&start_span, &end_span), + )) + } + } + (Token::Ident("ldr"), start_span) => { + let (destination, _) = consume_register(tokens)?; + let (address, end_span) = Location::consume(tokens)?; + Ok(( + Self::Load { + destination, + address, + }, + merge_spans(&start_span, &end_span), + )) + } + (Token::Ident("str"), start_span) => { + let (source, _) = consume_register(tokens)?; + let (address, end_span) = Location::consume(tokens)?; + Ok(( + Self::Store { source, address }, + merge_spans(&start_span, &end_span), + )) + } + (Token::Ident("out"), start_span) => { + let (source, end_span) = consume_register(tokens)?; + Ok((Self::Out { source }, merge_spans(&start_span, &end_span))) + } + (Token::Ident("in"), start_span) => { + let (destination, end_span) = consume_register(tokens)?; + Ok(( + Self::In { destination }, + merge_spans(&start_span, &end_span), + )) + } + (Token::Ident("alloc"), start_span) => { + let (destination, _) = consume_register(tokens)?; + let (length, end_span) = consume_register(tokens)?; + Ok(( + Self::Alloc { + length, + destination, + }, + merge_spans(&start_span, &end_span), + )) + } + (Token::Ident("free"), start_span) => { + let (block, end_span) = consume_register(tokens)?; + Ok((Self::Free { block }, merge_spans(&start_span, &end_span))) + } + (Token::Ident("jmp"), start_span) => { + let (location, end_span) = Location::consume(tokens)?; + Ok((Self::Jmp { location }, merge_spans(&start_span, &end_span))) + } + (Token::Ident("add"), start_span) => { + let (destination, mid_span) = consume_register(tokens)?; + let a = peek_register(tokens)?.and_then(|_| consume_register(tokens).ok()); + let b = peek_register(tokens)?.and_then(|_| consume_register(tokens).ok()); + match (a, b) { + (Some((a, _)), Some((b, end_span))) => Ok(( + Self::Add { destination, a, b }, + merge_spans(&start_span, &end_span), + )), + (Some((a, end_span)), None) => Ok(( + Self::AddAssign { destination, a }, + merge_spans(&start_span, &end_span), + )), + (None, None) => Ok(( + Self::AddSelf { destination }, + merge_spans(&start_span, &mid_span), + )), + _ => unreachable!(), + } + } + (Token::Ident("mul"), start_span) => { + let (destination, mid_span) = consume_register(tokens)?; + let a = peek_register(tokens)?.and_then(|_| consume_register(tokens).ok()); + let b = peek_register(tokens)?.and_then(|_| consume_register(tokens).ok()); + match (a, b) { + (Some((a, _)), Some((b, end_span))) => Ok(( + Self::Mul { destination, a, b }, + merge_spans(&start_span, &end_span), + )), + (Some((a, end_span)), None) => Ok(( + Self::MulAssign { destination, a }, + merge_spans(&start_span, &end_span), + )), + (None, None) => Ok(( + Self::MulSelf { destination }, + merge_spans(&start_span, &mid_span), + )), + _ => unreachable!(), + } + } + (Token::Ident("div"), start_span) => { + let (destination, mid_span) = consume_register(tokens)?; + let a = peek_register(tokens)?.and_then(|_| consume_register(tokens).ok()); + let b = peek_register(tokens)?.and_then(|_| consume_register(tokens).ok()); + match (a, b) { + (Some((a, _)), Some((b, end_span))) => Ok(( + Self::Div { destination, a, b }, + merge_spans(&start_span, &end_span), + )), + (Some((a, end_span)), None) => Ok(( + Self::DivAssign { destination, a }, + merge_spans(&start_span, &end_span), + )), + (None, None) => Ok(( + Self::DivSelf { destination }, + merge_spans(&start_span, &mid_span), + )), + _ => unreachable!(), + } + } + (Token::Ident("nand"), start_span) => { + let (destination, mid_span) = consume_register(tokens)?; + let a = peek_register(tokens)?.and_then(|_| consume_register(tokens).ok()); + let b = peek_register(tokens)?.and_then(|_| consume_register(tokens).ok()); + match (a, b) { + (Some((a, _)), Some((b, end_span))) => Ok(( + Self::Nand { destination, a, b }, + merge_spans(&start_span, &end_span), + )), + (Some((a, end_span)), None) => Ok(( + Self::NandAssign { destination, a }, + merge_spans(&start_span, &end_span), + )), + (None, None) => Ok(( + Self::NandSelf { destination }, + merge_spans(&start_span, &mid_span), + )), + _ => unreachable!(), + } + } + (_, span) => Err(Error::new("unrecognised instruction", &span))?, + } + } +} + +impl std::fmt::Display for Instruction<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ConditionalMove { + destination, + source, + condition, + } => write!(f, "mov {destination}, {source}, {condition}"), + Self::Load { + destination, + address, + } => write!( + f, + "ldr {destination}, [{}, {}]", + address.block, address.offset + ), + Self::Store { source, address } => { + write!(f, "str {source}, [{}, {}]", address.block, address.offset) + } + Self::Add { destination, a, b } => write!(f, "add {destination}, {a}, {b}"), + Self::AddAssign { destination, a } => write!(f, "add {destination}, {a}"), + Self::AddSelf { destination } => write!(f, "add {destination}"), + Self::Mul { destination, a, b } => write!(f, "mul {destination}, {a}, {b}"), + Self::MulAssign { destination, a } => write!(f, "mul {destination}, {a}"), + Self::MulSelf { destination } => write!(f, "mul {destination}"), + Self::Div { destination, a, b } => write!(f, "div {destination}, {a}, {b}"), + Self::DivAssign { destination, a } => write!(f, "div {destination}, {a}"), + Self::DivSelf { destination } => write!(f, "div {destination}"), + Self::Nand { destination, a, b } => write!(f, "nand {destination}, {a}, {b}"), + Self::NandAssign { destination, a } => write!(f, "nand {destination}, {a}"), + Self::NandSelf { destination } => write!(f, "nand {destination}"), + Self::Halt => write!(f, "halt"), + Self::Out { source } => write!(f, "out {source}"), + Self::In { destination } => write!(f, "in {destination}"), + Self::Alloc { + length, + destination, + } => write!(f, "alloc {destination}, {length}"), + Self::Free { block } => { + write!(f, "free {block}") + } + Self::Jmp { location } => write!(f, "jmp [{}, {}]", location.block, location.offset), + Self::LiteralMove { + destination, + literal, + } => write!(f, "mov {destination}, {literal}"), + Self::Address { + destination, + reference, + } => write!(f, "adr {destination}, {}", reference.label), + } + } +} + +/// Peeks at the next token and returns it iff it is a Register. +fn peek_register<'s, I>(tokens: &mut Peekable) -> Result, Error> +where + I: Iterator, Range)>, +{ + match tokens.peek() { + Some((Token::Register(r), _)) => Ok(Some(*r)), + Some(_) => Ok(None), + None => Err(Error::new("unexpected eof", &(0..0))), + } +} + +fn consume_register<'s, I>(tokens: &mut I) -> Result<(Register, Range), Error> +where + I: Iterator, Range)>, +{ + match tokens.next() { + Some((Token::Register(r), span)) => Ok((r, span)), + Some((token, span)) => Err(Error::new( + format!("expected a register, found: {token:?}"), + &span, + )), + None => Err(Error::eof()), + } +} + +fn consume_ident<'s, I>(tokens: &mut I) -> Result<(&'s str, Range), Error> +where + I: Iterator, Range)>, +{ + match tokens.next() { + Some((Token::Ident(ident), span)) => Ok((ident, span)), + Some((token, span)) => Err(Error::new( + format!("expected an identifier, found: {token:?}"), + &span, + )), + None => Err(Error::eof()), + } +} + +fn consume_number<'s, I>(tokens: &mut I) -> Result<(Platter, Range), Error> +where + I: Iterator, Range)>, +{ + match tokens.next() { + Some((Token::Number(value), span)) => Ok((value, span)), + Some((token, span)) => Err(Error::new( + format!("expected a number literal, found: {token:?}"), + &span, + )), + None => Err(Error::eof()), + } +} + +fn consume_string<'s, I>(tokens: &mut I) -> Result<(Cow<'s, str>, Range), Error> +where + I: Iterator, Range)>, +{ + match tokens.next() { + Some((Token::String(value), span)) => { + let unescaped = crate::str::unescape_str(value).map_err(|_| Error::eof())?; + Ok((unescaped, span)) + } + Some((token, span)) => Err(Error::new( + format!("expected a number literal, found: {token:?}"), + &span, + )), + None => Err(Error::eof()), + } +} + +fn merge_spans(start: &Range, end: &Range) -> Range { + start.start..end.end +} diff --git a/src/bin/uasm.rs b/src/bin/uasm.rs new file mode 100644 index 0000000..2976cb9 --- /dev/null +++ b/src/bin/uasm.rs @@ -0,0 +1,51 @@ +use std::path::{Path, PathBuf}; +use um::Platter; + +fn main() { + let mut output = PathBuf::from("./a.um"); + + let mut program = Vec::new(); + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "-o" | "--out" => { + output = PathBuf::from(args.next().expect("expected output path")); + } + _ => { + let path = Path::new(&arg); + program.extend_from_slice(&match load_program(path) { + Ok(p) => p, + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + }); + } + } + } + + // Convert the program to bytes. + let bytes: Vec<_> = program + .into_iter() + .flat_map(|word| word.to_be_bytes()) + .collect(); + + std::fs::write(&output, bytes).unwrap(); +} + +fn load_program(path: &Path) -> std::io::Result> { + match path.extension().map(|ext| ext.as_encoded_bytes()) { + Some(b"uasm") | Some(b"asm") => { + let source = std::fs::read_to_string(path)?; + let program = um::asm::assemble(&source); + Ok(program) + } + _ => { + let program = std::fs::read(path)?; + Ok(program + .chunks_exact(std::mem::size_of::()) + .map(|pl| Platter::from_be_bytes(pl.try_into().unwrap())) + .collect()) + } + } +} diff --git a/src/bin/um.rs b/src/bin/um.rs new file mode 100644 index 0000000..d4dc9f9 --- /dev/null +++ b/src/bin/um.rs @@ -0,0 +1,49 @@ +use std::{path::Path, time::Instant}; +use um::{Platter, Um}; + +fn main() { + let mut program = Vec::new(); + let mut time = false; + + for arg in std::env::args().skip(1) { + if arg == "--time" { + time = true; + continue; + } + + let path = Path::new(&arg); + program.extend_from_slice(&match load_program(path) { + Ok(p) => p, + Err(error) => { + eprintln!("{error}"); + std::process::exit(1); + } + }); + } + + let start = Instant::now(); + Um::new(program) + .stdout(&mut std::io::stdout()) + .stdin(&mut std::io::stdin()) + .run(); + + if time { + eprintln!("{:?}", start.elapsed()); + } +} + +fn load_program(path: &Path) -> std::io::Result> { + match path.extension().map(|ext| ext.as_encoded_bytes()) { + Some(b"uasm") | Some(b"asm") => { + let source = std::fs::read_to_string(path)?; + Ok(um::asm::assemble(&source)) + } + _ => { + let program = std::fs::read(path)?; + Ok(program + .chunks_exact(std::mem::size_of::()) + .map(|pl| Platter::from_be_bytes(pl.try_into().unwrap())) + .collect()) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 71eb796..1392ff7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,52 +1,147 @@ +use smallvec::SmallVec; +use std::{ + io::{Read, Write}, + ops, +}; + +pub mod asm; +pub mod str; + pub type Platter = u32; -pub type Parameter = u8; + +/// A reference to a register of the UM-32. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum Register { + #[default] + R0, + R1, + R2, + R3, + R4, + R5, + R6, + R7, +} + +impl std::fmt::Display for Register { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "r{}", *self as u8) + } +} + +impl Register { + /// Encodes the register as the 'a' parameter of an encoded + /// instruction (bits 6..=8). + fn encode_a(self) -> Platter { + ((self as Platter) & 0x7) << 6 + } + + /// Encodes the register as the 'b' parameter of an encoded + /// instruction (bits 3..=5). + fn encode_b(self) -> Platter { + ((self as Platter) & 0x7) << 3 + } + + /// Encodes the register as the 'c' parameter of an encoded + /// instruction (bits 0..=2). + fn encode_c(self) -> Platter { + (self as Platter) & 0x7 + } + + /// Encodes the register as the 'a' parameter of an `Orthography` + /// operation. + /// + /// This is *only* valid for `Orthography` operations. + fn encode_a_ortho(self) -> Platter { + ((self as Platter) & 0x7) << 25 + } + + fn from_u8(index: u8) -> Self { + match index { + 0 => Register::R0, + 1 => Register::R1, + 2 => Register::R2, + 3 => Register::R3, + 4 => Register::R4, + 5 => Register::R5, + 6 => Register::R6, + 7 => Register::R7, + _ => unreachable!(), + } + } +} + +/// A set of registers. +#[derive(Debug, Default)] +struct Page([Platter; 8]); + +impl ops::Index for Page { + type Output = Platter; + #[inline(always)] + fn index(&self, index: Register) -> &Self::Output { + &self.0[index as usize] + } +} + +impl ops::IndexMut for Page { + #[inline(always)] + fn index_mut(&mut self, index: Register) -> &mut Self::Output { + &mut self.0[index as usize] + } +} + +impl From<[Platter; 8]> for Page { + fn from(value: [Platter; 8]) -> Self { + Self(value) + } +} #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Operation { +enum Operation { /// Operator #0. Conditional Move. /// /// The register A receives the value in register B, /// unless the register C contains 0. ConditionalMove { - a: Parameter, - b: Parameter, - c: Parameter, + a: Register, + b: Register, + c: Register, }, /// Operator #1: Array Index. /// /// The register A receives the value stored at offset /// in register C in the array identified by B. ArrayIndex { - a: Parameter, - b: Parameter, - c: Parameter, + a: Register, + b: Register, + c: Register, }, /// Operator #2. Array Amendment. /// /// The array identified by A is amended at the offset /// in register B to store the value in register C. ArrayAmendment { - a: Parameter, - b: Parameter, - c: Parameter, + a: Register, + b: Register, + c: Register, }, /// Operator #3. Addition. /// /// The register A receives the value in register B plus /// the value in register C, modulo 2^32. Addition { - a: Parameter, - b: Parameter, - c: Parameter, + a: Register, + b: Register, + c: Register, }, /// Operator #4. Multiplication. /// /// The register A receives the value in register B times /// the value in register C, modulo 2^32. Multiplication { - a: Parameter, - b: Parameter, - c: Parameter, + a: Register, + b: Register, + c: Register, }, /// Operator #5. Division. /// @@ -54,9 +149,9 @@ pub enum Operation { /// divided by the value in register C, if any, where /// each quantity is treated as an unsigned 32 bit number. Division { - a: Parameter, - b: Parameter, - c: Parameter, + a: Register, + b: Register, + c: Register, }, /// Operator #6. Not-And. /// @@ -65,9 +160,9 @@ pub enum Operation { /// position. Otherwise the bit in register A receives /// the 0 bit. NotAnd { - a: Parameter, - b: Parameter, - c: Parameter, + a: Register, + b: Register, + c: Register, }, /// Operator #7. Halt. /// @@ -82,15 +177,15 @@ pub enum Operation { /// exclusively the 0 bit, and that identifies no other /// active allocated array, is placed in the B register. Allocation { - b: Parameter, - c: Parameter, + b: Register, + c: Register, }, /// Operator #9. Abandonment. /// /// The array identified by the register C is abandoned. /// Future allocations may then reuse that identifier. Abandonment { - c: Parameter, + c: Register, }, /// Operator #10. Output. /// @@ -98,7 +193,7 @@ pub enum Operation { /// immediately. Only values between and including 0 and 255 /// are allowed. Output { - c: Parameter, + c: Register, }, /// Operator #11. Input. /// @@ -109,7 +204,7 @@ pub enum Operation { /// register C is endowed with a uniform value pattern /// where every place is pregnant with the 1 bit. Input { - c: Parameter, + c: Register, }, /// Operator #12. Load Program. /// @@ -125,15 +220,15 @@ pub enum Operation { /// loading, and shall be handled with the utmost /// velocity. LoadProgram { - b: Parameter, - c: Parameter, + b: Register, + c: Register, }, /// Operator #13. Orthography. /// /// The value indicated is loaded into the register A /// forthwith. Orthography { - a: Parameter, + a: Register, value: u32, }, IllegalInstruction, @@ -142,10 +237,9 @@ pub enum Operation { impl From for Operation { #[inline] fn from(value: Platter) -> Self { - let a = ((value >> 6) & 0x07) as Parameter; - let b = ((value >> 3) & 0x07) as Parameter; - let c = (value & 0x07) as Parameter; - + let a = Register::from_u8(((value >> 6) & 0x07) as u8); + let b = Register::from_u8(((value >> 3) & 0x07) as u8); + let c = Register::from_u8((value & 0x07) as u8); match value & 0xf0000000 { 0x00000000 => Self::ConditionalMove { a, b, c }, 0x10000000 => Self::ArrayIndex { a, b, c }, @@ -161,7 +255,7 @@ impl From for Operation { 0xb0000000 => Self::Input { c }, 0xc0000000 => Self::LoadProgram { b, c }, 0xd0000000 => { - let a = ((value >> 25) & 0x07) as Parameter; + let a = Register::from_u8(((value >> 25) & 0x07) as u8); let value = value & 0x01ffffff; Self::Orthography { a, value } } @@ -170,9 +264,363 @@ impl From for Operation { } } -#[inline] -pub fn decode_ops(ops: &[Platter]) -> Vec { +fn decode_ops(ops: &[Platter]) -> Vec { ops.iter() .map(|&encoded| Operation::from(encoded)) .collect() } + +const SMALLVEC_SIZE: usize = 24; + +/// Lossless conversion to `usize`. +/// +/// This should only be implemented on types which can be losslessly +/// cast to a `usize`. +trait IntoIndex: Sized + Copy { + fn into_index(self) -> usize; +} + +macro_rules! impl_into_index { + ($t:ty) => { + impl IntoIndex for $t { + fn into_index(self) -> usize { + self as usize + } + } + }; +} + +#[cfg(target_pointer_width = "16")] +compile_error!("16 bit architectures are unsupported"); + +// usize *may* be 16 bits, so only implement if it is 32 or 64 bits. +#[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))] +impl_into_index!(Platter); + +#[derive(Default)] +pub struct Um<'a> { + pub program_counter: Platter, + registers: Page, + /// Program memory, modelled as a `Vec` of `SmallVec`. + /// + /// Memory allocations greater than `SMALLVEC_SIZE` will incur a memory + /// indirection penalty for every memory access within that block. + memory: Vec>, + free_blocks: Vec, + /// Partially decoded operations cache. + ops: Vec, + stdin: Option<&'a mut dyn Read>, + stdout: Option<&'a mut dyn Write>, +} + +impl<'a> Um<'a> { + /// Initialise a Universal Machine with the specified program scroll. + pub fn new(program: Vec) -> Self { + let ops = decode_ops(&program); + Self { + memory: vec![program.into()], + ops, + ..Default::default() + } + } + + /// Initialise a Universal Machine with a program read from a legacy + /// unsigned 8-bit character scroll. + pub fn from_bytes(program: impl AsRef<[u8]> + 'a) -> Self { + fn inner<'a>(bytes: &[u8]) -> Um<'a> { + let mut program = + Vec::with_capacity(bytes.len().div_ceil(std::mem::size_of::())); + + // Split the program into platters. + let mut chunks = bytes.chunks_exact(std::mem::size_of::()); + for word in &mut chunks { + program.push(Platter::from_be_bytes(unsafe { + // SAFETY: The `chunks_exact` iterator will *always* emit + // a slice of the correct length. + word.try_into().unwrap_unchecked() + })); + } + + if !chunks.remainder().is_empty() { + eprintln!( + "WARNING: program may be corrupt; {} bytes remain after platter conversion.", + chunks.remainder().len() + ); + } + + Um::new(program) + } + + inner(program.as_ref()) + } + + /// Sets the output for the universal machine. + pub fn stdout(mut self, stdout: &'a mut T) -> Self { + self.stdout.replace(stdout); + self + } + + /// Sets the input for the universal machine. + pub fn stdin(mut self, stdin: &'a mut T) -> Self { + self.stdin.replace(stdin); + self + } + + /// Begins the spin-cycle of the universal machine. + #[inline(never)] + pub fn run(mut self) -> Self { + loop { + // println!( + // "{:?}, pc: {:08x}, r: {:08x?}", + // self.ops[self.program_counter as usize], self.program_counter, self.registers + // ); + match self.ops[self.program_counter as usize] { + Operation::ConditionalMove { a, b, c } => self.conditional_move(a, b, c), + Operation::ArrayIndex { a, b, c } => self.array_index(a, b, c), + Operation::ArrayAmendment { a, b, c } => self.array_amendment(a, b, c), + Operation::Addition { a, b, c } => self.addition(a, b, c), + Operation::Multiplication { a, b, c } => self.multiplication(a, b, c), + Operation::Division { a, b, c } => self.division(a, b, c), + Operation::NotAnd { a, b, c } => self.not_and(a, b, c), + Operation::Halt => break, + Operation::Allocation { b, c } => self.allocation(b, c), + Operation::Abandonment { c } => self.abandonment(c), + Operation::Output { c } => self.output(c), + Operation::Input { c } => self.input(c), + Operation::LoadProgram { b, c } => { + self.load_program(b, c); + continue; + } + Operation::Orthography { a, value } => self.orthography(a, value), + Operation::IllegalInstruction => self.illegal_instruction(), + } + self.program_counter += 1; + } + + self + } + + // Un-commenting step() slows down the sandmark benchmark by ~3-5 seconds, even + // though it has *no* interaction with the code path in Um::run(). + // + // /// Steps one instruction. + // #[inline(never)] + // pub fn step(&mut self) -> bool { + // match self.ops[self.program_counter as usize] { + // Operation::ConditionalMove { a, b, c } => self.conditional_move(a, b, c), + // Operation::ArrayIndex { a, b, c } => self.array_index(a, b, c), + // Operation::ArrayAmendment { a, b, c } => self.array_amendment(a, b, c), + // Operation::Addition { a, b, c } => self.addition(a, b, c), + // Operation::Multiplication { a, b, c } => self.multiplication(a, b, c), + // Operation::Division { a, b, c } => self.division(a, b, c), + // Operation::NotAnd { a, b, c } => self.not_and(a, b, c), + // Operation::Halt => return false, + // Operation::Allocation { b, c } => self.allocation(b, c), + // Operation::Abandonment { c } => self.abandonment(c), + // Operation::Output { c } => self.output(c), + // Operation::Input { c } => self.input(c), + // Operation::LoadProgram { b, c } => { + // self.load_program(b, c); + // return true; + // } + // Operation::Orthography { a, value } => self.orthography(a, value), + // Operation::IllegalInstruction => self.illegal_instruction(), + // } + // self.program_counter += 1; + // true + // } + + /// Loads the value from the specified register. + fn load_register(&self, register: Register) -> Platter { + self.registers[register] + } + + /// Saves a value to the specified register. + fn save_register(&mut self, register: Register, value: Platter) { + self.registers[register] = value; + } + + fn conditional_move(&mut self, a: Register, b: Register, c: Register) { + if self.load_register(c) != 0 { + self.save_register(a, self.load_register(b)); + } + } + + fn array_index(&mut self, a: Register, b: Register, c: Register) { + let block = self.load_register(b); + let offset = self.load_register(c); + self.save_register(a, self.load_memory(block, offset)); + } + + fn array_amendment(&mut self, a: Register, b: Register, c: Register) { + let block = self.load_register(a); + let offset = self.load_register(b); + let value = self.load_register(c); + self.store_memory(block, offset, value); + } + + fn addition(&mut self, a: Register, b: Register, c: Register) { + self.save_register(a, self.load_register(b).wrapping_add(self.load_register(c))); + } + + fn multiplication(&mut self, a: Register, b: Register, c: Register) { + self.save_register(a, self.load_register(b).wrapping_mul(self.load_register(c))); + } + + fn division(&mut self, a: Register, b: Register, c: Register) { + self.save_register(a, self.load_register(b).wrapping_div(self.load_register(c))); + } + + fn not_and(&mut self, a: Register, b: Register, c: Register) { + self.save_register(a, !(self.load_register(b) & self.load_register(c))); + } + + fn allocation(&mut self, b: Register, c: Register) { + let length = self.load_register(c); + let index = self.allocate_memory(length); + self.save_register(b, index); + } + + fn abandonment(&mut self, c: Register) { + let block = self.load_register(c); + self.free_memory(block); + } + + fn output(&mut self, c: Register) { + let value = self.load_register(c); + if let Some(stdout) = self.stdout.as_mut() { + let buffer = [(value & 0xff) as u8]; + stdout.write_all(&buffer).unwrap(); + } + } + + fn input(&mut self, c: Register) { + if let Some(stdin) = self.stdin.as_mut() { + let mut buffer = vec![0]; + match stdin.read_exact(&mut buffer) { + Ok(()) => self.save_register(c, buffer[0] as u32), + Err(_) => self.save_register(c, Platter::MAX), + } + } else { + self.save_register(c, Platter::MAX); + } + } + + fn load_program(&mut self, b: Register, c: Register) { + let block = self.load_register(b); + + // Source array is always copied to array[0], but there + // is no point copying array[0] to array[0]. + if block != 0 { + let duplicated = self.duplicate_memory(block); + let ops = decode_ops(duplicated); + self.ops = ops; + } + + self.program_counter = self.load_register(c); + } + + fn orthography(&mut self, a: Register, value: Platter) { + self.save_register(a, value); + } + + #[cold] + #[inline(never)] + fn illegal_instruction(&self) -> ! { + panic!( + "illegal instruction: {:08x}, pc: {:08x}, r: {:08x?}", + self.memory[0][self.program_counter.into_index()], + self.program_counter, + self.registers + ) + } + + fn load_memory(&self, block: Platter, offset: Platter) -> Platter { + let block = block.into_index(); + let offset = offset.into_index(); + assert!(block < self.memory.len() && offset < self.memory[block].len()); + self.memory[block][offset] + } + + fn store_memory(&mut self, block: Platter, offset: Platter, value: Platter) { + let block = block.into_index(); + let offset = offset.into_index(); + assert!(block < self.memory.len() && offset < self.memory[block].len()); + self.memory[block][offset] = value + } + + /// Duplicates a block of memory. + /// + /// The block is copied to the first block of memory. + fn duplicate_memory(&mut self, block: Platter) -> &[Platter] { + let block = block.into_index(); + assert!(block < self.memory.len()); + self.memory[0] = self.memory[block].clone(); + &self.memory[0] + } + + /// Allocates a block of memory of the specified length. + fn allocate_memory(&mut self, length: Platter) -> Platter { + if let Some(index) = self.free_blocks.pop() { + self.memory[index.into_index()] = Self::new_block(length.into_index()); + index as Platter + } else { + self.memory.push(Self::new_block(length.into_index())); + (self.memory.len() - 1) as Platter + } + } + + /// Frees a block of memory. + fn free_memory(&mut self, block: Platter) { + assert!(block.into_index() < self.memory.len()); + self.free_blocks.push(block); + self.memory[block.into_index()] = Self::new_block(0); + } + + /// Creates a new block of memory. + /// + /// The block is initialised with `len` zeroes. + fn new_block(len: usize) -> SmallVec<[Platter; SMALLVEC_SIZE]> { + smallvec::smallvec![0; len] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic] + fn empty_program() { + Um::new(vec![]).run(); + } + + #[test] + fn just_halt() { + Um::new(vec![0x70000000]).run(); + } + + #[test] + fn hello_world() { + let program = asm::assemble(include_str!("../files/hello-world.asm")); + let mut buffer = Vec::new(); + Um::new(program).stdout(&mut buffer).run(); + assert_eq!(&buffer, b"Hello, world!\n"); + } + + #[test] + fn cat() { + let program = asm::assemble(include_str!("../files/cat.asm")); + let input = include_bytes!("lib.rs"); + + let mut reader = std::io::Cursor::new(input); + let mut buffer = Vec::new(); + + Um::new(program) + .stdin(&mut reader) + .stdout(&mut buffer) + .run(); + + assert_eq!(&buffer, &input); + } +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index d4b9b19..0000000 --- a/src/main.rs +++ /dev/null @@ -1,290 +0,0 @@ -use smallvec::SmallVec; -use std::io::{Read, Write}; -#[cfg(feature = "timing")] -use std::time::Instant; -use um::{Operation, Parameter, Platter}; - -const SMALLVEC_SIZE: usize = 24; - -fn main() { - let mut program = Vec::new(); - for arg in std::env::args().skip(1) { - let p = std::fs::read(arg).unwrap(); - program.extend_from_slice(&p); - } - - Um::from_bytes(program) - .stdout(&mut std::io::stdout()) - .stdin(&mut std::io::stdin()) - .run(); -} - -/// Lossless conversion to `usize`. -/// -/// This should only be implemented on types which can be losslessly -/// cast to a `usize`. -trait IntoIndex: Sized + Copy { - fn into_index(self) -> usize; -} - -macro_rules! impl_into_index { - ($t:ty) => { - impl IntoIndex for $t { - fn into_index(self) -> usize { - self as usize - } - } - }; -} - -#[cfg(target_pointer_width = "16")] -compile_error!("16 bit architectures are unsupported"); - -// usize *may* be 16 bits, so only implement if it is 32 or 64 bits. -#[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))] -impl_into_index!(Platter); -impl_into_index!(Parameter); - -#[derive(Default)] -pub struct Um<'a> { - program_counter: Platter, - registers: [Platter; 8], - memory: Vec>, - free_blocks: Vec, - ops: Vec, - stdin: Option<&'a mut dyn Read>, - stdout: Option<&'a mut dyn Write>, -} - -impl<'a> Um<'a> { - /// Initialise a Universal Machine with the specified program scroll. - pub fn new(program: Vec) -> Self { - let ops = um::decode_ops(&program); - Self { - memory: vec![program.into()], - ops, - ..Default::default() - } - } - - /// Initialise a Universal Machine with a program read from a legacy - /// unsigned 8-bit character scroll. - pub fn from_bytes(program: impl AsRef<[u8]>) -> Self { - let bytes = program.as_ref(); - let mut program = Vec::with_capacity(bytes.len().div_ceil(size_of::())); - - // Split the program into platters. - let mut chunks = bytes.chunks_exact(size_of::()); - for word in &mut chunks { - program.push(Platter::from_be_bytes([word[0], word[1], word[2], word[3]])); - } - - if !chunks.remainder().is_empty() { - eprintln!( - "WARNING: program may be corrupt; {} bytes remain after platter conversion.", - chunks.remainder().len() - ); - } - - Self::new(program) - } - - /// Sets the output for the universal machine. - pub fn stdout(mut self, stdout: &'a mut T) -> Self { - self.stdout.replace(stdout); - self - } - - /// Sets the input for the universal machine. - pub fn stdin(mut self, stdin: &'a mut T) -> Self { - self.stdin.replace(stdin); - self - } - - /// Begins the spin-cycle of the universal machine. - pub fn run(mut self) -> Self { - #[cfg(feature = "timing")] - let start = Instant::now(); - - while self.step() {} - - #[cfg(feature = "timing")] - eprintln!("um complete: {:?}", start.elapsed()); - - self - } - - /// Steps one instruction. - pub fn step(&mut self) -> bool { - match self.ops[self.program_counter as usize] { - Operation::ConditionalMove { a, b, c } => self.conditional_move(a, b, c), - Operation::ArrayIndex { a, b, c } => self.array_index(a, b, c), - Operation::ArrayAmendment { a, b, c } => self.array_amendment(a, b, c), - Operation::Addition { a, b, c } => self.addition(a, b, c), - Operation::Multiplication { a, b, c } => self.multiplication(a, b, c), - Operation::Division { a, b, c } => self.division(a, b, c), - Operation::NotAnd { a, b, c } => self.not_and(a, b, c), - Operation::Halt => return false, - Operation::Allocation { b, c } => self.allocation(b, c), - Operation::Abandonment { c } => self.abandonment(c), - Operation::Output { c } => self.output(c), - Operation::Input { c } => self.input(c), - Operation::LoadProgram { b, c } => { - self.load_program(b, c); - return true; - } - Operation::Orthography { a, value } => self.orthography(a, value), - Operation::IllegalInstruction => self.illegal_instruction(), - } - self.program_counter += 1; - true - } - - /// Loads the value from the specified register. - fn load_register(&self, index: Parameter) -> Platter { - assert!(index < 8, "register index out of bounds"); - self.registers[index.into_index()] - } - - /// Saves a value to the specified register. - fn save_register(&mut self, index: Parameter, value: Platter) { - assert!(index < 8, "register index out of bounds"); - self.registers[index.into_index()] = value; - } - - pub fn conditional_move(&mut self, a: Parameter, b: Parameter, c: Parameter) { - if self.load_register(c) != 0 { - self.save_register(a, self.load_register(b)); - } - } - - pub fn array_index(&mut self, a: Parameter, b: Parameter, c: Parameter) { - let block = self.load_register(b); - let offset = self.load_register(c); - self.save_register(a, self.load_memory(block, offset)); - } - - pub fn array_amendment(&mut self, a: Parameter, b: Parameter, c: Parameter) { - let block = self.load_register(a); - let offset = self.load_register(b); - let value = self.load_register(c); - self.store_memory(block, offset, value); - } - - pub fn addition(&mut self, a: Parameter, b: Parameter, c: Parameter) { - self.save_register(a, self.load_register(b).wrapping_add(self.load_register(c))); - } - - pub fn multiplication(&mut self, a: Parameter, b: Parameter, c: Parameter) { - self.save_register(a, self.load_register(b).wrapping_mul(self.load_register(c))); - } - - pub fn division(&mut self, a: Parameter, b: Parameter, c: Parameter) { - self.save_register(a, self.load_register(b).wrapping_div(self.load_register(c))); - } - - pub fn not_and(&mut self, a: Parameter, b: Parameter, c: Parameter) { - self.save_register(a, !(self.load_register(b) & self.load_register(c))); - } - - pub fn allocation(&mut self, b: Parameter, c: Parameter) { - let length = self.load_register(c); - let index = self.allocate_memory(length); - self.save_register(b, index); - } - - pub fn abandonment(&mut self, c: Parameter) { - let block = self.load_register(c); - self.free_memory(block); - } - - pub fn output(&mut self, c: Parameter) { - let value = self.load_register(c); - if let Some(stdout) = self.stdout.as_mut() { - let buffer = [(value & 0xff) as u8]; - stdout.write_all(&buffer).unwrap(); - } - } - - pub fn input(&mut self, c: Parameter) { - if let Some(stdin) = self.stdin.as_mut() { - let mut buffer = vec![0]; - match stdin.read_exact(&mut buffer) { - Ok(()) => self.save_register(c, buffer[0] as u32), - Err(_) => self.save_register(c, 0xff), - } - } else { - self.save_register(c, 0xff); - } - } - - pub fn load_program(&mut self, b: Parameter, c: Parameter) { - let block = self.load_register(b); - - // Source array is always copied to array[0], but there - // is no point copying array[0] to array[0]. - if block != 0 { - let duplicated = self.duplicate_memory(block); - let ops = um::decode_ops(duplicated); - self.ops = ops; - } - - self.program_counter = self.load_register(c); - } - - pub fn orthography(&mut self, a: Parameter, value: Platter) { - self.save_register(a, value); - } - - #[cold] - #[inline(never)] - fn illegal_instruction(&self) -> ! { - panic!( - "illegal instruction: {:08x}, pc: {:08x}, r: {:08x?}", - self.memory[0][self.program_counter.into_index()], - self.program_counter, - self.registers - ) - } - - fn load_memory(&self, block: Platter, offset: Platter) -> Platter { - let block = block.into_index(); - let offset = offset.into_index(); - assert!(block < self.memory.len() && offset < self.memory[block].len()); - self.memory[block][offset] - } - - fn store_memory(&mut self, block: Platter, offset: Platter, value: Platter) { - let block = block.into_index(); - let offset = offset.into_index(); - assert!(block < self.memory.len() && offset < self.memory[block].len()); - self.memory[block][offset] = value - } - - fn duplicate_memory(&mut self, block: Platter) -> &[Platter] { - let block = block.into_index(); - assert!(block < self.memory.len()); - self.memory[0] = self.memory[block].clone(); - &self.memory[0] - } - - fn allocate_memory(&mut self, length: Platter) -> Platter { - if let Some(index) = self.free_blocks.pop() { - self.memory[index.into_index()] = Self::new_block(length.into_index()); - index as Platter - } else { - self.memory.push(Self::new_block(length.into_index())); - (self.memory.len() - 1) as Platter - } - } - - fn free_memory(&mut self, block: Platter) { - assert!(block.into_index() < self.memory.len()); - self.free_blocks.push(block); - self.memory[block.into_index()] = Self::new_block(0); - } - - fn new_block(len: usize) -> SmallVec<[Platter; SMALLVEC_SIZE]> { - smallvec::smallvec![0; len] - } -} diff --git a/src/str.rs b/src/str.rs new file mode 100644 index 0000000..1026a40 --- /dev/null +++ b/src/str.rs @@ -0,0 +1,59 @@ +use std::{borrow::Cow, str::CharIndices}; + +#[derive(Debug)] +pub struct InvalidCharacterEscape(pub char, pub usize); + +pub fn unescape_str(s: &str) -> Result, InvalidCharacterEscape> { + fn escape_inner(c: &str, i: &mut CharIndices<'_>) -> Result { + let mut buffer = c.to_owned(); + let mut in_escape = true; + + for (index, c) in i { + match (in_escape, c) { + (false, '\\') => { + in_escape = true; + continue; + } + (false, c) => buffer.push(c), + (true, '\\') => buffer.push('\\'), + (true, 'n') => buffer.push('\n'), + (true, '0') => buffer.push('\0'), + (true, '"') => buffer.push('"'), + (true, '\'') => buffer.push('\''), + (true, 'r') => buffer.push('\r'), + (true, 't') => buffer.push('\t'), + (true, c) => Err(InvalidCharacterEscape(c, index))?, + } + + in_escape = false; + } + + Ok(buffer) + } + + let mut char_indicies = s.char_indices(); + for (index, c) in &mut char_indicies { + let scanned = &s[..index]; + if c == '\\' { + return Ok(Cow::Owned(escape_inner(scanned, &mut char_indicies)?)); + } + } + + Ok(Cow::Borrowed(s)) +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use super::unescape_str; + + #[test] + fn no_unescapes() { + let s = "Hello, this string should have no characters that need unescaping."; + let u = unescape_str(s).unwrap(); + + assert!(matches!(u, Cow::Borrowed(_))); + assert_eq!(s, u); + } +}