# hdass language reference this is the language reference. hdass transpiles to x86-64 (nasm or fasm, `-t nasm`/`-t fasm`) and aarch64 (`-t arm64`); for which target supports what, the portable [`logical_registers`](#logical_registers) model, and the per-architecture syscall abis, see [targets](targets.md). new here? start with [getting started](getting-started.md). pipeline: `lex → parse → analyze → emit`. the examples below use x86-64 register names. the output isn't tied to an os, but the examples and toolchain here target linux (elf, `ld`). ## 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]` (or bare `[key]`), configuring the whole program. | directive | meaning | | --- | --- | | `[bits: 64]` / `[bits: 32]` / `[bits: 16]` | target bitness. default 64. | | `[entry: NAME]` | makes procedure `NAME` the entry point. | | `[enable: NAME]` | turns on an [extension](#extensions). | | `[format: bin]` / `[format: elf]` | output a flat binary instead of an elf object. default elf. | | `[org: 0x7C00]` | set the load address of a flat binary. | | `[boot]` | pad a flat binary to 510 bytes and append the `0xAA55` boot signature. | the last three build a raw binary instead of a linked elf, enough for an x86 boot sector. in `bin` format there are no sections or exported symbols, and code comes first so execution starts at the origin. unknown keys, a bad `bits` value, and unknown extensions are errors. ## constants and data `const` names a constant integer expression — integer literals, character literals, other constants, a leading `-`, and `+` `-` `*` `/`. integers are decimal, `0x` hex, or `0b` binary (these forms work anywhere an integer does). `data` puts a string in `.data`; the name is its address and `.len` is its length in bytes. ```hdass const STDOUT = 1 const MASK = 0xFF const AREA = 8 * 6 // 48 data message = "type shi.\n" // message -> address, message.len -> 10 ``` ## enums and structs both describe compile-time values reached with `Name.member`, which folds to an integer. `enum` names a set of constants numbered from 0: ```hdass enum Status { Ok, // 0 Warn, // 1 Fail // 2 } rax = Status.Fail // mov rax, 2 ``` `struct` describes a packed memory layout (no padding). fields are `name` or `name: size`, where size defaults to `qword`. `Name.field` is the field's byte offset, and `Name.size` is the total size. ```hdass struct Point { x // qword, offset 0 y // qword, offset 8 flag: byte // offset 16 } rsi += Point.y // add rsi, 8 rax = Point.size // mov rax, 17 ``` a struct is layout only — it allocates nothing. pair it with a `stack` buffer sized by `Name.size` and pointer arithmetic (see [examples/records.hdass](../examples/records.hdass)). ## 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`](#logical_registers) extension adds `r1`–`r14`. the segment registers (`cs ds es fs gs ss`) and control registers (`cr0 cr2 cr3 cr4`) are also recognised, for systems code that sets up segments or switches cpu modes. ## statements ```hdass rax = SYS_WRITE // mov rcx -= 1 // += -= *= /= %= -> add sub imul idiv (idiv) rax = rbx * rcx // + - * / % in a value; / % and their = forms use rax:rdx rdx = buffer + 31 // address math loop: // label goto loop if rcx != 0 // == != < <= > >= ; guards the next statement or a { block } goto loop while rcx != 0 // same condition; repeats the next statement or { block } rcx -= 1 syscall print_number(r12) // call; args go into the callee's parameter registers stack buf[Point.size] // stack buffer (size is any constant); buf is its base address ``` ## control flow (`if` / `else` / `while`) `if ` guards either the single next statement or a `{ }` block, and an optional `else` takes its own statement or block. `else if` chains because the `else` body is itself a statement. comparisons are `==` `!=` `<` `<=` `>` `>=`; a float compare needs an `xmm` register on the left (see [floating point](#floating-point)). ```hdass if rax > rbx { rdi = 1 goto done } else if rax == rbx rdi = 0 else rdi = -1 ``` `while ` runs its statement or `{ }` block for as long as the condition holds, testing it before each pass. it is the same condition as `if`, and desugars to a label, the test, the body, and a jump back — the `loop:`/`goto` you would write by hand. use `goto` to break out early. ```hdass rbx = 0 while rcx > 0 { rbx += rcx rcx -= 1 } ``` an optional `.name` right after `while` names the loop's generated labels, so they read as `.name` (top) and `.name_end` (exit) instead of the anonymous `.while_N` — handy for finding a loop in the emitted assembly. give nested loops distinct names. ```hdass while .countdown rcx > 0 // emits `.countdown:` … `jmp .countdown` … `.countdown_end:` rcx -= 1 ``` a **conditional select** picks one of two register values without a branch: `dst = a if else b`. it lowers to `csel` on aarch64 (one instruction) and `cmov` on x86 (a default move plus a conditional move, arranged so `dst` may safely alias either source). both sources must be registers. ```hdass r3 = r1 if r1 > r2 else r2 // r3 = max(r1, r2), branchless ``` ## 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 ``` `^reg` is also a value — it loads from that address. a size keyword loads a narrower value and zero-extends it into the target: ```hdass rax = ^rsi // mov rax, [rsi] rbx = ^byte rsi // movzx rbx, byte [rsi] rcx = ^dword rsi // mov ecx, [rsi] (32-bit load zero-extends) rdx = ^rsi + 4 // load, then add 4 ``` `^signed` before the size sign-extends instead, so a narrower value keeps its sign in the full register. it needs a `byte`, `word`, or `dword` size (a full-width load has nothing to extend): ```hdass rax = ^signed byte rsi // movsx rax, byte [rsi] rbx = ^signed word rsi // movsx rbx, word [rsi] rcx = ^signed dword rsi // movsxd rcx, dword [rsi] ``` ## raw instructions any statement that isn't an assignment, label, call or keyword is emitted as a bare instruction: a mnemonic and its operands, written in hdass's own operand syntax. this is the escape hatch for everything outside the assignment and control-flow model: `int`, `hlt`, `cli`/`sti`, `lgdt`, port i/o, the mode switch. operands are the usual registers, immediates, constants and `^memory`, and share the mnemonic's line. ```hdass cli int 0x10 out 0x64, al lgdt ^gdt // lgdt [gdt] cr0 = eax // an ordinary move; segment/control regs work with `=` too hlt ``` mnemonics pass straight through, so a typo is reported by the assembler. operands render through the same path as everywhere else, so an instruction is as portable as the rest of the language, though the mnemonics themselves are architecture-specific. ## expressions assignment values and `if` operands: registers, integers, chars (`'0'`), constants, data names, member access (`data.len`), and `+` `-` `*` `/` `%`. operators are left-associative and each right-hand operand must be a single term, so `a * b + c` works but `a + b * c` (a nested right operand) doesn't yet. a leading `-` negates a term (`rax = -5`, `rbx = rax + -3`, `const OFFSET = -8`). it only applies to values that fold to a constant, so it emits a negative immediate; negating a register (`-rbx`) is not supported. ## 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. ## floating point floating-point values live in the sse registers `xmm0`–`xmm15` (double precision). float literals like `3.14` are placed in `.data` and loaded for you. ```hdass xmm0 = 3.5 // movsd from a .data slot xmm0 *= xmm1 // += -= *= /= -> addsd subsd mulsd divsd ``` an `=` between a float register and a general-purpose register converts: ```hdass xmm0 = rax // int -> float (cvtsi2sd) rbx = xmm0 // float -> int, truncating (cvttsd2si) ``` `^` loads and stores floats too, so float state can live in memory (a `stack` buffer or struct): ```hdass ^rsi = xmm0 // movsd [rsi], xmm0 xmm1 = ^rsi // movsd xmm1, [rsi] ``` `if` compares floats too, when the left side is an `xmm` register (`ucomisd`): ```hdass if xmm0 > 4.0 goto escaped ``` see [examples/mandelbrot.hdass](../examples/mandelbrot.hdass) for a float program. not yet supported: mixing floats and ints in one expression, and printing floats. ## building a program ```bash hdass program.hdass -o program.asm # nasm (default) nasm -f elf64 program.asm -o program.o ld -e main program.o -o program ./program ``` or target fasm with `-t fasm`, which assembles in one step: ```bash hdass -t fasm program.hdass -o program.asm fasm program.asm program.o ld -e main program.o -o program ``` for `-t arm64` and the full toolchain (including the aarch64 cross-assembler and qemu), see [getting started](getting-started.md); the [readme](../README.md) has the docker setup. ## some stinkies clobbering is your responsibility: `syscall` trashes `rcx` and `r11`, while a callee can trash any registers it touches, so nothing is saved automatically. `examples/fibonacci.hdass`, for example, keeps its counter in `r15` for this reason. register widths must also match, meaning something like `rax = r1.8` would become `mov rax, al`, which will not assemble. division clobbers extra registers: `/` `%` and their `=` forms use `idiv` through `rax:rdx`, so both are overwritten regardless of the destination. the divisor can be anything — a register, a constant, or an immediate — but an immediate or an `rax`/`rdx` divisor is first copied into `r11`, so those also clobber `r11`. labels and procedures become plain assembler symbols, so avoid names the target assembler reserves: `loop`, for instance, is an instruction mnemonic that fasm rejects as a label (nasm allows it). finally, the entry procedure has no `ret`; it should end with an exit syscall.