diff --git a/.gitignore b/.gitignore index 96ef862..0408c77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ .idea/ +.vscode/ diff --git a/ASM-SYNTAX.md b/ASM-SYNTAX.md index f9ffe06..2224f53 100644 --- a/ASM-SYNTAX.md +++ b/ASM-SYNTAX.md @@ -92,6 +92,8 @@ register-half register '.h' ``` +Comments are introduced with `#`, like in Bash. + ## Missing stuff There are no labels. diff --git a/Cargo.lock b/Cargo.lock index 34155ee..b8c8e7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,22 +12,175 @@ dependencies = [ ] [[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "clap" +version = "3.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] +name = "libc" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b" + +[[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "once_cell" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" + +[[package]] +name = "os_str_bytes" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] [[package]] name = "regex" -version = "1.5.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", @@ -36,9 +189,41 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "toy_cpu_4bit" @@ -50,7 +235,66 @@ dependencies = [ ] [[package]] +name = "toyasm" +version = "0.1.0" +dependencies = [ + "clap", + "toy_cpu_4bit", +] + +[[package]] +name = "toyvm" +version = "0.1.0" +dependencies = [ + "clap", + "toy_cpu_4bit", +] + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] name = "ux" -version = "0.1.3" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efdcf885b33bb81bc9336e66cebc10d503288449466c0e43e51250ddc93a3d8" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88dfeb711b61ce620c0cb6fd9f8e3e678622f0c971da2a63c4b3e25e88ed012f" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 902c4cd..7a3f37d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,7 @@ -[package] -name = "toy_cpu_4bit" -version = "0.1.0" -edition = "2021" +[workspace] -[dependencies] -ux = "0.1.3" -regex = "1.5.4" -lazy_static = "1.4.0" +members = [ + "toyvm", + "toyasm", + "toy_cpu_4bit", +] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b5a0e80 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: build clean test unit-tests acceptance-tests all + +all: build test + +build: + @cargo build --release + +clean: + @cargo clean + +test: unit-tests acceptance-tests + +unit-tests: + @cargo test + +acceptance-tests: build + @acceptance-tests/main.py diff --git a/acceptance-tests/add_four_ints.asm b/acceptance-tests/add_four_ints.asm new file mode 100644 index 0000000..987a3ea --- /dev/null +++ b/acceptance-tests/add_four_ints.asm @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: MIT +# Copyright Murad Karammaev, Nikita Kuzmin + +# This program computes 3 + 7 + 8 + 80 +# and stores result in [15] + +MOV R0.l, 3 +MOV R1.l, 7 +ADD +MOV R1.l, 8 +ADD +MOV R1.l, 0 +MOV R1.h, 5 +ADD +MOV [0xf], R0 +HALT diff --git a/acceptance-tests/add_four_ints.pgm b/acceptance-tests/add_four_ints.pgm new file mode 100644 index 0000000..456bf7e --- /dev/null +++ b/acceptance-tests/add_four_ints.pgm @@ -0,0 +1,14 @@ +P2 +10 1 +255 + +67 # MOV R0.l, 3 +87 # MOV R1.l, 7 +228 # ADD +88 # MOV R1.l, 8 +228 # ADD +80 # MOV R1.l, 0 +117 # MOV R1.h, 5 +228 # ADD +47 # MOV [0xf], R0 +245 # HALT diff --git a/acceptance-tests/main.py b/acceptance-tests/main.py new file mode 100755 index 0000000..d193e9f --- /dev/null +++ b/acceptance-tests/main.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import os +import subprocess +from tempfile import NamedTemporaryFile +import cv2 + + +class AssemblerAcceptanceTest: + def __init__(self, source, binary): + self.source_code = os.path.join(os.getcwd(), "acceptance-tests", source) + self.expected_binary = os.path.join(os.getcwd(), "acceptance-tests", binary) + + def __str__(self): + return f'acceptance_test(asm("{self.source_code}") → {self.expected_binary})' + + def run(self, toyasm): + tmp_file = NamedTemporaryFile(delete=False) + expected_binary = cv2.imread(self.expected_binary) + + result = subprocess.run([toyasm, self.source_code, tmp_file.name]) + assert result.returncode == 0 + compiled_binary = tmp_file.read() + + for i in range(len(expected_binary[0])): + assert compiled_binary[i] == expected_binary[0][i][0] + + for i in range(len(expected_binary[0]), 256): + assert compiled_binary[i] == 0 + + os.unlink(tmp_file.name) + print(f'OK: asm("{self.source_code}") == "{self.expected_binary}"') + + +class AcceptanceTestsRunner: + def __init__(self): + self.toyasm = self.prepare_tool("toyasm") + self.toyvm = self.prepare_tool("toyvm") + self.tests = [ + AssemblerAcceptanceTest("add_four_ints.asm", "add_four_ints.pgm") + ] + + def prepare_tool(self, name): + tool_path = os.path.join(os.getcwd(), "target", "release", name) + if not os.path.exists(tool_path): + raise FileNotFoundError(tool_path) + return tool_path + + def run(self): + for test in self.tests: + test.run(self.toyasm) + + +def main(): + runner = AcceptanceTestsRunner() + runner.run() + + +if __name__ == '__main__': + main() diff --git a/acceptance-tests/requirements.txt b/acceptance-tests/requirements.txt new file mode 100644 index 0000000..0dd006b --- /dev/null +++ b/acceptance-tests/requirements.txt @@ -0,0 +1 @@ +opencv-python diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 2e56219..0000000 --- a/src/main.rs +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright Murad Karammaev, Nikita Kuzmin - -mod assembler; -mod cpu; -mod instruction; - -use assembler::Assembler; -use cpu::*; - -fn main() { - let asm = Assembler::new(); - let code = asm - .assemble( - r#" - MOV R0.h, 0x0 - MOV R0.l, 0xF - MOV R1.l, 0x3 - ADD - MOV [0], R0 - ZERO R0 - HALT - "#, - ) - .unwrap(); - let mut cpu = Cpu::new(&code); - loop { - if cpu.step() { - break; - } - } - cpu.visualize(); -} diff --git a/toy_cpu_4bit/Cargo.toml b/toy_cpu_4bit/Cargo.toml new file mode 100644 index 0000000..902c4cd --- /dev/null +++ b/toy_cpu_4bit/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "toy_cpu_4bit" +version = "0.1.0" +edition = "2021" + +[dependencies] +ux = "0.1.3" +regex = "1.5.4" +lazy_static = "1.4.0" diff --git a/src/assembler.rs b/toy_cpu_4bit/src/assembler.rs similarity index 68% rename from src/assembler.rs rename to toy_cpu_4bit/src/assembler.rs index 2b48ba4..60e8400 100644 --- a/src/assembler.rs +++ b/toy_cpu_4bit/src/assembler.rs @@ -3,10 +3,15 @@ use crate::instruction::{decode, encode, Instruction, Register, ShiftMode}; use regex::Regex; -use std::error::Error; +use std::{ + error::Error, + fmt::{Display, Formatter}, + num, + ops::RangeInclusive, +}; use ux::{u2, u3, u4}; -type InstructionDecoder = fn(&str, &Regex) -> Result>; +type InstructionDecoder = fn(&str, &Regex) -> Result; const REGEX_REG: &str = r"(R[01])"; const REGEX_REG_HALF: &str = r"(R[01]\.[lh])"; @@ -22,6 +27,170 @@ macro_rules! asm_entry { }; } +#[derive(Debug, Clone)] +enum AssemblerErrorKind { + TooMuchInstructions, + UnknownInstruction { + instruction: String, + }, + InvalidRegister { + register: String, + }, + InvalidRegisterHalf { + register_half: String, + }, + InvalidCarry { + carry: String, + }, + NumberOutOfRange { + number: i128, + range: RangeInclusive, + }, + InvalidInteger { + parse_int_error: num::ParseIntError, + }, + InvalidRelativeJumpOffset { + jump_offset: i8, + }, + MalformedRegisterToRegisterCopy, +} + +impl AssemblerErrorKind { + fn unknown_instruction(instruction: &str) -> AssemblerErrorKind { + AssemblerErrorKind::UnknownInstruction { + instruction: instruction.to_string(), + } + } + + fn invalid_register(register: &str) -> AssemblerErrorKind { + AssemblerErrorKind::InvalidRegister { + register: register.to_string(), + } + } + + fn invalid_register_half(register_half: &str) -> AssemblerErrorKind { + AssemblerErrorKind::InvalidRegisterHalf { + register_half: register_half.to_string(), + } + } + + fn invalid_carry(carry: &str) -> AssemblerErrorKind { + AssemblerErrorKind::InvalidCarry { + carry: carry.to_string(), + } + } + + fn number_out_of_range + Copy>( + number: T, + range: RangeInclusive, + ) -> AssemblerErrorKind { + AssemblerErrorKind::NumberOutOfRange { + number: number.into(), + // am I doing this right? + range: (*range.start()).into()..=(*range.end()).into(), + } + } + + fn invalid_relative_jump_offset(jump_offset: i8) -> AssemblerErrorKind { + AssemblerErrorKind::InvalidRelativeJumpOffset { jump_offset } + } + + fn malformed_register_to_register_copy() -> AssemblerErrorKind { + AssemblerErrorKind::MalformedRegisterToRegisterCopy + } + + fn to_assembler_error(&self, line: usize) -> AssemblerError { + AssemblerError { + line, + error: self.clone(), + } + } +} + +impl From for AssemblerErrorKind { + fn from(parse_int_error: num::ParseIntError) -> AssemblerErrorKind { + AssemblerErrorKind::InvalidInteger { parse_int_error } + } +} + +impl Display for AssemblerErrorKind { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match &self { + AssemblerErrorKind::TooMuchInstructions => String::from("too much instructions"), + AssemblerErrorKind::UnknownInstruction { instruction } => + format!("unknown instruction: '{}'", instruction), + AssemblerErrorKind::InvalidRegister { register } => + format!("invalid register: '{}'", register), + AssemblerErrorKind::InvalidRegisterHalf { register_half } => + format!("invalid register half: '{}'", register_half), + AssemblerErrorKind::InvalidCarry { carry } => format!("invalid carry: '{}'", carry), + AssemblerErrorKind::NumberOutOfRange { number, range } => format!( + "number {} is out of range [{}, {}]", + number, + range.start(), + range.end() + ), + AssemblerErrorKind::InvalidInteger { parse_int_error } => + format!("failed to parse integer: {}", parse_int_error), + AssemblerErrorKind::InvalidRelativeJumpOffset { jump_offset } => + format!("{} is not a valid relative jump offset", jump_offset), + AssemblerErrorKind::MalformedRegisterToRegisterCopy => + String::from("malformed register to register copy"), + } + ) + } +} + +impl Error for AssemblerErrorKind { + fn description(&self) -> &str { + match self { + AssemblerErrorKind::TooMuchInstructions => "Too much instructions", + AssemblerErrorKind::UnknownInstruction { .. } => "Unknown instruction", + AssemblerErrorKind::InvalidRegister { .. } => "Invalid register", + AssemblerErrorKind::InvalidRegisterHalf { .. } => "Invalid register half", + AssemblerErrorKind::InvalidCarry { .. } => "Invalid carry", + AssemblerErrorKind::NumberOutOfRange { .. } => "Number out of range", + AssemblerErrorKind::InvalidInteger { .. } => "Failed to parse integer", + AssemblerErrorKind::InvalidRelativeJumpOffset { .. } => "Invalid relative jump offset", + AssemblerErrorKind::MalformedRegisterToRegisterCopy => { + "Malformed register to register copy" + } + } + } +} + +#[derive(Debug)] +pub struct AssemblerError { + line: usize, + error: AssemblerErrorKind, +} + +type AssemblerResult = Result; + +impl Display for AssemblerError { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "Failed to assemble, line {}: {}", self.line, self.error) + } +} + +impl Error for AssemblerError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(&self.error) + } +} + +impl AssemblerError { + fn too_much_instructions(line: usize) -> AssemblerError { + AssemblerError { + line, + error: AssemblerErrorKind::TooMuchInstructions, + } + } +} + pub struct Assembler { table: Vec<(Regex, InstructionDecoder)>, } @@ -111,17 +280,23 @@ impl Assembler { } } - pub fn assemble(&self, input: &str) -> Result<[u8; 0x100], Box> { + pub fn assemble(&self, input: &str) -> AssemblerResult<[u8; 0x100]> { let mut ret = [0u8; 0x100]; let mut i: usize = 0; - for line in input.lines().map(|line| line.trim()) { + for (line_num, line) in input + .lines() + .map(|line| strip_comment(line).trim()) + .enumerate() + { match line { "" => (), line => { if i > 0xFF { - return Err("Too much instructions".into()); + return Err(AssemblerError::too_much_instructions(line_num)); } - let insn = self.line_to_insn(line)?; + let insn = self + .line_to_insn(line) + .map_err(|e| e.to_assembler_error(line_num))?; ret[i] = encode(insn); i += 1; } @@ -130,17 +305,30 @@ impl Assembler { Ok(ret) } - fn line_to_insn(&self, line: &str) -> Result> { + fn line_to_insn(&self, line: &str) -> Result { for (regex, handler) in &self.table { if regex.is_match(line) { return handler(line, regex); } } - Err("Unknown instruction".into()) + Err(AssemblerErrorKind::unknown_instruction(line)) + } +} + +impl Default for Assembler { + fn default() -> Self { + Self::new() + } +} + +fn strip_comment(input: &str) -> &str { + match input.find('#') { + Some(x) => &input[0..x], + None => input, } } -fn parse_u128(src: &str) -> Result> { +fn parse_u128(src: &str) -> Result { if let Some(s) = src.strip_prefix("0b") { Ok(u128::from_str_radix(s, 2)?) } else if let Some(s) = src.strip_prefix("0o") { @@ -152,14 +340,14 @@ fn parse_u128(src: &str) -> Result> { } } -fn parse_u8(src: &str) -> Result> { +fn parse_u8(src: &str) -> Result { match parse_u128(src)? { x @ 0..=255 => Ok(x as u8), - x => Err(format!("{} is not in range [0, 255]", x).into()), + x => Err(AssemblerErrorKind::number_out_of_range(x as i128, 0..=255)), } } -fn parse_i8(src: &str) -> Result> { +fn parse_i8(src: &str) -> Result { let (sign, num): (i128, u128) = match src.strip_prefix('-') { Some(s) => (-1, parse_u128(s)?), None => (1, parse_u128(src)?), @@ -167,78 +355,81 @@ fn parse_i8(src: &str) -> Result> { match (sign, num) { (1, x) if x <= 127 => Ok((x as i128 * sign) as i8), (-1, x) if x <= 128 => Ok((x as i128 * sign) as i8), - (_, x) => Err(format!("{} is not in range [-128, 127]", x).into()), + (_, x) => Err(AssemblerErrorKind::number_out_of_range( + x as i128, + -128..=127, + )), } } -fn parse_u4(src: &str) -> Result> { +fn parse_u4(src: &str) -> Result { match parse_u8(src)? { x @ 0..=15 => Ok(u4::new(x)), - x => Err(format!("{} is not in range [0, 15]", x).into()), + x => Err(AssemblerErrorKind::number_out_of_range(x as i128, 0..=15)), } } -fn parse_u3(src: &str) -> Result> { +fn parse_u3(src: &str) -> Result { match parse_u8(src)? { x @ 0..=7 => Ok(u3::new(x)), - x => Err(format!("{} is not in range [0, 7]", x).into()), + x => Err(AssemblerErrorKind::number_out_of_range(x, 0..=7)), } } -fn parse_reg(src: &str) -> Result> { +fn parse_reg(src: &str) -> Result { match src { "R0" => Ok(Register::R0), "R1" => Ok(Register::R1), - x => Err(format!("'{}' is not a valid register", x).into()), + x => Err(AssemblerErrorKind::invalid_register(x)), } } -fn parse_reg_half(src: &str) -> Result<(Register, bool), Box> { +fn parse_reg_half(src: &str) -> Result<(Register, bool), AssemblerErrorKind> { match src { "R0.l" => Ok((Register::R0, true)), "R0.h" => Ok((Register::R0, false)), "R1.l" => Ok((Register::R1, true)), "R1.h" => Ok((Register::R1, false)), - x => Err(format!("'{}' is not a valid register half", x).into()), + x => Err(AssemblerErrorKind::invalid_register_half(x)), } } -fn parse_carry(src: &str) -> Result> { +fn parse_carry(src: &str) -> Result { match src { "0" => Ok(false), "1" => Ok(true), - x => Err(format!("'{}' is an invalid carry value", x).into()), + x => Err(AssemblerErrorKind::invalid_carry(x)), } } -fn parse_byte(line: &str, regex: &Regex) -> Result> { +fn parse_byte(line: &str, regex: &Regex) -> Result { Ok(decode(parse_u8( regex.captures(line).unwrap().get(1).unwrap().as_str(), )?)) } -fn parse_mov_reg_mem(line: &str, regex: &Regex) -> Result> { +fn parse_mov_reg_mem(line: &str, regex: &Regex) -> Result { let captures = regex.captures(line).unwrap(); let reg = parse_reg(captures.get(1).unwrap().as_str())?; let addr = parse_u4(captures.get(2).unwrap().as_str())?; Ok(Instruction::Load { reg, addr }) } -fn parse_mov_mem_reg(line: &str, regex: &Regex) -> Result> { +fn parse_mov_mem_reg(line: &str, regex: &Regex) -> Result { let captures = regex.captures(line).unwrap(); let addr = parse_u4(captures.get(1).unwrap().as_str())?; let reg = parse_reg(captures.get(2).unwrap().as_str())?; Ok(Instruction::Store { reg, addr }) } -fn parse_mov_reg_half_imm(line: &str, regex: &Regex) -> Result> { +fn parse_mov_reg_half_imm(line: &str, regex: &Regex) -> Result { let captures = regex.captures(line).unwrap(); let (reg, low) = parse_reg_half(captures.get(1).unwrap().as_str())?; let val = parse_u4(captures.get(2).unwrap().as_str())?; Ok(Instruction::LoadImmediate { low, reg, val }) } -fn parse_mov_reg_reg(line: &str, regex: &Regex) -> Result> { +fn parse_mov_reg_reg(line: &str, regex: &Regex) -> Result { let captures = regex.captures(line).unwrap(); let reg_to = parse_reg(captures.get(1).unwrap().as_str())?; let reg_from = parse_reg(captures.get(2).unwrap().as_str())?; @@ -249,7 +440,7 @@ fn parse_mov_reg_reg(line: &str, regex: &Regex) -> Result Ok(Instruction::Copy { reg_from: Register::R0, }), - _ => Err("Malformed register to register copy".into()), + _ => Err(AssemblerErrorKind::malformed_register_to_register_copy()), } } @@ -257,7 +448,7 @@ fn parse_jmp_impl( line: &str, regex: &Regex, use_carry: bool, -) -> Result> { +) -> Result { match parse_i8(regex.captures(line).unwrap().get(1).unwrap().as_str())? { x @ -3..=-1 => Ok(Instruction::NearJumpBackward { use_carry, @@ -267,15 +458,15 @@ fn parse_jmp_impl( use_carry, offset: u3::new(x as u8), }), - x => Err(format!("{} is not a valid relative jump offset", x).into()), + x => Err(AssemblerErrorKind::invalid_relative_jump_offset(x)), } } -fn parse_jmp(line: &str, regex: &Regex) -> Result> { +fn parse_jmp(line: &str, regex: &Regex) -> Result { parse_jmp_impl(line, regex, false) } -fn parse_jmpc(line: &str, regex: &Regex) -> Result> { +fn parse_jmpc(line: &str, regex: &Regex) -> Result { parse_jmp_impl(line, regex, true) } @@ -283,16 +474,16 @@ fn parse_ajmp_impl( line: &str, regex: &Regex, use_carry: bool, -) -> Result> { +) -> Result { let reg = parse_reg(regex.captures(line).unwrap().get(1).unwrap().as_str())?; Ok(Instruction::FarJump { reg, use_carry }) } -fn parse_ajmp(line: &str, regex: &Regex) -> Result> { +fn parse_ajmp(line: &str, regex: &Regex) -> Result { parse_ajmp_impl(line, regex, false) } -fn parse_ajmpc(line: &str, regex: &Regex) -> Result> { +fn parse_ajmpc(line: &str, regex: &Regex) -> Result { parse_ajmp_impl(line, regex, true) } @@ -301,7 +492,7 @@ fn parse_shift( regex: &Regex, right: bool, mode: ShiftMode, -) -> Result> { +) -> Result { let captures = regex.captures(line).unwrap(); let reg = parse_reg(captures.get(1).unwrap().as_str())?; let len = parse_u3(captures.get(2).unwrap().as_str())?; @@ -313,56 +504,58 @@ fn parse_shift( }) } -fn parse_shr(line: &str, regex: &Regex) -> Result> { +fn parse_shr(line: &str, regex: &Regex) -> Result { parse_shift(line, regex, true, ShiftMode::Logical) } -fn parse_shl(line: &str, regex: &Regex) -> Result> { +fn parse_shl(line: &str, regex: &Regex) -> Result { parse_shift(line, regex, false, ShiftMode::Logical) } -fn parse_rotr(line: &str, regex: &Regex) -> Result> { +fn parse_rotr(line: &str, regex: &Regex) -> Result { parse_shift(line, regex, true, ShiftMode::Circular) } -fn parse_rotl(line: &str, regex: &Regex) -> Result> { +fn parse_rotl(line: &str, regex: &Regex) -> Result { parse_shift(line, regex, false, ShiftMode::Circular) } -fn parse_inc(line: &str, regex: &Regex) -> Result> { +fn parse_inc(line: &str, regex: &Regex) -> Result { let reg = parse_reg(regex.captures(line).unwrap().get(1).unwrap().as_str())?; Ok(Instruction::Inc { reg }) } -fn parse_dec(line: &str, regex: &Regex) -> Result> { +fn parse_dec(line: &str, regex: &Regex) -> Result { let reg = parse_reg(regex.captures(line).unwrap().get(1).unwrap().as_str())?; Ok(Instruction::Dec { reg }) } -fn parse_compl(line: &str, regex: &Regex) -> Result> { +fn parse_compl(line: &str, regex: &Regex) -> Result { let reg = parse_reg(regex.captures(line).unwrap().get(1).unwrap().as_str())?; Ok(Instruction::Complement { reg }) } -fn parse_setc(line: &str, regex: &Regex) -> Result> { +fn parse_setc(line: &str, regex: &Regex) -> Result { let carry = parse_carry(regex.captures(line).unwrap().get(1).unwrap().as_str())?; Ok(Instruction::Setc { carry }) } -fn parse_cload(line: &str, regex: &Regex) -> Result> { +fn parse_cload(line: &str, regex: &Regex) -> Result { let reg = parse_reg(regex.captures(line).unwrap().get(1).unwrap().as_str())?; Ok(Instruction::Cload { reg }) } -fn parse_zero(line: &str, regex: &Regex) -> Result> { +fn parse_zero(line: &str, regex: &Regex) -> Result { let reg = parse_reg(regex.captures(line).unwrap().get(1).unwrap().as_str())?; Ok(Instruction::Zero { reg }) } #[cfg(test)] mod tests { - use crate::assembler::Assembler; - use crate::instruction::{decode, Instruction, Register, ShiftMode}; + use crate::{ + assembler::{strip_comment, Assembler}, + instruction::{decode, Instruction, Register, ShiftMode}, + }; use lazy_static::lazy_static; use std::fmt::{Binary, Display, LowerHex, Octal}; use ux::{u2, u3, u4}; @@ -714,4 +907,60 @@ mod tests { ); } } + + #[test] + fn asm_assemble_empty() { + assert_eq!(ASM.assemble("").unwrap(), [0u8; 256]); + } + + #[test] + fn asm_assemble() { + let expected = { + let mut code = [0u8; 256]; + code[0..6].copy_from_slice(&[0x4F, 0x53, 0xE4, 0x20, 0xF8, 0xF5]); + code + }; + assert_eq!( + ASM.assemble( + r#" + MOV R0.l, 0xF + MOV R1.l, 0x3 + ADD + MOV [0], R0 + ZERO R0 + HALT + "# + ) + .unwrap(), + expected + ); + } + + #[test] + fn asm_strip_comment() { + assert_eq!(strip_comment("MOV [2], R1 # comment"), "MOV [2], R1 "); + } + + #[test] + fn asm_assemble_comments() { + let expected = { + let mut code = [0u8; 256]; + code[0..6].copy_from_slice(&[0x4F, 0x53, 0xE4, 0x20, 0xF8, 0xF5]); + code + }; + assert_eq!( + ASM.assemble( + r#" + MOV R0.l, 0xF # R0 = 15 + MOV R1.l, 0x3 # R1 = 3 + ADD # R0 += R1 (18) + MOV [0], R0 # MEM[0] = R0 (18) + ZERO R0 # R0 = 0 + HALT # EXIT + "# + ) + .unwrap(), + expected + ); + } } diff --git a/src/cpu.rs b/toy_cpu_4bit/src/cpu.rs similarity index 99% rename from src/cpu.rs rename to toy_cpu_4bit/src/cpu.rs index 7f134f7..86bc4b5 100644 --- a/src/cpu.rs +++ b/toy_cpu_4bit/src/cpu.rs @@ -1,24 +1,21 @@ // SPDX-License-Identifier: MIT // Copyright Murad Karammaev, Nikita Kuzmin -use crate::instruction::*; - -use std::collections::VecDeque; -use std::num::Wrapping; -use std::{cmp, mem}; +use crate::instruction::{decode, Instruction, Register, ShiftMode}; +use std::{cmp, collections::VecDeque, mem, num::Wrapping}; use ux::{u3, u4}; #[allow(non_snake_case)] pub struct Cpu { - IP: Wrapping, - R0: Wrapping, - R1: Wrapping, - C: bool, - code: [u8; 0x100], - data: [u8; 0x10], - port_in: VecDeque, - port_out: VecDeque, - num_cycles: u64, + pub IP: Wrapping, + pub R0: Wrapping, + pub R1: Wrapping, + pub C: bool, + pub code: [u8; 0x100], + pub data: [u8; 0x10], + pub port_in: VecDeque, + pub port_out: VecDeque, + pub num_cycles: u64, } impl Cpu { @@ -39,6 +36,10 @@ impl Cpu { } } + pub fn next_instruction(&self) -> String { + format!("{:?}", decode(self.code[self.IP.0 as usize])) + } + fn load(&mut self, reg: Register, addr: u4) { match reg { Register::R0 => self.R0 = Wrapping(self.data[u8::from(addr) as usize]), @@ -409,7 +410,7 @@ impl Cpu { #[cfg(test)] mod tests { - use crate::Cpu; + use super::Cpu; use std::num::Wrapping; #[test] diff --git a/src/instruction.rs b/toy_cpu_4bit/src/instruction.rs similarity index 99% rename from src/instruction.rs rename to toy_cpu_4bit/src/instruction.rs index bd16169..139dc25 100644 --- a/src/instruction.rs +++ b/toy_cpu_4bit/src/instruction.rs @@ -343,7 +343,7 @@ pub fn encode(insn: Instruction) -> u8 { // This crate really needs more tests, but I can't be bothered #[cfg(test)] mod tests { - use crate::instruction::{decode, encode}; + use super::{decode, encode}; #[test] fn deterministic_instruction_encoding() { diff --git a/toy_cpu_4bit/src/lib.rs b/toy_cpu_4bit/src/lib.rs new file mode 100644 index 0000000..520cd80 --- /dev/null +++ b/toy_cpu_4bit/src/lib.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +// Copyright Murad Karammaev, Nikita Kuzmin + +pub mod assembler; +pub mod cpu; +pub mod instruction; + +#[cfg(test)] +mod tests { + use crate::{assembler::Assembler, cpu::Cpu}; + use std::num::Wrapping; + + #[test] + fn integration_assemble_execute() { + let code = r#" + MOV R0.l, 0xF # R0 = 15 + MOV R1.l, 0x3 # R1 = 3 + ADD # R0 += R1 (18) + MOV [0], R0 # MEM[0] = R0 (18) + ZERO R0 # R0 = 0 + HALT # EXIT + "#; + let code = Assembler::new().assemble(code).unwrap(); + let mut cpu = Cpu::new(&code); + for _ in 0..5 { + assert!(!cpu.step()); // CPU does not halt… + } + assert!(cpu.step()); // …until it reaches halt instruction + assert_eq!(cpu.IP, Wrapping(5)); // IP points at halt instruction + assert_eq!(cpu.R0, Wrapping(0)); + assert_eq!(cpu.R1, Wrapping(3)); + assert!(!cpu.C); + assert_eq!(cpu.data[0], 18); + for i in 1..=15 { + assert_eq!(cpu.data[i], 0); + } + } +} diff --git a/toyasm/Cargo.toml b/toyasm/Cargo.toml new file mode 100644 index 0000000..fef05d2 --- /dev/null +++ b/toyasm/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "toyasm" +version = "0.1.0" +edition = "2021" + +[dependencies] +toy_cpu_4bit = { version = "0.1.0", path = "../toy_cpu_4bit" } +clap = { version = "3.1.17", features = ["derive"] } diff --git a/toyasm/src/main.rs b/toyasm/src/main.rs new file mode 100644 index 0000000..ddfdb6f --- /dev/null +++ b/toyasm/src/main.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +// Copyright Murad Karammaev, Nikita Kuzmin + +use clap::Parser; +use std::{ + error::Error, + fs::{File, OpenOptions}, + io::{Read, Write}, + path::PathBuf, +}; +use toy_cpu_4bit::assembler::Assembler; + +/// ToyCPU-4bit Assembler +#[derive(Parser)] +#[clap(version, about, long_about = None)] +struct Args { + /// Path to input source code + input: PathBuf, + /// Path to output machine code + output: PathBuf, +} + +fn read_code(path: PathBuf) -> Result> { + let mut code = String::new(); + File::open(path)?.read_to_string(&mut code)?; + Ok(code) +} + +fn write_code(path: PathBuf, code: [u8; 256]) -> Result<(), Box> { + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(&code)?; + Ok(()) +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + let code = Assembler::new().assemble(&read_code(args.input)?)?; + write_code(args.output, code)?; + Ok(()) +} diff --git a/toyvm/Cargo.toml b/toyvm/Cargo.toml new file mode 100644 index 0000000..6416646 --- /dev/null +++ b/toyvm/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "toyvm" +version = "0.1.0" +edition = "2021" + +[dependencies] +toy_cpu_4bit = { version = "0.1.0", path = "../toy_cpu_4bit" } +clap = { version = "3.1.17", features = ["derive"] } diff --git a/toyvm/src/main.rs b/toyvm/src/main.rs new file mode 100644 index 0000000..1fa9aea --- /dev/null +++ b/toyvm/src/main.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +// Copyright Murad Karammaev, Nikita Kuzmin + +use clap::Parser; +use std::{error::Error, fs::File, io::Read, path::PathBuf}; +use toy_cpu_4bit::cpu::Cpu; + +/// ToyCPU-4bit Virtual Machine +#[derive(Parser)] +#[clap(version, about, long_about = None)] +struct Args { + /// Visualize every CPU tick + #[clap(short, long)] + trace: bool, + /// Path to 256-byte file with code for CPU + code: PathBuf, +} + +fn read_code(path: PathBuf) -> Result<[u8; 256], Box> { + let mut file = File::open(path)?; + match file.metadata()?.len() { + x if x != 256 => { + Err(format!("Wrong code file size, expected: 256, provided: {}", x).into()) + } + _ => { + let mut buf = [0u8; 256]; + file.read_exact(&mut buf)?; + Ok(buf) + } + } +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + let code = read_code(args.code)?; + let mut cpu = Cpu::new(&code); + loop { + println!("{}", cpu.next_instruction()); + if cpu.step() { + break; + } + if args.trace { + cpu.visualize(); + } + } + cpu.visualize(); + Ok(()) +}