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