From 05c1be2e94041288c8930db2f2f1c841ddad8859 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Sun, 17 Apr 2022 23:35:48 -0500 Subject: [PATCH] Couple bugfixes, integration tests ported --- .idea/vulcan-emu.iml | 1 + Cargo.lock | 8 ++ Cargo.toml | 3 +- vasm/src/ast.rs | 1 + vasm/src/lib.rs | 12 +- vasm/src/vasm.pest | 4 +- vasm/src/vasm_assembler.rs | 103 ++++++++++++--- vasm/src/vasm_parser.rs | 14 ++ vasm/src/vasm_preprocessor.rs | 4 +- vcore/src/cpu.rs | 55 ++++---- vcore/src/lib.rs | 6 +- vcore/src/memory.rs | 20 +++ vcore/src/word.rs | 7 + vtest/Cargo.toml | 10 ++ vtest/src/integration_tests.rs | 234 +++++++++++++++++++++++++++++++++ vtest/src/lib.rs | 2 + 16 files changed, 436 insertions(+), 48 deletions(-) create mode 100644 vtest/Cargo.toml create mode 100644 vtest/src/integration_tests.rs create mode 100644 vtest/src/lib.rs diff --git a/.idea/vulcan-emu.iml b/.idea/vulcan-emu.iml index 00eab34..a4bc6b2 100644 --- a/.idea/vulcan-emu.iml +++ b/.idea/vulcan-emu.iml @@ -6,6 +6,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index 52523bf..9008f8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1227,6 +1227,14 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vtest" +version = "0.1.0" +dependencies = [ + "vasm", + "vcore", +] + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 931cfa9..14c1ea2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ resolver = "2" members = [ "vcore", "vemu", - "vasm" + "vasm", + "vtest" ] \ No newline at end of file diff --git a/vasm/src/ast.rs b/vasm/src/ast.rs index e385ee0..a1f8b21 100644 --- a/vasm/src/ast.rs +++ b/vasm/src/ast.rs @@ -73,6 +73,7 @@ pub enum VASMLine { Equ(Label, Node), LabelDef(Label), Macro(Macro), + Blank, } impl VASMLine { diff --git a/vasm/src/lib.rs b/vasm/src/lib.rs index fb2b86c..f26d477 100644 --- a/vasm/src/lib.rs +++ b/vasm/src/lib.rs @@ -2,9 +2,11 @@ extern crate pest; #[macro_use] extern crate pest_derive; -pub mod ast; +mod ast; pub mod parse_error; -pub mod vasm_assembler; -pub mod vasm_evaluator; -pub mod vasm_parser; -pub mod vasm_preprocessor; +mod vasm_assembler; +mod vasm_evaluator; +mod vasm_parser; +mod vasm_preprocessor; + +pub use vasm_assembler::assemble_snippet; diff --git a/vasm/src/vasm.pest b/vasm/src/vasm.pest index 71dc13b..ccd4804 100644 --- a/vasm/src/vasm.pest +++ b/vasm/src/vasm.pest @@ -72,5 +72,7 @@ include = { "include" ~ string } control = { "if" | "unless" | "while" | "until" | "do" | "else" | "end" } preprocessor = {"#" ~ (control | include) } +blank = { WHITESPACE? ~ COMMENT? } + // Finally the entire pattern for an assembly line: -line = { SOI ~ (preprocessor | db_word | db_string | org_directive | equ_directive | instruction | label_def) ~ EOI } \ No newline at end of file +line = { SOI ~ (preprocessor | db_word | db_string | org_directive | equ_directive | instruction | label_def | blank) ~ EOI } \ No newline at end of file diff --git a/vasm/src/vasm_assembler.rs b/vasm/src/vasm_assembler.rs index c1c9266..577d24a 100644 --- a/vasm/src/vasm_assembler.rs +++ b/vasm/src/vasm_assembler.rs @@ -1,7 +1,7 @@ use crate::ast::{Label, Scope, VASMLine}; use crate::parse_error::AssembleError; use crate::vasm_evaluator::eval; -use crate::vasm_parser::parse_vasm_line; +use crate::vasm_preprocessor::{Line, LineSource}; use std::collections::BTreeMap; /// This will solve all the .equ directives and return a symbol table of them. @@ -72,7 +72,7 @@ fn measure_instructions(lines: &[VASMLine], scope: &Scope) -> LineLengths { VASMLine::StringDb(_, value) => { lengths.insert(line_num, value.len()); } - VASMLine::Org(_, _) | VASMLine::Equ(_, _) | VASMLine::LabelDef(_) => { + VASMLine::Org(_, _) | VASMLine::Equ(_, _) | VASMLine::LabelDef(_) | VASMLine::Blank => { lengths.insert(line_num, 0); } VASMLine::Macro(_) => unreachable!(), @@ -156,14 +156,37 @@ fn code_bounds( Ok((start, end + end_length - 1)) } -pub fn assemble<'a, T: IntoIterator>(lines: T) -> Result, AssembleError> { - let mut parsed = Vec::new(); - for (line_idx, line) in lines.into_iter().enumerate() { - let line_num = line_idx + 1; - parsed.push(parse_vasm_line(line).map_err(|err| AssembleError::ParseError(line_num, err))?) - } +/// Turn an iterable of strs into an assembled binary. This supports macros, but not +/// the `#include` macro. The resulting Vec is only as large as it needs to be; if your +/// code starts with `.org 0x400` and is five bytes long then the Vec will be five +/// bytes long and index 0 will represent 0x400. +/// ``` +/// assert_eq!( +/// vasm::assemble_snippet(vec![".org 0x400", "push 5", "add 7"]), +/// Ok(vec![0x01, 0x05, 0x05, 0x07]) +/// ) +/// ``` +pub fn assemble_snippet<'a, T: IntoIterator>( + lines: T, +) -> Result, AssembleError> { + let mut line_results: Vec> = + LineSource::new("_snippet", lines, |_file| { + Err(AssembleError::IncludeError( + 0, + "Including is not supported in assembling snippets".to_string(), + )) + }) + .collect(); - generate_code(parsed) + if let Some(Err(error)) = line_results.iter().find(|line| line.is_err()) { + Err(error.clone()) + } else { + generate_code( + line_results + .iter_mut() + .map(|line| line.clone().unwrap().line), + ) + } } /// At this point all lines have addresses and lengths, and all arguments are reduced to @@ -181,7 +204,7 @@ pub fn assemble<'a, T: IntoIterator>(lines: T) -> Result /// /// Vulcan is a little-endian architecture: multi-byte arguments / .dbs will store the /// least-significant byte at the lowest address, then the more significant bytes following. -fn generate_code(lines: Vec) -> Result, AssembleError> { +fn generate_code>(lines: T) -> Result, AssembleError> { let lines: Vec = lines.into_iter().collect(); let scope = solve_equs(&lines)?; let line_lengths = measure_instructions(&lines, &scope); @@ -195,13 +218,13 @@ fn generate_code(lines: Vec) -> Result, AssembleError> { let line_num = line_idx + 1; match line { VASMLine::Instruction(_, opcode, None) => { - code[current_addr] = u8::from(*opcode) << 2; + code[current_addr - start] = u8::from(*opcode) << 2; current_addr += 1; } VASMLine::Instruction(_, opcode, Some(arg)) => { let arg = eval(arg, line_num, &line_addresses, &scope) .map_err(|err| AssembleError::ArgError(line_num, err))?; - let len = arg_length(arg); + let len = line_lengths[&line_num] - 1; let instr = (u8::from(*opcode) << 2) + len as u8; code[current_addr - start] = instr; let [low, mid, high, _] = arg.to_le_bytes(); @@ -229,7 +252,7 @@ fn generate_code(lines: Vec) -> Result, AssembleError> { VASMLine::Org(_, _) => { current_addr = line_addresses[&(line_num + 1)] as usize; } - VASMLine::Equ(_, _) | VASMLine::LabelDef(_) => {} + VASMLine::Equ(_, _) | VASMLine::LabelDef(_) | VASMLine::Blank => {} VASMLine::Macro(_) => unreachable!(), } } @@ -333,6 +356,13 @@ mod test { measure_instructions(&parse([".org 256", "blah:", "foo: .equ 7"]), &[].into()), [(1, 0), (2, 0), (3, 0)].into() ); + assert_eq!( + measure_instructions( + &parse([".org 0x400", "push 3", "call blah", "hlt", "blah: mul 2"]), + &[].into() + ), + [(1, 0), (2, 2), (3, 4), (4, 1), (5, 2)].into() + ); assert_eq!( measure_instructions( &parse(["add 2 + foo", "add 3 + blah", "jmpr @foo"]), @@ -369,6 +399,28 @@ mod test { [("blah".to_string(), 266), ("start".to_string(), 256)].into() )) ); + assert_eq!( + place_labels_pass([ + ".org 1024", + "nop 3", + "call blah", + "hlt", + "blah: mul 2", + "ret" + ]), + Ok(( + [ + (1, 1024), + (2, 1024), + (3, 1026), + (4, 1030), + (5, 1031), + (6, 1033) + ] + .into(), + [("blah".to_string(), 1031)].into() + )) + ); } #[test] @@ -421,14 +473,33 @@ mod test { } #[test] - fn test_assemble() { - assert_eq!(assemble(["add"]), Ok(vec![4])); + fn test_assemble_snippet() { + assert_eq!(assemble_snippet(["add"]), Ok(vec![4])); assert_eq!( - assemble(["apple"]), + assemble_snippet(["apple"]), Err(ParseError( 1, parse_error::ParseError::InvalidInstruction("apple".into()) )) ); + + assert_eq!( + assemble_snippet( + ".org 0x400 + nop 3 + call blah + hlt + blah: mul 2 + ret" + .lines() + ), + Ok(vec![ + 0x01, 0x03, // nop 3 + 0x67, 0x07, 0x04, 0x00, // call blah (arg defaults to 3 bytes long) + 0x74, // hlt + 0x0d, 0x02, // mul 2 + 0x68 + ]) + ); } } diff --git a/vasm/src/vasm_parser.rs b/vasm/src/vasm_parser.rs index 6ee1efe..5f6aedd 100644 --- a/vasm/src/vasm_parser.rs +++ b/vasm/src/vasm_parser.rs @@ -192,6 +192,7 @@ pub fn parse_vasm_line(line: &str) -> Result { } Rule::label_def => Ok(VASMLine::LabelDef(Label::from(line.only().as_str()))), Rule::preprocessor => Ok(VASMLine::Macro(parse_macro(line))), + Rule::blank => Ok(VASMLine::Blank), _ => unreachable!(), } } @@ -201,6 +202,7 @@ fn parse_macro(line: Pair) -> Macro { match pre.as_rule() { Rule::control => match pre.as_str() { "if" => Macro::If, + "unless" => Macro::Unless, "else" => Macro::Else, "while" => Macro::While, "until" => Macro::Until, @@ -420,9 +422,21 @@ mod test { #[test] fn test_parse_macros() { assert_eq!(parse_vasm_line("#if"), Ok(VASMLine::Macro(Macro::If))); + assert_eq!( + parse_vasm_line("#unless"), + Ok(VASMLine::Macro(Macro::Unless)) + ); assert_eq!( parse_vasm_line("#include \"blah\""), Ok(VASMLine::Macro(Macro::Include("blah".to_string()))) ) } + + #[test] + fn test_parse_blank() { + assert_eq!(parse_vasm_line(""), Ok(VASMLine::Blank)); + assert_eq!(parse_vasm_line(" "), Ok(VASMLine::Blank)); + assert_eq!(parse_vasm_line("; foo"), Ok(VASMLine::Blank)); + assert_eq!(parse_vasm_line(" ;foo"), Ok(VASMLine::Blank)); + } } diff --git a/vasm/src/vasm_preprocessor.rs b/vasm/src/vasm_preprocessor.rs index 7e66ddd..88a3131 100644 --- a/vasm/src/vasm_preprocessor.rs +++ b/vasm/src/vasm_preprocessor.rs @@ -4,9 +4,9 @@ use crate::vasm_parser::parse_vasm_line; use std::collections::VecDeque; use std::iter::Enumerate; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct Line { - line: VASMLine, + pub line: VASMLine, line_num: usize, file: String, } diff --git a/vcore/src/cpu.rs b/vcore/src/cpu.rs index e48efca..f39a202 100644 --- a/vcore/src/cpu.rs +++ b/vcore/src/cpu.rs @@ -32,6 +32,12 @@ impl PeekPoke for CPU { } } +impl Default for CPU { + fn default() -> Self { + Self::new(Memory::default()) + } +} + impl CPU { pub fn new(memory: Memory) -> Self { Self { @@ -270,6 +276,33 @@ impl CPU { pub fn running(&self) -> bool { !self.halted } + + pub fn run_to_halt(&mut self) { + self.start(); + while !self.halted && self.pc < 1100 { + self.tick() + } + } + + pub fn get_stack(&self) -> Vec { + let mut v = Vec::new(); + let mut curr = Word::from(256); + while curr < self.dp { + v.push(self.memory.peek24(curr)); + curr += 3 + } + v + } + + pub fn get_call(&self) -> Vec { + let mut v = Vec::new(); + let mut curr = Word::from(1024); + while curr > self.sp { + curr -= 3; + v.push(self.memory.peek24(curr)); + } + v + } } impl Opcode { @@ -305,28 +338,6 @@ mod tests { use super::*; use Opcode::*; - impl CPU { - fn get_stack(&self) -> Vec { - let mut v = Vec::new(); - let mut curr = Word::from(256); - while curr < self.dp { - v.push(self.memory.peek24(curr)); - curr += 3 - } - v - } - - fn get_call(&self) -> Vec { - let mut v = Vec::new(); - let mut curr = Word::from(1024); - while curr > self.sp { - curr -= 3; - v.push(self.memory.peek24(curr)); - } - v - } - } - fn predicate_opcode_test(opcode: Opcode, given: P, pred: Q) where P: FnOnce(&mut CPU), diff --git a/vcore/src/lib.rs b/vcore/src/lib.rs index c5b0a31..eb7be94 100644 --- a/vcore/src/lib.rs +++ b/vcore/src/lib.rs @@ -1,4 +1,8 @@ pub mod cpu; pub mod memory; pub mod opcodes; -pub mod word; \ No newline at end of file +pub mod word; + +pub use cpu::CPU; +pub use memory::Memory; +pub use word::Word; diff --git a/vcore/src/memory.rs b/vcore/src/memory.rs index 5dbff8e..ec49ac9 100644 --- a/vcore/src/memory.rs +++ b/vcore/src/memory.rs @@ -20,6 +20,26 @@ impl From for Memory { } } +impl Memory { + /// Create a memory containing the given contents (probably code) starting at `0x400` (the + /// starting address for code). The rest of the memory is zeroes. + /// ``` + /// # use crate::vcore::memory::PeekPokeExt; + /// # use crate::vcore::Word; + /// # use crate::vcore::Memory; + /// let mem = Memory::with_program(vec![0x02, 0x12, 0x34]); + /// assert_eq!(mem.peek8(Word::from(0x400)), 0x02); + /// assert_eq!(mem.peek8(Word::from(0x401)), 0x12); // etc + /// ``` + pub fn with_program>(code: T) -> Memory { + let mut m = Memory::default(); + for (addr, byte) in code.into_iter().enumerate() { + m.poke8(addr + 0x400, byte) + } + m + } +} + impl std::ops::Index for Memory { type Output = u8; fn index(&self, index: Word) -> &Self::Output { diff --git a/vcore/src/word.rs b/vcore/src/word.rs index 391ee17..7d7df74 100644 --- a/vcore/src/word.rs +++ b/vcore/src/word.rs @@ -1,3 +1,5 @@ +use std::fmt::{Display, Formatter}; + // 128k, the amount of memory in a standard Vulcan machine pub const MEM_SIZE: u32 = 128 * 1024; @@ -45,6 +47,11 @@ impl From for u32 { } } +impl Display for Word { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", i32::from(*self)) + } +} #[test] fn to_from_u32() { assert_eq!(u32::from(Word::from(0x123456u32)), 0x123456u32); diff --git a/vtest/Cargo.toml b/vtest/Cargo.toml new file mode 100644 index 0000000..ca8e2fc --- /dev/null +++ b/vtest/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "vtest" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +vcore = { path = "../vcore" } +vasm = { path = "../vasm" } \ No newline at end of file diff --git a/vtest/src/integration_tests.rs b/vtest/src/integration_tests.rs new file mode 100644 index 0000000..84458ed --- /dev/null +++ b/vtest/src/integration_tests.rs @@ -0,0 +1,234 @@ +use std::collections::BTreeMap; +use vasm::assemble_snippet; +use vcore::memory::{Memory, PeekPokeExt}; +use vcore::CPU; + +fn cpu_test<'a, T: IntoIterator>(code: T) -> CPU { + let bin = assemble_snippet(code).unwrap(); + let mut cpu = vcore::cpu::CPU::new(Memory::with_program(bin)); + cpu.run_to_halt(); + cpu +} + +fn test_stack<'a, T: IntoIterator>(code: T, expected_stack: Vec) { + let cpu = cpu_test(code); + assert_eq!(cpu.get_stack(), expected_stack); +} + +fn test_rstack<'a, T: IntoIterator>( + code: T, + expected_stack: Vec, + expected_rstack: Vec, +) { + let cpu = cpu_test(code); + assert_eq!(cpu.get_stack(), expected_stack); + assert_eq!(cpu.get_call(), expected_rstack); +} + +fn test_mem<'a, T: IntoIterator>(code: T, expected_memory: BTreeMap) { + let cpu = cpu_test(code); + for (addr, byte) in expected_memory { + assert_eq!(cpu.peek8(addr), byte) + } +} + +#[test] +fn test_arithmetic() { + // Basic arithmetic + test_stack( + ".org 0x400 + push 2 + add 3 + hlt" + .lines(), + vec![5], + ); + + // Multibyte values + test_stack( + ".org 0x400 + push 7 + mul 1000 + hlt" + .lines(), + vec![7000], + ); +} + +#[test] +fn test_call_stack() { + test_stack( + ".org 0x400 + nop 3 + call blah + hlt + blah: mul 2 + ret" + .lines(), + vec![6], + ); + + test_rstack( + ".org 0x400 + push 10 + push 4 + push 3 + pushr + pushr + hlt" + .lines(), + vec![10], + vec![3, 4], + ); + + test_rstack( + ".org 0x400 + push 10 + pushr 20 + pushr 4 + push 3 + popr + add + hlt" + .lines(), + vec![10, 7], + vec![20], + ); + + test_rstack( + ".org 0x400 + pushr 5 + call blah + blah: pushr 3 + hlt" + .lines(), + vec![], + vec![5, 0x406, 3], + ); + + test_rstack( + ".org 0x400 + push 5 + pushr 10 + pushr 20 + peekr + popr + popr + hlt" + .lines(), + vec![5, 20, 20, 10], + vec![], + ) +} + +#[test] +fn test_comparison() { + test_stack( + ".org 0x400 + push 10 + gt 20 + push 20 + gt 5 + push 10 + lt 20 + push 10 + lt 5 + hlt" + .lines(), + vec![0, 1, 1, 0], + ); + + test_stack( + ".org 1024 + push 10 + mul 0xffffff + agt 20 + push 20 + agt -5 + push -10 + alt 20 + push 10 + alt -5 + hlt" + .lines(), + vec![0, 1, 1, 0], + ); + + test_stack( + ".org 1024 + not 10 + not 0 + hlt" + .lines(), + vec![0, 1], + ) +} + +#[test] +fn test_pick() { + test_stack( + ".org 1024 + nop 10 + nop 20 + pick 1 + hlt" + .lines(), + vec![10, 20, 10], + ) +} + +#[test] +fn test_store() { + test_mem( + ".org 0x400 + push 10 + store 201 + push 0x123456 + storew 203 + hlt" + .lines(), + [(201, 10), (203, 0x56), (204, 0x34), (205, 0x12)].into(), + ) +} + +#[test] +fn test_load() { + test_stack( + ".org 0x400 + load 0x501 + loadw 0x500 + hlt + .org 0x500 + .db 0x123456" + .lines(), + vec![0x34, 0x123456], + ) +} + +#[test] +fn test_rotate() { + test_stack( + ".org 0x400 + push 10 + push 100 + push 200 + push 300 + rot + hlt" + .lines(), + vec![10, 200, 300, 100], + ) +} + +#[test] +fn test_sdp() { + test_stack( + ".org 0x400 + push 10 + pushr 20 + sdp + hlt" + .lines(), + vec![10, 1021, 265], + ) +} diff --git a/vtest/src/lib.rs b/vtest/src/lib.rs new file mode 100644 index 0000000..941250f --- /dev/null +++ b/vtest/src/lib.rs @@ -0,0 +1,2 @@ +#[cfg(test)] +mod integration_tests;