Couple bugfixes, integration tests ported

This commit is contained in:
2022-04-17 23:35:48 -05:00
parent df111ce55b
commit 05c1be2e94
16 changed files with 436 additions and 48 deletions
+1
View File
@@ -6,6 +6,7 @@
<sourceFolder url="file://$MODULE_DIR$/vcore/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/vcore/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/vemu/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/vemu/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/vasm/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/vasm/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/vtest/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
Generated
+8
View File
@@ -1227,6 +1227,14 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vtest"
version = "0.1.0"
dependencies = [
"vasm",
"vcore",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.2+wasi-snapshot-preview1" version = "0.10.2+wasi-snapshot-preview1"
+2 -1
View File
@@ -3,5 +3,6 @@ resolver = "2"
members = [ members = [
"vcore", "vcore",
"vemu", "vemu",
"vasm" "vasm",
"vtest"
] ]
+1
View File
@@ -73,6 +73,7 @@ pub enum VASMLine {
Equ(Label, Node), Equ(Label, Node),
LabelDef(Label), LabelDef(Label),
Macro(Macro), Macro(Macro),
Blank,
} }
impl VASMLine { impl VASMLine {
+7 -5
View File
@@ -2,9 +2,11 @@ extern crate pest;
#[macro_use] #[macro_use]
extern crate pest_derive; extern crate pest_derive;
pub mod ast; mod ast;
pub mod parse_error; pub mod parse_error;
pub mod vasm_assembler; mod vasm_assembler;
pub mod vasm_evaluator; mod vasm_evaluator;
pub mod vasm_parser; mod vasm_parser;
pub mod vasm_preprocessor; mod vasm_preprocessor;
pub use vasm_assembler::assemble_snippet;
+3 -1
View File
@@ -72,5 +72,7 @@ include = { "include" ~ string }
control = { "if" | "unless" | "while" | "until" | "do" | "else" | "end" } control = { "if" | "unless" | "while" | "until" | "do" | "else" | "end" }
preprocessor = {"#" ~ (control | include) } preprocessor = {"#" ~ (control | include) }
blank = { WHITESPACE? ~ COMMENT? }
// Finally the entire pattern for an assembly line: // Finally the entire pattern for an assembly line:
line = { SOI ~ (preprocessor | db_word | db_string | org_directive | equ_directive | instruction | label_def) ~ EOI } line = { SOI ~ (preprocessor | db_word | db_string | org_directive | equ_directive | instruction | label_def | blank) ~ EOI }
+87 -16
View File
@@ -1,7 +1,7 @@
use crate::ast::{Label, Scope, VASMLine}; use crate::ast::{Label, Scope, VASMLine};
use crate::parse_error::AssembleError; use crate::parse_error::AssembleError;
use crate::vasm_evaluator::eval; use crate::vasm_evaluator::eval;
use crate::vasm_parser::parse_vasm_line; use crate::vasm_preprocessor::{Line, LineSource};
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// This will solve all the .equ directives and return a symbol table of them. /// 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) => { VASMLine::StringDb(_, value) => {
lengths.insert(line_num, value.len()); 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); lengths.insert(line_num, 0);
} }
VASMLine::Macro(_) => unreachable!(), VASMLine::Macro(_) => unreachable!(),
@@ -156,14 +156,37 @@ fn code_bounds(
Ok((start, end + end_length - 1)) Ok((start, end + end_length - 1))
} }
pub fn assemble<'a, T: IntoIterator<Item = &'a str>>(lines: T) -> Result<Vec<u8>, AssembleError> { /// Turn an iterable of strs into an assembled binary. This supports macros, but not
let mut parsed = Vec::new(); /// the `#include` macro. The resulting Vec is only as large as it needs to be; if your
for (line_idx, line) in lines.into_iter().enumerate() { /// code starts with `.org 0x400` and is five bytes long then the Vec will be five
let line_num = line_idx + 1; /// bytes long and index 0 will represent 0x400.
parsed.push(parse_vasm_line(line).map_err(|err| AssembleError::ParseError(line_num, err))?) /// ```
} /// 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<Item = &'a str>>(
lines: T,
) -> Result<Vec<u8>, AssembleError> {
let mut line_results: Vec<Result<Line, AssembleError>> =
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 /// 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<Item = &'a str>>(lines: T) -> Result<Vec<u8>
/// ///
/// Vulcan is a little-endian architecture: multi-byte arguments / .dbs will store the /// 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. /// least-significant byte at the lowest address, then the more significant bytes following.
fn generate_code(lines: Vec<VASMLine>) -> Result<Vec<u8>, AssembleError> { fn generate_code<T: IntoIterator<Item = VASMLine>>(lines: T) -> Result<Vec<u8>, AssembleError> {
let lines: Vec<VASMLine> = lines.into_iter().collect(); let lines: Vec<VASMLine> = lines.into_iter().collect();
let scope = solve_equs(&lines)?; let scope = solve_equs(&lines)?;
let line_lengths = measure_instructions(&lines, &scope); let line_lengths = measure_instructions(&lines, &scope);
@@ -195,13 +218,13 @@ fn generate_code(lines: Vec<VASMLine>) -> Result<Vec<u8>, AssembleError> {
let line_num = line_idx + 1; let line_num = line_idx + 1;
match line { match line {
VASMLine::Instruction(_, opcode, None) => { VASMLine::Instruction(_, opcode, None) => {
code[current_addr] = u8::from(*opcode) << 2; code[current_addr - start] = u8::from(*opcode) << 2;
current_addr += 1; current_addr += 1;
} }
VASMLine::Instruction(_, opcode, Some(arg)) => { VASMLine::Instruction(_, opcode, Some(arg)) => {
let arg = eval(arg, line_num, &line_addresses, &scope) let arg = eval(arg, line_num, &line_addresses, &scope)
.map_err(|err| AssembleError::ArgError(line_num, err))?; .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; let instr = (u8::from(*opcode) << 2) + len as u8;
code[current_addr - start] = instr; code[current_addr - start] = instr;
let [low, mid, high, _] = arg.to_le_bytes(); let [low, mid, high, _] = arg.to_le_bytes();
@@ -229,7 +252,7 @@ fn generate_code(lines: Vec<VASMLine>) -> Result<Vec<u8>, AssembleError> {
VASMLine::Org(_, _) => { VASMLine::Org(_, _) => {
current_addr = line_addresses[&(line_num + 1)] as usize; current_addr = line_addresses[&(line_num + 1)] as usize;
} }
VASMLine::Equ(_, _) | VASMLine::LabelDef(_) => {} VASMLine::Equ(_, _) | VASMLine::LabelDef(_) | VASMLine::Blank => {}
VASMLine::Macro(_) => unreachable!(), VASMLine::Macro(_) => unreachable!(),
} }
} }
@@ -333,6 +356,13 @@ mod test {
measure_instructions(&parse([".org 256", "blah:", "foo: .equ 7"]), &[].into()), measure_instructions(&parse([".org 256", "blah:", "foo: .equ 7"]), &[].into()),
[(1, 0), (2, 0), (3, 0)].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!( assert_eq!(
measure_instructions( measure_instructions(
&parse(["add 2 + foo", "add 3 + blah", "jmpr @foo"]), &parse(["add 2 + foo", "add 3 + blah", "jmpr @foo"]),
@@ -369,6 +399,28 @@ mod test {
[("blah".to_string(), 266), ("start".to_string(), 256)].into() [("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] #[test]
@@ -421,14 +473,33 @@ mod test {
} }
#[test] #[test]
fn test_assemble() { fn test_assemble_snippet() {
assert_eq!(assemble(["add"]), Ok(vec![4])); assert_eq!(assemble_snippet(["add"]), Ok(vec![4]));
assert_eq!( assert_eq!(
assemble(["apple"]), assemble_snippet(["apple"]),
Err(ParseError( Err(ParseError(
1, 1,
parse_error::ParseError::InvalidInstruction("apple".into()) 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
])
);
} }
} }
+14
View File
@@ -192,6 +192,7 @@ pub fn parse_vasm_line(line: &str) -> Result<VASMLine, ParseError> {
} }
Rule::label_def => Ok(VASMLine::LabelDef(Label::from(line.only().as_str()))), Rule::label_def => Ok(VASMLine::LabelDef(Label::from(line.only().as_str()))),
Rule::preprocessor => Ok(VASMLine::Macro(parse_macro(line))), Rule::preprocessor => Ok(VASMLine::Macro(parse_macro(line))),
Rule::blank => Ok(VASMLine::Blank),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -201,6 +202,7 @@ fn parse_macro(line: Pair) -> Macro {
match pre.as_rule() { match pre.as_rule() {
Rule::control => match pre.as_str() { Rule::control => match pre.as_str() {
"if" => Macro::If, "if" => Macro::If,
"unless" => Macro::Unless,
"else" => Macro::Else, "else" => Macro::Else,
"while" => Macro::While, "while" => Macro::While,
"until" => Macro::Until, "until" => Macro::Until,
@@ -420,9 +422,21 @@ mod test {
#[test] #[test]
fn test_parse_macros() { fn test_parse_macros() {
assert_eq!(parse_vasm_line("#if"), Ok(VASMLine::Macro(Macro::If))); assert_eq!(parse_vasm_line("#if"), Ok(VASMLine::Macro(Macro::If)));
assert_eq!(
parse_vasm_line("#unless"),
Ok(VASMLine::Macro(Macro::Unless))
);
assert_eq!( assert_eq!(
parse_vasm_line("#include \"blah\""), parse_vasm_line("#include \"blah\""),
Ok(VASMLine::Macro(Macro::Include("blah".to_string()))) 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));
}
} }
+2 -2
View File
@@ -4,9 +4,9 @@ use crate::vasm_parser::parse_vasm_line;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::iter::Enumerate; use std::iter::Enumerate;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
pub struct Line { pub struct Line {
line: VASMLine, pub line: VASMLine,
line_num: usize, line_num: usize,
file: String, file: String,
} }
+33 -22
View File
@@ -32,6 +32,12 @@ impl PeekPoke for CPU {
} }
} }
impl Default for CPU {
fn default() -> Self {
Self::new(Memory::default())
}
}
impl CPU { impl CPU {
pub fn new(memory: Memory) -> Self { pub fn new(memory: Memory) -> Self {
Self { Self {
@@ -270,6 +276,33 @@ impl CPU {
pub fn running(&self) -> bool { pub fn running(&self) -> bool {
!self.halted !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<Word> {
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<Word> {
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 { impl Opcode {
@@ -305,28 +338,6 @@ mod tests {
use super::*; use super::*;
use Opcode::*; use Opcode::*;
impl CPU {
fn get_stack(&self) -> Vec<Word> {
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<Word> {
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<P, Q>(opcode: Opcode, given: P, pred: Q) fn predicate_opcode_test<P, Q>(opcode: Opcode, given: P, pred: Q)
where where
P: FnOnce(&mut CPU), P: FnOnce(&mut CPU),
+4
View File
@@ -2,3 +2,7 @@ pub mod cpu;
pub mod memory; pub mod memory;
pub mod opcodes; pub mod opcodes;
pub mod word; pub mod word;
pub use cpu::CPU;
pub use memory::Memory;
pub use word::Word;
+20
View File
@@ -20,6 +20,26 @@ impl<R: Rng> From<R> 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<T: IntoIterator<Item = u8>>(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<Word> for Memory { impl std::ops::Index<Word> for Memory {
type Output = u8; type Output = u8;
fn index(&self, index: Word) -> &Self::Output { fn index(&self, index: Word) -> &Self::Output {
+7
View File
@@ -1,3 +1,5 @@
use std::fmt::{Display, Formatter};
// 128k, the amount of memory in a standard Vulcan machine // 128k, the amount of memory in a standard Vulcan machine
pub const MEM_SIZE: u32 = 128 * 1024; pub const MEM_SIZE: u32 = 128 * 1024;
@@ -45,6 +47,11 @@ impl From<Word> for u32 {
} }
} }
impl Display for Word {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", i32::from(*self))
}
}
#[test] #[test]
fn to_from_u32() { fn to_from_u32() {
assert_eq!(u32::from(Word::from(0x123456u32)), 0x123456u32); assert_eq!(u32::from(Word::from(0x123456u32)), 0x123456u32);
+10
View File
@@ -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" }
+234
View File
@@ -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<Item = &'a str>>(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<Item = &'a str>>(code: T, expected_stack: Vec<i32>) {
let cpu = cpu_test(code);
assert_eq!(cpu.get_stack(), expected_stack);
}
fn test_rstack<'a, T: IntoIterator<Item = &'a str>>(
code: T,
expected_stack: Vec<i32>,
expected_rstack: Vec<i32>,
) {
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<Item = &'a str>>(code: T, expected_memory: BTreeMap<u32, u8>) {
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],
)
}
+2
View File
@@ -0,0 +1,2 @@
#[cfg(test)]
mod integration_tests;