5. tmpl: the backend IR#
Between the frontend and the seven backends sits one form: tmpl.
It's a settled, typed, fully explicit program. Every declaration has an
id, every expression a type, every call says exactly what it calls, and
every operator says which type's operator it is. A backend translates
tmpl without asking the frontend anything, which is why each backend
crate depends only on temper-tmpl.
Diagram source
flowchart LR
settled["Settled library and its types"] --> totmpl["to_tmpl"]
totmpl --> program["tmpl Program"]
program --> validate["validate"]
validate --> shared["Shared lowerings each backend picks"]
shared --> naming["Naming pass by the backend's rules"]
naming --> backend["The backend's own translator"]
What's in a program#
A Program is one library: its
modules, plus tables they share for nodes, types, declarations, and
strings. Nodes live in one arena, Nodes,
and every kind of node is declared once, in a table in
schema, from which the
Rust types are generated. tmpl also has a text form that reads back
exactly (write and
read), which the goldens
use, and a validate
that checks a program's rules, each problem with its code. Why
columns, a text form, and coded rules:
decisions, in detail.
Worked example#
run-temper --tmpl triple.temper prints the program as text:
tmpl 1 model 1 library triple 0.0.0
uses `temper-core`#3 0.1.0 {
interface Console#4 {
connected method log#5(message: String): Void;
}
class Int32#7 {
connected method toString#8(radix?: Int32): String;
}
connected fn getConsole#9(name?: String): Console#4;
}
module triple#0 {
let ~console#6: Console#4;
fn triple#1(n#2: Int32): Int32 {
return %Int32.mul(n#2, 3);
}
init {
init ~console#6 = getConsole#9(omitted);
~console#6.log#5(%String.cat("triple(14) = ", triple#1(14).toString#8(omitted)));
}
}
#1,#2: every declaration's id. Names are only for reading; the ids are what count.uses temper-core: only what this program uses of the runtime, with signatures.connectedmeans each backend supplies it.%Int32.mul: a builtin operator, chosen by typing. Each backend spells it its own way.~console: made by the compiler (~), asconsole.logneeds a console to log to.init: the module's top-level code, in the order it runs.omitted: an optional argument left out.
This is triple.tmpl,
the testkit's golden. The functional tests' tmpl goldens are in
tests/to_tmpl/.
From the frontend: to_tmpl#
to_tmpl walks the
settled tree, reads types and resolutions from typing, and builds the
program with a Builder. It
resolves nothing again. Core's members are copied from one fixture,
core.tmpl, so their signatures have one source. Anything it doesn't
cover yet is a NotYet
saying what and where, never a panic. An error node becomes tmpl's
Error, which fails where it runs; when the error is only about a
value's type, the Error keeps the value as written, for the
lowering below to settle. Classes, function values, and
defaults have files of their own: to_tmpl_classes.rs,
to_tmpl_functions.rs, and to_tmpl_defaults.rs.
to_tmpl, in detail.
Shared lowerings#
Some languages lack something tmpl has, such as generators, switch,
or statements inside expressions. The fix is the same whichever
language lacks it, so it lives in temper-tmpl. Each backend's
Profile says what it
handles natively, and its translate calls the lowerings it needs,
each returning a new program only if it changed something. tmpl never
lists backends.
| Lowering | What it does | Used by |
|---|---|---|
errors::settle |
Code with a type-only error runs as written where the language is dynamically typed, and fails there where it isn't | all seven, by profile |
coroutines::lower |
Generators and async as state machines, for languages without their own generators |
C++, Java, Rust |
copy_captured_params |
A captured, reassigned parameter becomes a local, which can then be a cell | C++, Java, Rust |
switches::to_if_chains |
A Switch as ifs |
C#, JS, Lua, Python, and Java for Int64 |
spill and spill_with |
Keeps evaluation order when a backend moves statements out of an expression | C#, C++, Lua |
naming::assign |
Pretty, collision-free names by each language's NamingRules |
all seven |
Why profiles and hand-picked lowerings, rather than one fixed pipeline: in detail.
The naming pass is why there are no x__123 names in the output.
triple#1 becomes _triple in Python (module-private, so it starts
with _), Triple in C#, and triple everywhere else. A number is
added only when two names would really clash.
To see a lowering's effect, compare
control-flow-async.tmpl
with its state-machine form.
Backends also share analyses (analysis):
what a module imports, what a closure captures, what has effects. Plus
Host for a library's own
host code, and source_map
for source maps.
Where in the code#
Program,Module, andUses.schema!: every node kind.Builderandvalidate.Profile,Features, andCoroutineStrategy: a backend's choices.Backend: what tools need of a backend, which each backend crate exports asBACKEND(backends).to_tmpl,translate_with(modules, then classes, then members, then bodies),stmt_or_gap, andexpr.- Tests:
crates/temper-tmpl/tests/(round trips, naming, spill) andcrates/temper-frontend/tests/to_tmpl.rs(what each pins).
More: notes/tmpl/, one file per topic.
In detail#
tmpl, in detail: each design decision with its
alternatives and trade-offs, to_tmpl's rounds, the shared lowerings,
the main types, the tests, and known weaknesses.