# DSA Instruction Set Architecture Specification **Status:** Draft, grounded directly in `common::isa::instructions` and `common::isa::instructions::encode`. **Audience:** Emulator/interpreter authors, kernel writers, toolchain developers. --- ## 1. Architecture Overview | Feature | Detail | |------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Word size | 32 bits | | Instruction width | 32 bits, fixed (one word per instruction) | | Instruction endianness | Little-endian in memory | | Addressing mode | Base register + signed 16-bit offset for all loads/stores/jumps | | Stack direction | Downward — `push` decrements the stack pointer, `stw`s, then continues; `pop` reads then increments | | Program counter | Updated by jump/call/ret instructions; not directly writable by ordinary instructions | | Condition handling | **No flags register.** Comparisons (`ieq`/`ine`/`ilt`/`ile`/`igt`/`ige`) write a 1-or-0 result into an explicit destination register, which conditional jumps (`jez`/`jnz`) then test directly. There is no implicit zero/carry flag propagated between instructions. | > **Correction to earlier drafts:** some prior notes describe a `sts` status/flags register and carry-flag-driven branches (`jic`/`jnc` "jump if carry"). The current implementation has no flags register at all — `jic`/`jnc` exist as opcodes but their semantics are whatever the interpreter defines for them (not yet confirmed in this document); all conditional control flow actually exercised by the assembler/emulator today goes through `jez`/`jnz` testing an explicit register produced by a comparison instruction. --- ## 2. Register File Confirmed from the `Register` enum used throughout `Instruction`'s builders and the disassembler: | Register | Role | |-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Rg0`–`RgF` | 16 general-purpose registers. Caller-saved (not preserved across calls unless explicitly saved). | | `Acc` | Scratch register used by pseudo-instruction expansions (address computation for label-based load/store/jump). Volatile — never preserved across any instruction that touches it. | | `Spr` | Stack pointer. | | `Bpr` | Base/frame pointer. | | `Ret` | Return-address register, written internally by `call`/read by `ret`. | | `Zero` | Hardwired to 0. Writes are discarded. | | `Null` | Sentinel used internally by the assembler/disassembler for "unused/unencodable" register fields. Not a real writable register — referencing it in assembly should be treated as invalid. | Registers not independently confirmed against source in this conversation but described in earlier architecture notes (present pending confirmation against the actual `Register` enum): `Idr` (interrupt descriptor table base), `Mmr` (memory-map register), `Sts` (status), `Cir` (current instruction register), `Pcx` (program counter). Treat these as provisional until checked against `common::isa::register`. --- ## 3. Instruction Encoding ### R-type ``` | 31-26 | 25-21 | 20-16 | 15-11 | 10-6 | 5-0 | | opcode | src1 | dest | src2 | misc | shamt | ``` Built by `Instruction::build_r(opcode, src1, dest, src2, misc, shamt)`: ``` (opcode << 26) | (src1 << 21) | (dest << 16) | (src2 << 11) | (misc << 6) | (shamt & 0x3F) ``` ### I-type ``` | 31-26 | 25-21 | 20-16 | 15-0 | | opcode | src | dest | imm16 | ``` Built by `Instruction::build_i(opcode, src, dest, imm)`: ``` (opcode << 26) | (src << 21) | (dest << 16) | imm ``` ### No-arg ``` | 31-26 | 25-0 | | opcode | must be zero | ``` Built by `Instruction::build_noarg(opcode)`: `opcode << 26`. Decoding accessors (confirmed from `decode.rs`): - `opcode()` = bits 31–26 - `src1_checked()` = bits 25–21 → `Register` - `dest_checked()` = bits 20–16 → `Register` - `src2_checked()` = bits 15–11 → `Register` - `misc_checked()` = bits 10–6 → `Register` - `shamt()` = bits 5–0 → `u8` - `imm16()` = bits 15–0 → `u16` Register fields decode via `Register::from_u8`, falling back to `Register::Null` on an unrecognized 5-bit value (never a hard decode failure). --- ## 4. Opcode Table Authoritative values from `common::isa::instructions::Opcode`: | Value | Mnemonic | Type | Category | | ----- | -------- | ----- | -------------- | | 0x00 | NOP | noarg | Control | | 0x01 | MOV | R | Move | | 0x02 | CMOV | R | Move | | 0x03 | LDB | I | Load | | 0x04 | LDBS | I | Load (signed) | | 0x05 | LDH | I | Load | | 0x06 | LDHS | I | Load (signed) | | 0x07 | LDW | I | Load | | 0x08 | STB | I | Store | | 0x09 | STH | I | Store | | 0x0A | STW | I | Store | | 0x0B | LLI | I | Immediate load | | 0x0C | LUI | I | Immediate load | | 0x0D | IEQ | R | Comparison | | 0x0E | INE | R | Comparison | | 0x0F | ILT | R | Comparison | | 0x10 | ILE | R | Comparison | | 0x11 | IGT | R | Comparison | | 0x12 | IGE | R | Comparison | | 0x13 | JMP | I | Jump | | 0x14 | JEZ | I | Jump | | 0x15 | JNZ | I | Jump | | 0x16 | JIC | I | Jump | | 0x17 | JNC | I | Jump | | 0x18 | AND | R | Bitwise | | 0x19 | NAND | R | Bitwise | | 0x1A | OR | R | Bitwise | | 0x1B | NOR | R | Bitwise | | 0x1C | XOR | R | Bitwise | | 0x1D | XNOR | R | Bitwise | | 0x1E | NOT | R | Bitwise | | 0x1F | ADD | R | Arithmetic | | 0x20 | SUB | R | Arithmetic | | 0x21 | SHL | R | Shift | | 0x22 | SHR | R | Shift | | 0x23 | ADDI | I | Arithmetic | | 0x24 | SUBI | I | Arithmetic | | 0x25 | PUSH | R | Stack | | 0x26 | POP | R | Stack | | 0x27 | CALL | I | Control flow | | 0x28 | RET | noarg | Control flow | | 0x29 | INT | I | System | | 0x2A | IRET | noarg | System | | 0x2B | ACS | R | Atomic | | 0x2C | HLT | noarg | System | `Opcode::from_u8` accepts values `0..=0x2B` (i.e. `<= Hlt as u8`); anything higher decodes as an invalid instruction. There is no `ACS`/atomic-compare-swap opcode in the current enum — if one is planned, it is not yet implemented and should not be assumed present by tooling. --- ## 5. Instruction Semantics All arithmetic wraps modulo 2³² unless noted. ### 5.1 Move | Mnemonic | Fields | Effect | |------------------------|----------------------------------|--------------------------------------------| | `mov(src, dest)` | R: src1=src, dest=dest | `dest ← src` | | `cmov(src, dest, cmp)` | R: src1=src, dest=dest, src2=cmp | If `cmp ≠ 0`, `dest ← src`; else unchanged | ### 5.2 Load | Mnemonic | Fields | Effect | |---------------------------|--------|-------------------------------------------------------------------------| | `ldb(src, dest, offset)` | I | `dest ← zero_extend(byte at [src + sext(offset)])` | | `ldbs(src, dest, offset)` | I | Same, sign-extended | | `ldh(src, dest, offset)` | I | `dest ← zero_extend(halfword at [src + sext(offset)])` (2-byte aligned) | | `ldhs(src, dest, offset)` | I | Same, sign-extended | | `ldw(src, dest, offset)` | I | `dest ← word at [src + sext(offset)]` (4-byte aligned) | ### 5.3 Store | Mnemonic | Fields | Effect | |--------------------------|--------|---------------------------------------------------------| | `stb(src, dest, offset)` | I | `[dest + sext(offset)] ← src & 0xFF` | | `sth(src, dest, offset)` | I | `[dest + sext(offset)] ← src & 0xFFFF` (2-byte aligned) | | `stw(src, dest, offset)` | I | `[dest + sext(offset)] ← src` (4-byte aligned) | Note the field naming convention: for stores, the instruction's `dest` field holds the **base address** register, and `src` holds the **value** being written — this mirrors the load encoding's field layout even though the semantic roles are reversed. This matters when hand-assembling or reading raw encodings. ### 5.4 Immediate Load | Mnemonic | Fields | Effect | |--------------------|-------------|---------------------------------------------| | `lli(dest, imm16)` | I: src=Zero | `dest ← imm16` (zero-extended into 32 bits) | | `lui(dest, imm16)` | I: src=Zero | `dest ← (imm16 << 16) \| (dest & 0xFFFF)` | To build a full 32-bit constant: `lli` first (sets the low half, clearing the register), then `lui` (sets the high half, preserving the low half `lli` just wrote). `Instruction::load_imm32(dest, imm)` produces this pair directly. ### 5.5 Comparison All of the form `(sr1, sr2, dest)`, R-type, `dest ← (condition) ? 1 : 0`: | Mnemonic | Condition | |----------|-----------------------| | `ieq` | `sr1 == sr2` | | `ine` | `sr1 != sr2` | | `ilt` | `sr1 < sr2` (signed) | | `ile` | `sr1 <= sr2` (signed) | | `igt` | `sr1 > sr2` (signed) | | `ige` | `sr1 >= sr2` (signed) | ### 5.6 Jump / Branch | Mnemonic | Fields | Effect | |--------------------------|------------------------|-------------------------------------------------------------------| | `jmp(addr, offset)` | I: src=Zero, dest=addr | `PCX ← addr + sext(offset)`, unconditional | | `jez(cmp, addr, offset)` | I: src=cmp, dest=addr | If `cmp == 0`: `PCX ← addr + sext(offset)` | | `jnz(cmp, addr, offset)` | I: src=cmp, dest=addr | If `cmp != 0`: `PCX ← addr + sext(offset)` | | `jic(addr, offset)` | I: src=Zero, dest=addr | Encoded; semantics not exercised by the current assembler/codegen | | `jnc(addr, offset)` | I: src=Zero, dest=addr | Encoded; semantics not exercised by the current assembler/codegen | **Absolute vs. relative:** when `addr` is `Zero`, the effective target is just `offset` — this is how the assembler encodes absolute jumps to resolved symbol addresses today. Any other `addr` register makes the jump base-relative to that register's runtime value (e.g. an instruction-pointer-relative jump, as used internally by interpreters written in DSA assembly, such as the Brainfuck interpreter example). Because `offset` is a 16-bit field, **statically resolved absolute jump targets are currently capped at 0xFFFF** — a real constraint worth being aware of as programs grow past 64KB of combined text. ### 5.7 Bitwise All `(sr1, sr2, dest)` R-type except `not`: | Mnemonic | Effect | |------------------|------------------------| | `and` | `dest ← sr1 & sr2` | | `nand` | `dest ← !(sr1 & sr2)` | | `or` | `dest ← sr1 \| sr2` | | `nor` | `dest ← !(sr1 \| sr2)` | | `xor` | `dest ← sr1 ^ sr2` | | `xnor` | `dest ← !(sr1 ^ sr2)` | | `not(src, dest)` | `dest ← !src` | ### 5.8 Arithmetic | Mnemonic | Fields | Effect | |--------------------------|--------|----------------------------| | `add(sr1, sr2, dest)` | R | `dest ← sr1 + sr2` | | `sub(sr1, sr2, dest)` | R | `dest ← sr1 - sr2` | | `addi(sr1, dest, imm16)` | I | `dest ← sr1 + sext(imm16)` | | `subi(sr1, dest, imm16)` | I | `dest ← sr1 - sext(imm16)` | ### 5.9 Shift | Mnemonic | Fields | Effect | |----------------------------------|---------------------------------------------------|--------------------------------------------| | `shl(src, rshamt, dest, ishamt)` | R: src1=src, dest=dest, src2=rshamt, shamt=ishamt | `dest ← src << (rshamt + ishamt)` | | `shr(src, rshamt, dest, ishamt)` | R | `dest ← src >> (rshamt + ishamt)`, logical | `ishamt` is a literal encoded in the instruction (6 bits, asserted `< 64` at build time); `rshamt` is an additional runtime register value added to it. In practice, current codegen only ever populates the literal (`ishamt`) form with `rshamt = Zero`. ### 5.10 Stack | Mnemonic | Fields | Effect | |-------------|-------------|------------------------------| | `push(reg)` | R: src1=reg | `SPR ← SPR - 4; [SPR] ← reg` | | `pop(reg)` | R: dest=reg | `reg ← [SPR]; SPR ← SPR + 4` | ### 5.11 Function Call / Return | Mnemonic | Fields | Effect | |----------------------|------------------------|----------------------------------------------------| | `call(addr, offset)` | I: src=Zero, dest=addr | Pushes return context; `PCX ← addr + sext(offset)` | | `ret()` | noarg | Pops return context; `PCX ← saved return address` | Exact push/pop mechanics of `call`/`ret` (whether the return address goes through the explicit stack or the dedicated `Ret` register) are implemented in the interpreter and not independently re-derived here — confirm against the emulator's execution code if implementing a second interpreter. ### 5.12 System | Mnemonic | Fields | Effect | |-------------|-----------------------------------------|-------------------------| | `int(code)` | I: imm16 holds `code` (asserted `< 64`) | Software interrupt | | `irt()` | noarg | Return from interrupt | | `hlt()` | R, all-zero operands | Halts instruction fetch | | `nop()` | noarg | No effect | Exact interrupt dispatch mechanics (IDT layout, privilege transition) are not covered here — this is architecture the emulator's interrupt controller module defines, not something re-derived from the instruction builders alone. ### 5.13 Data Pseudo-word `Instruction::data(value)` constructs a raw `u32` word with no opcode semantics — used internally by the assembler pipeline as a placeholder for data words prior to the current data/text section split. Should not appear in the TEXT section of a well-formed program under the current `DsoBinary`/`DseExecutable` pipeline (see the DSE Executable spec) — data belongs exclusively in the DATA section now. --- ## 6. Known Gaps / Open Questions - `jic`/`jnc` are encoded but no confirmed semantics have been exercised in this codebase — treat as reserved/unimplemented until the interpreter's handling is confirmed. - The full `Register` enum (specifically any privileged/system registers) has not been independently confirmed in this conversation — the table in §2 lists only registers seen in actual builder/disassembler code. - Interrupt/IDT layout and `call`/`ret`'s exact stack mechanics live in the interpreter, not the instruction encoder, and should be documented separately once confirmed against that source.