Expressions
Every expression carries its resolved type in the ty field. This
eliminates the need for code generators to re-infer types.
Every variant also carries a span: IrSpan field. The listing below
leaves it out. See Source Spans.
Several expression variants also carry typed-id payloads
(ReferenceTarget, BindingId, FieldIdx, VariantIdx, MethodIdx,
FunctionId, DispatchKind). Lowering emits placeholder 0-valued ids
for BindingId, FieldIdx, VariantIdx and MethodIdx, and
Unresolved for ReferenceTarget. ResolveReferencesPass rewrites
them. The lowering sets FunctionId and DispatchKind itself.
Backends that consume integer-indexed code (wasm, JVM, native) should
run that pass; Pipeline::for_codegen holds it. Backends that re-walk
the module by name can skip it.
ReferenceTarget
Identifies what an IrExpr::Reference resolves to. Pre-resolve, every
reference carries Unresolved; ResolveReferencesPass rewrites it to
the matching variant. Backends dispatch on the variant directly without
re-walking module symbol tables. The original path is preserved
alongside for diagnostics.
#![allow(unused)] fn main() { pub enum ReferenceTarget { /// A standalone function (resolved against `IrModule::functions`). Function(FunctionId), /// A struct definition used as a value or type. Struct(StructId), /// An enum definition. Also the receiver of a static call on an /// enum, `Optional.from(value: 3)`. Enum(EnumId), /// A trait definition. Trait(TraitId), /// A module-scope `let` binding. ModuleLet(LetId), /// A function-local `let` binding (introduced by `IrBlockStatement::Let`). Local(BindingId), /// A function parameter (introduced by `IrFunctionParam`). Param(BindingId), /// A reference into another module that is not linked. The public /// entry points link each imported module, so their result has /// none of these. External { module_path: Vec<String>, name: String, kind: ImportedKind, }, /// The receiver of a static call on a type that is not a struct: a /// type parameter (`T` in `T.zero()`) or a primitive. The `ty` of /// the reference names the type, and the call reads no value of it. /// After `MonomorphisePass`, a receiver whose type became a struct /// is `Struct`. Type, /// Pre-`ResolveReferencesPass` placeholder; backends should never see it. Unresolved, } }
DispatchKind
How a method call should be dispatched.
#![allow(unused)] fn main() { pub enum DispatchKind { /// Direct call on a known concrete type: no runtime lookup needed. Static { impl_id: ImplId, }, /// Trait method call through a generic type parameter. There is no /// trait object: a trait cannot be the type of a value, so semantic /// analysis rejects one before lowering. `MonomorphisePass` /// rewrites every one of these to `Static` once the receiver is /// concrete, and reports any that survive. Virtual { trait_id: TraitId, method_name: String, /// The type arguments of the trait in the bound: `[I32]` for /// `<T: Container<I32>>`. Empty for a trait with no type /// parameters, and then left out of the JSON. trait_args: Vec<ResolvedType>, }, } }
One type can implement two instances of one generic trait, for
example Container<I32> and Container<String>. trait_args says
which instance a call means. MonomorphisePass points the dispatch at
the specialised trait of that instance, and then at its impl.
IrExpr
#![allow(unused)] fn main() { pub enum IrExpr { /// Literal value: string, number, boolean, nil Literal { value: Literal, ty: ResolvedType, }, /// Struct instantiation: `User(name: "Alice", age: 30)`. An optional /// field with no default that the call leaves out is in `fields` as /// a `nil` literal, as if the call wrote `field: nil`. StructInst { /// `None` for external structs: read `ty` instead. struct_id: Option<StructId>, /// Generic type args (e.g., `[String]` for `Box<String>`). type_args: Vec<ResolvedType>, /// Fields: `(name, field_idx, value)`. `field_idx` is the position /// in the target `IrStruct.fields`; lowering emits `FieldIdx(0)` /// and `ResolveReferencesPass` overwrites it. fields: Vec<(String, FieldIdx, IrExpr)>, ty: ResolvedType, }, /// Enum variant instantiation: `Status.active` or `.active` EnumInst { enum_id: Option<EnumId>, variant: String, /// Variant index in the target `IrEnum.variants`. variant_idx: VariantIdx, /// Associated data: `(name, field_idx, value)`. fields: Vec<(String, FieldIdx, IrExpr)>, ty: ResolvedType, }, /// Array literal: `[1, 2, 3]` Array { elements: Vec<IrExpr>, ty: ResolvedType, }, /// Tuple literal: `(x: 1, y: 2)` Tuple { fields: Vec<(String, IrExpr)>, ty: ResolvedType, }, /// Reference to a parameter, a loop variable, a module-level `let`, /// or a function-local binding. A path such as `user.name` keeps /// all its segments. Reference { /// Original source path (preserved for diagnostics). path: Vec<String>, /// Resolved target. `Unresolved` pre-`ResolveReferencesPass`. target: ReferenceTarget, ty: ResolvedType, }, /// `self.field` reference within an impl block. SelfFieldRef { field: String, /// Position in the impl's struct's `fields`. field_idx: FieldIdx, ty: ResolvedType, }, /// Field access on an arbitrary expression: `(a + b).len`. FieldAccess { object: Box<IrExpr>, field: String, field_idx: FieldIdx, ty: ResolvedType, }, /// Reference to a function-local `let` binding by name. The /// lowering also uses it for the closure of a `CallClosure` on a /// named binding. Module-scope `let`s use `Reference` with /// `ReferenceTarget::ModuleLet`. LetRef { name: String, /// Per-function-unique id, paired with the introducing /// `IrBlockStatement::Let::binding_id`. binding_id: BindingId, ty: ResolvedType, }, /// Binary operation: `a + b`, `x == y`, `p && q`. BinaryOp { left: Box<IrExpr>, op: BinaryOperator, right: Box<IrExpr>, ty: ResolvedType, }, /// Unary operation: `-x`, `!flag`. UnaryOp { op: UnaryOperator, operand: Box<IrExpr>, ty: ResolvedType, }, /// Conditional expression: `if cond { a } else { b }`. If { condition: Box<IrExpr>, then_branch: Box<IrExpr>, else_branch: Option<Box<IrExpr>>, ty: ResolvedType, }, /// For loop: `for item in items { body }`. For { var: String, var_ty: ResolvedType, /// Per-function-unique id for the loop variable, paired with /// `LetRef::binding_id` on references to `var` inside `body`. var_binding_id: BindingId, collection: Box<IrExpr>, body: Box<IrExpr>, /// `Seq<body_type>`: a lazy sequence. `.collect()` makes an array. ty: ResolvedType, }, /// Match expression: `match x { .a: ..., .b(v): ... }`. Match { scrutinee: Box<IrExpr>, arms: Vec<IrMatchArm>, ty: ResolvedType, }, /// Direct call to a top-level function: `sin(angle: x)` or /// `math::sin(angle: x)`. For closure values, see `CallClosure`. FunctionCall { /// Function path. For an imported function, the path holds /// the module path, for example `["geom", "double_x"]`. path: Vec<String>, /// Resolved target. The lowering sets it, and each pass keeps /// it in step with `path`. `None` when no function in the /// module has the path. Backends key on this id to dispatch /// directly without re-walking `IrModule.functions`. function_id: Option<FunctionId>, /// The type arguments that the call writes, in the order of /// the callee's type parameters: `I32` in `make<I32>()`. Empty /// when the call writes none. `MonomorphisePass` reads a type /// argument here when no argument gives it, and empties the /// list when it points the call at the specialised copy. The /// JSON form leaves an empty list out. type_args: Vec<ResolvedType>, /// `(optional_parameter_name, value)`. args: Vec<(Option<String>, IrExpr)>, ty: ResolvedType, }, /// Indirect call of a closure value. The lowering gives it for: /// - `f(x)`, where `f` is a closure-typed parameter, local binding /// or module-level `let` (the closure is a `LetRef`); /// - the AST node `Expr::Call`: a call of the value of any other /// expression, such as `make()(4)` (the closure is that /// expression, here a `FunctionCall`). CallClosure { /// Expression producing the closure value. closure: Box<IrExpr>, /// `(optional_label, value)`, as in `FunctionCall::args`. args: Vec<(Option<String>, IrExpr)>, /// `return_ty` from the closure type. ty: ResolvedType, }, /// Method call: `self.fill.sample(coords)`. /// A static method call, `Counter.zero()`, has a `Reference` to /// the struct as its receiver: its `target` is /// `ReferenceTarget::Struct` and its `ty` is the struct. A static /// method has no `self`, so a backend does not evaluate that /// receiver. MethodCall { receiver: Box<IrExpr>, method: String, /// Method position: index into the impl's `functions` for `Static`, /// or into the trait's `methods` for `Virtual`. method_idx: MethodIdx, args: Vec<(Option<String>, IrExpr)>, dispatch: DispatchKind, ty: ResolvedType, }, /// Closure expression: `(x: F32, y: F32) -> x + y`. /// /// Convention on each parameter constrains the **caller** of the /// closure (`Mut` requires a mutable argument; `Sink` moves it). /// /// `captures` lists every free variable referenced by the body that's /// bound in an enclosing scope. Each capture entry is /// `(outer_binding_id, name, capture_mode, resolved_type)`. The mode /// mirrors the outer binding's `ParamConvention` (or `Let` for plain /// immutable captures). A closure captures by value: it holds a copy /// of each value when it is made, and a later assignment to the /// binding does not change the copy. A returned closure can capture /// a local, so a capture never refers to the frame of its function. /// A `Sink` capture moves the value. Capture entries are deduplicated by name and ordered by /// the first reference encountered during the body walk. Both `params` /// and `captures` carry `BindingId`s assigned by `ResolveReferencesPass`. Closure { params: Vec<(ParamConvention, BindingId, String, ResolvedType)>, captures: Vec<(BindingId, String, ParamConvention, ResolvedType)>, body: Box<IrExpr>, /// `ResolvedType::Closure { param_tys, return_ty }`. ty: ResolvedType, }, /// Reference to a lifted closure: a top-level function paired with a /// runtime environment value carrying its captures. /// /// Produced by `ClosureConversionPass`. After that pass runs, every /// `IrExpr::Closure` has been replaced by a `ClosureRef` whose /// `funcref` names the lifted top-level function (its first parameter /// is the env struct, followed by the original closure parameters) /// and whose `env_struct` is an expression constructing the /// corresponding capture-environment `IrStruct`. Backends can render /// this as a function-pointer / environment pair (e.g. `funcref` + /// `call_indirect` in WebAssembly). ClosureRef { funcref: Vec<String>, env_struct: Box<IrExpr>, ty: ResolvedType, }, /// Dictionary literal: `["key": value, ...]`. DictLiteral { entries: Vec<(IrExpr, IrExpr)>, ty: ResolvedType, }, /// Dictionary access: `dict["key"]`. An array index `a[i]` is a /// `DictAccess` too. A string index `s[i]` lowers to the method /// call `s.byte_at(i)`. DictAccess { dict: Box<IrExpr>, key: Box<IrExpr>, ty: ResolvedType, }, /// Block expression: `{ ... }` with statements and a result. Block { statements: Vec<IrBlockStatement>, result: Box<IrExpr>, ty: ResolvedType, }, } }
IrMatchArm and IrBlockStatement: referenced from Match and Block
above: are defined on the Match Arms & Block Statements page.
Type Contract
The ty field is guaranteed correct after lowering:
| Expression | Type |
|---|---|
Literal { value: Number(_), .. } | Primitive(I32 / I64 / F32 / F64): the suffix, else the type that the context expects, else I32 for an integer and F64 for a fraction |
Literal { value: String(_), .. } | Primitive(String) |
Literal { value: Boolean(_), .. } | Primitive(Boolean) |
BinaryOp { op: Add/Sub/Mul/Div/Mod, .. } | Same as operands |
BinaryOp { op: Eq/Ne/Lt/Gt/Le/Ge, .. } | Primitive(Boolean) |
BinaryOp { op: And/Or, .. } | Primitive(Boolean) |
UnaryOp { op: Neg, .. } | Same as operand |
UnaryOp { op: Not, .. } | Primitive(Boolean) |
For { body, .. } | Generic { base: Struct(seq_id), args: [body.ty()] } |
If { then_branch, .. } | Same as branches |
Match { arms, .. } | Same as arm bodies |
Getting Expression Type
#![allow(unused)] fn main() { impl IrExpr { /// Get the resolved type of this expression pub fn ty(&self) -> &ResolvedType; } // Example let expr: &IrExpr = /* ... */; let ty = expr.ty(); match ty { ResolvedType::Primitive(PrimitiveType::String) => { // Generate string handling code } ResolvedType::Generic { .. } => { if let Some(inner) = module.array_element_ty(ty) { // Generate array handling code } } // ... } }