6. Backends#

Each backend is one crate, temper-be-LANGUAGE, with tmpl in and source files out. It writes code that reads as if a person wrote it: real loops, the source's names, and no needless temporaries. And the public API matches what Temper has always generated for that language, so library users can't tell the difference.

tmpl ProgramShared lowerings itsprofile needsNaming pass withnames.rs rulestranslate.rs with ops.rsand connected.rsOutput tree: tree.rsPrinterSource files, packagefiles
tmpl ProgramShared lowerings itsprofile needsNaming pass withnames.rs rulestranslate.rs with ops.rsand connected.rsOutput tree: tree.rsPrinterSource files, packagefiles
Diagram source
flowchart LR
  program["tmpl Program"] --> lower["Shared lowerings its profile needs"]
  lower --> naming["Naming pass with names.rs rules"]
  naming --> translate["translate.rs with ops.rs and connected.rs"]
  translate --> tree["Output tree: tree.rs"]
  tree --> printer["Printer"]
  printer --> files["Source files, package files"]

One backend's parts#

Every backend crate has much the same files:

File Its job
lib.rs Its Variant (profile, naming rules, and runtime version), its BACKEND description, and translate, which runs the pipeline and lays out the files, package files included
names.rs Naming rules: keywords, case (snake_case, PascalCase), and scoping
translate.rs tmpl to the language's output tree, statement by statement
ops.rs How each builtin operator is spelled
connected.rs How each of core's connected declarations maps to the language's runtime, temper-core
tree.rs The output tree and its printer, which adds parentheses only where precedence needs them, and lays out code in the language's usual style
runtime.rs The runtime's files, copied beside the output by tempest build
tools.rs Natively, how the language runs built libraries' tests, for tempest test -b, and a library's top level, for tempest run -b: finding its tools, their command lines, and reading their results

Which lowerings each backend runs, in what order, and how the seven differ: the pipeline and per-backend choices.

A crate's BACKEND, a Backend, is all that tools see of it: its name, its comment syntax, its runtime's files, its naming rules, its translate function, whether build writes it by default, and its tools.rs's Tools (None in wasm). temper-driver's registry keeps the one list of them, so adding a backend means one crate, one line there, and its name among the known ones (the command).

Each language's runtime is vendored from Temper in vendor/temper/, with a few fixes kept as patches in vendor/temper-patches/. So generated code calls the same runtime APIs as before (runtimes and their patches, in detail).

Worked example#

triple.temper becomes this Python module (golden), beside a pyproject.toml and the package's __init__.py:

from temper_core import LoggingConsole as _LoggingConsole, int_mul as _int_mul

__all__ = ()


def _triple(n: int, /) -> int:
    return _int_mul(n, 3)


_console: _LoggingConsole = _LoggingConsole(__name__)
_console.log(f'triple(14) = {_triple(14)}')

The same two lines of Temper, in each language:

Language n * 3 The log line
C++ core::Int::mul(n, 3) core::Console::log(console, "triple(14) = " + std::to_string(triple(14)));
C# n * 3 console.Log("triple(14) = " + Triple(14).ToString(CultureInfo.InvariantCulture));
Java n * 3 console.log("triple(14) = " + triple(14));
JavaScript Math.imul(n, 3) console.log(`triple(14) = ${triple(14)}`);
Lua temper.int32_mul(n, 3) print("triple(14) = " .. triple(14))
Python _int_mul(n, 3) _console.log(f'triple(14) = {_triple(14)}')
Rust n.wrapping_mul(3) println!("triple(14) = {}", triple(14));

Temper's Int is a 32-bit integer that wraps on overflow, so each language gets what gives that: Java's and C#'s * already do, Rust asks for wrapping_mul, JavaScript uses Math.imul, and C++, Lua, and Python call the runtime. Strings follow the runtime too: in C++ a String is temper::core::Str, shared, immutable text whose copies bump a count, with each literal made once (TEMPER_STR("…")), while + above makes a std::string that converts to it. Names follow each language's rules: triple is _triple in Python, where it's private to the module, and Triple in C#.

Statements inside expressions#

tmpl allows statements inside an expression: an orelse, or a block whose last value is the result. Rust has block expressions, so it writes them as they are. The others move the statements out, just before the statement that holds the expression, and keep earlier operands in temporaries so that everything still runs in order. The shared spill pass does the ordering part for C#, C++, and Lua; each backend's translator does the rest. C++ also keeps its own order of evaluation explicit, since C++ leaves the order of a call's arguments open (effects). Which translator hoists what: in detail.

Where in the code#

Language Description translate Naming rules Module translation
C++ BACKEND translate RULES module
C# BACKEND translate RULES module
Java BACKEND translate RULES module
JavaScript BACKEND translate RULES module
Lua BACKEND translate RULES module
Python BACKEND translate RULES module
Rust BACKEND translate RULES module

More: notes/design.md, section 9, and notes/api-compat.md on the public API each backend keeps.

In detail#

Backends, in detail: the decisions all seven share, with alternatives and trade-offs; each backend's pipeline, choices, and naming rules side by side; the tests; and known weaknesses.