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

322 lines
14 KiB
Markdown
Raw 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 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 |
| | | |