aboutsummaryrefslogtreecommitdiff
path: root/docs/language.md
blob: 42b178ee71919003b85d6ae75ceb2cf51fc55602 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# 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 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`](#extensions) extension adds `r1`–`r14`.

## 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 <expr> <cmp> <expr>` 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 <expr> <cmp> <expr>` 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
}
```

## 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]
```

## 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 -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.

## 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`. Finally, the entry procedure has no `ret`; it should end with an exit syscall.