mega refactor:

essentially completely reorganised the project.
previous contents of the dsa folder have now been split into emulator/ tooling/ and libraries/
libraries is for common functionality across crates, tooling is for core binaries needed for building and debugging DSA projects, including dsx client/server/build
emulator contains the dsa_emulator crate that implements the core DSA emulator, the dsa_gui crate that implements the graphical user interface for the emulator & all future tooling under a unified application, and finally the dsa_cli crate which will eventually provide lightweight wrapper for the core emulator, simply providing a tool for running DSA binaries without the overhead of a heavy GUI.
This commit is contained in:
2026-08-01 01:28:11 +01:00
parent 8c19497bc7
commit 27d38eaf89
120 changed files with 84 additions and 90 deletions
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "common"
version = "0.1.0"
edition = "2024"
[lib]
path = "src/lib.rs"
[dependencies]
binrw = "0.15.1"
strum = { version = "0.28.0", features = ["derive"] }
+317
View File
@@ -0,0 +1,317 @@
use strum::{Display, EnumString};
use crate::prelude::Register;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Location {
offset: u16,
register: Option<Register>,
label: Option<String>,
}
impl Location {
pub fn new(offset: u16, register: Option<Register>, label: Option<String>) -> Self {
Location {
offset,
register,
label,
}
}
// need implementation to intelligently figure out if we need to load immediates
// so if the label is a const and > u16::max, we do Lui $label temp, then use temp as an offset
// if a register is being used already we have to add the offset to that.
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImmConst {
Label(String),
Imm16(u16),
Imm32(u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)]
#[strum(ascii_case_insensitive)]
pub enum AsmOpcode {
Nop,
Mov,
CMov,
Ldb,
Ldbs,
Ldh,
Ldhs,
Ldw,
Stb,
Sth,
Stw,
Lli,
Lui,
Lwi,
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,
// data directives
Db,
Dh,
Dw,
// data reservations
Resb,
Resh,
Resw,
// Function pseudo-instructions
Func,
Return,
// include directive
Include,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AsmInstruction {
Nop,
// move
Mov {
src: Register,
dest: Register,
},
CMov {
src: Register,
dest: Register,
condition: Register,
},
// load
Ldb {
addr: Location,
dest: Register,
},
Ldbs {
addr: Location,
dest: Register,
},
Ldh {
addr: Location,
dest: Register,
},
Ldhs {
addr: Location,
dest: Register,
},
Ldw {
addr: Location,
dest: Register,
},
// store
Stb {
src: Register,
addr: Location,
},
Sth {
src: Register,
addr: Location,
},
Stw {
src: Register,
addr: Location,
},
// load immediate
Lli {
dest: Register,
value: ImmConst,
},
Lui {
dest: Register,
value: ImmConst,
},
Lwi {
dest: Register,
value: ImmConst,
},
// comparison
Ieq {
src1: Register,
src2: Register,
dest: Register,
},
Ine {
src1: Register,
src2: Register,
dest: Register,
},
Ilt {
src1: Register,
src2: Register,
dest: Register,
},
Ile {
src1: Register,
src2: Register,
dest: Register,
},
Igt {
src1: Register,
src2: Register,
dest: Register,
},
Ige {
src1: Register,
src2: Register,
dest: Register,
},
// jump
Jmp {
dest: Location,
},
Jez {
dest: Location,
cond: Register,
},
Jnz {
dest: Location,
cond: Register,
},
Jic {
dest: Location,
},
Jnc {
dest: Location,
},
// bitwise
And {
src1: Register,
src2: Register,
dest: Option<Register>,
},
Nand {
src1: Register,
src2: Register,
dest: Option<Register>,
},
Or {
src1: Register,
src2: Register,
dest: Option<Register>,
},
Nor {
src1: Register,
src2: Register,
dest: Register,
},
Xor {
src1: Register,
src2: Register,
dest: Option<Register>,
},
Xnor {
src1: Register,
src2: Register,
dest: Option<Register>,
},
Not {
src: Register,
dest: Option<Register>,
},
// arithmetic
Add {
src1: Register,
src2: Register,
dest: Option<Register>,
},
Sub {
src1: Register,
src2: Register,
dest: Option<Register>,
},
Shl {
src: Register,
shamt_r: Option<Register>,
shamt_i: Option<ImmConst>,
dest: Option<Register>,
},
Shr {
src: Register,
shamt_r: Option<Register>,
shamt_i: Option<ImmConst>,
dest: Option<Register>,
},
Addi {
src: Register,
value: ImmConst,
dest: Option<Register>,
},
Subi {
src: Register,
value: ImmConst,
dest: Option<Register>,
},
// utility
Push {
src: Register,
},
Pop {
dest: Register,
},
Call {
dest: Location,
},
Ret,
// system
Int {
code: ImmConst,
},
IRet,
Hlt,
}
View File
@@ -0,0 +1,65 @@
use crate::isa::instructions::Instruction;
use crate::isa::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
}
}
@@ -0,0 +1,332 @@
use std::ops::{Deref, DerefMut};
use crate::isa::instructions::{Instruction, Opcode};
use crate::isa::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 (sign‑extended) 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 (sign‑extended) 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 32‑bit 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 6‑bit 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)
}
}
@@ -0,0 +1,236 @@
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,
// Acs = 43,
Hlt = 44,
}
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:?}, {rshamt:?} + {ishamt}",
op = op,
src = self.src1_checked(),
dest = self.dest_checked(),
rshamt = self.src2_checked(),
ishamt = self.shamt()
)
}
}
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod register;
pub mod instructions;
+82
View File
@@ -0,0 +1,82 @@
use std::fmt;
use strum::{Display, EnumString};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, EnumString)]
#[repr(u8)]
#[non_exhaustive]
#[strum(ascii_case_insensitive)]
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,
Acc = 16, // temp/scratch register
// stack registers
Spr = 17,
Bpr = 18,
Idr = 20, // points to interrupt descriptor table
Mmr = 21, // points to page tables
// system - read only
Zero = 22, // always reads as zero.
Sts = 23, // status register for flags
Cir = 24, // current instruction register
Pcx = 25, // program counter
#[strum(disabled, to_string = "Ret")]
Ret = 19, // used under the hood by call/ret instructions but not used directly in asm.
#[default]
#[strum(disabled, to_string = "Null")] // don't include this in FromStr impl
Null = 26,
}
impl std::fmt::Display for Register {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Register {
/// Total number of registers, not counting Null as it's not a real register.
/// reading or writing to Null is undefined behaviour!!
pub const COUNT: u8 = Self::Null as u8;
/// # Safety
/// the caller must ensure that the index is always within the range 0..COUNT, failing to do so will result in undefined behaviour.
#[must_use]
#[inline]
pub unsafe fn from_u8_unchecked(idx: u8) -> Self {
debug_assert!(
idx < Self::COUNT,
"Tried to access a null register using the unchecked method. undefined behaviour alert!"
);
unsafe { std::mem::transmute(idx) }
}
#[inline]
pub fn from_u8(idx: u8) -> Result<Self, ()> {
if idx >= Self::COUNT {
return Err(());
}
Ok(unsafe { Self::from_u8_unchecked(idx) })
}
}
+8
View File
@@ -0,0 +1,8 @@
pub mod asm;
pub mod isa;
pub mod process;
pub mod prelude {
pub use crate::isa::instructions::Instruction;
pub use crate::isa::register::Register;
}
+53
View File
@@ -0,0 +1,53 @@
use std::fmt::Debug;
use std::sync::{mpsc, Arc};
use crate::process::AgentSink::{Callback, Channel};
pub struct ProcessAgent {
sink: Arc<AgentSink>,
}
impl ProcessAgent {
pub fn new(tx: mpsc::Sender<String>) -> Self {
Self { sink: Arc::new(Channel(tx)) }
}
fn new_sync(f: impl Fn(&str) + Send + Sync + 'static) -> Self {
Self { sink: Arc::new(Callback(Box::new(f))) }
}
pub fn log(&self, msg: impl Into<String>) {
let msg = msg.into();
match &*self.sink {
Channel(tx) => { let _ = tx.send(msg); }
Callback(f) => f(&msg),
}
}
}
enum AgentSink {
Channel(mpsc::Sender<String>),
Callback(Box<dyn Fn(&str) + Send + Sync>)
}
pub trait TraceableProcess: Debug + Send + 'static {
type Output: Send + 'static;
type Error: std::error::Error + Send + 'static;
fn label(&self) -> String {
format!("{self:?}")
}
fn run(self: Box<Self>, ctx: ProcessAgent) -> Result<Self::Output, Self::Error>;
///
fn run_sync(self) -> Result<Self::Output, Self::Error>
where
Self: Sized
{
Self::run(
Box::new(self),
ProcessAgent::new_sync(|line| println!("{line}") )
)
}
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "dsa_formats"
edition.workspace = true
version.workspace = true
authors.workspace = true
[dependencies]
binrw = "0.15.2"
common = { path = "../common" }
+252
View File
@@ -0,0 +1,252 @@
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
use binrw::{binrw, BinRead, BinReaderExt, BinWriterExt, BinWrite};
use std::fmt;
use common::isa::instructions::Instruction;
#[binrw]
#[brw(magic = b"DSAX", little)]
#[derive(Debug, Clone)]
pub struct DseHeader {
version: u16,
flags: u16,
pub entry_point: u32,
symbol_table_offset: u32,
symbol_table_size: u32,
string_table_offset: u32,
string_table_size: u32,
data_offset: u32,
data_size: u32,
pub text_offset: u32,
text_size: u32
}
#[binrw]
#[brw(little)]
#[derive(Debug, Clone)]
pub struct SymbolEntry {
pub name_offset: u32,
pub value: u32,
pub section: u8,
pub binding: u8,
pub _pad: u16
}
#[derive(Debug, Clone)]
pub struct DseExecutable {
pub header: DseHeader,
pub symbols: Vec<SymbolEntry>,
pub string_table: Vec<u8>,
pub data: Vec<u8>,
pub text: Vec<u32>
}
impl DseExecutable {
pub fn read<R: Read + Seek>(reader: &mut R) -> binrw::BinResult<Self> {
let header = DseHeader::read(reader)?;
reader.seek(SeekFrom::Start(header.symbol_table_offset as u64))?;
let symbols: Vec<SymbolEntry> = (0..header.symbol_table_size)
.map(|_| SymbolEntry::read(reader))
.collect::<binrw::BinResult<_>>()?;
reader.seek(SeekFrom::Start(header.string_table_offset as u64))?;
let mut string_table = vec![0u8; header.string_table_size as usize];
reader.read_exact(&mut string_table).map_err(|e| binrw::Error::Io(e))?;
reader.seek(SeekFrom::Start(header.data_offset as u64))?;
let mut data = vec![0u8; header.data_size as usize];
reader.read_exact(&mut data).map_err(|e| binrw::Error::Io(e))?;
reader.seek(SeekFrom::Start(header.text_offset as u64))?;
let text: Vec<u32> = (0..header.text_size)
.map(|_| reader.read_le::<u32>())
.collect::<binrw::BinResult<_>>()?;
Ok(DseExecutable { header, symbols, string_table, data, text })
}
pub fn write<W: Write + Seek>(&self, writer: &mut W) -> binrw::BinResult<()> {
self.header.write(writer)?;
for symbol in &self.symbols {
symbol.write(writer)?;
}
writer.write_all(&self.string_table)?;
writer.write_all(&self.data)?;
for word in &self.text {
writer.write_le(word)?;
}
Ok(())
}
pub fn new(
symbols: &[SymbolEntry],
string_table: &[u8],
data: &[u8],
text: &[u32],
entry_point: u32,
) -> binrw::BinResult<DseExecutable> {
// Derive sizes from real serialization instead of hand-counted constants.
let header_size = {
let mut buf = Cursor::new(Vec::new());
// dummy header just to measure length; fields' values don't matter here
let dummy = DseHeader {
version: 0, flags: 0, entry_point: 0,
symbol_table_offset: 0, symbol_table_size: 0,
string_table_offset: 0, string_table_size: 0,
data_offset: 0, data_size: 0,
text_offset: 0, text_size: 0,
};
dummy.write(&mut buf)?;
buf.into_inner().len() as u32
};
let symbol_entry_size = if symbols.is_empty() {
// measure using a zeroed entry even if symbols is empty
let mut buf = Cursor::new(Vec::new());
SymbolEntry { name_offset: 0, value: 0, section: 0, binding: 0, _pad: 0 }
.write(&mut buf)?;
buf.into_inner().len() as u32
} else {
let mut buf = Cursor::new(Vec::new());
symbols[0].write(&mut buf)?;
buf.into_inner().len() as u32
};
let symbol_table_offset = header_size;
let symbol_table_bytes = symbols.len() as u32 * symbol_entry_size;
let string_table_offset = symbol_table_offset + symbol_table_bytes;
let data_offset = string_table_offset + string_table.len() as u32;
let text_offset = data_offset + data.len() as u32;
let header = DseHeader {
version: 1,
flags: 0,
entry_point,
symbol_table_offset,
symbol_table_size: symbols.len() as u32,
string_table_offset,
string_table_size: string_table.len() as u32,
data_offset,
data_size: data.len() as u32,
text_offset,
text_size: text.len() as u32,
};
Ok(DseExecutable {
header,
symbols: symbols.to_vec(),
string_table: string_table.to_vec(),
data: data.to_vec(),
text: text.to_vec(),
})
}
/// Produces the runtime memory image: TEXT followed immediately by DATA,
/// matching the exact layout `resolve_address` assumed when the assembler
/// resolved symbol addresses. This is NOT the same as the on-disk file
/// layout (header/symtab/strtab/data/text) — this is what actually needs
/// to end up in dsa_emulator memory.
pub fn to_memory_image(&self) -> Vec<u8> {
let mut image = Vec::with_capacity(self.text.len() * 4 + self.data.len());
for word in &self.text {
image.extend_from_slice(&word.to_le_bytes());
}
image.extend_from_slice(&self.data);
image
}
/// Runtime address where DATA begins, given TEXT is loaded at `text_base`.
/// Matches the assembler's `resolve_address` convention exactly.
pub fn data_base(&self, text_base: u32) -> u32 {
text_base + (self.text.len() as u32 * 4)
}
/// Absolute runtime entry point address, given TEXT is loaded at `text_base`.
/// `self.header.entry_point` is already an absolute address under the
/// assumption text_base == 0, so this just re-bases it if you're loading
/// somewhere other than address 0.
pub fn entry_point(&self, text_base: u32) -> u32 {
text_base + self.header.entry_point
}
}
// Display implementations for dumping binary contents
impl fmt::Display for DseHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DseHeader {{ version: {}, flags: {}, entry_point: 0x{:08X}, symbol_table_offset: 0x{:08X}, symbol_table_size: {}, string_table_offset: 0x{:08X}, string_table_size: {}, data_offset: 0x{:08X}, data_size: {}, text_offset: 0x{:08X}, text_size: {} }}",
self.version,
self.flags,
self.entry_point,
self.symbol_table_offset,
self.symbol_table_size,
self.string_table_offset,
self.string_table_size,
self.data_offset,
self.data_size,
self.text_offset,
self.text_size)
}
}
impl fmt::Display for SymbolEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SymbolEntry {{ name_offset: 0x{:08X}, value: 0x{:08X}, section: {}, binding: {} }}",
self.name_offset,
self.value,
self.section,
self.binding)
}
}
impl fmt::Display for DseExecutable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Header
writeln!(f, "{}", self.header)?;
// Symbols
writeln!(f, "Symbols:")?;
for s in &self.symbols {
writeln!(f, " {}", s)?;
}
// String table as hex dump
writeln!(f, "String Table ({} bytes):", self.string_table.len())?;
for chunk in self.string_table.chunks(16) {
write!(f, "{}", String::from_utf8_lossy(chunk))?;
// for byte in chunk { write!(f, "{:02X} ", byte)?; }
writeln!(f)?;
}
// Data section
writeln!(f, "Data ({} bytes):", self.data.len())?;
for chunk in self.data.chunks(16) {
write!(f, " ")?;
for byte in chunk { write!(f, "{:02X} ", byte)?; }
for byte in chunk { write!(f, "{} ", *byte as char)?; }
writeln!(f)?;
}
// Text section as Instructions
writeln!(f, "Text ({} words):", self.text.len())?;
for word in &self.text {
writeln!(f, " {:#010X} {:?}", word, Instruction(*word))?;
}
Ok(())
}
}
#[test]
fn symbol_entry_size_is_12_bytes() {
let mut buf = Cursor::new(Vec::new());
SymbolEntry { name_offset: 0, value: 0, section: 0, binding: 0, _pad: 0 }
.write(&mut buf).unwrap();
assert_eq!(buf.into_inner().len(), 12);
}
+12
View File
@@ -0,0 +1,12 @@
pub mod binary_dse;
pub mod binary_dso;
#[cfg(test)]
mod tests {
// #[test]
// fn it_works() {
// let result = add(2, 2);
// assert_eq!(result, 4);
// }
}