Browse Source

Merge pull request 'Add CLI tools' (#7) from cli-tools into master

Reviewed-on: https://git.rfnull.com/ToyPC/ToyCPU-4bit/pulls/7
master
foxpy 2 years ago
parent
commit
0408c6d52d
  1. 1
      .gitignore
  2. 2
      ASM-SYNTAX.md
  3. 260
      Cargo.lock
  4. 14
      Cargo.toml
  5. 17
      Makefile
  6. 16
      acceptance-tests/add_four_ints.asm
  7. 14
      acceptance-tests/add_four_ints.pgm
  8. 59
      acceptance-tests/main.py
  9. 1
      acceptance-tests/requirements.txt
  10. 33
      src/main.rs
  11. 9
      toy_cpu_4bit/Cargo.toml
  12. 347
      toy_cpu_4bit/src/assembler.rs
  13. 31
      toy_cpu_4bit/src/cpu.rs
  14. 2
      toy_cpu_4bit/src/instruction.rs
  15. 38
      toy_cpu_4bit/src/lib.rs
  16. 8
      toyasm/Cargo.toml
  17. 44
      toyasm/src/main.rs
  18. 8
      toyvm/Cargo.toml
  19. 48
      toyvm/src/main.rs

1
.gitignore

@ -1,2 +1,3 @@
target/
.idea/
.vscode/

2
ASM-SYNTAX.md

@ -92,6 +92,8 @@ register-half
register '.h'
```
Comments are introduced with `#`, like in Bash.
## Missing stuff
There are no labels.

260
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"

14
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",
]

17
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

16
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

14
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

59
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()

1
acceptance-tests/requirements.txt

@ -0,0 +1 @@
opencv-python

33
src/main.rs

@ -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();
}

9
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"

347
src/assembler.rs → 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<Instruction, Box<dyn Error>>;
type InstructionDecoder = fn(&str, &Regex) -> Result<Instruction, AssemblerErrorKind>;
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<i128>,
},
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<T: Into<i128> + Copy>(
number: T,
range: RangeInclusive<T>,
) -> 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<num::ParseIntError> 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<T> = Result<T, AssemblerError>;
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<dyn Error>> {
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<Instruction, Box<dyn Error>> {
fn line_to_insn(&self, line: &str) -> Result<Instruction, AssemblerErrorKind> {
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<u128, Box<dyn Error>> {
fn parse_u128(src: &str) -> Result<u128, num::ParseIntError> {
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<u128, Box<dyn Error>> {
}
}
fn parse_u8(src: &str) -> Result<u8, Box<dyn Error>> {
fn parse_u8(src: &str) -> Result<u8, AssemblerErrorKind> {
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<i8, Box<dyn Error>> {
fn parse_i8(src: &str) -> Result<i8, AssemblerErrorKind> {
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<i8, Box<dyn Error>> {
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<u4, Box<dyn Error>> {
fn parse_u4(src: &str) -> Result<u4, AssemblerErrorKind> {
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<u3, Box<dyn Error>> {
fn parse_u3(src: &str) -> Result<u3, AssemblerErrorKind> {
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<Register, Box<dyn Error>> {
fn parse_reg(src: &str) -> Result<Register, AssemblerErrorKind> {
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<dyn Error>> {
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<bool, Box<dyn Error>> {
fn parse_carry(src: &str) -> Result<bool, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_byte(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
Ok(decode(parse_u8(
regex.captures(line).unwrap().get(1).unwrap().as_str(),
)?))
}
fn parse_mov_reg_mem(line: &str, regex: &Regex) -> Result<Instruction, Box<dyn Error>> {
fn parse_mov_reg_mem(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_mov_mem_reg(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_mov_reg_half_imm(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_mov_reg_reg(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn E
(Register::R1, Register::R0) => 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<Instruction, Box<dyn Error>> {
) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_jmp(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
parse_jmp_impl(line, regex, false)
}
fn parse_jmpc(line: &str, regex: &Regex) -> Result<Instruction, Box<dyn Error>> {
fn parse_jmpc(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
parse_jmp_impl(line, regex, true)
}
@ -283,16 +474,16 @@ fn parse_ajmp_impl(
line: &str,
regex: &Regex,
use_carry: bool,
) -> Result<Instruction, Box<dyn Error>> {
) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_ajmp(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
parse_ajmp_impl(line, regex, false)
}
fn parse_ajmpc(line: &str, regex: &Regex) -> Result<Instruction, Box<dyn Error>> {
fn parse_ajmpc(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
parse_ajmp_impl(line, regex, true)
}
@ -301,7 +492,7 @@ fn parse_shift(
regex: &Regex,
right: bool,
mode: ShiftMode,
) -> Result<Instruction, Box<dyn Error>> {
) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_shr(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
parse_shift(line, regex, true, ShiftMode::Logical)
}
fn parse_shl(line: &str, regex: &Regex) -> Result<Instruction, Box<dyn Error>> {
fn parse_shl(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
parse_shift(line, regex, false, ShiftMode::Logical)
}
fn parse_rotr(line: &str, regex: &Regex) -> Result<Instruction, Box<dyn Error>> {
fn parse_rotr(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
parse_shift(line, regex, true, ShiftMode::Circular)
}
fn parse_rotl(line: &str, regex: &Regex) -> Result<Instruction, Box<dyn Error>> {
fn parse_rotl(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
parse_shift(line, regex, false, ShiftMode::Circular)
}
fn parse_inc(line: &str, regex: &Regex) -> Result<Instruction, Box<dyn Error>> {
fn parse_inc(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_dec(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_compl(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_setc(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_cload(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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<Instruction, Box<dyn Error>> {
fn parse_zero(line: &str, regex: &Regex) -> Result<Instruction, AssemblerErrorKind> {
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
);
}
}

31
src/cpu.rs → 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<u8>,
R0: Wrapping<u8>,
R1: Wrapping<u8>,
C: bool,
code: [u8; 0x100],
data: [u8; 0x10],
port_in: VecDeque<u8>,
port_out: VecDeque<u8>,
num_cycles: u64,
pub IP: Wrapping<u8>,
pub R0: Wrapping<u8>,
pub R1: Wrapping<u8>,
pub C: bool,
pub code: [u8; 0x100],
pub data: [u8; 0x10],
pub port_in: VecDeque<u8>,
pub port_out: VecDeque<u8>,
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]

2
src/instruction.rs → 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() {

38
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);
}
}
}

8
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"] }

44
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<String, Box<dyn Error>> {
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<dyn Error>> {
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
file.write_all(&code)?;
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse();
let code = Assembler::new().assemble(&read_code(args.input)?)?;
write_code(args.output, code)?;
Ok(())
}

8
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"] }

48
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<dyn Error>> {
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<dyn Error>> {
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(())
}
Loading…
Cancel
Save