4. The interpreter#
A settled library can run straight away, without generating any code.
That's what tempest test does by default, what the playground's Run
button shows, and how the functional tests check the frontend. The
interpreter is small, and it's built to run code with errors in it too:
an error node fails when it runs, with its message, as generated code
would, and a value that's only of the wrong type runs as written, as in
the dynamically typed backends (typing).
Diagram source
flowchart LR
settled["Settled library"] --> lower["Lower a function on its first call"]
lower --> code["Code: flat ops, numbered slots"]
code --> frame["Frame loop runs the ops"]
frame --> results["Output, failures, test results"]
frame -->|"a call"| lower
How it runs#
- Lowered late, per function. On a function's first call, its
settled body becomes a
Code: a flat list ofOps for a small stack machine, with jumps for control flow and locals in numbered slots. Functions never called are never lowered. - One loop per frame.
run_framesteps through a frame's ops. Values are a plain enum,Val. - Closures get cells. A local that a nested function uses lives in a shared cell, made fresh each time its block is entered, so each pass of a loop has its own.
- Failures cost nothing until they happen. A failure jumps to the
innermost
orelsearound it, found in a table by position, so the ordinary path pays nothing. - Generators pause. A generator owns its body's frame, which returns
from the loop at each
yieldand picks up from there onnext(). Anasyncblock is a generator too, run from a queue, which also pauses at anawaitof a promise that isn't resolved yet. - Bounded. Optional fuel stops a run after a number of steps (the
playground sets it, and
tempest testgives each test its own, so a runaway test fails alone), output kept for tests is capped at 64 KiB, and calls nest at most 2000 deep (500 in the browser), so runaway code can't hang a test run, fill memory, or crash a tab (limits, in detail).
Why each of these, and what else was considered: decisions, in detail.
Host code (@connected functions) runs only where the interpreter
implements it: std's Date.today(), regex, and net, for now
(builtins and connected code).
Worked example#
When triple.temper's top level calls triple(14), the body
n * 3 is lowered on that first call to ops roughly like these
(a sketch, not printed output; run-temper can't print ops yet):
Const(builtin *) push the multiplication
Local(0) push n, the first slot
Const(3)
Call { args: 2 } n * 3
Return
The top level then concatenates the result into the string and logs
it. run-temper triple.temper prints:
triple(14) = 42
Running tests#
test("name") { … } blocks become test cases during settling.
run_tests runs a
library's top level, then each module's tests, and reports them as
JUnit XML, the same report std's test runner gives in every language.
tempest test reads those results into one summary
(the CLI page says more). What tests pin the interpreter
itself: its tests, in detail.
Where in the code#
runandrun_tests: the entry points, giving aRun.call: calling a Temper function, lowering it if needed.functionandmodule: lowering a function's body and a module's top level.Frameandrun_frame: the loop.- Builtins by topic:
strings,numbers,maps,promises,regexes, andnet. run-temper: a small tool that settles files and runs them, or prints the settled tree (--tree), the tmpl (--tmpl), or settle steps (--steps).
Later, the same interpreter is meant to evaluate constants and macros written in Temper during settling (notes/compile-time.md).
In detail#
The interpreter, in detail: each design decision with its alternatives and trade-offs, the main types, the tests, speed, and known weaknesses.