progress: new assembler works, instruction set fully implemented with call/ret and push/pop, bf.dsa works which means the dsa assembly language works reliably. still a few bugs to fix. might be able to squeeze out a tiny bit more performance

This commit is contained in:
2026-03-09 03:24:20 +00:00
parent fc972b9b7b
commit e01a9f2808
63 changed files with 4975 additions and 1579 deletions
+17
View File
@@ -0,0 +1,17 @@
The DSA libraries form the foundation of the entire project, acting as a bridge between highlevel software and the specialized DSA hardware. Their primary responsibilities are:
1. **Instruction Set Core Library**
- Encapsulates all architectural primitives: opcodes, operand formats, and execution semantics.
- Supplies utility functions to build, validate, and serialize instruction streams.
2. **Assembler Support**
- Translates DSA assembly language into binary instruction packets that the hardware can execute.
- Implements directives for sectioning, alignment, and relocation handling.
3. **HighLevel Language Compiler (DSC) Runtime**
- Generates DSA bytecode from DSC source files, leveraging the core library for instruction encoding.
- Offers debugging hooks, profiling counters, and exception handling mechanisms.
4. **Testing & Verification Utilities**
- Contains unit tests that run against both emulated and real hardware to ensure correctness.
- Provides logging facilities for lowlevel bus transactions and performance metrics.
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "assembler_old"
version.workspace = true
edition.workspace = true
authors.workspace = true
[[bin]]
name = "dsa-a"
path = "src/main.rs"
[lib]
name = "assembler"
path = "src/lib.rs"
[dependencies]
common = { path = "../common" }
num_cpus = "1.17.0"
threadpool = "1.8.1"
+399
View File
@@ -0,0 +1,399 @@
use common::prelude::*;
use crate::assembler::Token;
use crate::assembler::model::{Node, Opcode};
use crate::{assembler::AssembleError, expect_token};
fn log(message: &str) {
println!("\x1b[32mINFO:\x1b[0m {message}");
}
pub fn codegen(nodes: Vec<Node>) -> Result<Vec<Instruction>, AssembleError> {
let mut instructions = vec![];
for node in nodes {
println!("{:?}", node);
instructions.push(build_instruction(&node)?);
}
log("Assembly Successful ✅");
Ok(instructions)
}
fn build_instruction(node: &Node) -> Result<Instruction, AssembleError> {
let opcode = node.opcode();
let args = node.args();
match opcode {
Opcode::Nop => Ok(Instruction::nop()),
Opcode::Mov => ins_mov(opcode, &args),
Opcode::CMov => ins_cmov(opcode, &args),
Opcode::Ldb
| Opcode::Ldw
| Opcode::Ldh
| Opcode::Ldbs
| Opcode::Ldhs
| Opcode::Stb
| Opcode::Stw
| Opcode::Sth => ins_ldx_stx(opcode, &args),
Opcode::Lli | Opcode::Lui => ins_load_imm(opcode, &args),
Opcode::Push | Opcode::Pop => ins_stack(opcode, &args),
Opcode::Ieq | Opcode::Ine | Opcode::Igt | Opcode::Ige | Opcode::Ile | Opcode::Ilt => {
ins_comparison(opcode, &args)
}
Opcode::Jmp | Opcode::Call | Opcode::Jic | Opcode::Jnc => {
ins_jump_unconditional(opcode, &args)
}
Opcode::Jez | Opcode::Jnz => ins_jump_conditional(opcode, &args),
Opcode::Shl | Opcode::Shr => ins_bitshift(opcode, &args),
Opcode::Add
| Opcode::Sub
| Opcode::And
| Opcode::Or
| Opcode::Xor
| Opcode::Nand
| Opcode::Nor
| Opcode::Xnor => ins_arithmetic(opcode, &args),
Opcode::AddI | Opcode::SubI => ins_imm_arithmetic(opcode, &args),
Opcode::Not => ins_not(&args),
Opcode::Int => ins_interrupt(&args),
Opcode::Ret => Ok(Instruction::ret()),
Opcode::IRet => Ok(Instruction::irt()),
Opcode::Hlt => Ok(Instruction::hlt()),
Opcode::Data => build_data_instruction(&args),
Opcode::Segment => build_segment_instruction(&args),
Opcode::Db
| Opcode::Dh
| Opcode::Dw
| Opcode::Resb
| Opcode::Resh
| Opcode::Resw
| Opcode::Lwi
| Opcode::Include
| Opcode::Func
| Opcode::Return => Err(AssembleError::InvalidArg),
}
}
fn ins_mov(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(src_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(dest_token) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let src = expect_token!(src_token, Register)?;
let dest = expect_token!(dest_token, Register)?;
match opcode {
Opcode::Mov => Ok(Instruction::mov(src, dest)),
_ => unreachable!(),
}
}
fn ins_cmov(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(src_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(dest_token) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let Some(cmp_token) = args.get(2) else {
return Err(AssembleError::MissingArgument(1));
};
let src = expect_token!(src_token, Register)?;
let dest = expect_token!(dest_token, Register)?;
let cmp = expect_token!(cmp_token, Register)?;
match opcode {
Opcode::CMov => Ok(Instruction::cmov(src, dest, cmp)),
_ => unreachable!(),
}
}
fn ins_ldx_stx(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(src_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(dest_token) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let Some(offset_token) = args.get(2) else {
return Err(AssembleError::MissingArgument(2));
};
let src = expect_token!(src_token, Register)?;
let dest = expect_token!(dest_token, Register)?;
let offset = expect_token!(offset_token, Immediate)?;
match opcode {
Opcode::Ldb => Ok(Instruction::ldb(src, dest, offset as u16)),
Opcode::Ldbs => Ok(Instruction::ldbs(src, dest, offset as u16)),
Opcode::Ldh => Ok(Instruction::ldh(src, dest, offset as u16)),
Opcode::Ldhs => Ok(Instruction::ldhs(src, dest, offset as u16)),
Opcode::Ldw => Ok(Instruction::ldw(src, dest, offset as u16)),
Opcode::Stb => Ok(Instruction::stb(src, dest, offset as u16)),
Opcode::Sth => Ok(Instruction::sth(src, dest, offset as u16)),
Opcode::Stw => Ok(Instruction::stw(src, dest, offset as u16)),
_ => unreachable!(),
}
}
fn ins_stack(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(reg_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let reg = expect_token!(reg_token, Register)?;
match opcode {
Opcode::Push => Ok(Instruction::push(reg)),
Opcode::Pop => Ok(Instruction::pop(reg)),
_ => unreachable!(),
}
}
fn ins_load_imm(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(value_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(dest_token) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let value = expect_token!(value_token, Immediate)?;
let dest = expect_token!(dest_token, Register)?;
match opcode {
Opcode::Lli => Ok(Instruction::lli(dest, value as u16)),
Opcode::Lui => Ok(Instruction::lui(dest, (value >> 16) as u16)),
_ => unreachable!(),
}
}
fn ins_jump_unconditional(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(addr_imm_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
// addr_reg is optional; default to Register::Zero if missing
let addr_reg_token = args.get(1).unwrap_or(&Token::Register(Register::Zero));
let addr_imm = expect_token!(addr_imm_token, Immediate)?;
let addr_reg = expect_token!(addr_reg_token, Register)?;
match opcode {
Opcode::Jmp => Ok(Instruction::jmp(addr_reg, addr_imm as u16)),
Opcode::Jic => Ok(Instruction::jic(addr_reg, addr_imm as u16)),
Opcode::Jnc => Ok(Instruction::jnc(addr_reg, addr_imm as u16)),
Opcode::Call => Ok(Instruction::call(addr_reg, addr_imm as u16)),
_ => unreachable!(),
}
}
fn ins_jump_conditional(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(condition_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(addr_imm_token) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let addr_reg_token = if let Some(token) = args.get(2) {
token
} else {
&Token::Register(Register::Zero)
};
let condition = expect_token!(condition_token, Register)?;
let addr_imm = expect_token!(addr_imm_token, Immediate)?;
let addr_reg = expect_token!(addr_reg_token, Register)?;
Ok(match opcode {
Opcode::Jez => Instruction::jez(condition, addr_reg, addr_imm as u16),
Opcode::Jnz => Instruction::jnz(condition, addr_reg, addr_imm as u16),
_ => unreachable!(),
})
}
fn ins_comparison(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(left_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(right_token) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let Some(dest_token) = args.get(2) else {
return Err(AssembleError::MissingArgument(2));
};
let left = expect_token!(left_token, Register)?;
let right = expect_token!(right_token, Register)?;
let dest = expect_token!(dest_token, Register)?;
Ok(match opcode {
Opcode::Ieq => Instruction::ieq(left, right, dest),
Opcode::Ine => Instruction::ine(left, right, dest),
Opcode::Igt => Instruction::igt(left, right, dest),
Opcode::Ige => Instruction::ige(left, right, dest),
Opcode::Ile => Instruction::ile(left, right, dest),
Opcode::Ilt => Instruction::ilt(left, right, dest),
_ => unreachable!(),
})
}
fn ins_bitshift(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(src_reg) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(r_shamt) = args.get(1) else {
return Err(AssembleError::MissingArgument(0));
};
let Some(i_shamt) = args.get(2) else {
return Err(AssembleError::MissingArgument(1));
};
let Some(dest_reg) = args.get(3) else {
return Err(AssembleError::MissingArgument(1));
};
let src = expect_token!(src_reg, Register)?;
let r_shamt = expect_token!(r_shamt, Register)?;
let i_shamt = expect_token!(i_shamt, Immediate)? as u8;
let dest = expect_token!(dest_reg, Register)?;
Ok(match opcode {
Opcode::Shl => Instruction::shl(src, r_shamt, dest, i_shamt),
Opcode::Shr => Instruction::shr(src, r_shamt, dest, i_shamt),
_ => unreachable!(),
})
}
fn ins_arithmetic(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(left_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(right_token) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let Some(dest_token) = args.get(2) else {
return Err(AssembleError::MissingArgument(2));
};
let left = expect_token!(left_token, Register)?;
let right = expect_token!(right_token, Register)?;
let dest = expect_token!(dest_token, Register)?;
Ok(match opcode {
Opcode::Add => Instruction::add(left, right, dest),
Opcode::Sub => Instruction::sub(left, right, dest),
Opcode::And => Instruction::and(left, right, dest),
Opcode::Or => Instruction::or(left, right, dest),
Opcode::Xor => Instruction::xor(left, right, dest),
Opcode::Nand => Instruction::nand(left, right, dest),
Opcode::Nor => Instruction::nor(left, right, dest),
Opcode::Xnor => Instruction::xnor(left, right, dest),
_ => unreachable!(),
})
}
fn ins_imm_arithmetic(
opcode: Opcode,
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(reg_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(immediate_token) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let Some(dest_token) = args.get(2) else {
return Err(AssembleError::MissingArgument(2));
};
let reg = expect_token!(reg_token, Register)?;
let immediate = expect_token!(immediate_token, Immediate)? as u16;
let dest = expect_token!(dest_token, Register)?;
Ok(match opcode {
Opcode::AddI => Instruction::addi(reg, dest, immediate),
Opcode::SubI => Instruction::subi(reg, dest, immediate),
_ => unreachable!(),
})
}
fn ins_not(args: &[crate::assembler::model::Token]) -> Result<Instruction, AssembleError> {
let Some(reg_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(dest_token) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let src = expect_token!(reg_token, Register)?;
let dest = expect_token!(dest_token, Register)?;
Ok(Instruction::not(src, dest))
}
fn ins_interrupt(args: &[crate::assembler::model::Token]) -> Result<Instruction, AssembleError> {
let Some(code_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let code = expect_token!(code_token, Immediate)? as u8;
Ok(Instruction::int(code))
}
fn build_data_instruction(
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(immediate_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let immediate = expect_token!(immediate_token, Immediate)?;
Ok(Instruction::data(immediate))
}
fn build_segment_instruction(
args: &[crate::assembler::model::Token],
) -> Result<Instruction, AssembleError> {
let Some(immediate_token) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let immediate = expect_token!(immediate_token, Immediate)?;
Ok(Instruction::data(
// mask the first 6 bits for the opcode
// this is really deprecated tbh. may remove in future.
Opcode::Segment as u32 | (immediate & 0x05FFFFFF),
))
}
+252
View File
@@ -0,0 +1,252 @@
use common::prelude::Register;
use crate::assembler::model::{Node, Opcode, Token};
use crate::{assembler::AssembleError, expect_token, expect_type, node};
pub fn expand_pseudo_ops(mut nodes: Vec<Node>, module: u64) -> Result<Vec<Node>, AssembleError> {
let mut result = Vec::<Node>::with_capacity(nodes.len());
for node in &mut nodes {
if try_expand(node.clone(), &mut result, module).is_err() {
result.push(node.clone());
}
}
Ok(result)
}
fn try_expand(node: Node, result: &mut Vec<Node>, _module: u64) -> Result<(), AssembleError> {
match node.opcode() {
Opcode::Func => expand_func(&node, result),
Opcode::Return => expand_return(&node, result),
Opcode::Ldb | Opcode::Ldbs | Opcode::Ldh | Opcode::Ldhs | Opcode::Ldw => {
expand_ldx(&node, result)?;
}
Opcode::Stb | Opcode::Sth | Opcode::Stw => expand_stx(&node, result)?,
Opcode::Lwi => expand_lwi(&node, result)?,
Opcode::Resb | Opcode::Resh | Opcode::Resw => expand_resx(&node, result)?,
Opcode::Db | Opcode::Dh | Opcode::Dw => expand_dx(&node, result)?,
_ => result.push(node),
}
Ok(())
}
/// Function stack frame initialisation
fn expand_func(current: &Node, nodes: &mut Vec<Node>) {
let label = current.label();
let spr = Token::Register(Register::Spr);
let bpr = Token::Register(Register::Bpr);
nodes.extend(vec![
node!(label, Opcode::Push, bpr),
node!(None, Opcode::Mov, spr, bpr),
]);
}
/// Return from a function
fn expand_return(current: &Node, nodes: &mut Vec<Node>) {
let label = current.label();
let spr = Token::Register(Register::Spr);
let ret = Token::Register(Register::Ret);
let bpr = Token::Register(Register::Bpr);
nodes.extend(vec![
node!(label, Opcode::Mov, bpr, spr),
node!(None, Opcode::Pop, bpr),
node!(None, Opcode::Ret),
]);
}
fn expand_ldx(current: &Node, nodes: &mut Vec<Node>) -> Result<(), AssembleError> {
let opcode = current.opcode();
let args: Vec<Token> = current.args().into_iter().take(3).collect();
let Some(name) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(reg) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let Some(offset) = args.get(2) else {
return Err(AssembleError::MissingArgument(2));
};
let name = expect_type!(name, Symbol)?;
let reg = expect_type!(reg, Register)?;
let offset = expect_type!(offset, Immediate)?;
nodes.extend(vec![
node!(current.label(), Opcode::Lli, name, reg),
node!(None, Opcode::Lui, name, reg),
node!(None, opcode, reg, reg, offset),
]);
Ok(())
}
fn expand_stx(current: &Node, nodes: &mut Vec<Node>) -> Result<(), AssembleError> {
let opcode = current.opcode();
let args: Vec<Token> = current.args().into_iter().take(3).collect();
let Some(base) = args.first() else {
return Err(AssembleError::MissingArgument(0));
};
let Some(dest) = args.get(1) else {
return Err(AssembleError::MissingArgument(1));
};
let Some(offset) = args.get(2) else {
return Err(AssembleError::MissingArgument(2));
};
let base = expect_type!(base, Register)?;
let dest = expect_type!(dest, Symbol)?;
let offset = expect_type!(offset, Immediate)?;
let temp = Token::Register(Register::Acc);
nodes.extend(vec![
node!(current.label(), Opcode::Lli, dest, temp),
node!(None, Opcode::Lui, dest, temp),
node!(None, opcode, base, temp, offset),
]);
Ok(())
}
fn expand_lwi(current: &Node, nodes: &mut Vec<Node>) -> Result<(), AssembleError> {
let Ok(val) = current.arg(0) else {
return Err(AssembleError::MissingArgument(0));
};
let Ok(reg) = current.arg(1) else {
return Err(AssembleError::MissingArgument(1));
};
let val = expect_type!(val, Symbol, Immediate)?;
let reg = expect_type!(reg, Register)?;
nodes.extend(vec![
node!(current.label(), Opcode::Lli, val, reg),
node!(None, Opcode::Lui, val, reg),
]);
Ok(())
}
fn expand_resx(current: &Node, nodes: &mut Vec<Node>) -> Result<(), AssembleError> {
let Ok(region_label) = current.arg(0) else {
return Err(AssembleError::MissingArgument(0));
};
let Ok(size) = current.arg(1) else {
return Err(AssembleError::MissingArgument(1));
};
let region_label = expect_token!(region_label, Symbol)?;
let size = expect_token!(size, Immediate)?;
let units_per = match current.opcode() {
Opcode::Resb => 4,
Opcode::Resh => 2,
Opcode::Resw => 1,
_ => unreachable!(),
};
let mut buffer = vec![];
// push the inital node with the label
for _ in 0..size.div_ceil(units_per) {
// push the rest of the nodes
buffer.push(node!(None, Opcode::Data, 0));
}
buffer[0].symbol = Some(region_label);
nodes.extend(buffer);
Ok(())
}
fn expand_dx(current: &Node, nodes: &mut Vec<Node>) -> Result<(), AssembleError> {
let Ok(region_label) = current.arg(0) else {
return Err(AssembleError::MissingArgument(0));
};
let region_label = expect_token!(region_label, Symbol)?;
let size = match current.opcode() {
Opcode::Db => 4,
Opcode::Dh => 2,
Opcode::Dw => 1,
_ => unreachable!(),
};
let mut buffer = vec![];
let mut args = current.args();
let _label = args.remove(0);
for word in process_dx_data(args, size)? {
buffer.push(node!(None, Opcode::Data, Token::Immediate(word)));
}
buffer[0].symbol = Some(region_label);
nodes.extend(buffer);
Ok(())
}
fn process_dx_data(args: Vec<Token>, size: usize) -> Result<Vec<u32>, AssembleError> {
assert!(matches!(size, 1 | 2 | 4));
let mut buffer = Vec::<u8>::new();
// Process each token
for token in args {
match token {
Token::StringLit(mut s) => {
s.push('\0');
// Split string into chars and write as bytes
for ch in s.chars() {
// Convert char to bytes (UTF-8 encoding)
let mut char_buf = [0u8; 4];
let char_bytes = ch.encode_utf8(&mut char_buf);
buffer.extend_from_slice(char_bytes.as_bytes());
}
}
Token::Immediate(value) => {
// Split u32 into bytes (little-endian)
buffer.extend_from_slice(&value.to_le_bytes());
}
_ => {
return Err(AssembleError::Generic);
}
}
// Pad buffer to alignment boundary with zeros
let remainder = buffer.len() % size;
if remainder != 0 {
let padding = size - remainder;
buffer.resize(buffer.len() + padding, 0);
}
}
// Convert byte buffer to u32 chunks
// Pad final buffer to u32 boundary if needed
let remainder = buffer.len() % 4;
if remainder != 0 {
let padding = 4 - remainder;
buffer.resize(buffer.len() + padding, 0);
}
// Convert bytes to u32s efficiently using chunks_exact
let result = buffer
.chunks_exact(4)
.map(|chunk| {
// Convert 4 bytes to u32 (little-endian)
u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])
})
.collect();
Ok(result)
}
+167
View File
@@ -0,0 +1,167 @@
use std::str::FromStr;
use crate::assembler::AssembleError;
use crate::assembler::model::{Module, Opcode, Symbol, Token};
use common::prelude::Register;
pub fn lexer(mut program: String, module: u64) -> Result<Vec<Token>, AssembleError> {
let mut tokens = Vec::new();
let lines = program.lines();
let mut literal = String::new();
for line in lines {
for (i, token) in line.split_whitespace().enumerate() {
if token.starts_with("//") {
break;
}
if let Some(stripped) = token.strip_prefix('"') {
literal.push_str(stripped);
}
if !literal.is_empty() {
if !token.starts_with('"') {
if i > 0 {
literal.push(' ');
}
literal.push_str(token);
}
if token.ends_with('"') {
literal.pop(); // remove the closing quote
tokens.push(Token::StringLit(literal));
literal = String::new();
}
continue;
}
let token = token.trim_end_matches(',');
if token.is_empty() {
continue;
}
if let Some(token) = parse_register(token)? {
tokens.push(token);
} else if let Some(token) = parse_opcode(token)? {
tokens.push(token);
} else if let Some(token) = parse_hex(token)? {
tokens.push(token);
} else if let Some(token) = parse_octal(token)? {
tokens.push(token);
} else if let Some(token) = parse_binary(token)? {
tokens.push(token);
} else if let Some(token) = parse_decimal(token)? {
tokens.push(token);
} else if let Some(token) = parse_label(token, module)? {
tokens.push(token);
} else if let Some(token) = parse_symbol(token, module)? {
tokens.push(token);
} else {
return Err(AssembleError::Generic);
}
}
}
// println!("{:#?}", tokens);
Ok(tokens)
}
pub fn parse_register(token: &str) -> Result<Option<Token>, AssembleError> {
Ok(Register::from_str(token).map(Token::Register).ok())
}
pub fn parse_opcode(token: &str) -> Result<Option<Token>, AssembleError> {
Ok(Opcode::from_str(token).ok().map(Token::Opcode))
}
pub fn parse_hex(token: &str) -> Result<Option<Token>, AssembleError> {
if (token.len() < 3) | !token.starts_with("0x") {
return Ok(None);
}
let Some(lit) = &token.get(2..) else {
return Err(AssembleError::InvalidArg);
};
u32::from_str_radix(lit, 16).map_or(Err(AssembleError::Generic), |value| {
Ok(Some(Token::Immediate(value)))
})
}
pub fn parse_octal(token: &str) -> Result<Option<Token>, AssembleError> {
if (token.len() < 3) | !token.starts_with("0o") {
return Ok(None);
}
let Some(lit) = &token.get(2..) else {
return Err(AssembleError::InvalidArg);
};
u32::from_str_radix(lit, 8).map_or(Err(AssembleError::Generic), |value| {
Ok(Some(Token::Immediate(value)))
})
}
pub fn parse_binary(token: &str) -> Result<Option<Token>, AssembleError> {
if (token.len() < 3) | !token.starts_with("0b") {
return Ok(None);
}
let Some(lit) = &token.get(2..) else {
return Err(AssembleError::InvalidArg);
};
u32::from_str_radix(lit, 2).map_or(Err(AssembleError::Generic), |value| {
Ok(Some(Token::Immediate(value)))
})
}
pub fn parse_decimal(token: &str) -> Result<Option<Token>, AssembleError> {
let Ok(tok) = token.parse::<u32>() else {
return Ok(None);
};
Ok(Some(Token::Immediate(tok)))
}
pub fn parse_label(token: &str, module: u64) -> Result<Option<Token>, AssembleError> {
if token.ends_with(':') {
Ok(Some(Token::Symbol(Symbol {
name: token[0..token.len() - 1].to_string(),
module: Module::Resolved(module),
})))
} else {
Ok(None)
}
}
pub fn parse_symbol(token: &str, module: u64) -> Result<Option<Token>, AssembleError> {
let Some(tokc) = token.chars().next() else {
return Err(AssembleError::Generic); // TODO: What is this error?
};
if tokc.is_numeric() {
return Ok(None);
}
let mut split = token.splitn(2, "::");
let Some(symbol1) = split.next() else {
return Err(AssembleError::InvalidArg);
};
let symbol1 = symbol1.to_string();
if let Some(symbol2) = split.next() {
Ok(Some(Token::Symbol(Symbol {
name: symbol2.to_string(),
module: Module::Unresolved(symbol1),
})))
} else {
Ok(Some(Token::Symbol(Symbol {
name: symbol1,
module: Module::Resolved(module),
})))
}
}
+139
View File
@@ -0,0 +1,139 @@
//! Macros used throughout the assembler
use crate::assembler::model::{Node, Opcode, Symbol, Token};
/// Parse DSA assembly code with optional formatting
///
/// # Examples
/// ```rs
/// use assembler::macros::dsa;
/// // With formatting:
/// let nodes = dsa!(hash, "mov r1, {}", 42)?;
///
/// // Without formatting:
/// let nodes = dsa!(hash, "mov r1, 42")?;
/// ```
#[macro_export]
macro_rules! dsa {
// Version with formatting arguments
($hash:expr, $input:expr, $($args:expr),+) => {{
let input = format!($input, $($args),+);
let tokens = $crate::lexer::lexer(input, $hash)?;
let parsed = $crate::parser::Parser::parse_nodes(tokens)?;
parsed
}};
// Version without formatting
($hash:expr, $input:expr) => {{
let input = String::from($input);
let tokens = $crate::lexer::lexer(input, $hash)?;
let parsed = $crate::parser::Parser::parse_nodes(tokens)?;
parsed
}};
}
/// Creates a new Node with the given symbol, opcode, and tokens
#[macro_export]
macro_rules! node {
($symbol: expr, $opcode: expr, args: $tokens: expr) => {
$crate::assembler::model::Node::new($symbol.clone(), $opcode.clone(), $tokens.clone())
};
($symbol: expr, $opcode: expr, $($tokens: expr),+) => {
$crate::assembler::model::Node::new(
$symbol.clone(),
$opcode.clone(),
vec![$(node!(@convert_token $tokens)),+]
)
};
($symbol: expr, $opcode: expr) => {
$crate::assembler::model::Node::new(
$symbol.clone(),
$opcode.clone(),
Vec::new()
)
};
(@convert_token $token: literal) => {
$crate::assembler::model::Token::Immediate($token)
};
(@convert_token $token: expr) => {
$token.clone()
};
}
/// Extracts a specific token type from a token
#[macro_export]
macro_rules! expect_token {
($token:expr, Symbol) => {
match $token {
$crate::assembler::model::Token::Symbol(value) => Ok(value.clone()),
other => Err($crate::assembler::AssembleError::UnexpectedToken(
other.clone(),
$crate::assembler::model::TokenType::Symbol,
)),
}
};
($token:expr, Register) => {
match $token {
$crate::assembler::model::Token::Register(value) => Ok(value.clone()),
other => Err($crate::assembler::AssembleError::UnexpectedToken(
other.clone(),
$crate::assembler::model::TokenType::Register,
)),
}
};
($token:expr, Immediate) => {
match $token {
$crate::assembler::model::Token::Immediate(value) => Ok(value.clone()),
other => Err($crate::assembler::AssembleError::UnexpectedToken(
other.clone(),
$crate::assembler::model::TokenType::Immediate,
)),
}
};
($token:expr, StringLit) => {
match $token {
$crate::assembler::model::Token::StringLit(value) => Ok(value.clone()),
other => Err($crate::assembler::AssembleError::UnexpectedToken(
other.clone(),
$crate::assembler::model::TokenType::StringLit,
)),
}
};
($token:expr, Opcode) => {
match $token {
$crate::assembler::model::Token::Opcode(value) => Ok(value.clone()),
other => Err($crate::assembler::AssembleError::UnexpectedToken(
other.clone(),
$crate::assembler::model::TokenType::Opcode,
)),
}
};
}
/// Checks if a token matches any of the specified types
#[macro_export]
macro_rules! expect_type {
($token:expr, $($variant:ident),+) => {{
let token = $token;
match &token {
$(
$crate::assembler::model::Token::$variant(_) => Ok(token.clone()),
)+
other => {
let expected_type = expect_type!(@get_first_type $($variant),+);
Err($crate::assembler::AssembleError::UnexpectedToken(
other.clone().clone(),
expected_type,
))
}
}
}};
(@get_first_type Symbol $(, $rest:ident)*) => { $crate::assembler::model::TokenType::Symbol };
(@get_first_type Register $(, $rest:ident)*) => { $crate::assembler::model::TokenType::Register };
(@get_first_type Immediate $(, $rest:ident)*) => { $crate::assembler::model::TokenType::Immediate };
(@get_first_type StringLit $(, $rest:ident)*) => { $crate::assembler::model::TokenType::StringLit };
(@get_first_type Opcode $(, $rest:ident)*) => { $crate::assembler::model::TokenType::Opcode };
}
+263
View File
@@ -0,0 +1,263 @@
#![allow(dead_code, unused)]
use std::{
collections::HashSet,
fmt, fs,
hash::{DefaultHasher, Hash, Hasher},
path::{Path, PathBuf},
sync::{
Arc, Mutex,
mpsc::{self, Receiver, Sender},
},
thread,
};
use common::prelude::Instruction;
// Module declarations
#[macro_use]
pub mod macros;
#[allow(clippy::module_inception)]
pub mod codegen;
pub mod expand;
pub mod lexer;
pub mod model;
pub mod parser;
pub mod resolver;
// Re-exports
pub use self::{
codegen::codegen,
expand::expand_pseudo_ops,
lexer::lexer,
model::{Module, Node, Opcode, Symbol, Token, TokenType},
parser::{Parser, Program},
resolver::{create_sections, resolve_dependencies, resolve_symbols},
};
pub struct Assembler {
src_path: PathBuf,
result_tx: mpsc::Sender<Result<Vec<u8>, AssembleError>>,
result_rx: Option<mpsc::Receiver<Result<Vec<u8>, AssembleError>>>,
is_running: bool,
}
impl Assembler {
#[must_use]
pub fn new(src_path: impl Into<PathBuf>) -> Self {
let (tx, rx) = mpsc::channel();
Self {
src_path: src_path.into(),
result_tx: tx,
result_rx: Some(rx),
is_running: false,
}
}
/// Start the compilation process in a separate thread
pub fn start(&mut self, args: ()) {
if self.is_running {
return;
}
let src = self.src_path.clone();
let tx = self.result_tx.clone();
thread::spawn(move || match assemble(&src) {
Ok(res) => {
let buffer: Vec<u8> = res
.iter()
.flat_map(|instruction| instruction.to_le_bytes())
.collect();
tx.send(Ok(buffer))
.expect("Failed to send compilation result from worker thread");
}
Err(err) => {
tx.send(Err(err))
.expect("Failed to send compilation error from worker thread");
}
});
self.is_running = true;
}
pub fn poll(&mut self) -> Option<Result<Vec<u8>, String>> {
if !self.is_running {
return None;
}
match self
.result_rx
.as_ref()
.expect("result_rx should be Some while compilation is running")
.try_recv()
{
Ok(result) => {
self.is_running = false;
Some(result.map_err(|e| e.to_string()))
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
self.is_running = false;
Some(Err(String::from(
"Compilation terminated before a result was returned",
)))
}
}
}
/// Block until compilation is complete and return the result
pub fn output(&mut self) -> Result<Vec<u8>, String> {
if let Ok(result) = self
.result_rx
.take()
.expect("result_rx should be Some while waiting for compilation result")
.recv()
{
self.is_running = false;
result.map_err(|e| e.to_string())
} else {
self.is_running = false;
Err(String::from(
"Compilation terminated before a result was returned",
))
}
}
}
impl Assembler {}
fn assemble(src: &Path) -> Result<Vec<Instruction>, AssembleError> {
let mut modules = HashSet::new();
let mut program = Program::new();
let hash = quick_hash(src);
if modules.contains(&hash) {
return Ok(vec![]);
}
prepare_dependency(src, &mut modules, &mut program)?;
let mut nodes = program.nodes.clone();
create_sections(&mut nodes)?;
resolve_symbols(&mut nodes)?;
println!("Generating assembly output...");
let instructions = codegen(nodes)?;
println!("Compilation Successful");
Ok(instructions)
}
fn prepare_dependency(
path: &Path,
modules: &mut HashSet<u64>,
program: &mut Program,
) -> Result<(), AssembleError> {
let filename = path
.file_name()
.and_then(|n| n.to_str())
.expect("Failed to get file name from path");
if let Ok(path) = path.canonicalize() {
println!("{:20} {:20} [{}]", "Building", filename, path.display());
}
let src =
fs::read_to_string(path).map_err(|_| AssembleError::InvalidFile(path.to_path_buf()))?;
let file_hash = quick_hash(path);
println!("{:20} {:20}", "Tokenising", filename);
let tokens = lexer::lexer(src, file_hash)?;
println!("{:20} {:20}", "Parsing", filename);
let parsed = Parser::parse_nodes(tokens)?;
println!("{:20} {:20}", "Resolving Deps", filename);
// Get the parent directory of the source file to use as the base directory
let base_dir = path
.parent()
.ok_or_else(|| AssembleError::InvalidFile(path.to_path_buf()))?;
let mut nodes = expand_pseudo_ops(parsed, file_hash)?;
nodes = resolve_dependencies(nodes, base_dir)?;
let deps = Parser::get_dependencies(&nodes, path)?;
println!("{:20} {:20}", "Expanding Pseudo-ops", filename);
// add a section instruction
nodes.insert(
0,
node!(None, Opcode::Segment, Token::Immediate(file_hash as u32)),
);
// for n in &nodes {
// println!("{n}");
// }
program.add_module(nodes);
for dep in deps {
println!(
"{:20} {:20}",
"Including",
dep.file_name()
.and_then(|f| f.to_str())
.expect("Dependency path has no file name or is not valid UTF-8")
);
let dep_hash = quick_hash(&dep);
if modules.insert(dep_hash) {
prepare_dependency(dep.as_path(), modules, program)?;
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub enum AssembleError {
Generic,
UnexpectedEof,
InvalidFile(PathBuf),
UnexpectedToken(Token, TokenType),
InvalidArg,
UndefinedSymbol(Symbol),
/// Contains the nth element missing from the instruction.
MissingArgument(u8),
}
impl fmt::Display for AssembleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Generic => write!(f, "Generic error"),
Self::UnexpectedToken(tok, expected) => {
write!(f, "Unexpected token {tok:?}, expected {expected:?}")
}
Self::UnexpectedEof => write!(f, "Unexpected end of file"),
Self::InvalidFile(path) => write!(f, "Invalid file `{}`", path.display()),
Self::InvalidArg => write!(f, "Invalid argument"),
Self::UndefinedSymbol(symbol) => {
write!(f, "Undefined symbol {symbol}")
}
Self::MissingArgument(n) => {
write!(f, "Missing argument #{n} from instruction arguments.")
}
}
}
}
fn quick_hash(value: &Path) -> u64 {
let mut hasher = DefaultHasher::new();
value
.canonicalize()
.expect("Failed to canonicalize path for quick_hash")
.to_str()
.hash(&mut hasher);
hasher.finish()
}
+480
View File
@@ -0,0 +1,480 @@
use std::{fmt, str::FromStr};
use common::prelude::Register;
use crate::assembler::AssembleError;
#[derive(Debug, Clone)]
pub struct Node {
pub symbol: Option<Symbol>,
pub opcode: Opcode,
pub tokens: Vec<Token>,
}
impl Node {
#[must_use]
pub const fn new(symbol: Option<Symbol>, opcode: Opcode, tokens: Vec<Token>) -> Self {
Self {
symbol,
opcode,
tokens,
}
}
#[must_use]
pub fn label(&self) -> Option<Symbol> {
self.symbol.clone()
}
#[must_use]
pub const fn opcode(&self) -> Opcode {
self.opcode
}
#[must_use]
pub fn args(&self) -> Vec<Token> {
self.tokens.clone()
}
pub fn arg(&self, index: usize) -> Result<Token, AssembleError> {
self.args()
.get(index)
.cloned()
.ok_or(AssembleError::InvalidArg)
}
}
impl fmt::Display for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let symbol = self
.label()
.as_ref()
.map_or_else(String::new, |symbol| format!("{symbol}:\n"));
let args = self
.args()
.into_iter()
.map(|arg| arg.to_string())
.collect::<Vec<_>>()
.join(" ");
write!(
f,
"\x1b[93m{} \t\x1b[94m{} \x1b[37m{} \x1b[0m",
symbol,
self.opcode(),
args,
)
}
}
impl fmt::Display for Symbol {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} [ID:{}]", self.name, self.module)
}
}
impl fmt::Display for Module {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Unresolved(name) => write!(f, "{name}"),
Self::Resolved(name) => write!(f, "{name}"),
}
}
}
impl fmt::Display for Opcode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Nop => write!(f, "nop"),
Self::Mov => write!(f, "mov"),
Self::CMov => write!(f, "movs"),
Self::Ldb => write!(f, "ldb"),
Self::Ldbs => write!(f, "ldbs"),
Self::Ldh => write!(f, "ldh"),
Self::Ldhs => write!(f, "ldhs"),
Self::Ldw => write!(f, "ldw"),
Self::Stb => write!(f, "stb"),
Self::Sth => write!(f, "sth"),
Self::Stw => write!(f, "stw"),
Self::Lli => write!(f, "lli"),
Self::Lui => write!(f, "lui"),
Self::Jmp => write!(f, "jmp"),
Self::Jez => write!(f, "jez"),
Self::Jnz => write!(f, "jnz"),
Self::Jic => write!(f, "jic"),
Self::Jnc => write!(f, "jnc"),
Self::Ieq => write!(f, "ieq"),
Self::Ine => write!(f, "ine"),
Self::Igt => write!(f, "igt"),
Self::Ige => write!(f, "ige"),
Self::Ilt => write!(f, "ilt"),
Self::Ile => write!(f, "ile"),
Self::Shl => write!(f, "shl"),
Self::Shr => write!(f, "shr"),
Self::Add => write!(f, "add"),
Self::Sub => write!(f, "sub"),
Self::AddI => write!(f, "addi"),
Self::SubI => write!(f, "subi"),
Self::And => write!(f, "and"),
Self::Or => write!(f, "or"),
Self::Not => write!(f, "not"),
Self::Xor => write!(f, "xor"),
Self::Nand => write!(f, "nand"),
Self::Nor => write!(f, "nor"),
Self::Xnor => write!(f, "xnor"),
Self::Int => write!(f, "int"),
Self::IRet => write!(f, "irt"),
Self::Hlt => write!(f, "hlt"),
Self::Db => write!(f, "db"),
Self::Dh => write!(f, "dh"),
Self::Dw => write!(f, "dw"),
Self::Resb => write!(f, "resb"),
Self::Resh => write!(f, "resh"),
Self::Resw => write!(f, "resw"),
Self::Push => write!(f, "push"),
Self::Pop => write!(f, "pop"),
Self::Lwi => write!(f, "lwi"),
Self::Func => write!(f, "func"),
Self::Call => write!(f, "call"),
Self::Ret => write!(f, "ret"),
Self::Return => write!(f, "return"),
Self::Include => write!(f, "include"),
Self::Data => write!(f, "data"),
Self::Segment => write!(f, "[SEGMENT]"),
}
}
}
#[derive(Debug, Clone, Eq)]
pub struct Symbol {
pub name: String,
pub module: Module,
}
impl std::hash::Hash for Symbol {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.module.hash(state);
}
}
impl PartialEq for Symbol {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.module == other.module
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Module {
Resolved(u64),
Unresolved(String),
}
#[derive(Debug, Clone)]
pub enum Token {
Symbol(Symbol),
Register(Register),
Immediate(u32),
StringLit(String),
Opcode(Opcode),
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Symbol(symbol) => write!(f, "{symbol}"),
Self::Register(register) => write!(f, "{register}",),
Self::Immediate(immediate) => write!(f, "{immediate}",),
Self::StringLit(string_lit) => write!(f, "{string_lit}",),
Self::Opcode(opcode) => write!(f, "{opcode}",),
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum TokenType {
Symbol,
Register,
Immediate,
StringLit,
Opcode,
}
impl TokenType {
#[must_use]
pub const fn from_token(token: &Token) -> Self {
match token {
Token::Symbol(_) => Self::Symbol,
Token::Register(_) => Self::Register,
Token::Immediate(_) => Self::Immediate,
Token::StringLit(_) => Self::StringLit,
Token::Opcode(_) => Self::Opcode,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Opcode {
// Real instructions (0x00-0x26)
Nop,
Mov,
CMov,
Ldb,
Ldbs,
Ldh,
Ldhs,
Ldw,
Stb,
Sth,
Stw,
Lli,
Lui,
Jmp,
Jez,
Jnz,
Jic,
Jnc,
Ieq,
Ine,
Igt,
Ige,
Ilt,
Ile,
Shl,
Shr,
Add,
Sub,
AddI,
SubI,
And,
Or,
Not,
Xor,
Nand,
Nor,
Xnor,
Int,
IRet,
Hlt,
// Function instructions
Call,
Ret,
// Stack ops
Push,
Pop,
// Pseudo-instructions
Db,
Dh,
Dw,
Resb,
Resh,
Resw,
Lwi,
Func,
Return,
// meta instructions (these aren't present in the binary as instructions)
Include,
Data,
Segment,
}
#[derive(Debug)]
pub enum OpcodeFromStrError {
InvalidRegister(&'static str),
InvalidOpcode(String),
}
impl std::fmt::Display for OpcodeFromStrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidRegister(reg) => write!(f, "register does not exist: {reg}"),
Self::InvalidOpcode(op) => write!(f, "instruction does not exist: {op}"),
}
}
}
impl std::error::Error for OpcodeFromStrError {}
impl FromStr for Opcode {
type Err = OpcodeFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"nop" => Ok(Self::Nop),
"mov" => Ok(Self::Mov),
"cmov" => Ok(Self::CMov),
"ldb" => Ok(Self::Ldb),
"ldbs" => Ok(Self::Ldbs),
"ldh" => Ok(Self::Ldh),
"ldhs" => Ok(Self::Ldhs),
"ldw" => Ok(Self::Ldw),
"stb" => Ok(Self::Stb),
"sth" => Ok(Self::Sth),
"stw" => Ok(Self::Stw),
"lli" => Ok(Self::Lli),
"lui" => Ok(Self::Lui),
// comparison
"ieq" => Ok(Self::Ieq),
"ine" => Ok(Self::Ine),
"igt" => Ok(Self::Igt),
"ige" => Ok(Self::Ige),
"ilt" => Ok(Self::Ilt),
"ile" => Ok(Self::Ile),
// jumps
"jmp" => Ok(Self::Jmp),
"jez" => Ok(Self::Jez),
"jnz" => Ok(Self::Jnz),
"jic" => Ok(Self::Jic),
"jnc" => Ok(Self::Jnc),
"shl" => Ok(Self::Shl),
"shr" => Ok(Self::Shr),
"add" => Ok(Self::Add),
"sub" => Ok(Self::Sub),
"and" => Ok(Self::And),
"or" => Ok(Self::Or),
"not" => Ok(Self::Not),
"xor" => Ok(Self::Xor),
"nand" => Ok(Self::Nand),
"nor" => Ok(Self::Nor),
"xnor" => Ok(Self::Xnor),
"addi" => Ok(Self::AddI),
"subi" => Ok(Self::SubI),
// stack ops
"push" => Ok(Self::Push),
"pop" => Ok(Self::Pop),
// function instructions
"call" => Ok(Self::Call),
"ret" => Ok(Self::Ret),
"int" => Ok(Self::Int),
"irt" | "iret" => Ok(Self::IRet),
"hlt" => Ok(Self::Hlt),
// pseudoinstructions
"func" => Ok(Self::Func),
"return" => Ok(Self::Return),
"lwi" => Ok(Self::Lwi),
// directives
"include" => Ok(Self::Include),
"db" => Ok(Self::Db),
"dh" => Ok(Self::Dh),
"dw" => Ok(Self::Dw),
"resb" => Ok(Self::Resb),
"resh" => Ok(Self::Resh),
"resw" => Ok(Self::Resw),
// function pseudoinstructions
_ => Err(OpcodeFromStrError::InvalidOpcode(s.to_string())),
}
}
}
impl Opcode {
pub const OPCODES: &[&str] = &[
// Real instructions (0x00-0x26)
"nop", "mov", "movs", "ldb", "ldbs", "ldh", "ldhs", "ldw", "stb", "sth", "stw", "lli",
"lui", "jmp", "jeq", "jne", "jgt", "jge", "jlt", "jle", "cmp", "inc", "dec", "shl", "shr",
"add", "sub", "and", "or", "not", "xor", "nand", "nor", "xnor", "int", "iret", "hlt",
"addi", "subi", "call", "ret", // Pseudo-instructions
"db", "dh", "dw", "resb", "resh", "resw", "lwi", "func", "return",
// meta instructions
"include",
];
#[must_use]
pub const fn to_opcode_value(&self) -> Option<u8> {
match self {
Self::Nop => Some(0x00),
Self::Mov => Some(0x01),
Self::CMov => Some(0x02),
Self::Ldb => Some(0x03),
Self::Ldbs => Some(0x04),
Self::Ldh => Some(0x05),
Self::Ldhs => Some(0x06),
Self::Ldw => Some(0x07),
Self::Stb => Some(0x08),
Self::Sth => Some(0x09),
Self::Stw => Some(0x0A),
Self::Lli => Some(0x0B),
Self::Lui => Some(0x0C),
Self::Ieq => Some(0x0D),
Self::Ine => Some(0x0E),
Self::Ilt => Some(0x0F),
Self::Ile => Some(0x10),
Self::Igt => Some(0x11),
Self::Ige => Some(0x12),
Self::Jmp => Some(0x13),
Self::Jez => Some(0x14),
Self::Jnz => Some(0x15),
Self::Jic => Some(0x16),
Self::Jnc => Some(0x17),
Self::And => Some(0x18),
Self::Nand => Some(0x19),
Self::Or => Some(0x1A),
Self::Nor => Some(0x1B),
Self::Xor => Some(0x1C),
Self::Xnor => Some(0x1D),
Self::Not => Some(0x1E),
Self::Add => Some(0x1F),
Self::Sub => Some(0x20),
Self::Shl => Some(0x21),
Self::Shr => Some(0x22),
Self::AddI => Some(0x23),
Self::SubI => Some(0x24),
Self::Push => Some(0x25),
Self::Pop => Some(0x26),
Self::Call => Some(0x27),
Self::Ret => Some(0x28),
Self::Int => Some(0x29),
Self::IRet => Some(0x2A),
Self::Hlt => Some(0x2B),
Self::Segment => Some(0x2C),
// Pseudo-instructions don't have opcode values
_ => None,
}
}
#[must_use]
pub const fn is_pseudo_instruction(&self) -> bool {
matches!(
self,
Self::Db
| Self::Dh
| Self::Dw
| Self::Resb
| Self::Resh
| Self::Resw
| Self::Lwi
| Self::Func
| Self::Return
)
}
}
+427
View File
@@ -0,0 +1,427 @@
use std::path::{Path, PathBuf};
use crate::assembler::TokenType;
use crate::{assembler::AssembleError, expect_token, expect_type, node};
use crate::assembler::model::{Node, Opcode, Token};
use common::prelude::*;
pub struct Parser {
tokens: Vec<Token>,
nodes: Vec<Node>,
}
#[derive(Debug)]
pub struct Program {
pub nodes: Vec<Node>,
}
impl Program {
#[must_use]
pub const fn new() -> Self {
Self { nodes: vec![] }
}
pub fn add_module(&mut self, module: Vec<Node>) {
self.nodes.extend(module);
}
pub fn parser(&mut self) -> Parser {
Parser {
tokens: vec![],
nodes: self.nodes.clone(),
}
}
}
impl Default for Program {
fn default() -> Self {
Self::new()
}
}
impl Parser {
pub fn parse_nodes(tokens: Vec<Token>) -> Result<Vec<Node>, AssembleError> {
println!("parsing");
let mut self_ = Self {
tokens: tokens.into_iter().rev().collect(),
nodes: vec![],
};
while !self_.tokens.is_empty() {
println!("NEXT {}", self_.peek_next().unwrap());
let ins = self_.parse_instruction()?;
self_.nodes.push(ins);
}
Ok(self_.nodes.clone())
}
pub fn get_dependencies(
nodes: &Vec<Node>,
source_path: &Path,
) -> Result<Vec<PathBuf>, AssembleError> {
let mut dependencies = Vec::new();
// Get the parent directory of the source file to use as the base directory
let base_dir = source_path
.parent()
.ok_or_else(|| AssembleError::InvalidFile(source_path.to_path_buf()))?;
for node in nodes {
if node.opcode() == Opcode::Include {
let path_str =
expect_token!(node.args().get(1).ok_or(AssembleError::Generic)?, StringLit)?;
let path = PathBuf::from(path_str);
// If the path is not absolute, make it relative to the base directory
let full_path = if path.is_absolute() {
path
} else {
base_dir.join(path)
};
dependencies.push(full_path);
}
}
Ok(dependencies)
}
#[expect(clippy::too_many_lines, clippy::cognitive_complexity)]
fn parse_instruction(&mut self) -> Result<Node, AssembleError> {
if self.tokens.is_empty() {
unreachable!();
}
// check if the Node starts with a label
let label = expect_token!(self.peek_next()?, Symbol).ok();
if label.is_some() {
self.tokens.pop();
}
let opcode = expect_token!(self.next()?, Opcode)?;
let args: Vec<Token>;
#[allow(clippy::match_same_arms)]
match opcode {
// R-type instructions
Opcode::Mov | Opcode::CMov => {
let reg1 = expect_type!(self.next()?, Register, Symbol)?;
let reg2 = expect_type!(self.next()?, Register, Symbol)?;
args = vec![reg1, reg2];
}
Opcode::Ldb | Opcode::Ldbs | Opcode::Ldh | Opcode::Ldhs | Opcode::Ldw => {
let base = expect_type!(self.next()?, Register, Symbol)?;
let dest = expect_type!(self.next()?, Register)?;
let offset = match self.peek_next() {
Ok(next) if expect_type!(next.clone(), Immediate).is_ok() => self.next()?,
_ => Token::Immediate(0),
};
args = vec![base, dest, offset];
}
Opcode::Stb | Opcode::Sth | Opcode::Stw => {
let base = expect_type!(self.next()?, Register)?;
let dest = expect_type!(self.next()?, Register, Symbol)?;
let offset = match self.peek_next() {
Ok(next) if expect_type!(next.clone(), Immediate).is_ok() => self.next()?,
_ => Token::Immediate(0),
};
args = vec![base, dest, offset];
}
Opcode::Add
| Opcode::Sub
| Opcode::And
| Opcode::Or
| Opcode::Xor
| Opcode::Nand
| Opcode::Nor
| Opcode::Xnor => {
let src1 = expect_type!(self.next()?, Register, Symbol)?;
let src2 = expect_type!(self.next()?, Register, Symbol)?;
let dest = expect_type!(self.next()?, Register, Symbol)?;
args = vec![src1, src2, dest];
}
Opcode::Not => {
let src = expect_type!(self.next()?, Register, Symbol)?;
let dest = expect_type!(self.next()?, Register, Symbol)?;
args = vec![src, dest];
}
Opcode::Shl | Opcode::Shr => {
let src = expect_type!(self.next()?, Register, Symbol)?;
// First operand after src: could be immediate or register
let first = self.next()?;
let (r_shamt, i_shamt) = match first {
Token::Register(_) => (
first,
if let Ok(tok) = self.peek_next() {
if expect_type!(tok, Immediate).is_ok() {
self.next()?
} else {
Token::Immediate(0)
}
} else {
Token::Immediate(0)
},
),
Token::Immediate(_) => (Token::Register(Register::Zero), first),
_ => {
return Err(AssembleError::UnexpectedToken(first, TokenType::Immediate));
}
};
let dest = if let Ok(tok) = self.peek_next() {
if expect_type!(tok, Register).is_ok() {
self.next()?
} else {
src.clone() // Default to src if no dest specified
}
} else {
src.clone() // Default to src if no dest specified
};
args = vec![src, r_shamt, i_shamt, dest];
}
Opcode::Include => {
let mod_name = expect_type!(self.next()?, Symbol)?;
let path = expect_type!(self.next()?, StringLit)?;
args = vec![mod_name, path];
}
// Unconditional jump
Opcode::Jmp | Opcode::Jic | Opcode::Jnc => {
let imm = expect_type!(self.next()?, Immediate, Symbol)?;
let offset = match self.peek_next() {
Ok(token) => {
if expect_type!(token, Register).is_ok() {
self.next()?
} else {
Token::Register(Register::Zero)
}
}
Err(_) => Token::Register(Register::Zero),
};
args = vec![imm, offset];
}
Opcode::Ieq | Opcode::Ine | Opcode::Ilt | Opcode::Igt | Opcode::Ile | Opcode::Ige => {
println!("yes");
let src1 = expect_type!(self.next()?, Register, Symbol)?;
let src2 = expect_type!(self.next()?, Register, Symbol)?;
let dest = expect_type!(self.next()?, Register, Symbol)?;
args = vec![src1, src2, dest];
println!("done");
}
Opcode::Jez | Opcode::Jnz => {
let condition = expect_type!(self.next()?, Register)?;
let imm = expect_type!(self.next()?, Immediate, Symbol)?;
let offset = match self.peek_next() {
Ok(token) => {
if expect_type!(token, Register).is_ok() {
self.next()?
} else {
Token::Register(Register::Zero)
}
}
Err(_) => Token::Register(Register::Zero),
};
args = vec![condition, imm, offset];
}
Opcode::Call => {
let addr = expect_type!(self.next()?, Symbol)?;
args = vec![addr];
}
// I-type instructions
Opcode::Lui | Opcode::Lli | Opcode::Lwi => {
let imm = expect_type!(self.next()?, Immediate, Symbol)?;
let reg = expect_type!(self.next()?, Register)?;
args = vec![imm, reg];
}
// Immediate Arithmetic
Opcode::AddI | Opcode::SubI => {
let reg = expect_type!(self.next()?, Register)?;
let imm = expect_type!(self.next()?, Immediate)?;
let reg2 = if expect_type!(self.peek_next()?, Register).is_ok() {
self.next()?
} else {
reg.clone()
};
args = vec![reg, imm, reg2];
}
// D-type pseudoinstructions (data definition)
Opcode::Resb | Opcode::Resh | Opcode::Resw => {
let name = expect_type!(self.next()?, Symbol)?;
let num = expect_type!(self.next()?, Immediate)?;
args = vec![name, num];
}
Opcode::Db | Opcode::Dh | Opcode::Dw => {
args = self.parse_data_definition(opcode)?;
}
// E-type pseudoinstructions (stack operations)
Opcode::Push | Opcode::Pop => {
let reg = expect_type!(self.next()?, Register, Symbol)?;
args = vec![reg];
}
// Special instructions
Opcode::Int => {
let val = expect_type!(self.next()?, Immediate)?;
args = vec![val];
}
// Instructions with no arguments
Opcode::Hlt
| Opcode::Nop
| Opcode::Ret
| Opcode::IRet
| Opcode::Return
| Opcode::Func => {
args = vec![];
}
Opcode::Data | Opcode::Segment => {
return Err(AssembleError::Generic);
}
}
Ok(node!(label, opcode, args: args))
}
fn parse_data_definition(&mut self, opcode: Opcode) -> Result<Vec<Token>, AssembleError> {
let mut values = Vec::new();
println!("e {:?}", self.peek_next());
let name = expect_type!(self.next()?, Symbol)?;
values.push(name);
match opcode {
Opcode::Db => {
// db can take string literals or u8 immediates
while !self.tokens.is_empty() {
let token = self
.tokens
.last()
.expect("Expected a token for data definition, but found none");
match token {
Token::StringLit(_) => {
values.push(
self.tokens
.pop()
.expect("Expected a token for data definition, but found none"),
);
}
Token::Immediate(val) if u8::try_from(*val).is_ok() => {
values.push(
self.tokens
.pop()
.expect("Expected a token for data definition, but found none"),
);
}
_ => break,
}
}
}
Opcode::Dh => {
// dh can take u16 immediates
while !self.tokens.is_empty() {
let token = self
.tokens
.last()
.expect("Expected a token for data definition, but found none");
match token {
Token::StringLit(_) => {
values.push(
self.tokens
.pop()
.expect("Expected a token for data definition, but found none"),
);
}
Token::Immediate(val) if u16::try_from(*val).is_ok() => {
values.push(
self.tokens
.pop()
.expect("Expected a token for data definition, but found none"),
);
}
_ => break,
}
}
}
Opcode::Dw => {
// dw can take u32 immediates
while !self.tokens.is_empty() {
match self
.tokens
.last()
.expect("Expected a token for data definition, but found none")
{
Token::StringLit(_) => {
values.push(
self.tokens
.pop()
.expect("Expected a token for data definition, but found none"),
);
}
Token::Immediate(val) => {
values.push(
self.tokens
.pop()
.expect("Expected a token for data definition, but found none"),
);
}
_ => break,
}
}
}
_ => unreachable!(),
}
Ok(values)
}
fn next(&mut self) -> Result<Token, AssembleError> {
if self.tokens.is_empty() {
Err(AssembleError::UnexpectedEof)
} else {
Ok(self
.tokens
.pop()
.expect("tokens vector was unexpectedly empty in next()"))
}
}
fn peek_next(&self) -> Result<Token, AssembleError> {
if self.tokens.is_empty() {
Err(AssembleError::UnexpectedEof)
} else {
Ok(self
.tokens
.last()
.expect("peek_next called on empty tokens vector")
.clone())
}
}
}
+157
View File
@@ -0,0 +1,157 @@
use std::{
collections::HashMap,
fs::canonicalize,
path::{Path, PathBuf},
};
use common::prelude::Register;
use crate::assembler::model::{Module, Node, Opcode, Symbol, Token};
use crate::assembler::quick_hash;
use crate::{assembler::AssembleError, node};
pub fn resolve_symbols(nodes: &mut [Node]) -> Result<(), AssembleError> {
let symbol_table = generate_symbol_table(nodes);
for node in nodes.iter_mut() {
match node.opcode() {
Opcode::Jmp | Opcode::Call | Opcode::Jic | Opcode::Jnc | Opcode::Lli | Opcode::Lui => {
if let Token::Symbol(symbol) = node
.arg(0)
.expect("Expected argument 0 for jump-like opcode")
{
if let Some(address) = symbol_table.get(&symbol) {
node.tokens[0] = Token::Immediate(*address);
} else {
return Err(AssembleError::UndefinedSymbol(symbol));
}
}
}
Opcode::Jez | Opcode::Jnz => {
if let Token::Symbol(symbol) = node
.arg(1)
.expect("Expected argument 0 for jump-like opcode")
{
if let Some(address) = symbol_table.get(&symbol) {
node.tokens[1] = Token::Immediate(*address);
} else {
return Err(AssembleError::UndefinedSymbol(symbol));
}
}
}
_ => (),
}
}
Ok(())
}
fn generate_symbol_table(nodes: &[Node]) -> HashMap<Symbol, u32> {
let mut table = HashMap::new();
for (i, node) in nodes.iter().enumerate() {
if let Some(symbol) = node.label() {
table.insert(symbol, 4 * i as u32);
}
}
table
}
pub fn resolve_dependencies(
mut nodes: Vec<Node>,
base_dir: &Path,
) -> Result<Vec<Node>, AssembleError> {
// First we get a list of imports.
let mut dependencies = Vec::new();
for node in &nodes {
if node.opcode() == Opcode::Include {
// we want the path, and the name
let name = if let Token::Symbol(name) = node
.arg(0)
.expect("Expected argument #0 for Include directive.")
{
name.name.clone()
} else {
unreachable!()
}; //node.2.get(0).unwrap()
let Ok(Token::StringLit(path)) = node.arg(1) else {
unreachable!()
};
let full_path = base_dir.join(path);
let canonical_path = full_path
.canonicalize()
.map_err(|_| AssembleError::InvalidFile(full_path.clone()))?;
let hash = quick_hash(&canonical_path);
dependencies.push((name, hash));
}
}
let mut changes = Vec::<(u32, u32, Symbol)>::new();
// now we resolve the symbols on all the nodes
// we need to check all operands for unresolved signals
for (i, node) in nodes.clone().iter().enumerate() {
let Node {
tokens: operands, ..
} = node;
for (j, token) in operands.iter().enumerate() {
if let Token::Symbol(symbol) = token {
for d in &dependencies {
if let Module::Unresolved(name) = symbol.module.clone() {
if name != d.0 {
continue;
}
let symbol = Symbol {
name: symbol.name.clone(),
module: Module::Resolved(d.1),
};
changes.push((i as u32, j as u32, symbol));
}
}
}
}
}
for (i, j, symbol) in changes {
nodes[i as usize].tokens[j as usize] = Token::Symbol(symbol);
}
Ok(nodes)
}
pub fn create_sections(nodes: &mut Vec<Node>) -> Result<(), AssembleError> {
let mut res = Vec::<Node>::with_capacity(nodes.len());
res.push(node!(None, Opcode::Segment, Token::Immediate(0)));
for n in nodes.iter() {
if n.opcode() == Opcode::Data {
res.push(n.clone());
}
}
let start = res.len() + 2;
res.insert(
0,
node!(
None,
Opcode::Jmp,
Token::Immediate(start as u32 * 4),
Token::Register(Register::Zero)
),
);
for n in nodes.iter() {
if !matches!(n.opcode(), Opcode::Data | Opcode::Include) {
res.push(n.clone());
}
}
*nodes = res;
Ok(())
}
+20
View File
@@ -0,0 +1,20 @@
#![deny(
clippy::unwrap_used,
clippy::nursery,
clippy::perf,
clippy::pedantic,
clippy::complexity
)]
#![allow(
clippy::cast_possible_truncation,
clippy::missing_panics_doc,
clippy::missing_errors_doc,
clippy::match_wildcard_for_single_variants
)]
pub mod assembler;
// mod util;
pub mod prelude {
pub use crate::assembler::Assembler;
}
+21
View File
@@ -0,0 +1,21 @@
use common::{self as _};
use assembler::prelude::*;
use std::{fs, io::Write, path::PathBuf};
fn main() {
// Parse command line arguments
let args: Vec<String> = std::env::args().collect();
let input_path = &args[1];
let output_path = &args[2];
let mut engine = Assembler::new(PathBuf::from(input_path));
engine.start(());
let result = engine.output().expect("assembler failed.");
if let Err(e) = fs::write(output_path, result) {
eprintln!("Failed to write to output file: {e}");
std::process::exit(1);
}
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "common"
version = "0.1.0"
edition = "2024"
[lib]
path = "src/lib.rs"
[dependencies]
+64
View File
@@ -0,0 +1,64 @@
use crate::{instructions::Instruction, register::Register};
impl Instruction {
#[inline]
pub fn opcode(self) -> u8 {
((self.0 >> 26) & 0x3F) as u8
}
// Src shared between R type and I type
#[inline]
pub unsafe fn src1_uc(self) -> Register {
unsafe { Register::from_u8_unchecked(((self.0 >> 21) & 0x1F) as u8) }
}
#[inline]
pub fn src1_checked(self) -> Register {
Register::from_u8(((self.0 >> 21) & 0x1F) as u8).unwrap_or(Register::Null)
}
// Dest shared between R type and I type
#[inline]
pub unsafe fn dest_uc(self) -> Register {
unsafe { Register::from_u8_unchecked(((self.0 >> 16) & 0x1F) as u8) }
}
#[inline]
pub fn dest_checked(self) -> Register {
Register::from_u8(((self.0 >> 16) & 0x1F) as u8).unwrap_or(Register::Null)
}
// Secondary Src for R type only
#[inline]
pub unsafe fn src2_uc(self) -> Register {
unsafe { Register::from_u8_unchecked(((self.0 >> 11) & 0x1F) as u8) }
}
#[inline]
pub fn src2_checked(self) -> Register {
Register::from_u8(((self.0 >> 11) & 0x1F) as u8).unwrap_or(Register::Null)
}
// Misc/Conditional reg for R type only
#[inline]
pub unsafe fn misc_uc(self) -> Register {
unsafe { Register::from_u8_unchecked(((self.0 >> 6) & 0x1F) as u8) }
}
#[inline]
pub fn misc_checked(self) -> Register {
Register::from_u8(((self.0 >> 6) & 0x1F) as u8).unwrap_or(Register::Null)
}
// Shift amount / misc data for R type only
#[inline]
pub fn shamt(self) -> u8 {
(self.0 & 0x3F) as u8
}
// 16 Bit immediate for I type only
#[inline]
pub fn imm16(self) -> u16 {
self.0 as u16
}
}
+334
View File
@@ -0,0 +1,334 @@
use std::ops::{Deref, DerefMut};
use crate::{
instructions::{Instruction, Opcode},
register::Register,
};
impl From<u32> for Instruction {
#[inline]
fn from(word: u32) -> Self {
Self(word)
}
}
impl Deref for Instruction {
type Target = u32;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Instruction {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Instruction {
// ---- R Type Builders ----
#[inline]
fn build_r(
opcode: Opcode,
src1: Register,
dest: Register,
src2: Register,
misc: Register,
shamt: u8,
) -> Self {
Self(
((opcode as u32) << 26)
| ((src1 as u32) << 21)
| ((dest as u32) << 16)
| ((src2 as u32) << 11)
| ((misc as u32) << 6)
| ((shamt as u32) & 0x3F),
)
}
#[inline]
fn build_i(opcode: Opcode, src: Register, dest: Register, imm: u16) -> Self {
Self(((opcode as u32) << 26) | ((src as u32) << 21) | ((dest as u32) << 16) | (imm as u32))
}
#[inline]
fn build_noarg(opcode: Opcode) -> Self {
Self((opcode as u32) << 26)
}
/// No operation.
pub fn nop() -> Self {
Self::build_noarg(Opcode::Nop)
}
// ---- Move ----
/// Move a value from `src` to `dest`.
pub fn mov(src: Register, dest: Register) -> Self {
Self::build_r(Opcode::Mov, src, dest, Register::Zero, Register::Zero, 0)
}
/// Conditional move: if `cmp` is true then move `src` to `dest`.
pub fn cmov(src: Register, dest: Register, cmp: Register) -> Self {
Self::build_r(Opcode::CMov, src, dest, cmp, Register::Zero, 0)
}
// ---- Load ----
/// Load byte from memory at `src + offset` into `dest`.
pub fn ldb(src: Register, dest: Register, offset: u16) -> Self {
Self::build_i(Opcode::Ldb, src, dest, offset)
}
/// Load signed byte (signextended) from memory at `src + offset` into `dest`.
pub fn ldbs(src: Register, dest: Register, offset: u16) -> Self {
Self::build_i(Opcode::Ldbs, src, dest, offset)
}
/// Load halfword from memory at `src + offset` into `dest`.
pub fn ldh(src: Register, dest: Register, offset: u16) -> Self {
Self::build_i(Opcode::Ldh, src, dest, offset)
}
/// Load signed halfword (signextended) from memory at `src + offset` into `dest`.
pub fn ldhs(src: Register, dest: Register, offset: u16) -> Self {
Self::build_i(Opcode::Ldhs, src, dest, offset)
}
/// Load word from memory at `src + offset` into `dest`.
pub fn ldw(src: Register, dest: Register, offset: u16) -> Self {
Self::build_i(Opcode::Ldw, src, dest, offset)
}
// ---- Store ----
/// Store byte from `src` to memory at `dest + offset`.
pub fn stb(src: Register, dest: Register, offset: u16) -> Self {
Self::build_i(Opcode::Stb, src, dest, offset)
}
/// Store halfword from `src` to memory at `dest + offset`.
pub fn sth(src: Register, dest: Register, offset: u16) -> Self {
Self::build_i(Opcode::Sth, src, dest, offset)
}
/// Store word from `src` to memory at `dest + offset`.
pub fn stw(src: Register, dest: Register, offset: u16) -> Self {
Self::build_i(Opcode::Stw, src, dest, offset)
}
// ---- Load Immediate ----
/// Load lower 16 bits of an immediate into `dest`.
pub fn lli(dest: Register, imm: u16) -> Self {
Self::build_i(Opcode::Lli, Register::Zero, dest, imm)
}
/// Load upper 16 bits of an immediate into `dest`.
pub fn lui(dest: Register, imm: u16) -> Self {
Self::build_i(Opcode::Lui, Register::Zero, dest, imm)
}
/// Load a full 32bit immediate using an `lli` + `lui` pair.
pub fn load_imm32(dest: Register, imm: u32) -> [Self; 2] {
[
Self::lli(dest, imm as u16),
Self::lui(dest, (imm >> 16) as u16),
]
}
// ---- Comparison ----
/// Set `dest` to 1 if `sr1 == sr2`, else 0.
pub fn ieq(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Ieq, sr1, dest, sr2, Register::Zero, 0)
}
/// Set `dest` to 1 if `sr1 != sr2`, else 0.
pub fn ine(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Ine, sr1, dest, sr2, Register::Zero, 0)
}
/// Set `dest` to 1 if `sr1 < sr2`, else 0.
pub fn ilt(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Ilt, sr1, dest, sr2, Register::Zero, 0)
}
/// Set `dest` to 1 if `sr1 <= sr2`, else 0.
pub fn ile(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Ile, sr1, dest, sr2, Register::Zero, 0)
}
/// Set `dest` to 1 if `sr1 > sr2`, else 0.
pub fn igt(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Igt, sr1, dest, sr2, Register::Zero, 0)
}
/// Set `dest` to 1 if `sr1 >= sr2`, else 0.
pub fn ige(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Ige, sr1, dest, sr2, Register::Zero, 0)
}
// ---- Jump ----
/// Unconditional jump to address in `addr` with offset.
pub fn jmp(addr: Register, offset: u16) -> Self {
Self::build_i(Opcode::Jmp, Register::Zero, addr, offset)
}
/// Jump if zero flag set (`jez`).
pub fn jez(cmp: Register, addr: Register, offset: u16) -> Self {
Self::build_i(Opcode::Jez, cmp, addr, offset)
}
/// Jump if zero flag clear (`jnz`).
pub fn jnz(cmp: Register, addr: Register, offset: u16) -> Self {
Self::build_i(Opcode::Jnz, cmp, addr, offset)
}
/// Jump if carry set (`jic`).
pub fn jic(addr: Register, offset: u16) -> Self {
Self::build_i(Opcode::Jic, Register::Zero, addr, offset)
}
/// Jump if carry clear (`jnc`).
pub fn jnc(addr: Register, offset: u16) -> Self {
Self::build_i(Opcode::Jnc, Register::Zero, addr, offset)
}
// ---- Bitwise ----
/// Bitwise AND of `sr1` and `sr2`, result in `dest`.
pub fn and(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::And, sr1, dest, sr2, Register::Zero, 0)
}
/// Bitwise NAND of `sr1` and `sr2`, result in `dest`.
pub fn nand(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Nand, sr1, dest, sr2, Register::Zero, 0)
}
/// Bitwise OR of `sr1` and `sr2`, result in `dest`.
pub fn or(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Or, sr1, dest, sr2, Register::Zero, 0)
}
/// Bitwise NOR of `sr1` and `sr2`, result in `dest`.
pub fn nor(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Nor, sr1, dest, sr2, Register::Zero, 0)
}
/// Bitwise XOR of `sr1` and `sr2`, result in `dest`.
pub fn xor(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Xor, sr1, dest, sr2, Register::Zero, 0)
}
/// Bitwise XNOR of `sr1` and `sr2`, result in `dest`.
pub fn xnor(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Xnor, sr1, dest, sr2, Register::Zero, 0)
}
/// Bitwise NOT of `src`, result in `dest`.
pub fn not(src: Register, dest: Register) -> Self {
Self::build_r(Opcode::Not, src, dest, Register::Zero, Register::Zero, 0)
}
// ---- Arithmetic ----
/// Add `sr1` and `sr2`, store result in `dest`.
pub fn add(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Add, sr1, dest, sr2, Register::Zero, 0)
}
/// Subtract `sr2` from `sr1`, store result in `dest`.
pub fn sub(sr1: Register, sr2: Register, dest: Register) -> Self {
Self::build_r(Opcode::Sub, sr1, dest, sr2, Register::Zero, 0)
}
/// Shift left logical: shift `src` by (`rshamt` + `ishamt`) bits into `dest`.
pub fn shl(src: Register, rshamt: Register, dest: Register, ishamt: u8) -> Self {
debug_assert!(ishamt < 64);
Self::build_r(Opcode::Shl, src, dest, rshamt, Register::Zero, ishamt)
}
/// Shift right logical: shift `src` by (`rshamt` + `ishamt`) bits into `dest`.
pub fn shr(src: Register, rshamt: Register, dest: Register, ishamt: u8) -> Self {
debug_assert!(ishamt < 64);
Self::build_r(Opcode::Shr, src, dest, rshamt, Register::Zero, ishamt)
}
/// Add immediate `imm` to `sr1`, store result in `dest`.
pub fn addi(sr1: Register, dest: Register, imm: u16) -> Self {
Self::build_i(Opcode::Addi, sr1, dest, imm)
}
/// Subtract immediate `imm` from `sr1`, store result in `dest`.
pub fn subi(sr1: Register, dest: Register, imm: u16) -> Self {
Self::build_i(Opcode::Subi, sr1, dest, imm)
}
// ---- Util -----
pub fn call(addr: Register, offset: u16) -> Self {
Self::build_i(Opcode::Call, Register::Zero, addr, offset)
}
pub fn ret() -> Self {
Self::build_noarg(Opcode::Ret)
}
pub fn push(reg: Register) -> Self {
Self::build_r(
Opcode::Push,
reg,
Register::Zero,
Register::Zero,
Register::Zero,
0,
)
}
pub fn pop(reg: Register) -> Self {
Self::build_r(
Opcode::Pop,
Register::Zero,
reg,
Register::Zero,
Register::Zero,
0,
)
}
// ---- System ----
/// Trigger an interrupt with the given 6bit code.
pub fn int(code: u8) -> Self {
debug_assert!(code < 64);
Self::build_i(Opcode::Int, Register::Zero, Register::Zero, code as u16)
}
/// Return from interrupt.
pub fn irt() -> Self {
Self::build_noarg(Opcode::IRet)
}
/// Halt execution.
pub fn hlt() -> Self {
Self::build_r(
Opcode::Hlt,
Register::Zero,
Register::Zero,
Register::Zero,
Register::Zero,
0,
)
}
/// Raw data (not an instruction)
pub fn data(value: u32) -> Self {
Self(value)
}
}
+235
View File
@@ -0,0 +1,235 @@
pub mod decode;
pub mod encode;
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct Instruction(pub u32);
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Opcode {
Nop = 0,
// move
Mov = 1,
CMov = 2,
// load
Ldb = 3,
Ldbs = 4,
Ldh = 5,
Ldhs = 6,
Ldw = 7,
// store
Stb = 8,
Sth = 9,
Stw = 10,
// load immediate
Lli = 11,
Lui = 12,
// comparison
Ieq = 13,
Ine = 14,
Ilt = 15,
Ile = 16,
Igt = 17,
Ige = 18,
// jump
Jmp = 19,
Jez = 20,
Jnz = 21,
Jic = 22,
Jnc = 23,
// bitwise
And = 24,
Nand = 25,
Or = 26,
Nor = 27,
Xor = 28,
Xnor = 29,
Not = 30,
// arithmetic
Add = 31,
Sub = 32,
Shl = 33,
Shr = 34,
Addi = 35,
Subi = 36,
// utility
Push = 37,
Pop = 38,
Call = 39,
Ret = 40,
// system
Int = 41,
IRet = 42,
Hlt = 43,
}
impl Opcode {
#[inline]
pub fn from_u8(val: u8) -> Option<Self> {
if val <= Self::Hlt as u8 {
Some(unsafe { std::mem::transmute(val) })
} else {
None
}
}
}
use std::fmt;
impl fmt::Debug for Instruction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let opcode = Opcode::from_u8(self.opcode());
match opcode {
None => write!(f, "Instruction(INVALID OPCODE {:#04x})", self.opcode()),
Some(op @ (Opcode::Nop | Opcode::Hlt | Opcode::Ret | Opcode::IRet)) => {
write!(f, "{op:?}")
}
Some(op @ Opcode::Push) => {
write!(f, "{op:?} {src1:?}", op = op, src1 = self.src1_checked())
}
Some(op @ Opcode::Pop) => {
write!(f, "{op:?} {dest:?}", op = op, dest = self.dest_checked())
}
// I-type: single reg + immediate
Some(op @ (Opcode::Lli | Opcode::Lui)) => {
write!(
f,
"{op:?} {dest:?}, {imm:#06x}",
op = op,
dest = self.dest_checked(),
imm = self.imm16()
)
}
// I-type: no reg, immediate only
Some(op @ Opcode::Int) => {
write!(f, "{op:?} {imm:#06x}", op = op, imm = self.imm16())
}
// I-type: src + dest + immediate
Some(
op @ (Opcode::Ldb
| Opcode::Ldbs
| Opcode::Ldh
| Opcode::Ldhs
| Opcode::Ldw
| Opcode::Stb
| Opcode::Sth
| Opcode::Stw
| Opcode::Addi
| Opcode::Subi),
) => {
write!(
f,
"{op:?} {src:?}, {dest:?}, {imm:#06x}",
op = op,
src = self.src1_checked(),
dest = self.dest_checked(),
imm = self.imm16()
)
}
// I-type: cmp + addr + immediate (conditional jumps)
Some(op @ (Opcode::Jez | Opcode::Jnz)) => {
write!(
f,
"{op:?} {cmp:?}, {addr:?}, {imm:#06x}",
op = op,
cmp = self.src1_checked(),
addr = self.dest_checked(),
imm = self.imm16()
)
}
// I-type: addr + immediate only (unconditional/carry jumps)
Some(op @ (Opcode::Jmp | Opcode::Call | Opcode::Jic | Opcode::Jnc)) => {
write!(
f,
"{op:?} {addr:?}, {imm:#06x}",
op = op,
addr = self.dest_checked(),
imm = self.imm16()
)
}
// R-type: src + dest (two reg)
Some(op @ (Opcode::Mov | Opcode::Not)) => {
write!(
f,
"{op:?} {src:?}, {dest:?}",
op = op,
src = self.src1_checked(),
dest = self.dest_checked()
)
}
// R-type: src + dest + cmp (cmov)
Some(op @ Opcode::CMov) => {
write!(
f,
"{op:?} {src:?}, {dest:?}, {cmp:?}",
op = op,
src = self.src1_checked(),
dest = self.dest_checked(),
cmp = self.src2_checked()
)
}
// R-type: sr1 + sr2 + dest (arithmetic/bitwise/comparison)
Some(
op @ (Opcode::Add
| Opcode::Sub
| Opcode::And
| Opcode::Nand
| Opcode::Or
| Opcode::Nor
| Opcode::Xor
| Opcode::Xnor
| Opcode::Ieq
| Opcode::Ine
| Opcode::Ilt
| Opcode::Ile
| Opcode::Igt
| Opcode::Ige),
) => {
write!(
f,
"{op:?} {sr1:?}, {sr2:?}, {dest:?}",
op = op,
sr1 = self.src1_checked(),
sr2 = self.src2_checked(),
dest = self.dest_checked()
)
}
// R-type: src + rshamt + dest + ishamt
Some(op @ (Opcode::Shl | Opcode::Shr)) => {
write!(
f,
"{op:?} {src:?}, {dest:?}, r:{rshamt:?} + i:{ishamt}",
op = op,
src = self.src1_checked(),
dest = self.dest_checked(),
rshamt = self.src2_checked(),
ishamt = self.shamt()
)
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
pub mod instructions;
pub mod register;
pub mod prelude {
pub use crate::instructions::Instruction;
pub use crate::register::Register;
}
+151
View File
@@ -0,0 +1,151 @@
use std::fmt;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
#[repr(u8)]
#[non_exhaustive]
pub enum Register {
// general purpose
Rg0 = 0,
Rg1 = 1,
Rg2 = 2,
Rg3 = 3,
Rg4 = 4,
Rg5 = 5,
Rg6 = 6,
Rg7 = 7,
Rg8 = 8,
Rg9 = 9,
Rga = 10,
Rgb = 11,
Rgc = 12,
Rgd = 13,
Rge = 14,
Rgf = 15,
// special purpose
Zero = 16,
Acc = 17,
Spr = 18,
Bpr = 19,
Ret = 20,
Idr = 21,
Mmr = 22,
// system - read only
Mar = 23,
Mdr = 24,
Sts = 25,
Cir = 26,
Pcx = 27,
#[default]
Null = 28,
}
impl Register {
#[must_use]
#[inline]
pub unsafe fn from_u8_unchecked(idx: u8) -> Self {
debug_assert!(idx <= Self::Null as u8);
unsafe { std::mem::transmute(idx) }
}
#[inline]
pub fn from_u8(idx: u8) -> Result<Self, ()> {
if idx > 28 {
return Err(());
}
Ok(unsafe { Self::from_u8_unchecked(idx) })
}
}
impl fmt::Display for Register {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
// general purpose
Self::Rg0 => "Rg0",
Self::Rg1 => "Rg1",
Self::Rg2 => "Rg2",
Self::Rg3 => "Rg3",
Self::Rg4 => "Rg4",
Self::Rg5 => "Rg5",
Self::Rg6 => "Rg6",
Self::Rg7 => "Rg7",
Self::Rg8 => "Rg8",
Self::Rg9 => "Rg9",
Self::Rga => "Rga",
Self::Rgb => "Rgb",
Self::Rgc => "Rgc",
Self::Rgd => "Rgd",
Self::Rge => "Rge",
Self::Rgf => "Rgf",
// special purpose
Self::Zero => "Zero",
Self::Acc => "Acc",
Self::Spr => "Spr",
Self::Bpr => "Bpr",
Self::Ret => "Ret",
Self::Idr => "Idr",
Self::Mmr => "Mmr",
// system - read only
Self::Mar => "Mar",
Self::Mdr => "Mdr",
Self::Sts => "Sts",
Self::Cir => "Cir",
Self::Pcx => "Pcx",
Self::Null => "Null",
}
)
}
}
impl std::str::FromStr for Register {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
// general purpose
"rg0" => Ok(Self::Rg0),
"rg1" => Ok(Self::Rg1),
"rg2" => Ok(Self::Rg2),
"rg3" => Ok(Self::Rg3),
"rg4" => Ok(Self::Rg4),
"rg5" => Ok(Self::Rg5),
"rg6" => Ok(Self::Rg6),
"rg7" => Ok(Self::Rg7),
"rg8" => Ok(Self::Rg8),
"rg9" => Ok(Self::Rg9),
"rga" => Ok(Self::Rga),
"rgb" => Ok(Self::Rgb),
"rgc" => Ok(Self::Rgc),
"rgd" => Ok(Self::Rgd),
"rge" => Ok(Self::Rge),
"rgf" => Ok(Self::Rgf),
// special purpose
"zero" => Ok(Self::Zero),
"acc" => Ok(Self::Acc),
"spr" => Ok(Self::Spr),
"bpr" => Ok(Self::Bpr),
// "ret" => Ok(Self::Ret),
"idr" => Ok(Self::Idr),
"mmr" => Ok(Self::Mmr),
// system - read only
"mar" => Ok(Self::Mar),
"mdr" => Ok(Self::Mdr),
"sts" => Ok(Self::Sts),
"cir" => Ok(Self::Cir),
"pcx" => Ok(Self::Pcx),
"null" => Ok(Self::Null),
_ => Err(()),
}
}
}
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "compiler"
edition.workspace = true
version.workspace = true
authors.workspace = true
[dependencies]
+14
View File
@@ -0,0 +1,14 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
+40
View File
@@ -0,0 +1,40 @@
[package]
name = "emulator"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "dsa"
path = "src/main.rs"
[lib]
name = "dsa"
path = "src/lib.rs"
[dev-dependencies]
criterion = { version = "0.5.0", features = ["html_reports"] }
[dependencies]
arc-swap = "1.8.2"
clap = { version = "4.5.60", features = ["derive"] }
fxhash = "0.2.1"
ron = "0.12.0"
serde = { version = "1.0.228", features = ["derive"] }
common = { path = "../common" }
#assembler = { path = "../assembler" }
[[bench]]
name = "bench_mainstore"
harness = false
[features]
default = ["mainstore-bulkalloc"]
# Memory Bank Features
mainstore-bulkalloc = [] # Fastest for Writes
mainstore-prealloc = [] # Fastest for Reads
mainstore-stackarray = [] # Slightly outperforms ArrayMap but requires a large stack
mainstore-arraymap = [] # Simple implementation
mainstore-hashmap = [] # Old implementation, no raw pointers. Uses a hashmap
+208
View File
@@ -0,0 +1,208 @@
use criterion::{
BenchmarkGroup, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
measurement::WallTime,
};
use std::time::Duration;
use dsa::{Page, RandomAccessMemory};
type PhysAddr = u32;
// ── address generators ────────────────────────────────────────────────────────
fn sequential_addrs(n: usize, max_addr: u32) -> Vec<PhysAddr> {
(0..n).map(|i| ((i as u32 * 4) & (max_addr - 1))).collect()
}
fn random_addrs(n: usize, max_addr: u32) -> Vec<PhysAddr> {
let mut addrs = Vec::with_capacity(n);
let mut x: u32 = 0xdeadbeef;
for _ in 0..n {
x = x.wrapping_mul(1664525).wrapping_add(1013904223);
addrs.push((x & (max_addr - 1)) & !3);
}
addrs
}
fn page_stride_addrs(n: usize) -> Vec<PhysAddr> {
(0..n).map(|i| (i as u32 * 4096) & 0x00FF_FFFF).collect()
}
fn random_page_addrs(n: usize, max_addr: u32) -> Vec<PhysAddr> {
let mut addrs = Vec::with_capacity(n);
let mut x: u32 = 0xc0ffee42;
for _ in 0..n {
x = x.wrapping_mul(1664525).wrapping_add(1013904223);
addrs.push((x & (max_addr - 1)) & !0xFFF);
}
addrs
}
// ── runners ───────────────────────────────────────────────────────────────────
fn run_read_byte(
group: &mut BenchmarkGroup<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
group.throughput(Throughput::Elements(addrs.len() as u64));
for (name, mem) in implementations.iter() {
group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
b.iter(|| {
let mut sum: u32 = 0;
for &addr in addrs {
sum = sum.wrapping_add(mem.read_byte(black_box(addr)) as u32);
}
black_box(sum)
})
});
}
}
fn run_write_byte(
group: &mut BenchmarkGroup<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
group.throughput(Throughput::Elements(addrs.len() as u64));
for (name, mem) in implementations.iter_mut() {
group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
b.iter(|| {
for (i, &addr) in addrs.iter().enumerate() {
mem.write_byte(black_box(addr), black_box(i as u8));
}
})
});
}
}
fn run_read_word(
group: &mut BenchmarkGroup<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
group.throughput(Throughput::Elements(addrs.len() as u64));
for (name, mem) in implementations.iter() {
group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
b.iter(|| {
let mut sum: u32 = 0;
for &addr in addrs {
sum = sum.wrapping_add(mem.read_word(black_box(addr)));
}
black_box(sum)
})
});
}
}
fn run_write_word(
group: &mut BenchmarkGroup<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
group.throughput(Throughput::Elements(addrs.len() as u64));
for (name, mem) in implementations.iter_mut() {
group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
b.iter(|| {
for (i, &addr) in addrs.iter().enumerate() {
mem.write_word(black_box(addr), black_box(i as u32));
}
})
});
}
}
fn run_read_page(
group: &mut BenchmarkGroup<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
group.throughput(Throughput::Bytes(addrs.len() as u64 * 4096));
for (name, mem) in implementations.iter() {
group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
b.iter(|| {
let mut sum: u64 = 0;
for &addr in addrs {
let page = mem.read_page(black_box(addr));
for chunk in page.chunks_exact(8) {
sum = sum.wrapping_add(u64::from_le_bytes(chunk.try_into().unwrap()));
}
}
black_box(sum)
})
});
}
}
fn run_write_page(
group: &mut BenchmarkGroup<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
let page_data = Page::from([0xABu8; 4096]);
group.throughput(Throughput::Bytes(addrs.len() as u64 * 4096));
for (name, mem) in implementations.iter_mut() {
group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
b.iter(|| {
for &addr in addrs {
mem.write_page(black_box(addr), black_box(&page_data));
}
})
});
}
}
// ── entry point ───────────────────────────────────────────────────────────────
fn benchmarks(c: &mut Criterion) {
const N: usize = 4096;
const MAX_ADDR: u32 = 0x00FF_FFFF;
let seq_addrs = sequential_addrs(N, MAX_ADDR);
let rand_addrs = random_addrs(N, MAX_ADDR);
let page_addrs_seq = page_stride_addrs(N);
let page_addrs_rand = random_page_addrs(N, MAX_ADDR);
let mut implementations: Vec<(&str, Box<dyn RandomAccessMemory>)> = vec![
#[cfg(feature = "mainstore-hashmap")]
("HashStore", Box::new(dsa::HashStore::new())),
#[cfg(feature = "mainstore-arraymap")]
("ArrayStore", Box::new(dsa::ArrayStore::new())),
#[cfg(feature = "mainstore-stackarray")]
("StackArrayStore", Box::new(dsa::StackArrayStore::new())),
#[cfg(feature = "mainstore-prealloc")]
("PreAllocStore", Box::new(dsa::PreAllocStore::new())),
#[cfg(feature = "mainstore-bulkalloc")]
("BulkAllocStore", Box::new(dsa::BulkAllocStore::new())),
];
macro_rules! group {
($name:expr, $secs:expr, $runner:ident, $addrs:expr) => {{
let mut g = c.benchmark_group($name);
g.measurement_time(Duration::from_secs($secs));
$runner(&mut g, $addrs, &mut implementations);
g.finish();
}};
}
group!("write_word/random", 30, run_write_word, &rand_addrs);
group!("write_byte/random", 30, run_write_byte, &rand_addrs);
group!("read_byte/sequential", 30, run_read_byte, &seq_addrs);
group!("read_byte/random", 30, run_read_byte, &rand_addrs);
group!("read_word/sequential", 30, run_read_word, &seq_addrs);
group!("read_word/random", 30, run_read_word, &rand_addrs);
group!("write_byte/sequential", 30, run_write_byte, &seq_addrs);
group!("write_word/sequential", 30, run_write_word, &seq_addrs);
group!("read_page/sequential", 30, run_read_page, &page_addrs_seq);
group!("read_page/random", 30, run_read_page, &page_addrs_rand);
group!("write_page/sequential", 30, run_write_page, &page_addrs_seq);
group!("write_page/random", 30, run_write_page, &page_addrs_rand);
}
criterion_group!(benches, benchmarks);
criterion_main!(benches);
+73
View File
@@ -0,0 +1,73 @@
use std::{fs, path::PathBuf};
use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use crate::{MemoryMap, RandomAccessMemory, config::IoMapping, io::DeviceId};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct DsaArgs {
/// Memory map to use on boot.
/// Overrides default value.
#[arg(long = "mmap")]
memory_map: Option<PathBuf>,
#[arg(long = "bin")]
binary: Option<PathBuf>,
#[arg(long = "iomap")]
io_map: Option<PathBuf>,
#[arg(value_enum, long = "mem", default_value = "bulk-alloc")]
pub memory_bank: MemoryBank,
}
impl DsaArgs {
pub fn get_memory_map(&self) -> MemoryMap {
self.memory_map
.as_ref()
.and_then(|m| {
fs::read_to_string(m)
.ok()
.and_then(|map| ron::from_str(&map).ok())
})
.unwrap_or_else(|| {
ron::from_str(include_str!("./config/default/dsa.mmap.ron")).unwrap()
})
}
pub fn get_binary(&self) -> Option<Vec<u8>> {
self.binary
.clone()
.and_then(|path| Some(fs::read(path).unwrap()))
}
pub fn get_io_map(&self) -> Vec<IoMapping> {
self.io_map
.as_ref()
.and_then(|m| {
fs::read_to_string(m)
.ok()
.and_then(|map| ron::from_str(&map).ok())
})
.unwrap_or_else(|| {
ron::from_str(include_str!("./config/default/dsa.iomap.ron")).unwrap()
})
}
}
#[derive(Debug, Clone, ValueEnum, Default)]
pub enum MemoryBank {
#[cfg(feature = "mainstore-bulkalloc")]
#[default]
BulkAlloc,
#[cfg(feature = "mainstore-arraymap")]
ArrayMap,
#[cfg(feature = "mainstore-hashmap")]
HashMap,
#[cfg(feature = "mainstore-prealloc")]
PreAlloc,
#[cfg(feature = "mainstore-stackarray")]
StackArray,
}
+330
View File
@@ -0,0 +1,330 @@
use common::instructions::Instruction;
use dsa::{BulkAllocStore, Emulator, Page, RandomAccessMemory};
/// DSA Emulator Benchmark
///
/// Place this in `dsa/src/bin/bench.rs` and run with:
/// cargo run --bin bench --release
///
/// Three workloads are tested:
/// 1. ALU-heavy — tight arithmetic loop, no memory, no branches
/// 2. Memory-heavy — repeated load/store to a working set of addresses
/// 3. Branch-heavy — the bf.dsa dispatch pattern (ieq + jnz chains)
///
/// Each workload is a hand-assembled flat binary loaded directly into
/// the emulator, bypassing all the args/display/IO plumbing.
use std::{
thread,
time::{Duration, Instant},
u16,
};
// ---------------------------------------------------------------------------
// Minimal no-op memory map + IO so the emulator boots without a config file.
// ---------------------------------------------------------------------------
fn make_emulator() -> Emulator<BulkAllocStore> {
use dsa::{
config::{MemoryMap, Region, RegionType},
io::display::DisplayDevice,
};
let mmap = MemoryMap {
table_addr: 0x40000,
regions: vec![
Region {
base: 0x0000_0000,
size: 0x0010_0000, // 1 MiB RAM
region_type: RegionType::Usable,
identity_map_on_boot: true,
},
Region {
base: 0x0020_0000,
size: 0x0002_0000, // 128 KiB MMIO (display)
region_type: RegionType::MMIO,
identity_map_on_boot: false,
},
],
};
let iomap = vec![dsa::config::IoMapping {
device: dsa::io::DeviceId::Display,
base: 0x0020_0000,
size: 0x0000_07D0,
}];
let (display, _handle) = DisplayDevice::new(80, 25);
Emulator::new(BulkAllocStore::new())
.with_device(display)
.apply_memory_map(mmap)
.apply_io_map(iomap)
.unwrap()
}
// ---------------------------------------------------------------------------
// Instruction encoding helpers (little-endian words)
// ---------------------------------------------------------------------------
/// Encode an I-type instruction: [op:6][src:5][dst:5][imm:16]
fn enc_i(op: u8, src: u8, dst: u8, imm: u16) -> u32 {
((op as u32 & 0x3F) << 26)
| ((src as u32 & 0x1F) << 21)
| ((dst as u32 & 0x1F) << 16)
| imm as u32
}
/// Encode an R-type instruction: [op:6][r1:5][r2:5][r3:5][r4:5][u6:6]
fn enc_r(op: u8, r1: u8, r2: u8, r3: u8, r4: u8, u6: u8) -> u32 {
((op as u32 & 0x3F) << 26)
| ((r1 as u32 & 0x1F) << 21)
| ((r2 as u32 & 0x1F) << 16)
| ((r3 as u32 & 0x1F) << 11)
| ((r4 as u32 & 0x1F) << 6)
| (u6 as u32 & 0x3F)
}
// Register indices — must match your Register enum discriminants
const RG0: u8 = 0;
const RG1: u8 = 1;
const RG2: u8 = 2;
const RG3: u8 = 3;
const RG4: u8 = 4;
const RG5: u8 = 5;
const ZERO: u8 = 16;
// Opcodes — must match your Opcode enum discriminants.
// Fill these in from your ISA.md / encode.rs.
const OP_NOP: u8 = 0x0D; // adjust to match your actual opcode values
const OP_HLT: u8 = 0x2B;
const OP_ADD: u8 = 0x22;
const OP_ADDI: u8 = 0x23;
const OP_SUBI: u8 = 0x24;
const OP_LDW: u8 = 0x07;
const OP_STW: u8 = 0x0A;
const OP_LLI: u8 = 0x0B;
const OP_LUI: u8 = 0x0C;
const OP_IEQ: u8 = 0x0E;
const OP_JNZ: u8 = 0x15;
const OP_JMP: u8 = 0x12;
// ---------------------------------------------------------------------------
// Workload 1: ALU-heavy
//
// Counts down from 10_000_000 using subi, jumps back to top.
// Pure register arithmetic, no memory accesses after init.
// Instruction mix: lli, subi, ieq, jnz, hlt
//
// Layout (each word at address = index * 4):
// 0: lli rg0, 0 (upper half of count) [lui follows]
// 4: lui rg0, <high>
// 8: loop: subi rg0, 1, rg0
// 12: ieq rg0, zero, rg1
// 16: jnz rg1, zero, 8 [jump to loop]
// 20: hlt
// ---------------------------------------------------------------------------
fn alu_workload() -> Vec<u8> {
let count: u32 = 10_000_000;
let lo = (count & 0xFFFF) as u16;
let hi = ((count >> 16) & 0xFFFF) as u16;
let words: &[u32] = &[
enc_i(OP_LLI, 0, RG0, lo), // 0x00: lli rg0, lo(count)
enc_i(OP_LUI, 0, RG0, hi), // 0x04: lui rg0, hi(count)
// loop @ 0x08:
enc_i(OP_SUBI, RG0, RG0, 1), // 0x08: subi rg0, rg0, 1
enc_r(OP_IEQ, RG0, ZERO, RG1, 0, 0), // 0x0C: ieq rg0, zero, rg1
enc_i(OP_JNZ, RG1, ZERO, 0x0008), // 0x10: jnz rg1, zero, 0x08 <- BUG: jnz jumps when rg1!=0
// rg1=1 when rg0==0, so we want jez here to keep looping
// fix: use jez (jump when rg1==0, i.e. rg0!=0)
enc_i(OP_HLT, 0, 0, 0), // 0x14: hlt
];
// Note: replace OP_JNZ above with your JEZ opcode so the loop continues
// while rg0 != 0. JNZ exits when rg1 != 0, i.e. when rg0 == 0 (done).
// The ieq sets rg1=1 only when rg0==0, so jnz rg1 exits the loop correctly.
// Actually this IS correct: loop while ieq returns 0 (rg0 != 0),
// exit when ieq returns 1 (rg0 == 0). jnz rg1 = jump when rg1 != 0 = jump when done.
// We want to jump BACK while not done, so we need jez:
// jez rg1, zero, 0x08 = jump back when rg1==0 (rg0 != 0, not done yet)
// Swap OP_JNZ for your JEZ opcode.
words_to_bytes(words)
}
// ---------------------------------------------------------------------------
// Workload 2: Memory-heavy
//
// Repeatedly writes and reads a 64-word working set.
// Tests page table lookup performance under repeated memory access.
//
// rg0 = base address (0x1000, well within RAM, page-aligned)
// rg1 = loop counter (1000 outer iterations)
// rg2 = inner counter (64 words per pass)
// rg3 = scratch for store value
// ---------------------------------------------------------------------------
fn memory_workload() -> Vec<u8> {
let base: u32 = 0x1000;
let outer: u16 = 1000;
let inner: u16 = 64;
let words: &[u32] = &[
// init
enc_i(OP_LLI, 0, RG0, (base & 0xFFFF) as u16), // rg0 = base
enc_i(OP_LLI, 0, RG1, outer), // rg1 = outer count
// outer_loop @ 0x08:
enc_i(OP_LLI, 0, RG2, inner), // rg2 = inner count
enc_i(OP_LLI, 0, RG3, 0xABCD), // rg3 = value to write
// inner_loop @ 0x10:
enc_i(OP_STW, RG3, RG0, 0), // stw rg3, rg0, 0
enc_i(OP_LDW, RG0, RG3, 0), // ldw rg0, rg3, 0 (read back)
enc_i(OP_ADDI, RG0, RG0, 4), // rg0 += 4
enc_i(OP_SUBI, RG2, RG2, 1), // rg2 -= 1
enc_r(OP_IEQ, RG2, ZERO, RG4, 0, 0), // ieq rg2, zero, rg4
enc_i(OP_JNZ, RG4, ZERO, 0x0010), // jez rg4 -> inner_loop (swap opcode)
// restore base, dec outer
enc_i(OP_LLI, 0, RG0, (base & 0xFFFF) as u16),
enc_i(OP_SUBI, RG1, RG1, 1),
enc_r(OP_IEQ, RG1, ZERO, RG4, 0, 0),
enc_i(OP_JNZ, RG4, ZERO, 0x0008), // jez rg4 -> outer_loop (swap opcode)
enc_i(OP_HLT, 0, 0, 0),
];
words_to_bytes(words)
}
// ---------------------------------------------------------------------------
// Workload 3: Branch-heavy
//
// Mimics the bf.dsa dispatch pattern: compare a value against 8 constants,
// branch on each. Worst case for branch predictor.
// rg0 = current "instruction" (cycles through 8 values)
// rg8-rgf = the 8 bf opcode constants
// ---------------------------------------------------------------------------
fn branch_workload() -> Vec<u8> {
// 8 bf opcodes: + - > < . , [ ]
let opcodes: [u16; 8] = [43, 45, 62, 60, 46, 44, 91, 93];
// We'll cycle rg0 through these values and do the full dispatch chain.
// To keep it self-contained: load opcodes, set outer loop counter,
// inner loop cycles rg0 through all 8 values doing ieq+jnz for each.
let outer: u16 = u16::MAX;
let mut w: Vec<u32> = Vec::new();
// Load 8 opcode constants into rg8-rgf (regs 8-15)
for (i, &op) in opcodes.iter().enumerate() {
w.push(enc_i(OP_LLI, 0, 8 + i as u8, op));
}
// rg1 = outer counter
w.push(enc_i(OP_LLI, 0, RG1, outer));
// outer_loop:
let outer_loop_addr = (w.len() * 4) as u16;
// rg5 = inner counter (8 opcodes)
w.push(enc_i(OP_LLI, 0, RG5, 8));
// rg2 = pointer into opcode table (we'll use addi to step rg0 through values)
// For simplicity: just set rg0 to each opcode in sequence via lli each iteration
// This is branch-heavy, not arithmetic-heavy, so the lli overhead is fine.
for (i, &op) in opcodes.iter().enumerate() {
let next_offset = ((w.len() + 4) * 4) as u16; // addr after the jnz
w.push(enc_i(OP_LLI, 0, RG0, op)); // rg0 = opcode
w.push(enc_r(OP_IEQ, RG0, 8 + i as u8, RG4, 0, 0)); // ieq rg0, rgX, rg4
w.push(enc_i(OP_JNZ, RG4, ZERO, next_offset)); // jnz rg4 (jez to skip)
w.push(enc_i(OP_NOP, 0, 0, 0)); // "handler" nop
}
// dec outer, loop
let after_dispatch = (w.len() * 4) as u16;
w.push(enc_i(OP_SUBI, RG1, RG1, 1));
w.push(enc_r(OP_IEQ, RG1, ZERO, RG4, 0, 0));
w.push(enc_i(OP_JNZ, RG4, ZERO, outer_loop_addr)); // jez -> outer_loop
w.push(enc_i(OP_HLT, 0, 0, 0));
words_to_bytes(&w)
}
fn words_to_bytes(words: &[u32]) -> Vec<u8> {
words.iter().flat_map(|w| w.to_le_bytes()).collect()
}
// ---------------------------------------------------------------------------
// Runner
// ---------------------------------------------------------------------------
struct BenchResult {
name: &'static str,
instructions: u64,
elapsed: Duration,
mips: f64,
}
fn run_bench(name: &'static str, binary: Vec<u8>) -> BenchResult {
let mut emu = make_emulator();
let handle = emu.state_handle();
let pages: Vec<Page> = Page::paginate(&binary).collect();
for (i, page) in pages.iter().enumerate() {
emu.memory_mut().write_page(i as u32 * 4096, page);
}
let start = Instant::now();
// run() blocks until HLT
emu.run().expect("emulator run failed");
let elapsed = start.elapsed();
// read clock from emulator — we need to expose it.
// For now, estimate from elapsed + target MIPS.
// TODO: expose emulator.clock() -> u64 and use that directly.
let instructions = handle.proc.load().clock as u64;
let mips = instructions as f64 / elapsed.as_micros() as f64;
BenchResult {
name,
instructions,
elapsed,
mips,
}
}
fn main() {
println!("DSA Emulator Benchmark");
println!("======================");
println!();
// NOTE: you need to expose `pub fn clock(&self) -> usize` on Emulator
// and ensure `run()` returns normally on HLT without blocking in idle_wait.
// The idle_wait loop currently spins forever after HLT — for benchmarking,
// you want run() to return when the program halts. Add a flag or check:
// if !self.internal_state.running { break 'emu; }
// after the HLT handler sets running=false.
let workloads: &[(&'static str, fn() -> Vec<u8>)] = &[
("ALU-heavy (10M countdown)", alu_workload),
("Memory-heavy (64K rw ops)", memory_workload),
("Branch-heavy (bf dispatch)", branch_workload),
];
let mut results = Vec::new();
for &(name, build) in workloads {
print!("Running {}... ", name);
let binary = build();
let result = run_bench(name, binary);
println!("done ({:.1}ms)", result.elapsed.as_secs_f64() * 1000.0);
results.push(result);
}
println!();
println!(
"{:<35} {:>12} {:>12} {:>10}",
"Workload", "Instructions", "Time", "MIPS"
);
println!("{}", "-".repeat(72));
for r in &results {
println!(
"{:<35} {:>12} {:>10.1}ms {:>10.1}",
r.name,
r.instructions,
r.elapsed.as_secs_f64() * 1000.0,
r.mips,
);
}
let avg_mips: f64 = results.iter().map(|r| r.mips).sum::<f64>() / results.len() as f64;
println!("{}", "-".repeat(72));
println!("{:<35} {:>35.1} MIPS avg", "Overall", avg_mips);
}
@@ -0,0 +1,7 @@
[
IoMapping(
device: Display,
base: 0x0002_0000,
size: 0x0000_07D0, // 2000 bytes (80x25)
),
]
@@ -0,0 +1,41 @@
MemoryMap(
table_addr: 0x1000,
regions: [
// 0000-1000 = reserved
// Region(
// base: 0x0,
// size: 0x1000,
// region_type: Reserved
// ),
// // 1000-4000 = bootloader
// Region(
// base: 0x1000,
// size: 0x3000,
// region_type: Bootloader
// ),
// 5000-0FFFFFFF = MMIO
Region(
base: 0x20000,
size: 0x0FFF_FFFF,
region_type: MMIO
),
// 10000000-BFFFFFFF = userspace
Region(
base: 0x1000_0000,
size: 0xBFFF_FFFF,
region_type: Usable
),
// C0000000-C0FFFFFF = kernel page tables
Region(
base: 0xC000_0000,
size: 0x100_0000,
region_type: KernelTables
),
// C1000000-FFFFFFFF = kernel
Region(
base: 0xC100_0000,
size: 0x3EFF_FFFF,
region_type: Kernel
)
]
)
+80
View File
@@ -0,0 +1,80 @@
use serde::{Deserialize, Serialize};
use crate::{
Emulator, RandomAccessMemory,
io::DeviceId,
memory::{PhysAddr, mmu::MMU},
};
#[repr(u32)]
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub enum RegionType {
Reserved,
Bootloader,
Kernel,
KernelTables,
Usable,
MMIO,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct Region {
pub base: PhysAddr,
pub size: u32,
pub region_type: RegionType,
// flags for the emulator
#[serde(default)]
pub identity_map_on_boot: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MemoryMap {
pub table_addr: PhysAddr,
pub regions: Vec<Region>,
}
#[derive(Serialize, Deserialize)]
pub struct IoMapping {
pub device: DeviceId,
pub base: u32,
pub size: u32,
}
impl MemoryMap {
pub const DEFAULT_MEMORY_MAP_ADDR: PhysAddr = 0x1000;
pub fn new() -> Self {
MemoryMap {
table_addr: Self::DEFAULT_MEMORY_MAP_ADDR,
regions: Vec::new(),
}
}
pub fn apply(&self, cpu: &mut Emulator<impl RandomAccessMemory>) {
for reg in &self.regions {
if reg.identity_map_on_boot {
Self::identity_map(cpu.mmu_mut(), reg);
}
}
let mem = cpu.memory_mut();
let mut offset = self.table_addr;
for region in &self.regions {
println!("Wrote entry: {region:?} to page table");
mem.write_word(offset, region.base);
mem.write_word(offset + 4, region.size);
mem.write_word(offset + 8, region.region_type as u32);
offset += 12;
}
}
pub fn identity_map(mmu: &mut MMU, region: &Region) {
let first_page = region.base >> 12;
let page_count = region.size >> 12;
for page in (first_page..first_page + page_count).map(|p| p << 12) {
mmu.create_mapping(page, page);
}
}
}
+80
View File
@@ -0,0 +1,80 @@
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU8, Ordering},
};
use crate::{
io::{IoAccess, IoDevice},
processor::interrupts::Interrupt,
};
pub struct DisplayDevice {
buffer: Arc<Box<[AtomicU8]>>,
dirty: Arc<AtomicBool>,
}
impl DisplayDevice {
pub fn new(width: usize, height: usize) -> (Self, DisplayHandle) {
let buffer = Arc::new(
(0..width * height)
.map(|_| AtomicU8::new(0))
.collect::<Vec<_>>()
.into_boxed_slice(),
);
let dirty = Arc::new(AtomicBool::new(false));
let handle = DisplayHandle {
buffer: Arc::clone(&buffer),
dirty: Arc::clone(&dirty),
};
(Self { buffer, dirty }, handle)
}
}
impl IoDevice for DisplayDevice {
fn size(&self) -> u32 {
(self.buffer.len()) as u32
}
fn access(&self) -> IoAccess {
IoAccess::WRITE
}
fn id(&self) -> super::DeviceId {
super::DeviceId::Display
}
fn write_word(&mut self, offset: u32, val: u32) -> Result<(), Interrupt> {
let bytes = val.to_le_bytes();
self.buffer[offset as usize].store(bytes[0], Ordering::Relaxed);
self.buffer[offset as usize + 1].store(bytes[1], Ordering::Relaxed);
self.buffer[offset as usize + 2].store(bytes[2], Ordering::Relaxed);
self.buffer[offset as usize + 3].store(bytes[3], Ordering::Relaxed);
self.dirty.store(true, Ordering::Relaxed);
Ok(())
}
fn write_byte(&mut self, offset: u32, val: u8) -> Result<(), Interrupt> {
self.buffer[offset as usize].store(val, Ordering::Relaxed);
self.dirty.store(true, Ordering::Relaxed);
Ok(())
}
}
pub struct DisplayHandle {
pub buffer: Arc<Box<[AtomicU8]>>,
pub dirty: Arc<AtomicBool>,
}
impl DisplayHandle {
pub fn is_dirty(&self) -> bool {
self.dirty.swap(false, Ordering::Relaxed)
}
pub fn read_all(&self) -> Vec<u8> {
self.buffer
.iter()
.map(|b| b.load(Ordering::Relaxed))
.collect()
}
}
+124
View File
@@ -0,0 +1,124 @@
use serde::{Deserialize, Serialize};
use crate::{Emulator, RandomAccessMemory, processor::interrupts::Interrupt};
pub mod display;
pub struct MappedDevice {
pub base: u32,
pub device: Box<dyn IoDevice>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IoAccess(u8);
impl IoAccess {
pub const READ: Self = Self(0b01);
pub const WRITE: Self = Self(0b10);
/// Convenience alias for Read + Write.
pub const READWRITE: Self = Self(Self::READ.0 | Self::WRITE.0);
pub const NONE: Self = Self(0);
/// Bitwise OR returns a new `IoAccess` that contains all flags from both operands.
#[inline]
pub fn or(self, other: Self) -> Self {
Self(self.0 | other.0)
}
/// Check if a flag is present.
#[inline]
pub fn contains(&self, flag: Self) -> bool {
self.0 & flag.0 != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeviceId {
Display,
Serial,
Random,
Timer,
}
/// Trait representing an input/output device that can be mapped into a memory space.
///
/// - Default methods return dummy data.
pub trait IoDevice: Send {
/// Return the size (in bytes) of the device's addressable region.
fn size(&self) -> u32;
/// Return the access permissions for the device.
fn access(&self) -> IoAccess;
/// Return the DeviceId
fn id(&self) -> DeviceId;
#[inline]
/// Read a single byte from the specified address.
///
/// By default, writeonly devices return `None`. All other devices
/// return `Some(0)` as placeholder data. Implementations should override
/// this method to provide actual read logic.
fn read_byte(&self, offset: u32) -> Result<u8, Interrupt> {
if self.access().contains(IoAccess::READ) {
Ok(0)
} else {
Err(Interrupt::ReadFromWriteOnly)
}
}
#[inline]
/// Write a single byte to the specified address.
///
/// By default, readonly devices return `false` indicating failure.
/// All other devices return `true`. Implementations should override
/// this method to perform real write operations and return whether
/// the operation succeeded.
fn write_byte(&mut self, offset: u32, val: u8) -> Result<(), Interrupt> {
if self.access().contains(IoAccess::WRITE) {
Ok(())
} else {
Err(Interrupt::WriteToReadOnly)
}
}
/// Read a 32bit word from the specified address in littleendian order.
///
/// The default implementation composes four consecutive byte reads using
/// `read_byte`. If any byte read fails, the whole operation returns `None`.
/// Writeonly devices return `None` immediately. Override this method for
/// more efficient word reads.
#[inline]
fn read_word(&self, offset: u32) -> Result<u32, Interrupt> {
if self.access().contains(IoAccess::READ) {
// default: compose from bytes
let b0 = self.read_byte(offset)? as u32;
let b1 = self.read_byte(offset + 1)? as u32;
let b2 = self.read_byte(offset + 2)? as u32;
let b3 = self.read_byte(offset + 3)? as u32;
return Ok(b0 | b1 << 8 | b2 << 16 | b3 << 24);
}
Err(Interrupt::ReadFromWriteOnly)
}
/// Write a 32bit word to the specified address in littleendian order.
///
/// The default implementation splits the value into bytes and writes each
/// using `write_byte`. Readonly devices return `false`; otherwise returns
/// `true` after attempting all byte writes. Override for more efficient
/// word writes or to handle partial failures.
fn write_word(&mut self, offset: u32, val: u32) -> Result<(), Interrupt> {
if self.access().contains(IoAccess::WRITE) {
let bytes = val.to_le_bytes();
self.write_byte(offset, bytes[0]);
self.write_byte(offset + 1, bytes[1]);
self.write_byte(offset + 2, bytes[2]);
self.write_byte(offset + 3, bytes[3]);
return Ok(());
}
Err(Interrupt::WriteToReadOnly)
}
}
+11
View File
@@ -0,0 +1,11 @@
#![feature(likely_unlikely)]
pub mod args;
pub mod config;
pub mod io;
mod memory;
mod processor;
pub use {config::MemoryMap, processor::processor::Emulator, processor::state::SharedState};
pub use memory::ram::*;
+96
View File
@@ -0,0 +1,96 @@
use clap::Parser;
use common::{instructions::Instruction, register::Register};
use dsa::{
BulkAllocStore, Emulator, Page, RandomAccessMemory, SharedState,
args::DsaArgs,
io::display::{DisplayDevice, DisplayHandle},
};
use std::{
os::unix::thread::JoinHandleExt,
process::exit,
sync::{Arc, atomic::Ordering},
thread,
time::Duration,
};
fn main() {
let args = DsaArgs::parse();
let mmap = args.get_memory_map();
let iomap = args.get_io_map();
let store = BulkAllocStore::new();
let (display, handle) = DisplayDevice::new(80, 25);
let mut emulator = Emulator::new(store)
.with_device(display)
.apply_memory_map(mmap)
.apply_io_map(iomap)
.unwrap();
if let Some(bin) = args.get_binary() {
for (i, ch) in bin.chunks(4).enumerate() {
let instruction = Instruction(u32::from_le_bytes(ch.try_into().unwrap()));
let hex = ch
.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<_>>()
.join(" ");
let chars = ch
.iter()
.map(|b| {
if b.is_ascii_graphic() {
*b as char
} else {
'.'
}
})
.collect::<String>();
println!("{:04X}: {:<12} {:4} {:?}", i * 4, hex, chars, instruction);
}
let program: Vec<Page> = Page::paginate(&bin).collect();
for (i, page) in program.iter().enumerate() {
emulator.memory_mut().write_page(i as u32 * 4096, &page);
}
}
let state = emulator.state_handle();
const STACK_SIZE: usize = 1024 * 1024 * 16;
let runner = thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(move || emulator.run().unwrap())
.unwrap();
let observer = thread::spawn(|| observe(state, handle));
runner.join().unwrap();
thread::sleep(Duration::from_millis(100));
// yeah we don't need this tbh.
// observer.join().unwrap();
}
/// todo: remove this!
fn observe(state: Arc<SharedState>, handle: DisplayHandle) {
loop {
state.update_req.store(true, Ordering::Relaxed);
thread::sleep(std::time::Duration::from_millis(20));
let state = state.proc.load();
// println!(
// "clock: {}, GP Registers: {:?}",
// state.clock, state.registers
// );
if handle.is_dirty() {
for line in handle.read_all().chunks(80) {
for (i, byte) in line.iter().enumerate() {
print!("{}", *byte as char);
}
println!();
}
}
}
}
View File
+109
View File
@@ -0,0 +1,109 @@
use crate::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB};
pub struct MMU {
buffer: TLB,
enable_paging: bool,
// Memory Management Register (MMR) Tells MMU where to find page tables
mmr_value: u32,
page_table: Box<[Option<PageTable>; 1024]>,
}
impl MMU {
pub fn new() -> Self {
MMU {
buffer: TLB::new(),
enable_paging: false,
mmr_value: 0,
page_table: Box::new([const { None }; 1024]),
}
}
pub fn paging_enabled(&mut self, enable: bool) {
self.enable_paging = enable;
}
pub fn tlb_insert(&mut self, virtual_address: VirtAddr, physical_address: PhysAddr) {
self.buffer.insert(virtual_address, physical_address);
}
pub fn create_mapping(&mut self, virtual_address: VirtAddr, physical_address: PhysAddr) {
let table = (virtual_address >> 22) as usize;
let page = ((virtual_address >> 12) & 0x3FF) as usize;
self.page_table[table].get_or_insert_default().entries[page] =
Some(PageTableEntry::new(physical_address, true, true, false));
}
#[inline(always)]
pub fn lookup(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
if !self.enable_paging {
return Ok(virtual_address);
}
if let Some(addr) = self.buffer.lookup(virtual_address) {
return Ok(addr);
}
self.walk(virtual_address)
}
#[cold]
#[inline(never)]
fn walk(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
let phys_addr = self.walk_page_table(virtual_address)?;
self.buffer.insert(virtual_address, phys_addr);
Ok(phys_addr)
}
fn walk_page_table(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
let table = (virtual_address >> 22) as usize;
let page = ((virtual_address >> 12) & 0x3FF) as usize;
if let Some(table) = &self.page_table[table] {
return table.entries[page]
.map(|entry| entry.physical_addr)
.ok_or(FaultInfo::PageFault);
} else {
Err(FaultInfo::PageFault)
}
}
}
#[derive(Debug, Clone)]
struct PageDir {
entries: Box<[PageTable; 1024]>,
}
#[derive(Debug, Clone)]
struct PageTable {
entries: Box<[Option<PageTableEntry>; 1024]>,
}
impl Default for PageTable {
fn default() -> Self {
Self {
entries: Box::new([None; 1024]),
}
}
}
#[derive(Debug, Clone, Copy)]
struct PageTableEntry {
physical_addr: PhysAddr,
writable: bool,
present: bool,
user: bool,
}
impl PageTableEntry {
fn new(physical_addr: PhysAddr, writable: bool, present: bool, user: bool) -> Self {
Self {
physical_addr,
writable,
present,
user,
}
}
}
+15
View File
@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use crate::{RandomAccessMemory, memory::mmu::MMU, processor::processor::Emulator};
mod cache;
pub mod mmu;
pub mod ram;
mod tlb;
pub type VirtAddr = u32;
pub type PhysAddr = u32;
pub enum FaultInfo {
PageFault,
}
+107
View File
@@ -0,0 +1,107 @@
use std::{
alloc::{Layout, alloc_zeroed},
hint::{likely, unlikely},
};
use crate::memory::{
PhysAddr, RandomAccessMemory,
ram::{NUM_PAGES, Page, idx},
};
pub struct ArrayStore {
pages: Box<[*mut Page; NUM_PAGES]>,
}
unsafe impl Send for ArrayStore {}
impl ArrayStore {
pub fn new() -> Self {
let pages = vec![0 as *mut Page; 2 << 20]
.into_boxed_slice()
.try_into()
.unwrap();
Self { pages }
}
fn alloc(&mut self) -> *mut Page {
let layout = Layout::from_size_align(4096, 4096).unwrap();
unsafe { alloc_zeroed(layout) as *mut Page }
}
}
impl RandomAccessMemory for ArrayStore {
#[inline(always)]
fn read_word(&self, addr: PhysAddr) -> u32 {
debug_assert_eq!(addr % 4, 0);
let (idx, offset) = idx(addr);
if likely(!self.pages[idx].is_null()) {
return unsafe { (self.pages[idx] as *const u32).byte_add(offset).read() };
}
// Slow path: MMIO, fault, etc — never inlined
// Since we're only reading, we can safely return 0
return 0;
}
#[inline(always)]
fn read_byte(&self, addr: PhysAddr) -> u8 {
let (idx, offset) = idx(addr);
if likely(!self.pages[idx].is_null()) {
return unsafe { (self.pages[idx] as *const u8).byte_add(offset).read() };
}
return 0;
}
#[inline(always)]
fn read_page(&self, addr: PhysAddr) -> &Page {
debug_assert_eq!(addr % 4096, 0);
let (idx, _) = idx(addr);
if likely(!self.pages[idx].is_null()) {
return unsafe { &*(self.pages[idx] as *const Page) };
}
return &Page([0; 4096]);
}
#[inline(always)]
fn write_byte(&mut self, addr: PhysAddr, value: u8) {
let (idx, offset) = idx(addr);
if unlikely(self.pages[idx].is_null()) {
self.pages[idx] = self.alloc();
}
unsafe {
(self.pages[idx] as *mut u8).byte_add(offset).write(value);
}
}
#[inline(always)]
fn write_word(&mut self, addr: PhysAddr, value: u32) {
debug_assert_eq!(addr % 4, 0);
let (idx, offset) = idx(addr);
if unlikely(self.pages[idx].is_null()) {
self.pages[idx] = self.alloc();
}
unsafe { (self.pages[idx] as *mut u32).byte_add(offset).write(value) }
}
#[inline(always)]
fn write_page(&mut self, addr: PhysAddr, value: &Page) {
debug_assert_eq!(addr % 4096, 0);
let (idx, _) = idx(addr);
if unlikely(self.pages[idx].is_null()) {
self.pages[idx] = self.alloc();
}
unsafe {
(self.pages[idx] as *mut Page).copy_from(value, 1);
}
}
}
+134
View File
@@ -0,0 +1,134 @@
use std::{
alloc::{Layout, alloc_zeroed},
hint::{likely, unlikely},
};
use crate::memory::{
PhysAddr, RandomAccessMemory,
ram::{Page, idx},
};
pub struct BulkAllocStore {
pages: Box<[*mut Page; Page::COUNT]>,
pre_allocated: [*mut Page; Self::ALLOC_AT_ONCE],
idx: u8,
}
unsafe impl Send for BulkAllocStore {}
impl BulkAllocStore {
const ALLOC_AT_ONCE: usize = 256 as usize;
pub fn new() -> Self {
let pages = vec![0 as *mut Page; Page::COUNT]
.into_boxed_slice()
.try_into()
.unwrap();
Self {
pages,
pre_allocated: Self::alloc_set(),
idx: 0,
}
}
fn alloc_set() -> [*mut Page; Self::ALLOC_AT_ONCE] {
let layout = Layout::from_size_align(Page::SIZE * Self::ALLOC_AT_ONCE, Page::SIZE).unwrap();
let mut region = unsafe { alloc_zeroed(layout) as *mut Page };
let mut set = [0 as *mut Page; Self::ALLOC_AT_ONCE];
for i in 0..Self::ALLOC_AT_ONCE {
set[i] = region;
region = unsafe { region.byte_add(4096) };
}
set
}
fn alloc(&mut self) -> *mut Page {
if self.idx < Self::ALLOC_AT_ONCE as u8 {
let page = self.pre_allocated[self.idx as usize];
self.idx += 1;
page
} else {
self.idx = 0;
self.pre_allocated = Self::alloc_set();
self.pre_allocated[0]
}
}
}
impl RandomAccessMemory for BulkAllocStore {
#[inline(always)]
fn read_word(&self, addr: PhysAddr) -> u32 {
debug_assert_eq!(addr % 4, 0);
let (idx, offset) = idx(addr);
if likely(!self.pages[idx].is_null()) {
return unsafe { (self.pages[idx] as *const u32).byte_add(offset).read() };
}
// Slow path: MMIO, fault, etc — never inlined
// Since we're only reading, we can safely return 0
return 0;
}
#[inline(always)]
fn read_byte(&self, addr: PhysAddr) -> u8 {
let (idx, offset) = idx(addr);
if likely(!self.pages[idx].is_null()) {
return unsafe { (self.pages[idx] as *const u8).byte_add(offset).read() };
}
return 0;
}
#[inline(always)]
fn read_page(&self, addr: PhysAddr) -> &Page {
debug_assert_eq!(addr % Page::SIZE as u32, 0);
let (idx, _) = idx(addr);
if likely(!self.pages[idx].is_null()) {
return unsafe { &*(self.pages[idx] as *const Page) };
}
return &Page::ZERO;
}
#[inline(always)]
fn write_byte(&mut self, addr: PhysAddr, value: u8) {
let (idx, offset) = idx(addr);
if unlikely(self.pages[idx].is_null()) {
self.pages[idx] = self.alloc();
}
unsafe {
(self.pages[idx] as *mut u8).byte_add(offset).write(value);
}
}
#[inline(always)]
fn write_word(&mut self, addr: PhysAddr, value: u32) {
debug_assert_eq!(addr % 4, 0);
let (idx, offset) = idx(addr);
if unlikely(self.pages[idx].is_null()) {
self.pages[idx] = self.alloc();
}
unsafe { (self.pages[idx] as *mut u32).byte_add(offset).write(value) }
}
#[inline(always)]
fn write_page(&mut self, addr: PhysAddr, value: &Page) {
debug_assert_eq!(addr % Page::SIZE as u32, 0);
let (idx, _) = idx(addr);
if unlikely(self.pages[idx].is_null()) {
self.pages[idx] = self.alloc();
}
unsafe {
(self.pages[idx] as *mut Page).copy_from(value, 1);
}
}
}
+66
View File
@@ -0,0 +1,66 @@
use fxhash::FxHashMap;
use crate::memory::{PhysAddr, RandomAccessMemory, ram::Page};
pub struct HashStore {
pages: FxHashMap<PhysAddr, Page>,
}
impl HashStore {
pub fn new() -> Self {
Self {
pages: FxHashMap::default(),
}
}
#[inline(always)]
const fn segment_addr(addr: PhysAddr) -> (PhysAddr, usize) {
(addr & !(0xFFF), (addr & 0xFFF) as usize)
}
}
impl RandomAccessMemory for HashStore {
#[inline(always)]
fn read_byte(&self, addr: PhysAddr) -> u8 {
let (page, offset) = Self::segment_addr(addr);
self.pages
.get(&page)
.map(|p| p.as_ref()[offset])
.unwrap_or(0)
}
#[inline(always)]
fn read_word(&self, addr: PhysAddr) -> u32 {
let (page, offset) = Self::segment_addr(addr);
debug_assert_eq!(offset % 4, 0);
let page = self.pages.get(&page).unwrap_or(&Page::ZERO);
u32::from_be_bytes(page[offset..=offset + 3].try_into().unwrap())
}
#[inline(always)]
fn read_page(&self, addr: PhysAddr) -> &Page {
debug_assert_eq!(addr % 0x1000, 0);
self.pages.get(&addr).unwrap_or(&Page::ZERO)
}
#[inline(always)]
fn write_byte(&mut self, addr: PhysAddr, value: u8) {
let (page, offset) = Self::segment_addr(addr);
self.pages.entry(page).or_insert_with(|| Page::zeroed())[offset] = value;
}
#[inline(always)]
fn write_word(&mut self, addr: PhysAddr, value: u32) {
let (page, offset) = Self::segment_addr(addr);
debug_assert_eq!(offset % 4, 0);
let page = self.pages.entry(page).or_insert_with(|| Page::zeroed());
page[offset..=offset + 3].copy_from_slice(&value.to_be_bytes());
}
#[inline(always)]
fn write_page(&mut self, addr: PhysAddr, value: &Page) {
debug_assert_eq!(addr % 0x1000, 0);
let page = self.pages.entry(addr).or_insert_with(|| Page::zeroed());
page.0 = value.0;
}
}
+108
View File
@@ -0,0 +1,108 @@
use std::ops::{Deref, DerefMut};
use crate::memory::PhysAddr;
#[cfg(feature = "mainstore-arraymap")]
pub mod arraymap;
#[cfg(feature = "mainstore-arraymap")]
pub use arraymap::ArrayStore;
#[cfg(feature = "mainstore-hashmap")]
pub mod hashmap;
#[cfg(feature = "mainstore-hashmap")]
pub use hashmap::HashStore;
#[cfg(feature = "mainstore-stackarray")]
pub mod stackarray;
#[cfg(feature = "mainstore-stackarray")]
pub use stackarray::StackArrayStore;
#[cfg(feature = "mainstore-bulkalloc")]
pub mod bulkalloc;
#[cfg(feature = "mainstore-bulkalloc")]
pub use bulkalloc::BulkAllocStore;
#[cfg(feature = "mainstore-prealloc")]
pub mod prealloc;
#[cfg(feature = "mainstore-prealloc")]
pub use prealloc::PreAllocStore;
#[repr(transparent)]
#[derive(Clone)]
pub struct Page([u8; 4096]);
impl Page {
pub const SIZE: usize = 4096;
pub const COUNT: usize = u32::MAX as usize / Self::SIZE + 1;
pub const MASK: u32 = 0xFFF;
pub const ZERO: Self = Page::zeroed();
pub const fn zeroed() -> Self {
Self([0; 4096])
}
pub const fn from(data: [u8; 4096]) -> Self {
Self(data)
}
pub fn read_word(&self, offset: usize) -> u32 {
u32::from_le_bytes(self.0[offset..offset + 4].try_into().unwrap())
}
pub fn write_word(&mut self, offset: usize, value: u32) {
self.0[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
pub fn paginate(data: &[u8]) -> impl Iterator<Item = Page> + '_ {
data.chunks(4096).map(|chunk| {
let mut arr = [0u8; 4096];
arr[..chunk.len()].copy_from_slice(chunk);
Page(arr)
})
}
}
impl Deref for Page {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Page {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TryFrom<Vec<u8>> for Page {
type Error = String;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
let arr: [u8; 4096] = data
.try_into()
.map_err(|v: Vec<u8>| format!("Expected 4096 bytes, got {}", v.len()))?;
Ok(Page(arr))
}
}
#[inline(always)]
const fn idx(addr: PhysAddr) -> (usize, usize) {
((addr >> 12) as usize, (addr & 0xFFF) as usize)
}
pub trait RandomAccessMemory {
fn read_byte(&self, addr: PhysAddr) -> u8;
fn write_byte(&mut self, addr: PhysAddr, value: u8);
fn read_word(&self, addr: PhysAddr) -> u32;
fn write_word(&mut self, addr: PhysAddr, value: u32);
fn read_page(&self, addr: PhysAddr) -> &Page;
fn write_page(&mut self, addr: PhysAddr, value: &Page);
}
+50
View File
@@ -0,0 +1,50 @@
use crate::memory::{
PhysAddr, RandomAccessMemory,
ram::{NUM_PAGES, Page},
};
pub struct PreAllocStore {
heap: Box<[u8; NUM_PAGES * 4096]>,
}
unsafe impl Send for PreAllocStore {}
impl PreAllocStore {
pub fn new() -> Self {
Self {
heap: unsafe { Box::new_zeroed().assume_init() },
}
}
}
impl RandomAccessMemory for PreAllocStore {
#[inline(always)]
fn read_word(&self, addr: PhysAddr) -> u32 {
unsafe { (self.heap.as_ptr().add(addr as usize) as *const u32).read() }
}
#[inline(always)]
fn read_byte(&self, addr: PhysAddr) -> u8 {
self.heap[addr as usize]
}
#[inline(always)]
fn read_page(&self, addr: PhysAddr) -> &Page {
debug_assert_eq!(addr % 4096, 0);
unsafe { (self.heap.as_ptr().add(addr as usize) as *const Page).as_ref_unchecked() }
}
#[inline(always)]
fn write_byte(&mut self, addr: PhysAddr, value: u8) {
self.heap[addr as usize] = value;
}
#[inline(always)]
fn write_word(&mut self, addr: PhysAddr, value: u32) {
unsafe { (self.heap.as_ptr().add(addr as usize) as *mut u32).write(value) };
}
#[inline(always)]
fn write_page(&mut self, addr: PhysAddr, value: &Page) {
self.heap[addr as usize..addr as usize + 4096].copy_from_slice(value);
}
}
+103
View File
@@ -0,0 +1,103 @@
use std::{
alloc::{Layout, alloc_zeroed},
hint::{likely, unlikely},
};
use crate::memory::{
PhysAddr, RandomAccessMemory,
ram::{NUM_PAGES, Page, idx},
};
pub struct StackArrayStore {
pages: [*mut Page; NUM_PAGES],
}
unsafe impl Send for StackArrayStore {}
impl StackArrayStore {
pub fn new() -> Self {
Self {
pages: [0 as *mut Page; 2 << 20],
}
}
fn alloc(&mut self) -> *mut Page {
let layout = Layout::from_size_align(4096, 4096).unwrap();
unsafe { alloc_zeroed(layout) as *mut Page }
}
}
impl RandomAccessMemory for StackArrayStore {
#[inline(always)]
fn read_word(&self, addr: PhysAddr) -> u32 {
debug_assert_eq!(addr % 4, 0);
let (idx, offset) = idx(addr);
if likely(!self.pages[idx].is_null()) {
return unsafe { (self.pages[idx] as *const u32).byte_add(offset).read() };
}
// Slow path: MMIO, fault, etc — never inlined
// Since we're only reading, we can safely return 0
return 0;
}
#[inline(always)]
fn read_byte(&self, addr: PhysAddr) -> u8 {
let (idx, offset) = idx(addr);
if likely(!self.pages[idx].is_null()) {
return unsafe { (self.pages[idx] as *const u8).byte_add(offset).read() };
}
return 0;
}
#[inline(always)]
fn read_page(&self, addr: PhysAddr) -> &Page {
debug_assert_eq!(addr % 4096, 0);
let (idx, _) = idx(addr);
if likely(!self.pages[idx].is_null()) {
return unsafe { &*(self.pages[idx] as *const Page) };
}
return &Page::ZERO;
}
#[inline(always)]
fn write_byte(&mut self, addr: PhysAddr, value: u8) {
let (idx, offset) = idx(addr);
if unlikely(self.pages[idx].is_null()) {
self.pages[idx] = self.alloc();
}
unsafe {
(self.pages[idx] as *mut u8).byte_add(offset).write(value);
}
}
#[inline(always)]
fn write_word(&mut self, addr: PhysAddr, value: u32) {
debug_assert_eq!(addr % 4, 0);
let (idx, offset) = idx(addr);
if unlikely(self.pages[idx].is_null()) {
self.pages[idx] = self.alloc();
}
unsafe { (self.pages[idx] as *mut u32).byte_add(offset).write(value) }
}
#[inline(always)]
fn write_page(&mut self, addr: PhysAddr, value: &Page) {
debug_assert_eq!(addr % 4096, 0);
let (idx, _) = idx(addr);
if unlikely(self.pages[idx].is_null()) {
self.pages[idx] = self.alloc();
}
unsafe { (self.pages[idx] as *mut Page).copy_from(value, 1) }
}
}
+54
View File
@@ -0,0 +1,54 @@
use crate::memory::{PhysAddr, VirtAddr};
const TLB_SIZE: usize = 256;
const TLB_MASK: u32 = (TLB_SIZE - 1) as u32;
pub struct TLB {
entries: [TlbEntry; TLB_SIZE],
}
#[derive(Debug, Clone, Copy)]
struct TlbEntry {
virtual_address: VirtAddr,
physical_address: PhysAddr,
valid: bool,
}
impl TLB {
pub fn new() -> Self {
TLB {
entries: [TlbEntry {
virtual_address: 0,
physical_address: 0,
valid: false,
}; TLB_SIZE],
}
}
#[inline(always)]
fn index(page: u32) -> usize {
let hash = page ^ (page >> 12);
(hash & TLB_MASK) as usize
}
#[inline(always)]
pub fn lookup(&self, virtual_address: VirtAddr) -> Option<PhysAddr> {
let page = virtual_address >> 12; // for 4KB pages
let entry = self.entries[Self::index(page)];
if entry.valid && entry.virtual_address == virtual_address {
Some(entry.physical_address)
} else {
None
}
}
#[inline(always)]
pub fn insert(&mut self, virt: VirtAddr, phys: PhysAddr) {
let idx = Self::index(virt) as usize;
self.entries[idx] = TlbEntry {
virtual_address: virt,
physical_address: phys,
valid: true,
};
}
}
+52
View File
@@ -0,0 +1,52 @@
#[repr(u8)]
#[derive(Clone, Copy, Debug)]
pub enum Interrupt {
// CPU exceptions 0-31
InvalidInterrupt = 0,
PageFault = 1,
ProtectionFault = 2,
InvalidOpcode = 3,
DivideByZero = 4,
StackOverflow = 5,
WriteToReadOnly = 6,
ReadFromWriteOnly = 7,
UnmappedIo = 8,
// 8-31 reserved for future CPU exceptions
// IRQ's (32-63)
Hardware(u8),
// Syscalls (64-255)
// OS defined, program uses INT 64-127
Software(u8) = 128,
}
impl Interrupt {
pub fn code(&self) -> u8 {
match self {
Interrupt::InvalidInterrupt => 0,
Interrupt::PageFault => 1,
Interrupt::ProtectionFault => 2,
Interrupt::InvalidOpcode => 3,
Interrupt::DivideByZero => 4,
Interrupt::StackOverflow => 5,
Interrupt::WriteToReadOnly => 6,
Interrupt::ReadFromWriteOnly => 7,
Interrupt::UnmappedIo => 8,
Interrupt::Hardware(x) => {
if *x < 32 || *x > 63 {
0
} else {
*x
}
}
Interrupt::Software(x) => {
if *x < 64 {
0
} else {
*x
}
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod interrupts;
pub mod processor;
pub mod state;
+653
View File
@@ -0,0 +1,653 @@
use std::{
hint::unlikely,
ops::{Add, AddAssign},
sync::{
Arc,
atomic::Ordering,
mpsc::{self, TryRecvError},
},
thread,
time::{Duration, Instant},
};
use common::{
instructions::{Instruction, Opcode},
register::Register,
};
use crate::{
Page,
config::{IoMapping, MemoryMap, RegionType},
io::{IoAccess, IoDevice, MappedDevice},
memory::{mmu::MMU, ram::RandomAccessMemory},
processor::{interrupts::Interrupt, state::SharedState},
};
pub struct Emulator<Mem: RandomAccessMemory> {
internal_state: ProcessorSnapshot,
shared_state: Arc<SharedState>,
// Interrupts
interrupts: mpsc::Receiver<u8>,
pending_fault: Option<Interrupt>,
// memory
mmu: MMU,
mainstore: Mem,
// IO
mmio: Vec<MappedDevice>,
mmio_region: Option<(u32, u32)>, // start end end of segment
// optionals - not necessarily set on boot.
memory_map: Option<MemoryMap>,
// Config params
__mmap_configured: bool,
__io_configured: bool,
}
unsafe impl<Mem: RandomAccessMemory> Send for Emulator<Mem> {}
impl<Mem: RandomAccessMemory> Emulator<Mem> {
pub fn new(mem: Mem) -> Self {
let (sender, receiver) = mpsc::channel::<u8>();
Self {
interrupts: receiver,
pending_fault: None,
internal_state: ProcessorSnapshot::default(),
shared_state: Arc::new(SharedState::new(sender)),
memory_map: None,
mmu: MMU::new(),
mainstore: mem,
// mmio
mmio: Vec::new(),
mmio_region: None,
// Config params
__io_configured: false,
__mmap_configured: false,
}
}
pub fn with_device(mut self, device: impl IoDevice + 'static) -> Self {
self.mmio.push(MappedDevice {
base: 0,
device: Box::new(device),
});
self
}
pub fn state_handle(&self) -> Arc<SharedState> {
self.shared_state.clone()
}
#[inline]
pub fn mmu_mut(&mut self) -> &mut MMU {
&mut self.mmu
}
#[inline]
pub fn memory_mut(&mut self) -> &mut impl RandomAccessMemory {
&mut self.mainstore
}
#[must_use]
#[inline]
pub fn reg(&self, reg: Register) -> u32 {
if reg as u8 == Register::Zero as u8 {
return 0;
}
debug_assert!((reg as usize) < ProcessorSnapshot::REG_COUNT);
unsafe { *self.internal_state.registers.get_unchecked(reg as usize) }
}
#[inline]
pub fn mut_reg(&mut self, reg: Register) -> &mut u32 {
debug_assert!((reg as usize) < ProcessorSnapshot::REG_COUNT);
unsafe {
self.internal_state
.registers
.get_unchecked_mut(reg as usize)
}
}
#[cold]
pub fn apply_memory_map(mut self, map: MemoryMap) -> Self {
self.mmio_region = map
.regions
.iter()
.find(|r| matches!(r.region_type, RegionType::MMIO))
.map(|r| (r.base, r.base + r.size));
map.apply(&mut self);
self.memory_map = Some(map);
self.__mmap_configured = true;
self
}
pub fn apply_io_map(mut self, map: Vec<IoMapping>) -> Result<Self, String> {
if !self.__mmap_configured {
return Err("You must map memory before applying I/O mappings".to_string());
}
for entry in map {
if let Some(idx) = self.mmio.iter().position(|d| d.device.id() == entry.device) {
self.mmio[idx].base = entry.base;
} else {
eprintln!("WARN: no device registgered for {:?}", entry.device);
}
}
self.__io_configured = true;
Ok(self)
}
#[cold]
fn update(&mut self) {
self.shared_state
.proc
.store(Arc::new(self.internal_state.clone()));
}
#[cold]
fn boot(&mut self) -> Result<(), String> {
if !(self.__io_configured && self.__mmap_configured) {
return Err("Processor not configured".to_string());
}
if let Some(map) = &self.memory_map {
MemoryMap::identity_map(&mut self.mmu, map.regions.get(0).unwrap());
}
self.internal_state.running = true;
Ok(())
}
#[cold]
fn shutdown(&mut self) {
self.internal_state.running = false;
}
#[cold]
pub fn idle_wait(&mut self) {
self.internal_state.running = false;
// Wait for an interrupt or state update to continue.
loop {
// Check for interrupts.
if let Ok(code) = self.interrupts.recv_timeout(Duration::from_millis(100)) {
self.interrupt(Interrupt::Software(code));
break;
}
// // If we've received a request to continue running. DEPRECATED (we can run through interrupts)
// if self.shared_state.running.load(Ordering::Relaxed) {
// panic!("3");
// break;
// }
// UI requested a state update.
if self.shared_state.update_req.load(Ordering::Relaxed) {
self.update();
}
}
self.internal_state.running = true;
}
pub fn run(&mut self) -> Result<(), String> {
self.boot()?;
let mut time = Instant::now();
'emu: loop {
// Update UI thread (roughly every 512k cycles targeting 60 UPS)
if unlikely(self.internal_state.clock & 0x7FFFF == 0) {
if self.shared_state.update_req.load(Ordering::Relaxed) {
self.update();
}
}
// Check for commands or hardware Interrupts every 32k cycles
if self.internal_state.clock % 0x7FFF == 0 {
if self.shared_state.running.load(Ordering::Relaxed) == false {
self.idle_wait();
}
match self.interrupts.try_recv() {
Ok(code) => self.interrupt(Interrupt::Software(code)),
Err(TryRecvError::Disconnected) => break 'emu,
Err(TryRecvError::Empty) => {}
}
}
// if we got a halt instruction, wait
if unlikely(!self.internal_state.running) {
let mips = (self.internal_state.clock as u128 / time.elapsed().as_micros());
println!(
"TIME TAKEN: {:?}, clock: {}, {}MIPS",
time.elapsed(),
self.internal_state.clock,
mips
);
time = Instant::now();
// temporary while i figure out a better solution
// exit when we hit halt (not useful for a real os ofc)
break 'emu;
self.idle_wait();
}
let pc = self.reg(Register::Pcx);
let instruction = self.mem_read_word(pc);
if cfg!(debug_assertions) {
println!(
"Clock: {} Executing {:?} PCX: {}, reg: {:?}",
self.internal_state.clock,
Instruction(instruction),
pc,
self.internal_state.registers
);
thread::sleep(Duration::from_micros(10));
}
self.mut_reg(Register::Pcx).add_assign(4);
self.execute(Instruction(instruction));
// Check if executing the interrupt caused a fault.
if let Some(fault) = self.pending_fault {
self.pending_fault = None;
println!("WARN fault: {:?}", fault);
self.interrupt(fault);
}
// always increment clock.
self.internal_state.clock += 1;
}
self.shutdown();
Ok(())
}
#[inline]
fn interrupt(&mut self, int: Interrupt) {
let idt = self.reg(Register::Idr);
*self.mut_reg(Register::Spr) -= 4;
let spr = self.reg(Register::Spr);
let pcx = self.reg(Register::Pcx);
self.mem_write_word(spr, pcx);
*self.mut_reg(Register::Pcx) = self.mem_read_word(idt + int.code() as u32 * 4)
}
#[inline]
fn execute(&mut self, word: Instruction) {
// This needs to be unsafe as we're using word.xxxx_uc() functions for decoding which are unsafe
// as they perform unchecked transmute operations (performance critical)
unsafe {
match Opcode::from_u8(word.opcode()) {
// Nothing
Some(Opcode::Nop) => {}
// move
Some(Opcode::Mov) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc());
}
Some(Opcode::CMov) => {
if self.reg(word.misc_uc()) != 0 {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc());
}
}
// load
Some(Opcode::Ldbs) => todo!(),
Some(Opcode::Ldhs) => todo!(),
Some(Opcode::Ldb) => {
*self.mut_reg(word.dest_uc()) = u32::from(
self.mem_read_byte(self.reg(word.src1_uc()) + word.imm16() as u32),
)
}
Some(Opcode::Ldh) => {
*self.mut_reg(word.dest_uc()) = u32::from(
self.mem_read_word(self.reg(word.src1_uc()) + word.imm16() as u32) >> 16,
)
}
Some(Opcode::Ldw) => {
*self.mut_reg(word.dest_uc()) = u32::from(
self.mem_read_word(self.reg(word.src1_uc()) + word.imm16() as u32),
)
}
// store
Some(Opcode::Stb) => {
self.mem_write_byte(
self.reg(word.dest_uc()) + word.imm16() as u32,
self.reg(word.src1_uc()) as u8,
);
}
Some(Opcode::Sth) => {
self.mem_write_byte(
self.reg(word.dest_uc()) + word.imm16() as u32,
(self.reg(word.src1_uc()) as u16 >> 8) as u8,
);
self.mem_write_byte(
self.reg(word.dest_uc()) + word.imm16() as u32 + 1,
self.reg(word.src1_uc()) as u8,
);
}
Some(Opcode::Stw) => {
self.mem_write_word(
self.reg(word.dest_uc()) + word.imm16() as u32,
self.reg(word.src1_uc()),
);
}
// load immediate
Some(Opcode::Lli) => {
*self.mut_reg(word.dest_uc()) = word.imm16() as u32;
}
Some(Opcode::Lui) => {
*self.mut_reg(word.dest_uc()) =
(word.imm16() as u32) << 16 | self.reg(word.dest_uc());
}
// Comparison
Some(Opcode::Ieq) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) == self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Ine) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) != self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Ilt) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) < self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Ile) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) <= self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Igt) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) > self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Ige) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) >= self.reg(word.src2_uc())) as u32;
}
// Jump
Some(Opcode::Jmp) => {
*self.mut_reg(Register::Pcx) = self.reg(word.dest_uc()) + word.imm16() as u32
}
Some(Opcode::Jez) => {
if self.reg(word.src1_uc()) == 0 {
*self.mut_reg(Register::Pcx) =
self.reg(word.dest_uc()) + word.imm16() as u32
}
}
Some(Opcode::Jnz) => {
if self.reg(word.src1_uc()) != 0 {
*self.mut_reg(Register::Pcx) =
self.reg(word.dest_uc()) + word.imm16() as u32
}
}
Some(Opcode::Jnc) => todo!(),
Some(Opcode::Jic) => todo!(),
// Bitwise
Some(Opcode::And) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) & self.reg(word.src2_uc());
}
Some(Opcode::Nand) => {
*self.mut_reg(word.dest_uc()) =
!(self.reg(word.src1_uc()) & self.reg(word.src2_uc()));
}
Some(Opcode::Or) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) | self.reg(word.src2_uc());
}
Some(Opcode::Nor) => {
*self.mut_reg(word.dest_uc()) =
!(self.reg(word.src1_uc()) | self.reg(word.src2_uc()));
}
Some(Opcode::Xor) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) ^ self.reg(word.src2_uc());
}
Some(Opcode::Xnor) => {
*self.mut_reg(word.dest_uc()) =
!(self.reg(word.src1_uc()) ^ self.reg(word.src2_uc()));
}
Some(Opcode::Not) => {
*self.mut_reg(word.dest_uc()) = !self.reg(word.src1_uc());
}
// Arithmetic
Some(Opcode::Add) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) + self.reg(word.src2_uc());
}
Some(Opcode::Sub) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) - self.reg(word.src2_uc());
}
Some(Opcode::Shl) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc())
<< (self.reg(word.src2_uc()) + word.shamt() as u32);
}
Some(Opcode::Shr) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc())
>> (self.reg(word.src2_uc()) + word.shamt() as u32);
}
Some(Opcode::Addi) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc()) + word.imm16() as u32;
}
Some(Opcode::Subi) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc()) - word.imm16() as u32;
}
// Utility
Some(Opcode::Push) => {
self.push(word.src1_uc());
}
Some(Opcode::Pop) => {
self.pop(word.dest_uc());
}
// Function
Some(Opcode::Call) => {
self.push(Register::Pcx);
*self.mut_reg(Register::Pcx) = self.reg(word.dest_uc()) + word.imm16() as u32
}
Some(Opcode::Ret) => {
self.pop(Register::Ret);
*self.mut_reg(Register::Pcx) = self.reg(Register::Ret);
}
Some(Opcode::Int) => {
self.interrupt(Interrupt::Software(word.imm16() as u8));
}
Some(Opcode::Hlt) => self.internal_state.running = false,
Some(Opcode::IRet) => todo!(),
None => {}
}
}
}
#[inline]
fn push(&mut self, reg: Register) {
*self.mut_reg(Register::Spr) -= 4;
self.mem_write_word(self.reg(Register::Spr), self.reg(reg));
}
#[inline]
fn pop(&mut self, reg: Register) {
*self.mut_reg(reg) = self.mem_read_word(self.reg(Register::Spr));
*self.mut_reg(Register::Spr) += 4;
}
#[inline]
fn mem_read_byte(&mut self, addr: u32) -> u8 {
if unlikely(self.is_mmio(addr)) {
return self.io_read_byte(addr);
}
self.mainstore.read_byte(addr)
}
#[inline]
fn mem_write_byte(&mut self, addr: u32, val: u8) {
if unlikely(self.is_mmio(addr)) {
self.io_write_byte(addr, val);
return;
}
self.mainstore.write_byte(addr, val);
}
#[inline]
fn mem_read_word(&mut self, addr: u32) -> u32 {
if unlikely(self.is_mmio(addr)) {
return self.io_read_word(addr);
}
self.mainstore.read_word(addr)
}
#[inline]
fn mem_write_word(&mut self, addr: u32, val: u32) {
if unlikely(self.is_mmio(addr)) {
self.io_write_word(addr, val);
return;
}
self.mainstore.write_word(addr, val);
}
#[inline]
fn mem_read_page(&mut self, addr: u32) -> &Page {
if unlikely(self.is_mmio(addr)) {
// pages spanning MMIO don't really make sense
// treat as fault
self.pending_fault = Some(Interrupt::ProtectionFault);
return &Page::ZERO;
}
self.mainstore.read_page(addr)
}
#[inline]
fn mem_write_page(&mut self, addr: u32, val: &Page) {
if unlikely(self.is_mmio(addr)) {
self.pending_fault = Some(Interrupt::ProtectionFault);
return;
}
self.mainstore.write_page(addr, val);
}
// single MMIO check reused by all of the above
#[inline]
fn is_mmio(&self, addr: u32) -> bool {
self.mmio_region
.map(|(base, end)| addr >= base && addr < end)
.unwrap_or(false)
}
fn fault(&mut self, interrupt: Interrupt) {
self.pending_fault = Some(interrupt);
}
#[inline]
fn get_device(&mut self, addr: u32) -> Option<(&mut Box<dyn IoDevice>, u32)> {
// Find the device that covers the address.
self.mmio
.iter_mut()
.find(|d| d.base <= addr && addr < d.base + d.device.size())
.map(|d| (&mut d.device, addr - d.base))
}
// --- cold IO paths ---
#[cold]
fn io_read_byte(&mut self, addr: u32) -> u8 {
if let Some((device, offset)) = self.get_device(addr) {
device.read_byte(offset).unwrap_or_else(|fault| {
self.fault(fault);
0
})
} else {
self.fault(Interrupt::UnmappedIo);
0
}
}
#[cold]
fn io_read_word(&mut self, addr: u32) -> u32 {
if let Some((device, offset)) = self.get_device(addr) {
device.read_word(offset).unwrap_or_else(|fault| {
self.fault(fault);
0
})
} else {
self.fault(Interrupt::UnmappedIo);
0
}
}
#[cold]
fn io_write_byte(&mut self, addr: u32, val: u8) {
if let Some((dev, offset)) = self.get_device(addr) {
dev.write_byte(offset, val).unwrap_or_else(|fault| {
self.fault(fault);
});
} else {
self.fault(Interrupt::UnmappedIo);
}
}
#[cold]
fn io_write_word(&mut self, addr: u32, val: u32) {
if let Some((dev, offset)) = self.get_device(addr) {
dev.write_word(offset, val).unwrap_or_else(|fault| {
self.fault(fault);
});
} else {
self.fault(Interrupt::UnmappedIo);
}
}
}
#[derive(Debug, Clone)]
pub struct ProcessorSnapshot {
pub running: bool,
pub clock: usize,
pub registers: [u32; Self::REG_COUNT],
}
impl Default for ProcessorSnapshot {
fn default() -> Self {
Self {
running: false,
clock: 0,
registers: [0; Self::REG_COUNT],
}
}
}
impl ProcessorSnapshot {
const REG_COUNT: usize = 28;
}
+25
View File
@@ -0,0 +1,25 @@
use std::sync::{Arc, atomic::AtomicBool, mpsc};
use crate::processor::processor::ProcessorSnapshot;
use arc_swap::ArcSwap;
pub struct SharedState {
pub proc: ArcSwap<ProcessorSnapshot>,
pub running: AtomicBool,
pub update_req: AtomicBool,
sender: mpsc::Sender<u8>,
}
impl SharedState {
pub fn new(sender: mpsc::Sender<u8>) -> Self {
Self {
proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())),
running: AtomicBool::new(true),
sender: sender,
update_req: AtomicBool::new(false),
}
}
}
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "linker"
edition.workspace = true
version.workspace = true
authors.workspace = true
[dependencies]
+14
View File
@@ -0,0 +1,14 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}