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.

Settled library and itstypesto_tmpltmpl ProgramvalidateShared lowerings eachbackend picksNaming pass by thebackend's rulesThe backend's owntranslator
Settled library and itstypesto_tmpltmpl ProgramvalidateShared lowerings eachbackend picksNaming pass by thebackend's rulesThe backend's owntranslator
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)));
  }
}

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#

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.