3. Typing#

Once the macros are done, every node is a core construct, and tempest works out types. It does that in the same settle loop, as a second round, and then in a few passes over the settled tree. This page is about where that happens; the rules themselves are in notes/typing.md.

Expansion settledFlow facts: null and ischecksTyping round: one itemper inferred letCheck: report typeerrorsRecord: everyexpression's type andwhat each call callsTyping table: read by theinterpreter and to_tmpl
Expansion settledFlow facts: null and ischecksTyping round: one itemper inferred letCheck: report typeerrorsRecord: everyexpression's type andwhat each call callsTyping table: read by theinterpreter and to_tmpl
Diagram source
flowchart LR
  expanded["Expansion settled"] --> flow["Flow facts: null and is checks"]
  flow --> infer["Typing round: one item per inferred let"]
  infer --> check["Check: report type errors"]
  check --> record["Record: every expression's type and what each call calls"]
  record --> typing["Typing table: read by the interpreter and to_tmpl"]

The steps#

Types are interned: each distinct type is stored once and named by a TypeId, so comparing types is comparing two numbers. An unknown type never causes an error. Whatever typing can't work out simply goes unchecked, rather than producing a cascade of complaints. The trade-offs are in one interned type representation and unknown is never an error; how inference and the loop fit together, in types inside the settle loop.

Worked example#

For triple.temper, the parameter n and the result are declared Int, which is Int32. So n * 3 is Int32 * Int32, and typing resolves * to Int32's multiplication. That choice is what tmpl later writes as %Int32.mul(n#2, 3). In the string template, triple(14) is an Int32 too, so it's converted with Int32's toString.

Now a mistake:

let triple(n: Int): Int { n * 3 }
let s: String = triple(14);
console.log(s);

run-temper --tree prints the same kind of settled tree as before, then the error, with its byte span:

error 50..60: Cannot assign to String from Int32

The error is a value in the library's diagnostics, and the tree still has every node. What happens next depends on where the code runs. The value is only of the wrong type, so the interpreter runs it as written and logs 42, as JavaScript, Lua, and Python do. The statically typed backends, whose compilers would reject it, fail there instead, with the message. Either way, the rest of the library still builds and runs (tmpl's errors). Why messages follow Kotlin Temper's: checks after inference.

Where typing lives#

Typing has one table, Typing, and its steps are methods on the settle loop's state, split by topic into typing/. The types themselves are TypeData values in Types.

File Topic
signatures.rs, core_members.rs Builtin operators' and core types' members' signatures
members.rs, overrides.rs, overloads.rs, fields_first.rs Classes, their members, overriding, and overloads
connected.rs Checks of a user's @connected classes
generic_calls.rs Generic calls, solved per call site
targets.rs, object_literals.rs, fun_interfaces.rs The type a place wants: list literals, object literals, and @fun interfaces
initializers.rs Locals typed from their first assignments
bubbles.rs What may fail where it can't, and constant folding for divisions
sealed.rs, rtti.rs Sealed types and when, and run-time type checks
exprs.rs, resolve.rs Recording every expression's type and every call's target

Some of these change which programs compile, on purpose; their status is in language changes typing makes.

Where in the code#

More: notes/typing.md has the rules in full, and notes/language.md the language decisions they follow.

In detail: Typing in detail: each decision with its alternatives and Kotlin Temper's way, the main types, the tests, and known weaknesses.