Skip to content
The compiler

The compiler

The compiler is a qdx program in compiler/. It reads source files and writes one executable, in five stages. Each stage is one file, and each takes the output of the one before.

StageFileTakesGives
Lexerlexer.qdxThe bytes of a file.A list of tokens, each with its file, line and column.
Parserparser.qdxThe tokens of every file.One syntax tree for the program.
Checkercheck.qdxThe syntax tree.A context of every declaration, or an error.
Code generatorcodegen.qdxThe tree and the context.Machine code as bytes.
Writerelf.qdxThe machine code.The bytes of an executable file.

ast.qdx declares the syntax tree, and main.qdx reads the command line and runs the stages.

Errors

Every stage reports a problem the same way: it calls fail with a position and a message, which writes the line and ends the compiler. There is no recovery and no second error. A stage can assume everything before it succeeded.

The lexer

The lexer walks the bytes with an index and builds the token list backwards, reversing it once at the end. That keeps its main function calling itself in last position, so the length of a file does not cost stack.

A bytes literal is checked for its closing quote and valid escapes first, then decoded into new bytes.

The parser

The parser is recursive descent with one function per rule of the grammar. Nothing is mutated: each function takes the remaining tokens and returns a record of what it read and the tokens after it.

Before parsing, it scans every file for record and union followed by a name. That gives it the full set of type names, so a type can be used before or in a different file from its declaration.

A name followed by parentheses is read as a value being made if the first thing inside is a name and a colon, and as a call otherwise. The checker settles the empty case.

The checker

The checker does two things. First it claims every top-level name in one table, so a second declaration of any name is an error that says what already holds it. Then it checks each function body, computing a type for every expression.

It keeps no annotations on the tree. When the code generator needs a type, for a field’s position or a match’s union, it asks the checker again for that expression.

The code generator

The code generator returns machine code as a value: a tree whose leaves are bytes and whose nodes know their total size. Because sizes are known, a jump over a block is computed from the block’s size, with nothing to patch later.

Two things cannot be known while a function is being generated: the address of another function, and the start of the current one for a self tail call. Those are left as placeholders in the tree. A last pass lays the functions out in order, computes every address, and writes the tree into a buffer of exactly the right size.

That final write is the only place in the compiler where memory is changed after it is made.

The writer

The executable is two fixed headers followed by the machine code. elf.qdx builds the headers field by field and joins the three parts.