262 lines
18 KiB
Markdown
262 lines
18 KiB
Markdown
# 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.
|