used AI to generate documentation. verified all documentation is correct and accurately represents the functionality of the emulator
This commit is contained in:
-147
@@ -1,147 +0,0 @@
|
|||||||
## Architecture
|
|
||||||
32 bit instruction width/word size
|
|
||||||
|
|
||||||
## Instruction Types:
|
|
||||||
|
|
||||||
Type Sizes:
|
|
||||||
- u16: [16 bits]
|
|
||||||
- u6: [6 bits]
|
|
||||||
- Reg: [5 bits]
|
|
||||||
- Opcode: [6 bits]
|
|
||||||
|
|
||||||
Instruction Types:
|
|
||||||
- R: [Opcode] [Reg] [Reg] [Reg] [Reg] [u6]
|
|
||||||
- I: [Opcode] [Reg] [Reg] [u16 ]
|
|
||||||
|
|
||||||
## Instruction Set
|
|
||||||
|
|
||||||
// move
|
|
||||||
mov <src: Reg> <dst: Reg> [ unused ]
|
|
||||||
cmov <src: Reg> <dst: Reg> <cmp: Reg>
|
|
||||||
|
|
||||||
// load
|
|
||||||
ldb <src: Reg> <dst: Reg> <offset: u16>
|
|
||||||
ldbs <src: Reg> <dst: Reg> <offset: u16>
|
|
||||||
ldh <src: Reg> <dst: Reg> <offset: u16>
|
|
||||||
ldhs <src: Reg> <dst: Reg> <offset: u16>
|
|
||||||
ldw <src: Reg> <dst: Reg> <offset: u16>
|
|
||||||
|
|
||||||
// store
|
|
||||||
stb <src: Reg> <dst: Reg> <offset: u16>
|
|
||||||
sth <src: Reg> <dst: Reg> <offset: u16>
|
|
||||||
stw <src: Reg> <dst: Reg> <offset: u16>
|
|
||||||
|
|
||||||
// immediate
|
|
||||||
lli <dst: Reg> [unused] <imm: u16>
|
|
||||||
lui <dst: Reg> [unused] <imm: u16>
|
|
||||||
|
|
||||||
// comparison
|
|
||||||
ieq <sr1: Reg> <sr2: Reg> <dst: Reg> [unused]
|
|
||||||
ine <sr1: Reg> <sr2: Reg> <dst: Reg> [unused]
|
|
||||||
igt <sr1: Reg> <sr2: Reg> <dst: Reg> [unused]
|
|
||||||
ige <sr1: Reg> <sr2: Reg> <dst: Reg> [unused]
|
|
||||||
ilt <sr1: Reg> <sr2: Reg> <dst: Reg> [unused]
|
|
||||||
ile <sr1: Reg> <sr2: Reg> <dst: Reg> [unused]
|
|
||||||
|
|
||||||
// Jump
|
|
||||||
jmp [ unused ] <addr: Reg> <offset: u16> // unconditional
|
|
||||||
|
|
||||||
jez <cmp: Reg> <addr: Reg> <offset: u16> // conditional on cmp == 0
|
|
||||||
jnz <cmp: Reg> <addr: Reg> <offset: u16>
|
|
||||||
|
|
||||||
jic [ unused ] <addr: Reg> <offset: u16> // conditional on carry
|
|
||||||
jnc [ unused ] <addr: Reg> <offset: u16>
|
|
||||||
|
|
||||||
// Bitwise
|
|
||||||
and <sr1: Reg> <sr2: Reg> <dst: Reg> [unused] // and
|
|
||||||
nnd <sr1: Reg> <sr2: Reg> <dst: Reg> [unused] // nand
|
|
||||||
or <sr1: Reg> <sr2: Reg> <dst: Reg> [unused] // or
|
|
||||||
nor <sr1: Reg> <sr2: Reg> <dst: Reg> [unused] // nor
|
|
||||||
xor <sr1: Reg> <sr2: Reg> <dst: Reg> [unused] // xor
|
|
||||||
xnr <sr1: Reg> <sr2: Reg> <dst: Reg> [unused] // xnor
|
|
||||||
not <src: Reg> [ unused ] <dst: Reg> [unused] // not
|
|
||||||
|
|
||||||
// Arithmetic
|
|
||||||
add <sr1: Reg> <sr2: Reg> <dst: Reg>
|
|
||||||
sub <sr1: Reg> <sr2: Reg> <dst: Reg>
|
|
||||||
shl <src: Reg> <rshamt: Reg> <dst: Reg> <ishamt: u6> // left shift of rshamt+ishamt
|
|
||||||
shr <src: Reg> <rshamt: Reg> <dst: Reg> <ishamt: u6> // right shift
|
|
||||||
|
|
||||||
// Immediate Arithmetic
|
|
||||||
addi <sr1: Reg> <dst: Reg> <imm: u16>
|
|
||||||
subi <sr1: Reg> <dst: Reg> <imm: u16>
|
|
||||||
|
|
||||||
// Misc
|
|
||||||
nop (zero/do nothing)
|
|
||||||
int <imm16: u16>
|
|
||||||
irt
|
|
||||||
hlt
|
|
||||||
|
|
||||||
// Atomic
|
|
||||||
|
|
||||||
// Atomic Compare and Swap - compares src and dst, if src
|
|
||||||
acs <src: Reg> <cmp: Reg> <dst: Reg> <res: Reg>
|
|
||||||
|
|
||||||
## Assembler Pseudoinstructions
|
|
||||||
Definitions:
|
|
||||||
Label - named address, a function or global
|
|
||||||
|
|
||||||
```
|
|
||||||
lwi <src: Label|Imm> <dest: Reg> // loads the literal value of Label|Imm into dest
|
|
||||||
// if src is a Label, take literal value of Label
|
|
||||||
=> lli $src, $dest
|
|
||||||
=> lui $src, $dest
|
|
||||||
lli <src: Label> <dest: Reg> // loads the lower 16 bits of the literal value of Label into dest
|
|
||||||
... -> similarly defined for lui
|
|
||||||
|
|
||||||
ldw <src: Label> <dest: Reg> // loads the word at the address of Label into dest
|
|
||||||
... => similarly defined for ldb, ldbs, ldh, ldhs
|
|
||||||
// load the label value of src into dest with lui/lli
|
|
||||||
=> lli $src, $dest
|
|
||||||
=> lui $src, $dest
|
|
||||||
=> ldw $dest, $dest
|
|
||||||
|
|
||||||
stw <src: Reg> <dest: Label> // stores the word in src using the address/label that is dest.
|
|
||||||
// load the literal value of dest into the accumulator with lui/lli
|
|
||||||
=> lli $dest, acc
|
|
||||||
=> lui $dest, acc
|
|
||||||
=> stw $src, acc
|
|
||||||
|
|
||||||
jmp <dest: Label>
|
|
||||||
CASE dest > u16::MAX:
|
|
||||||
=> lli $dest, acc
|
|
||||||
=> lui $dest, acc
|
|
||||||
=> jmp $dest, 0
|
|
||||||
CASE dest < u16::MAX:
|
|
||||||
// assembler inserts the literal value of dest
|
|
||||||
=> jmp $dest, 0
|
|
||||||
|
|
||||||
jmp <lbl: Label> <dest: Reg>
|
|
||||||
CASE lbl > u16::MAX:
|
|
||||||
AssemblerError!
|
|
||||||
CASE lbl < u16::MAX:
|
|
||||||
=> jmp $dest, lbl
|
|
||||||
|
|
||||||
call <addr: Label>
|
|
||||||
=> subi spr, 4, spr
|
|
||||||
=> stw pcx, spr, 0
|
|
||||||
=> jmp $addr
|
|
||||||
|
|
||||||
func
|
|
||||||
// push bpr
|
|
||||||
=> subi spr, 4, spr
|
|
||||||
=> stw bpr, spr, 0
|
|
||||||
// replace bpr with spr
|
|
||||||
=> mov spr, bpr
|
|
||||||
|
|
||||||
return
|
|
||||||
=> mov bpr, spr
|
|
||||||
// pop bpr
|
|
||||||
=> addi spr, 4, spr
|
|
||||||
=> ldw spr, bpr, 0
|
|
||||||
// load return addr
|
|
||||||
=> ldw spr, ret, 0
|
|
||||||
=> addi, spr, 4, spr
|
|
||||||
=> jmp ret, 4
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
# DSA Assembly Language Specification
|
||||||
|
|
||||||
|
**Status:** Draft, grounded in the current `assembler` crate (`model.rs`, `expand.rs`, `resolver.rs`,
|
||||||
|
`codegen.rs`) and the example programs `print.dsa`, `bf.dsa`, `animation.dsa`, `serial.dsa`.
|
||||||
|
|
||||||
|
This describes the assembly language as the current pipeline actually implements it — pseudo-op
|
||||||
|
expansion, symbol resolution, and argument order all reflect the real `expand.rs`/`codegen.rs` logic,
|
||||||
|
not just the ISA's hardware semantics (see the companion ISA spec for those).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Source File Structure
|
||||||
|
|
||||||
|
A `.dsa` file is a sequence of directives, labels, and instructions. Comments start with `//` and run to end of line.
|
||||||
|
|
||||||
|
```asm
|
||||||
|
include print "./print.dsa" // pulls in another module by relative path
|
||||||
|
|
||||||
|
dw display: 0x20000 // data declaration with a label
|
||||||
|
db message: "hello\0" // string data
|
||||||
|
|
||||||
|
start: // a label — marks the address of the next node
|
||||||
|
lli 0, rg0
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.1 Modules and `include`
|
||||||
|
|
||||||
|
```asm
|
||||||
|
include <name> "<relative-path>"
|
||||||
|
```
|
||||||
|
|
||||||
|
`<name>` becomes the namespace prefix used to reference symbols from that file — e.g. `include print "./print.dsa"` lets you call `print::print`, `print::newline`, etc. Each included file is compiled once per program even if reached via multiple include paths (deduplicated by canonicalized file path). Symbols are disambiguated internally by a hash of the defining file's path, not by name alone — two files can each define a label called `start` without collision, as long as they're referenced through their respective module namespace.
|
||||||
|
|
||||||
|
**Known limitation:** cross-module symbol references (`module::symbol`) resolve correctly today
|
||||||
|
because the current pipeline concatenates all included files' nodes into one list before running
|
||||||
|
symbol resolution — it is not yet a true separate-compilation linker. A planned `DsoBinary`
|
||||||
|
intermediate format (see the compiler roadmap if there is one available) will make per-file compilation independent of the whole program; until then, changing any included file requires reassembling the whole program.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Data Declarations
|
||||||
|
|
||||||
|
| Directive | Unit size | Meaning |
|
||||||
|
|-------------------------|------------------------------------|-------------------------------------------------|
|
||||||
|
| `db <label>: <value>` | 4 bytes/item internally (see note) | Byte-oriented data — typically a string literal |
|
||||||
|
| `dh <label>: <value>` | 2 bytes/item | Halfword data |
|
||||||
|
| `dw <label>: <value>` | 1 word/item | Word data |
|
||||||
|
| `resb <label>: <count>` | reserves `count` bytes, zeroed | Uninitialized byte buffer |
|
||||||
|
| `resh <label>: <count>` | reserves `count` halfwords, zeroed | Uninitialized halfword buffer |
|
||||||
|
| `resw <label>: <count>` | reserves `count` words, zeroed | Uninitialized word buffer |
|
||||||
|
|
||||||
|
Example (from `animation.dsa`):
|
||||||
|
```asm
|
||||||
|
dw DISPLAY: 0x30000
|
||||||
|
dw COL0: 0xFFFFFFFF
|
||||||
|
dw COL_IDX: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Example (from `bf.dsa`):
|
||||||
|
```asm
|
||||||
|
db program: "++++++++++...(brainfuck source)..."
|
||||||
|
db error: "Invalid Instruction!"
|
||||||
|
dw stack: 0x10000
|
||||||
|
resb data: 1024
|
||||||
|
```
|
||||||
|
|
||||||
|
String literals (`db label: "text"`) are packed and null-terminated; the assembler pads between
|
||||||
|
successive data items to keep each item's *own* internal alignment, but does **not** guarantee every
|
||||||
|
subsequent item starts on a word boundary — a `db` string can leave the following item at an
|
||||||
|
arbitrary byte offset. Tooling that walks the data section (disassemblers, debuggers) must scan
|
||||||
|
byte-by-byte rather than assuming word-aligned entries throughout.
|
||||||
|
|
||||||
|
Every `db`/`dh`/`dw`/`resb`/`resh`/`resw` directive lowers, during pseudo-op expansion, into one or
|
||||||
|
more internal `Data` pseudo-nodes (never real instructions) — these are split into the DATA section
|
||||||
|
at codegen time and never appear as TEXT.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Labels and Symbols
|
||||||
|
|
||||||
|
A label (`name:`) attaches to the *next* node emitted — including nodes produced by pseudo-instruction
|
||||||
|
expansion, so a label on a `func`/`return`/`ldw <label>, reg` line attaches to whatever real
|
||||||
|
instruction that expands to first.
|
||||||
|
|
||||||
|
**Known hazard:** the current symbol table is a single flat namespace per module — a data label and a
|
||||||
|
text label sharing the same name will silently collide, with the later-inserted definition winning.
|
||||||
|
Always give data and code labels distinct names within a module (e.g. don't declare `dw input: ...`
|
||||||
|
and also have a code label `input:` in the same file).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Instruction Syntax
|
||||||
|
|
||||||
|
Register-register instructions and hardware instructions take arguments in the order the ISA spec
|
||||||
|
defines (see the ISA spec, §5, for exact field semantics per instruction). A few argument-order
|
||||||
|
points worth calling out explicitly, since they're easy to get backwards:
|
||||||
|
|
||||||
|
- **Loads:** `ldw <base>, <dest>, <offset>` — base address register first, destination second.
|
||||||
|
- **Stores:** `stw <src>, <base>, <offset>` — value register first, base address register second.
|
||||||
|
This mirrors the *encoding* field order (`src`/`dest` slots), even though the semantic roles for
|
||||||
|
stores are reversed from loads.
|
||||||
|
- **Immediate load:** `lli <value>, <dest>` / `lui <value>, <dest>` — value first, destination second.
|
||||||
|
- **Arithmetic/bitwise (register form):** `add <sr1>, <sr2>, <dest>` — operands first, destination
|
||||||
|
last.
|
||||||
|
- **Immediate arithmetic:** `addi <src>, <imm> [, <dest>]` — if `<dest>` is omitted, it defaults to
|
||||||
|
`<src>` (in-place update). Both two- and three-argument forms are valid.
|
||||||
|
- **Jumps:** `jmp <addr-reg>, <offset>` at the hardware level, but see §5 for the far more common
|
||||||
|
label-based `jmp <label>` pseudo-instruction, which is what you'll write in practice.
|
||||||
|
|
||||||
|
Offsets on load/store instructions are optional — if omitted, the assembler inserts an implicit `0`.
|
||||||
|
`ldw bpr, rg0, 8` and `ldw bpr, rg0` (offset 0) are both valid; `stw rg8, COL_IDX` (no offset) is
|
||||||
|
likewise valid and equivalent to `stw rg8, COL_IDX, 0`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Pseudo-Instructions
|
||||||
|
|
||||||
|
These expand into one or more real instructions during assembly (`expand_pseudo_ops`). Understanding
|
||||||
|
the expansion matters because several of them clobber `Acc`.
|
||||||
|
|
||||||
|
### 5.1 `lwi <value-or-label>, <dest>`
|
||||||
|
```
|
||||||
|
lli <value>, <dest>
|
||||||
|
lui <value>, <dest>
|
||||||
|
```
|
||||||
|
Loads a full 32-bit immediate or a resolved label address into `<dest>`. This is the standard way to
|
||||||
|
get a pointer to a data symbol into a register:
|
||||||
|
```asm
|
||||||
|
lwi program, rg0 // rg0 = address of `program`
|
||||||
|
lwi data, rg1 // rg1 = address of `data`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Label-based load: `ldb`/`ldbs`/`ldh`/`ldhs`/`ldw <label>, <dest> [, offset]`
|
||||||
|
```
|
||||||
|
lli <label>, <dest>
|
||||||
|
lui <label>, <dest>
|
||||||
|
<real-op> <dest>, <dest>, <offset>
|
||||||
|
```
|
||||||
|
Reads the label's address into `<dest>` via the `lli`/`lui` pair, then performs the actual load using
|
||||||
|
that same register as its own base — i.e. `ldw stack, bpr` loads the *value stored at* the address
|
||||||
|
`stack`, into `bpr`, not the address itself (contrast with `lwi`, which loads the address).
|
||||||
|
|
||||||
|
### 5.3 Label-based store: `stb`/`sth`/`stw <src>, <label> [, offset]`
|
||||||
|
```
|
||||||
|
lli <label>, acc
|
||||||
|
lui <label>, acc
|
||||||
|
<real-op> <src>, acc, <offset>
|
||||||
|
```
|
||||||
|
Stores `<src>`'s value to the memory location named by `<label>`. **Clobbers `Acc`.**
|
||||||
|
|
||||||
|
### 5.4 `func` (prologue)
|
||||||
|
```
|
||||||
|
push bpr
|
||||||
|
mov spr, bpr
|
||||||
|
```
|
||||||
|
Marks the start of a function — establishes a new stack frame by saving the caller's frame pointer and
|
||||||
|
setting the new frame base. Attach `func` (or, per current usage, place the label directly on a line
|
||||||
|
ending in `: func`) to the function's entry label.
|
||||||
|
|
||||||
|
### 5.5 `return` (epilogue)
|
||||||
|
```
|
||||||
|
mov bpr, spr
|
||||||
|
pop bpr
|
||||||
|
ret
|
||||||
|
```
|
||||||
|
Tears down the current frame and returns to the caller.
|
||||||
|
|
||||||
|
**Style note from the example programs:** `print.dsa` factors a shared epilogue into a single `_end:`
|
||||||
|
label that every function `jmp`s to rather than repeating `return` inline in each function body — this
|
||||||
|
is a manual optimization on top of the pseudo-instruction, not a different pseudo-op; the underlying
|
||||||
|
epilogue sequence is identical.
|
||||||
|
|
||||||
|
### 5.6 Two-argument arithmetic defaults
|
||||||
|
`addi <src>, <imm>` and `subi <src>, <imm>` (destination omitted) update `<src>` in place — equivalent
|
||||||
|
to `addi <src>, <imm>, <src>`. Seen throughout the example programs, e.g. `addi rg1, 1` in `print.dsa`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Calling Convention
|
||||||
|
|
||||||
|
### 6.1 Stack Frame Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
Higher addresses
|
||||||
|
│ ... caller's frame ...
|
||||||
|
├───────────────┤
|
||||||
|
│ Arg N │
|
||||||
|
│ ... │
|
||||||
|
│ Arg 1 │
|
||||||
|
│ Arg 0 │ ← bpr + 8 (first argument)
|
||||||
|
├───────────────┤
|
||||||
|
│ Return addr │ ← pushed by `call`
|
||||||
|
│ Old Bpr │ ← pushed by `func` prologue; bpr now points here
|
||||||
|
├───────────────┤ ← spr, bpr after prologue
|
||||||
|
│ Locals / saved │
|
||||||
|
│ registers │
|
||||||
|
└───────────────┘
|
||||||
|
Lower addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
Arguments are accessed at `bpr + 8 + 4*n` for the n-th argument (0-indexed) — confirmed directly from
|
||||||
|
usage in `print.dsa` (`ldw bpr, rg0, 8` retrieves the first/only argument) and `dsa.md`'s stated
|
||||||
|
convention (`bpr+8` as the first-argument slot).
|
||||||
|
|
||||||
|
### 6.2 Caller Responsibilities
|
||||||
|
|
||||||
|
1. Push arguments **in reverse order** (last argument first), so argument 0 ends up closest to `bpr`.
|
||||||
|
2. `call <function>`.
|
||||||
|
3. After return, pop/discard each pushed argument slot (commonly `pop zero` for slots you don't need
|
||||||
|
the value of).
|
||||||
|
|
||||||
|
```asm
|
||||||
|
push rg2 // arg2 pushed first (will be furthest from bpr)
|
||||||
|
push rg1
|
||||||
|
push rg0 // arg0 pushed last (closest to bpr, at bpr+8)
|
||||||
|
call some_func
|
||||||
|
pop zero // discard arg0 slot (or capture return value if the callee wrote one there)
|
||||||
|
pop rg1
|
||||||
|
pop rg2
|
||||||
|
```
|
||||||
|
### 6.3 Callee Responsibilities
|
||||||
|
|
||||||
|
1. `func` prologue (or its manual expansion) at entry.
|
||||||
|
2. ensure **all** the registers that you intend to access in the function body are saved!
|
||||||
|
3. Access arguments via `ldw bpr, <dest>, 8 + 4*n`.
|
||||||
|
4. If returning a value, the convention (per `dsa.md` and consistent with example usage) is to store it
|
||||||
|
back into the caller's argument-0 slot (`bpr + 8`) before returning, so the caller can retrieve it by
|
||||||
|
popping that slot after `call` returns.
|
||||||
|
5. `return` epilogue.
|
||||||
|
|
||||||
|
Real example, from `print.dsa`:
|
||||||
|
```asm
|
||||||
|
print: func
|
||||||
|
push rg0
|
||||||
|
push rg1
|
||||||
|
push rg2
|
||||||
|
push rg3
|
||||||
|
|
||||||
|
ldw bpr, rg0, 8 // arg0: string address
|
||||||
|
ldw current, rg1 // load cursor position from the `current` data symbol
|
||||||
|
|
||||||
|
_print_loop:
|
||||||
|
ldb rg0, rg2 // load next character
|
||||||
|
ieq rg2, zero, rg3
|
||||||
|
jnz rg3, _print_end // stop at null terminator
|
||||||
|
stb rg2, rg1
|
||||||
|
|
||||||
|
addi rg0, 1
|
||||||
|
addi rg1, 1
|
||||||
|
jmp _print_loop
|
||||||
|
|
||||||
|
_print_end:
|
||||||
|
stw rg1, current // persist cursor position
|
||||||
|
|
||||||
|
pop rg3
|
||||||
|
pop rg2
|
||||||
|
pop rg1
|
||||||
|
pop rg0
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 Register Preservation
|
||||||
|
|
||||||
|
- Per the ISA spec's register conventions: `rg0`–`rgf` are callee-saved, therefore the callee should
|
||||||
|
1) push any registers they intend to write to onto the stack in the function prologue
|
||||||
|
2) pop those registers back in reverse order in the function epilogue
|
||||||
|
- `Acc` is always volatile — never rely on its value surviving any instruction, pseudo-op expansion, or function call.
|
||||||
|
- `Spr`/`Bpr` are managed by the `func`/`return` convention and in most cases should not be manipulated directly except when implementing that convention itself (e.g. `_init_stack:` routines that set up the initial stack pointer before any function has been called).
|
||||||
|
-
|
||||||
|
---
|
||||||
|
## 7. Complete Worked Example
|
||||||
|
|
||||||
|
From `print.dsa`, showing data declarations, a label-based load without an explicit offset, and the
|
||||||
|
full calling convention in a small, real function (simplified for the sake of this example):
|
||||||
|
|
||||||
|
```asm
|
||||||
|
dw display: 0x20000
|
||||||
|
dw current: 0x20000
|
||||||
|
|
||||||
|
print_byte: func
|
||||||
|
push rg0
|
||||||
|
push rg1
|
||||||
|
|
||||||
|
ldw bpr, rg0, 8 // arg0 = byte to print
|
||||||
|
ldw current, rg1 // rg1 = cursor position
|
||||||
|
|
||||||
|
stb rg0, rg1
|
||||||
|
addi rg1, 1
|
||||||
|
|
||||||
|
stw rg1, current // save the cursor position for the next caller
|
||||||
|
|
||||||
|
pop rg1
|
||||||
|
pop rg0
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
Caller side (from `bf.dsa`):
|
||||||
|
```asm
|
||||||
|
db error: "Failed!"
|
||||||
|
|
||||||
|
printerr: func
|
||||||
|
lwi error, rg2
|
||||||
|
push rg2
|
||||||
|
call print::print_byte
|
||||||
|
pop zero
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Common Pitfalls
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `Acc` value unexpectedly changed | A label-based store pseudo-op (§5.3) ran between your set and use of `Acc` | Don't hold live state in `Acc` across any label-based store, or any pseudo-op expansion in general |
|
||||||
|
| Wrong value loaded from a "pointer" | Confusing `lwi <label>, reg` (loads the *address*) with `ldw <label>, reg` (loads the *value stored at* that address) | Double-check which one your access pattern actually needs |
|
||||||
|
| Symbol silently resolves to the wrong address | A data label and a text label share the same name within one module | Rename one of them — the assembler does not currently detect this collision |
|
||||||
|
| Jump target wraps or lands somewhere wrong in a large program | Static jump-target immediates are 16-bit; combined TEXT beyond ~64KB can overflow that field | Keep an eye on total program size until a wider addressing/relocation scheme lands |
|
||||||
|
| A function's argument reads garbage | Arguments pushed in the wrong order, or the stack not balanced after the call | Push last-to-first (reverse order), and ensure every pushed argument is popped exactly once after `call` returns |
|
||||||
|
| | | |
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# DSE Executable Format Specification (v1)
|
||||||
|
|
||||||
|
**Status:** Implemented — matches `DseHeader`/`SymbolEntry`/`DseExecutable` as built in the `assembler`
|
||||||
|
crate's format module.
|
||||||
|
**Scope:** Single-module, fully-linked executables produced by assembling one `.dsa` program (with its `include`s already concatenated in). Does not yet cover the planned `DsoBinary` (relocatableobject/`.o`-equivalent) format or a real multi-unit linker — see §7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Design Goals
|
||||||
|
|
||||||
|
- Minimal, purpose-built for DSA — not an ELF-compatible or ELF-inspired-to-the-letter format; borrows
|
||||||
|
only ELF's useful idea of offset+size pairs in the header rather than assuming fixed contiguous
|
||||||
|
section layout.
|
||||||
|
- Separate DATA and TEXT payloads, addressed independently — no jump-over-data hack required to skip
|
||||||
|
past a data blob embedded in the instruction stream.
|
||||||
|
- Symbol table sufficient for post-hoc disassembly/debugging (name, resolved address, section,
|
||||||
|
binding), even though the current single-file assembler doesn't need cross-file linking yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. File Layout
|
||||||
|
|
||||||
|
All multi-byte fields are little-endian. On-disk section order is:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ Header ] [ Symbol Table ] [ String Table ] [ Data ] [ Text ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**This on-disk order is not the same as the runtime memory layout** — see §5. Do not assume file byte
|
||||||
|
offsets correspond to runtime addresses.
|
||||||
|
|
||||||
|
### 2.1 Header
|
||||||
|
|
||||||
|
Magic bytes `"DSAX"` (4 bytes) precede the struct fields below. Total header size on disk is **48
|
||||||
|
bytes** (4-byte magic + 44 bytes of fields).
|
||||||
|
|
||||||
|
| Field | Type | Meaning |
|
||||||
|
|-------|------|---------|
|
||||||
|
| `version` | `u16` | Format version. Currently `1`. |
|
||||||
|
| `flags` | `u16` | Reserved, currently `0`. |
|
||||||
|
| `entry_point` | `u32` | Absolute runtime address of the program's entry point, assuming TEXT is loaded at address 0 (see §5). |
|
||||||
|
| `symbol_table_offset` | `u32` | Byte offset from start of file to the first `SymbolEntry`. |
|
||||||
|
| `symbol_table_size` | `u32` | **Count** of symbol entries (not byte size). |
|
||||||
|
| `string_table_offset` | `u32` | Byte offset to the string table. |
|
||||||
|
| `string_table_size` | `u32` | **Byte length** of the string table. |
|
||||||
|
| `data_offset` | `u32` | Byte offset to the DATA section. |
|
||||||
|
| `data_size` | `u32` | **Byte length** of the DATA section. |
|
||||||
|
| `text_offset` | `u32` | Byte offset to the TEXT section. |
|
||||||
|
| `text_size` | `u32` | **Count** of 32-bit words in TEXT (not byte size). |
|
||||||
|
|
||||||
|
### 2.2 Symbol Table
|
||||||
|
|
||||||
|
An array of `symbol_table_size` fixed-size entries, each **12 bytes**:
|
||||||
|
|
||||||
|
| Field | Type | Meaning |
|
||||||
|
|-------|------|---------|
|
||||||
|
| `name_offset` | `u32` | Byte offset into the string table where this symbol's name begins. |
|
||||||
|
| `value` | `u32` | Resolved runtime address (see §5 for the base-address convention this assumes). |
|
||||||
|
| `section` | `u8` | `0` = absolute, `1` = data, `2` = text, any other value = undefined/unknown. |
|
||||||
|
| `binding` | `u8` | `0` = local, `1` = global, `2` = weak. |
|
||||||
|
| `_pad` | `u16` | Reserved, currently unused; always `0`. |
|
||||||
|
|
||||||
|
### 2.3 String Table
|
||||||
|
|
||||||
|
Raw bytes: a concatenation of symbol names, each null-terminated, referenced by `name_offset` into
|
||||||
|
this buffer. No length prefix — read to the next `0x00` byte from the given offset.
|
||||||
|
|
||||||
|
### 2.4 Data Section
|
||||||
|
|
||||||
|
Raw bytes — whatever `db`/`dh`/`dw`/`resb`/`resh`/`resw` directives produced, concatenated in program
|
||||||
|
order. **Not guaranteed to be internally word-aligned at every sub-item boundary** — a string literal
|
||||||
|
can leave the next data item starting at an arbitrary byte offset within the section. Consumers that
|
||||||
|
walk this section looking for structure (disassemblers, memory viewers) must scan byte-by-byte, not
|
||||||
|
assume 4-byte-aligned entries throughout.
|
||||||
|
|
||||||
|
### 2.5 Text Section
|
||||||
|
|
||||||
|
`text_size` raw 32-bit words, each a fully-encoded `Instruction` per the ISA spec. No padding between
|
||||||
|
words; word `i` begins at `text_offset + 4*i` on disk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Symbol Resolution Convention
|
||||||
|
|
||||||
|
Symbol addresses (`SymbolEntry.value`, and the header's `entry_point`) are computed under a fixed
|
||||||
|
runtime-layout assumption, **not** relative to anything about the file's on-disk layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
data_base = text_len_words * 4 // where text_len_words = TEXT section's word count
|
||||||
|
```
|
||||||
|
|
||||||
|
- A TEXT-section symbol's `value` is its offset within TEXT (i.e. assumes TEXT is loaded starting at
|
||||||
|
runtime address 0).
|
||||||
|
- A DATA-section symbol's `value` is `data_base + <offset within DATA>`.
|
||||||
|
|
||||||
|
This is a v1 stopgap, not a general relocation scheme — see §7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. On-Disk Offset Computation (Writer Responsibilities)
|
||||||
|
|
||||||
|
When constructing a `DseExecutable`, offsets must be derived from the **actual serialized size** of
|
||||||
|
each preceding section — not hand-counted constants. The header magic (4 bytes) and each
|
||||||
|
`SymbolEntry`'s true packed size (12 bytes, not a naively-summed field-width guess) have both been
|
||||||
|
sources of real, silent file corruption during development, each shifting every subsequent offset by a
|
||||||
|
fixed amount and making downstream sections unreadable at their claimed positions. Compute:
|
||||||
|
|
||||||
|
```
|
||||||
|
symbol_table_offset = header_size // 48
|
||||||
|
string_table_offset = symbol_table_offset + (symbol_count * 12)
|
||||||
|
data_offset = string_table_offset + string_table.len()
|
||||||
|
text_offset = data_offset + data.len()
|
||||||
|
```
|
||||||
|
|
||||||
|
A unit test that round-trips a `DseExecutable` through `write()` then `read()` and asserts structural
|
||||||
|
equality is the recommended guard against this class of bug recurring — this is far more reliable than
|
||||||
|
re-deriving constants by hand whenever a struct's fields change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Loading Into Runtime Memory
|
||||||
|
|
||||||
|
**The on-disk file layout and the runtime memory image are different things.** Loading a `.dse` file
|
||||||
|
means:
|
||||||
|
|
||||||
|
1. Parse the header and extract `data` and `text` as separate in-memory byte/word buffers (via
|
||||||
|
`DseExecutable::load`/`read`).
|
||||||
|
2. Copy `text` into memory starting at whatever base address the loader chooses (`text_base`,
|
||||||
|
conventionally `0` for the current single-program, single-load-address model).
|
||||||
|
3. Copy `data` into memory immediately following, at `text_base + text.len() * 4`.
|
||||||
|
4. Set the program counter to `text_base + header.entry_point`.
|
||||||
|
|
||||||
|
**Important:** copying the *raw file bytes* wholesale into memory and treating a file offset (e.g. `header.text_offset`) as if it were a runtime address is incorrect — the header/symtab/strtab/data sections sitting ahead of TEXT *in the file* have no relationship to what sits ahead of TEXT *in the runtime memory image the assembler assumed* (which is nothing — TEXT is assumed to start at address 0). The loader must unpack `data`/`text` into the layout described above, not memory-map the file as-is.
|
||||||
|
|
||||||
|
If `text_base != 0`, every address already baked into instruction words and the symbol table (computed under the `text_base == 0` assumption at assembly time) will be wrong at runtime, since nothing currently patches them for a different load address. Until relocations exist (§7), load at `text_base = 0`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Reading and Writing (Reference Implementation Shape)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct DseExecutable {
|
||||||
|
header: DseHeader,
|
||||||
|
symbols: Vec<SymbolEntry>,
|
||||||
|
string_table: Vec<u8>,
|
||||||
|
data: Vec<u8>,
|
||||||
|
text: Vec<u32>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `DseExecutable::load(path)` / `::read(reader)` — parses the header, then seeks to each
|
||||||
|
`*_offset` field in turn to read the symbol table, string table, data, and text sections. Sections are addressed by absolute offset, not read sequentially by assumed contiguous layout, so this must seek rather than just read-through.
|
||||||
|
- `DseExecutable::save(path)` / `::write(writer)` — writes header, then symtab, then strtab, then data, then text, in that fixed order, with offsets computed per §4.
|
||||||
|
- `DseExecutable::to_memory_image()` — produces the runtime image described in §5 (TEXT bytes followed immediately by DATA bytes), ready to be copied into emulator memory at `text_base`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Known Limitations / Planned Extensions
|
||||||
|
|
||||||
|
- **No relocations.** All addresses assume a single fixed load address (TEXT at 0). Loading at any other base, or supporting a multi-program/OS environment with per-process load addresses, requires adding a relocation table — additive to this format (new optional header fields, `0`/absent in
|
||||||
|
existing files) rather than a breaking change.
|
||||||
|
- **Single compilation unit only.** The current assembler concatenates all `include`d files into one node list before producing a `DseExecutable` — there is no independent per-file compilation or linking step yet. The planned `DsoBinary` format (a `.o`-equivalent, carrying local symbols + unresolved imports + a relocation table per compiled unit) plus a dedicated linker step will decouple "compile a module" from "produce a runnable program," enabling per-file caching and parallel builds. `DseExecutable` is intended to remain the final, fully-linked output format even after that change — only the path that produces it will change.
|
||||||
|
- **No debug info section.** Symbol names/addresses are present, but there's no line-number or
|
||||||
|
source-mapping table. Worth considering if source-level debugging in the egui debugger becomes a priority.
|
||||||
|
- **`flags` field is currently unused** — reserved for future use (e.g. marking an executable as
|
||||||
|
requiring a specific kernel/loader feature).
|
||||||
@@ -71,7 +71,8 @@ pub enum Opcode {
|
|||||||
// system
|
// system
|
||||||
Int = 41,
|
Int = 41,
|
||||||
IRet = 42,
|
IRet = 42,
|
||||||
Hlt = 43,
|
// Acs = 43,
|
||||||
|
Hlt = 44,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Opcode {
|
impl Opcode {
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
DSX is a collection of higher‑level tools that orchestrate the DSA ecosystem.
|
DSX is a collection of higher‑level tools that orchestrate the DSA ecosystem.
|
||||||
While **DSA** supplies the core emulator, compiler, assembler and runtime, DSX provides:
|
While **DSA** supplies the core emulator, compiler, assembler and runtime, DSX provides:
|
||||||
|
|
||||||
* **dsx-build** – A build system and package manager that compiles DSA projects, resolves dependencies, and produces deployable artifacts.
|
* **dsx-client** – A package manager that interacts with servers, managing packages and DSA repositories on the local machine
|
||||||
* **dsx-server** – A lightweight package repository server that hosts DSX packages, enabling sharing and version control across teams, with CI/CD configured out‑of‑the‑box.
|
* **dsx-server** – A lightweight package repository server that hosts DSX packages, enabling sharing and version control across teams, with CI/CD configured out‑of‑the‑box.
|
||||||
|
* **dsx-common** - A set of common utilities between dsx-client and dsx-server, including the build system itself
|
||||||
|
|
||||||
Together, these tools streamline development workflows, automate builds, and manage the distribution of DSA components within an organization.
|
Together, these tools streamline development workflows, automate builds, and manage the distribution of DSA components within an organization.
|
||||||
|
|||||||
Reference in New Issue
Block a user