Files

262 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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.