18 KiB
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, stws, 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
stsstatus/flags register and carry-flag-driven branches (jic/jnc"jump if carry"). The current implementation has no flags register at all —jic/jncexist 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 throughjez/jnztesting 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–26src1_checked()= bits 25–21 →Registerdest_checked()= bits 20–16 →Registersrc2_checked()= bits 15–11 →Registermisc_checked()= bits 10–6 →Registershamt()= bits 5–0 →u8imm16()= 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/jncare encoded but no confirmed semantics have been exercised in this codebase — treat as reserved/unimplemented until the interpreter's handling is confirmed.- The full
Registerenum (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.