Skip to main content

Module ir

Module ir 

Source
Expand description

The intermediate representation code generation reads.

The typer produces it. It is the only phase that knows a node’s type, and every node here carries one (DEC-18 decision 1). Term is the untyped half — what the translation from the canonical AST builds — and TypedTerm is the same tree once inference has solved a type for each of its nodes. A Module holds the unions a module declares and one Declaration per value: everything about a module that only emission asks for.

It is not the whole of what a backend is handed. check_module answers with a CheckedModule, which is this beside the canonical::Module it was built from, and two of the things emission needs are still only on that half — a module’s exports, which is what a JavaScript module has to export, and canonical::Value::TypedValue’s marked_unsafe, which zelkova_js::emit reads because an unsafe signature and an effectful one emit differently. Nothing here duplicates them.

The type language itself is still typer::Type. It is the typer’s own representation and unification is written against it, so it stays there; this module names it and adds nothing to it.

§What this shape owes WebAssembly

One IR serves both targets and JavaScript is written first (DEC-18 decision 2), so a reader arriving while only the JavaScript backend exists will find things JavaScript has no use for. None of them is spare:

  • A type on every node, and what a constrained one asks of it. JavaScript needs almost none of them: the canonical AST already separates an Int literal from a Float one, and arithmetic and equality are ordinary functions behind facades. WebAssembly is statically typed, and a node’s representation class — an i64, an f64, a reference — is read off its type. Solved types are also what monomorphisation consumes, which is the only way polymorphism reaches a target where a class dictionary is erased by specialisation and never passed. A specialiser needs the other half of a constrained name too, and there are three places it is written. A reference to a name whose type has a context carries that context as instantiated at the use, one Predicate per constraint, with the final substitution applied like every other type on the node: eq used at Int carries Eq Int. A Declaration carries its own context, read off its solved type the same way. And a Module carries its Instances, each with its class, head, context and one checked body per member. A hole, a name that did not resolve, has a type too: the one inference solved for its position, which is an unsolved variable when nothing around it constrains it.
  • A constructor’s index within its declaration, and not only its name. A union is a WIT variant with one case per constructor and a tuple is a tuple (A union crosses as a tagged value), so a constructor is reached by its position there. JavaScript writes the name into the $ field and never asks for the index.
  • Arity as a fact and saturation per call site. A declaration emits as a plain n-ary function on both targets (DEC-18 decision 3), and a facade’s plain parameter list is the same call shape either way. WebAssembly has no closure primitive, so how a partial application is represented there is open — and an IR that made a call site’s saturation something to re-derive would make that question harder rather than leaving it open.
  • A record’s fields by label, and the order a positional target reads them in. A record crosses as a WIT record, which reaches a field by position (Which types may cross the boundary), and a record type is a set of fields with no order of its own (a record type is a set of fields), so nothing in the IR numbers them: TypedTermKind::Access, TypedTermKind::Accessor, TypedTermKind::Update and a record pattern’s entries (Step::Field) name a field by its label, and JavaScript reads it as the property of that name. A positional target reads the order off the type, which is on the access node already — the Type::Record of the record operand, of the accessor’s parameter, of the update’s own type, of the value a pattern is matched against. That order is label order: the labels sorted by their characters, each compared by its code point, a label that another begins with first. It is the order a Type::Record’s map iterates in, and the one two spellings of one type agree on — { a : Int, b : Int } and { b : Int, a : Int } are one type and have one layout — which is why Records and derivation walks a record’s fields in it too: sorting is the only order available that does. The order a record expression’s fields were written in is not a position. It is on the term (TypedTermKind::Record, TypedTermKind::Update) as an order of evaluation, and a target evaluates in it and lays the value out in label order.

GEN-15 holds the questions a WebAssembly backend still has to answer — linear memory or WasmGC, how a partial application is represented, whether monomorphisation is whole-program — and is unscheduled. Nothing in this module is a JavaScript decision, and a change that makes one of the facts above unavailable is a change that closes that ticket’s options.

§What specialisation adds

build knows one module and leaves each reference that carried obligations as the typer solved it. specialise reads every module of a build, resolves each of them to the instance’s member or to a Specialisation of the module that uses it, and changes the modules in place: a ReferenceKind::InstanceMember or a ReferenceKind::Specialised replaces the name and its obligations, and Module::specialisations holds the copies. A constrained Declaration and the members of an instance that has a context stay as they were written: they are what is copied, and nothing emits them as they stand. initialisation_items is the order of the parameterless items once those are among them. The module doc comment of specialise is the account, and GEN-15 needs the same pass for a reason of its own.

§What is not here yet

One ticket adds to this shape and is deliberately not written into it yet: a self tail call (GEN-6). Module holds its declarations in a Vec sorted by name, which is a deterministic order and not an evaluation order; Module::initialisation_order is the evaluation order, over the parameterless declarations alone, and initialisation_items extends it to the items specialisation adds; zelkova_js::emit emits them in it.

A case’s branches keep TypedTermKind::Case’s own flat shape rather than growing a tree in place: the typer still wants a flat list, since every branch is checked against the same scrutinee type and order does not matter to it. The decision module is a second, additional view of the same branches — built from them on demand rather than stored alongside them — and Decision is the tree a backend walks instead of re-deriving, at emission time, which test distinguishes which branch (GEN-5). See that module’s doc comment.

Structs§

Binding
One name a Decision::Leaf binds: the value at occurrence, of type tpe.
Body
A declaration’s parameters and the expression they are in scope over.
Constructor
A constructor, and its place in the declaration that declares it.
Declaration
One value of a module: what a backend emits as a named binding.
Field
One field of a record or an update — TermKind::Record, TermKind::Update and their typed counterparts — over the term type T of the tree it is in; and, as a Field<SubPattern>, one entry of a record pattern.
Instance
One instance declaration of a module: which class, which type, what it needs of that type’s arguments, and a checked body for each member.
InstanceMember
A member of an instance that has no context, which is an ordinary declaration of the module that declares the instance, emitted once.
Module
One checked module’s emittable shape: what a backend reads that no earlier phase carried.
ModuleErrors
The errors one module of the build has, by the module the code that raised them was written in: the module whose file the labels point into.
Predicate
A class required of a type: Eq Int, Comparable a.
Reference
A reference to a name, and what kind of name it is.
SolvedInstance
What the typer has to say about one instance declaration: the type it is for, what it needs, and an answer for each binding of its body.
Specialisation
A constrained declaration, or a member of an instance with a context, at one assignment of ground types to its constrained variables: an ordinary declaration of the module that uses it.
SubPattern
A pattern written in a position inside another one — a constructor’s argument, a tuple’s element or a record pattern’s entry — and the type of the value found there.
Term
An untyped term, and where the expression it was translated from was written.
TermPattern
Simplified pattern used inside a Term, and where it was written.
TypeBinder
Bind a name and a type together. Used in function and let expression
TypedTerm
Like a Term but with an associated Type, and still with its position. Any term introducing a name will have a TypeBinder instead.
Unchecked
A declaration the typer did not type, and which therefore has no IR.
Union
A union declaration: the type a constructor builds, and the constructors that build it.
Variant
One constructor of a Union.

Enums§

CaseForm
What the source wrote that a Case term was built from.
Decision
The tree a case’s branches lower to: what test distinguishes which branch, in what order they are tried, and the names each leaf binds.
Item
One thing a module emits under a name of its own: where it is, in the module.
LiteralValue
The concrete value a TermPatternKind::Literal pattern tests for.
Occurrence
A path to one value reached from a case’s scrutinee: the scrutinee itself, or one step inside another occurrence. A pattern nested n deep is reached by a path of n Steps.
Outcome
The value a Decision::Test checks for.
ReferenceKind
Which of the things a Reference names: a name bound in the declaration, a declaration of this module, one of another, or a constructor — and, once specialise has run, a specialisation or an instance’s member, which are what a name that asked for an instance becomes.
Saturation
Whether an application supplies every argument its callee takes.
Solved
What the typer has to say about one declaration.
SpecialiseError
Why the specialisations of a build could not be found.
Step
One step of an Occurrence: which sub-value of a constructor, a tuple or a record. A constructor’s argument and a tuple’s element are counted from zero the way Constructor::index counts a case; a record’s field is named by its label.
Subject
What a Specialisation is a copy of.
TermKind
TermPatternKind
TypedTermKind

Constants§

SPECIALISATION_LIMIT
How many times one declaration may appear in one chain of specialisations before the chain is reported as having no end.

Functions§

build
Turn a checked module and what the typer solved for it into the IR a backend reads.
decision_tree
Lower one case’s branches, in source order, into the Decision a backend walks.
initialisation_items
The items of module that take no parameter, in the order they must be initialised: each only after every parameterless item it depends on, directly or through a function it mentions.
mentioned_by_copies
The values of module that a copy of its code, placed in another module, names: the declarations its constrained declarations and the bindings of its instances mention.
specialise
Find every specialisation modules need and resolve every reference that carried obligations, in place.