From 81e960be2953a9953fd89c42cc6f71ce6ff54c85 Mon Sep 17 00:00:00 2001 From: hachem Date: Mon, 31 Aug 2026 03:22:54 +0200 Subject: feat: add diagnostics module with source-anchored caret errors --- .gitignore | 3 +- README.md | 3 + docs/language.md | 163 +++++++++++++++++++++++++++++++++++++++++++++++ examples/fibonacci.hdasm | 62 ------------------ examples/fibonacci.hdass | 62 ++++++++++++++++++ scripts/test_examples.sh | 2 +- 6 files changed, 231 insertions(+), 64 deletions(-) create mode 100644 docs/language.md delete mode 100644 examples/fibonacci.hdasm create mode 100644 examples/fibonacci.hdass diff --git a/.gitignore b/.gitignore index de4c3a4..843c7fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ bin/ bin-int/ -# premake-generated files Makefile *.make *.sln @@ -12,3 +11,5 @@ compile_commands.json compile_flags.txt .clangd +.DS_Store + diff --git a/README.md b/README.md index 4cf4444..6784154 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ proc main The program still directly controls the registers used for the system calls. Nothing is hiding what the program is doing. More examples can be found in [examples/](examples/). +## Documentation +The full language reference — directives, declarations, statements, expressions, registers and extensions — lives in [docs/language.md](docs/language.md). + ## Building The build is driven by [premake5](https://premake.github.io/). Generate the makefiles and build the compiler: ```bash diff --git a/docs/language.md b/docs/language.md new file mode 100644 index 0000000..a2b4ffa --- /dev/null +++ b/docs/language.md @@ -0,0 +1,163 @@ +# hdass language reference + +hdass emits NASM for x86-64; fasm and masm are planned. The compiler output itself isn't tied to an OS, but the examples and toolchain here target Linux (Linux syscall numbers, `nasm -f elf64`, `ld`). Pipeline: `lex → parse → analyze → emit`. + +## A first program + +```hdass +[entry: main] + +const SYS_WRITE = 1 +const SYS_EXIT = 60 +const STDOUT = 1 + +data message = "type shi.\n" + +proc main +{ + rax = SYS_WRITE + rdi = STDOUT + rsi = message + rdx = message.len + syscall + + rax = SYS_EXIT + rdi = 0 + syscall +} +``` + +Writes `type shi.` to stdout and exits. + +## Comments + +```hdass +rax = 1 // line +/* block */ +``` + +## Directives + +Top-level `[key: value]`, configuring the whole program. + +| Directive | Meaning | +| --- | --- | +| `[bits: 64]` / `[bits: 32]` | Target bitness. Default 64. | +| `[entry: NAME]` | Makes procedure `NAME` the entry point. | +| `[enable: NAME]` | Turns on an [extension](#extensions). | + +Unknown keys, a `bits` value other than 32/64, and unknown extensions are errors. + +## Constants and data + +`const` names an integer (becomes a NASM `%define`). `data` puts a string in `.data`; the name is its address and `.len` is its length in bytes. + +```hdass +const STDOUT = 1 +data message = "type shi.\n" // message -> address, message.len -> 10 +``` + +## Procedures + +`proc` groups a body. Parameters name registers — `value` below is `rdi`. + +```hdass +proc print_number(value: rdi) +{ + rax = value +} +``` + +Each procedure ends with `ret`, except the entry point. `[entry: NAME]` exports `NAME` with `global` and drops its `ret`, so it must end the program itself (an exit syscall). Link with `ld -e NAME`. + +## Registers + +Written by their architecture names — `rax`–`rdi`, `rbp`, `rsp`, `r8`–`r15` — and their sub-registers (`al`, `ax`, `eax`, `dl`, …), which imply a store's size. The [`logical_registers`](#extensions) extension adds `r1`–`r14`. + +## Statements + +```hdass +rax = SYS_WRITE // mov +rcx -= 1 // += -= *= /= -> add sub imul idiv (/= targets rax) +rdx = buffer + 31 // address math with + and - +loop: // label +goto loop +if rcx != 0 // == != < <= > >= ; runs the next statement only + goto loop +syscall +print_number(r12) // call; args go into the callee's parameter registers +stack buffer[32] // stack buffer; buffer is its base address +``` + +## Dereference (`^`) + +`^reg` is the memory at the address in `reg` — NASM's `[reg]`. On the left of `=` it stores there. The store width comes from the value operand, so a sized sub-register picks the size: + +```hdass +^rsi = rdx // mov [rsi], rdx (qword) +^rsi = dl // mov [rsi], dl (byte) +^rsi = eax // mov [rsi], eax (dword) +``` + +A leading size keyword sets the width explicitly. It down-converts a full register to the matching sub-register, and gives an immediate a width NASM would otherwise reject: + +```hdass +^byte rsi = rdx // mov byte [rsi], dl (rdx -> its low byte) +^word rsi = rax // mov word [rsi], ax +^dword rsi = r12 // mov dword [rsi], r12d +^byte rsi = '0' // mov byte [rsi], '0' +^byte rsi = 10 // mov byte [rsi], 10 +``` + +`^` is store-only for now; loading with `rax = ^rsi` isn't supported yet. + +## Expressions + +Assignment values and `if` operands: registers, integers, chars (`'0'`), constants, data names, member access (`data.len`), and `+`/`-`. Multiply and divide come from the `*=` and `/=` compound assignments, not from `*`/`/` inside a value. + +## Extensions + +### logical_registers + +Uniform names for the general-purpose registers, so you don't juggle the irregular `rax`/`rbx`/`rsi`/… spellings. `r1`–`r14` map to: + +| `r1` | `r2` | `r3` | `r4` | `r5` | `r6` | `r7` | `r8` | `r9` | `r10` | `r11` | `r12` | `r13` | `r14` | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| rax | rbx | rcx | rdx | rsi | rdi | r8 | r9 | r10 | r11 | r12 | r13 | r14 | r15 | + +`rsp`/`rbp` and the instruction pointer keep their own names. + +```hdass +r1 = 5 // mov rax, 5 +r4 = r10 // mov rdx, r11 +r6 += r1 // add rdi, rax +``` + +A `.8`/`.16`/`.32`/`.64` suffix selects the width, mapping to the sub-register: + +```hdass +r1.8 // al +r1.16 // ax +r1.32 // eax +r1.64 // rax +^byte rsi = r4 // mov byte [rsi], dl (r4 -> rdx -> dl) +``` + +Arch `r8`–`r15` share the `rN` spelling, so with the extension on a bare `r8` is the *logical* register (which is arch `r9`). Reach arch `r8`–`r15` through logical `r7`–`r14`. Architecture names like `rax` and `rsi` still work everywhere. + +## Building a program + +```bash +hdass program.hdass -o program.asm +nasm -f elf64 program.asm -o program.o +ld -e main program.o -o program +./program +``` + +The [README](../README.md) has a Docker setup with these tools. + +## Gotchas + +- **Clobbering is yours.** `syscall` trashes `rcx`/`r11`; a callee trashes what it touches. Nothing is saved for you — `examples/fibonacci.hdass` keeps its counter in `r15` for this reason. +- **Widths must match.** `rax = r1.8` becomes `mov rax, al`, which won't assemble. +- **The entry procedure has no `ret`** — end it with an exit syscall. \ No newline at end of file diff --git a/examples/fibonacci.hdasm b/examples/fibonacci.hdasm deleted file mode 100644 index 95a59c8..0000000 --- a/examples/fibonacci.hdasm +++ /dev/null @@ -1,62 +0,0 @@ -[entry: main] - -const SYS_WRITE = 1 -const SYS_EXIT = 60 - -const STDOUT = 1 - -data newline = "\n" - -proc print_number(value: rdi) -{ - stack buffer[32] - - rax = value - rbx = 10 - rsi = buffer+31 - -convert: - rdx = 0 - rax /= rbx - rdx += '0' - - ^byte rsi = rdx - rsi -= 1 - - if rax != 0 - goto convert - rsi += 1 - - rax = SYS_WRITE - rdi = STDOUT - rdx = buffer+32-rsi - syscall - - rax = SYS_WRITE - rdi = STDOUT - rsi = newline - rdx = newline.len - syscall -} - -proc main -{ - r12 = 0 - r13 = 1 - r15 = 10 // syscall clobbers rcx/r11, so keep the counter in r15 - -loop: - print_number(r12) - - rax = r12 - r12 = r13 - r13 += rax - - r15 -= 1 - if r15 != 0 - goto loop - - rax = SYS_EXIT - rdi = 0 - syscall -} diff --git a/examples/fibonacci.hdass b/examples/fibonacci.hdass new file mode 100644 index 0000000..95a59c8 --- /dev/null +++ b/examples/fibonacci.hdass @@ -0,0 +1,62 @@ +[entry: main] + +const SYS_WRITE = 1 +const SYS_EXIT = 60 + +const STDOUT = 1 + +data newline = "\n" + +proc print_number(value: rdi) +{ + stack buffer[32] + + rax = value + rbx = 10 + rsi = buffer+31 + +convert: + rdx = 0 + rax /= rbx + rdx += '0' + + ^byte rsi = rdx + rsi -= 1 + + if rax != 0 + goto convert + rsi += 1 + + rax = SYS_WRITE + rdi = STDOUT + rdx = buffer+32-rsi + syscall + + rax = SYS_WRITE + rdi = STDOUT + rsi = newline + rdx = newline.len + syscall +} + +proc main +{ + r12 = 0 + r13 = 1 + r15 = 10 // syscall clobbers rcx/r11, so keep the counter in r15 + +loop: + print_number(r12) + + rax = r12 + r12 = r13 + r13 += rax + + r15 -= 1 + if r15 != 0 + goto loop + + rax = SYS_EXIT + rdi = 0 + syscall +} diff --git a/scripts/test_examples.sh b/scripts/test_examples.sh index 84299ce..4a01a0e 100755 --- a/scripts/test_examples.sh +++ b/scripts/test_examples.sh @@ -81,7 +81,7 @@ check loop_sum "sums 1..5 with a countdown loop" examples/loop_sum.hdas check branch "selects the larger of two values" examples/branch.hdass 8 "" check call "passes an argument through a proc" examples/call.hdass 21 "" check logical "logical register names (r1..r14)" examples/logical.hdass 21 "" -check fibonacci "prints the first ten Fibonacci numbers" examples/fibonacci.hdasm 0 "0 +check fibonacci "prints the first ten Fibonacci numbers" examples/fibonacci.hdass 0 "0 1 1 2 -- cgit v1.3