Files
DSA/doc/reference/dsa-isa-spec.md
T

18 KiB
Raw Blame History

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 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
Rg0RgF 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 3126
  • src1_checked() = bits 2521 → Register
  • dest_checked() = bits 2016 → Register
  • src2_checked() = bits 1511 → Register
  • misc_checked() = bits 106 → Register
  • shamt() = bits 50 → u8
  • imm16() = bits 150 → 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.