Skip to content
Generated code

Generated code

The code generator is deliberately simple. It does not allocate registers or optimise. Every expression leaves its value in one register, and everything else goes on the stack.

Expressions

Evaluating an expression leaves its value in rax. A binary operator evaluates the left side, pushes it, evaluates the right side, moves it to rcx, pops the left side back into rax, and applies the operation.

Functions and calls

The calling convention is the compiler’s own, and is not compatible with C.

  • The caller pushes the arguments left to right, calls, and removes them afterwards.
  • The result comes back in rax.
  • The function saves rbp and points it at its frame. Argument number i of n, counted from 0, is at rbp + 16 + 8 * (n - 1 - i).

Locals

A let evaluates its value and pushes it. The name then refers to a fixed position below rbp, known from how many values are already on the stack at that point. At the end of a block its locals are popped in one step.

The generator tracks that depth as it descends, so every local’s position is known without a separate pass, and sibling blocks reuse the same stack space.

if and match

An if tests rax and jumps over the first branch when it is zero. The first branch ends with a jump over the second.

A match pushes the value it is matching on. Each arm but the last compares the case number at the start of the value’s block with its own and jumps to the next arm if they differ. An arm pushes each field it names, runs its body, and pops them.

Self tail calls

When a function calls itself in last position, the generator evaluates the new arguments, stores them over the old ones, resets the stack to the frame, and jumps to the start of the function body.

Reserved registers

Three registers are set when the program starts and never used for anything else.

RegisterHolds
r13The stack pointer as the kernel left it, which points at the argument count.
r14The end of the memory the heap has been given.
r15The next free address on the heap.

Allocation

Making a record, a union value or new bytes calls one shared routine with a size. It returns the current value of r15 and moves r15 up. If that passes r14, it asks the kernel to extend the heap to the next page boundary first.

Runtime errors

A division first tests the divisor. If it is zero, the code loads the address and length of a message stored right after the call, and calls a shared routine that writes it to standard error and exits with status 1. The message is built by the compiler from the position of the operator.

A divisor of -1 is handled without the processor’s divide instruction, since dividing the smallest int by -1 would otherwise fault.

Layout of the executable

The file is a 64-byte file header, a 56-byte program header, then the code: a start routine, the allocation routine, the runtime error routine, and every function in the order the compiler read them. A bytes literal sits inside the function that uses it, with a jump around it.

All jumps and calls are relative, so the kernel can load the program at any address.