How tempest works#
Tempest is Temper's compiler written from scratch in Rust. It reads
Temper source, works out what every name means and what type every
value has, and then writes the same program in seven languages: C++,
C#, Java, JavaScript, Lua, Python, and Rust. It can also run a program
directly, with a small interpreter, which is how tempest test and the
playground's Run button work. The whole compiler is one small binary,
and the same code builds to wasm for the browser.
These pages walk through it one stage at a time. Each is short, follows one tiny program through its stage, and ends with links into the code. They cover the compiler's architecture, not the language: for Temper itself, see the language reference.
The big picture#
Diagram source
flowchart LR
src["Temper source"] --> syntax["Syntax: tokens and trees"]
syntax --> settle["Settle loop: names, macros, types"]
settle --> interp["Interpreter: run and test"]
settle --> totmpl["to_tmpl"]
totmpl --> tmpl["tmpl: the backend IR"]
tmpl --> lower["Shared lowerings and naming"]
lower --> backends["Seven backends"]
backends --> files["Source and package files"]
Only three data structures carry the program from start to end:
- Tokens, from the lexer: a kind and a byte span each.
- The tree, one arena per library. Parsing builds it, and the settle loop rewrites it in place until every node is one of a small set of core constructs.
- tmpl, the backend IR: a settled, typed, fully explicit program that a backend can translate without asking the frontend anything.
Errors are values at every stage. A syntax error, an undeclared name, or a type that doesn't fit is reported and becomes an error node, and everything around it carries on: it's typed, run, and translated. The error fails only where it runs, so one mistake never hides the rest of a library's diagnostics, tests, or generated code.
The crates#
Diagram source
flowchart LR
syntax["temper-syntax"] --> frontend["temper-frontend"]
tmplc["temper-tmpl"] --> frontend
tmplc --> be["temper-be-* (seven)"]
frontend --> driver["temper-driver"]
frontend --> analysis["temper-analysis"]
be --> driver
analysis --> lsp["temper-lsp"]
analysis --> wasm
driver --> cli["tempest (command)"]
lsp --> cli
driver --> wasm["temper-wasm (playground)"]
| Crate | Its job |
|---|---|
temper-syntax |
Source to trees: literate files, lexing, parsing, the grammar step |
temper-frontend |
Trees to a settled library: the settle loop, typing, the interpreter, and translation to tmpl |
temper-analysis |
Editor queries over a settled library: diagnostics, hover, definition, completion, references, rename, outlines, and signature help |
temper-tmpl |
tmpl itself, plus the lowerings and naming that every backend shares, and what a backend is to tools (Backend), with the helpers their test runners share (tools) |
temper-be-* |
One crate per language: tmpl in, source files out, and natively, how the language runs tests (its tools module). Each depends only on temper-tmpl |
temper-driver |
The registry of backends, the one compile path, build, and running tests with any backend's tools |
temper-lsp |
The language server, tempest lsp |
tempest |
The command: argument parsing and output, for build, test, lsp, and one backend's files |
temper-wasm |
The compiler as wasm, for the playground |
Reading order#
- Syntax: from text to tokens to trees.
- The settle loop: names and macros, as work items that wait on facts.
- Typing: types as more facts in the same loop, then checks.
- The interpreter: running a settled library.
- tmpl: the backend IR, and the lowerings backends share.
- Backends: tmpl in, C++, C#, Java, JavaScript, Lua, Python, or Rust out.
- The tempest command:
buildandtest, and running tests with each language's own tools. - Wasm, the playground, watch, and a language server: the same compiler in a browser, and in an editor.
- The language server and editors:
tempest lsp, its queries, and the VS Code extension.
The running example#
Every page follows this program, triple.temper:
let triple(n: Int): Int { n * 3 }
console.log("triple(14) = ${triple(14)}");
Run, it prints triple(14) = 42. By the end, you'll have seen it as
tokens, as a tree, settled, typed, as tmpl, and in all seven languages.
It's one of the playground's examples,
and a shared test program
(triple.temper.md),
so the outputs these pages show are goldens that the tests keep
current: its tmpl,
and its Python, for
one. For a bigger example, the functional test algos/fibonacci has
goldens at every stage, such as its
tmpl.
Going deeper#
Each page has a companion in detail/ that goes a level
further: every important decision with its alternatives, pros and cons,
and reasons (and how Kotlin Temper does it), the main types and
functions, the tests that pin it, and known weaknesses:
syntax,
the settle loop,
typing,
the interpreter,
tmpl,
backends,
the command and driver,
the playground, and
the language server and editors.
The design notes explain the why: notes/README.md for the goals, notes/design.md for the whole design, notes/typing.md for typing, and notes/tmpl/ for tmpl. The Kotlin compiler's own backend guide and output grammars are kept in kotlin/.
Links to code go to a file and name the type or function to look for
there. CI checks that each file still defines what its link names, and
that every link to a page or heading still leads somewhere
(uv run compat/check_anchors.py).