HIR uses typed arena nodes. A node refers to children by compact IDs and to ordered child lists by ranges into list arenas. This keeps cloning and walking cheap while preserving the semantic order of arguments, statements, elements, fields, patterns, and match arms.
Expressions
Expression kinds fall into these semantic families:
| Family | Representative forms |
|---|---|
| Values | literals, symbols, tuples, lists, arrays, structs, empty maps and sets |
| Operations | binary, unary, field, tuple index, index, call |
| Functions | lambda and resolved function value |
| Control | block, if, match, unified loop, break, continue, return |
| Errors | propagation, catch, error return, error-union coercion |
| Concurrency | spawn and select |
| Conversion | explicit coercion and layout intrinsic |
| Compile time | type match |
| Recovery | typed error placeholder |
This list is semantic, not a promise that every source spelling maps one to
one. For example, a for and a while both lower to the unified loop shape;
an omitted else produces a synthetic unit branch. On the other hand,
postfix propagation remains a distinct Try node because MIR needs to make
its success and error control paths explicit.
Blocks and lists
An HIR block contains a HirStmtRange plus an optional tail expression.
Ranges are half-open contiguous slices in the corresponding arena. A range:
- preserves source order;
- must lie wholly inside its owning arena;
- must not be interpreted against another project’s or file’s arena;
- changes when local HIR is relocated into the merged project.
The same rule applies to call arguments and tuple, list, and array elements. Owning a range is not owning a Rust slice; passes must resolve it through the project that issued it.
Calls
A call node stores the callee expression and ordered argument range. Dispatch is recorded separately because several call shapes share the same source node.
DispatchResolution distinguishes:
- a direct concrete target;
- a trait-bound witness;
- a method selected from the receiver;
- an indirect function-value call.
When analysis knows the dispatch, the entry is keyed by the call expression’s ID. A consumer must use that decision rather than search for a method with the same name. Monomorphization may replace a generic direct target with a specialized symbol, but it preserves the fact that dispatch was already settled.
Patterns
Patterns have their own IDs and arena because a pattern is not merely an
expression returning bool. It can test structure, introduce bindings,
constrain a value, and participate in exhaustiveness. Match arms connect a
pattern with an optional guard and body.
Lowering may introduce primitive binding patterns for destructuring, but those
nodes retain a PatternBindingDesugar origin. Later tools can therefore
separate user-written patterns from generated binding work.
Coercions
An implicit source conversion becomes an explicit HIR coercion when its existence matters downstream. Examples include numeric representation changes, optional wrapping, union injection, result/error-channel shaping, and reference dereference.
The coercion records the target type and coercion kind. MIR lowers the specified conversion; it must not rediscover whether a conversion was legal. If analysis cannot establish one, it emits a diagnostic and an error recovery node instead of a plausible-looking cast.
Origins
Every HIR node has one Origin:
| Origin | Meaning |
|---|---|
Expr |
Directly produced from an AST expression |
Stmt |
Produced from an AST statement |
Pattern |
Produced from an AST pattern |
Decl |
Anchored to a declaration |
Synthetic |
Introduced by lowering, with a source range and reason |
Synthetic reasons form a closed taxonomy. Categories cover loop and control desugaring, default arguments, guard promotion, pattern bindings, generated dispatchers, specializations, member bodies, string templates, option/result wrapping, structural dispatch, lazy iteration rewrites, zero initialization, and error recovery.
The reason is part of provenance. A synthetic node is not “source unknown.” Its range identifies the user construct that required it, while its kind explains why no direct AST node exists.
Type match
TypeMatch is a compile-time branch over a type, usually a generic parameter.
Its arms remain in generic HIR so specialization can select using the concrete
substitution. Exactly one viable body proceeds; MIR and code generation must
never receive an unresolved type match.
This distinguishes type-directed compilation from runtime match. Runtime
matches lower to control flow and inspect a value; type matches disappear
before runtime IR exists.