11 KiB
004 — HIR Optimizer
- Status: Draft
- Normative language:
MUST,MUST NOT,SHOULD,SHOULD NOT, andMAYare to be interpreted as described in RFC 2119.
1. Purpose
The HIR optimizer is the discovery and small-fold stage. It validates frontend-suggested traits,
derives attributes and relationships, evaluates compile-time expressions, runs cheap folds to a
fixpoint, and expands defer. Its output is annotated HIR whose traits are complete and
epoch-valid, and which hands VIR a "dictionary" of facts.
The HIR optimizer MUST NOT perform heavy transformations. In particular, it MUST NOT inline code (see the "HIR never inlines" requirement below).
2. Scope
In scope: the HIR pass framework, triggers, the worklist scheduler and cascade-to-fixpoint, per-
function parallelism and the module-level fixpoint, epoch-based staleness, the comptime evaluator,
defer expansion, optimization-level behavior, and default budgets.
Out of scope: the trait vocabulary (see 002 — Traits), the node model and builder contract (see 003 — HIR), and the HIR-to-VIR translation (see 005 — Lowering).
3. Definitions
| Term | Definition |
|---|---|
| Pass | A unit of discovery or folding over one function's HIR. |
| Trigger | A record that a rewrite succeeded and may enable further work. |
| Worklist | The deduplicated queue of (pass, node) work items. |
| Cascade | The chain of rewrites triggered by one fold. |
| Fixpoint | The state in which the worklist drains with no new work. |
| Epoch | The per-node generation counter that invalidates stale traits. |
| Step budget | The bounded amount of work allowed before the scheduler stops. |
4. Pass framework
A pass is a named unit with the following shape:
interface HirPass {
string name();
void run(HirFunction fn, HirContext ctx);
}
Normative requirements:
- A pass MUST mutate the annotated AST in place. It MUST NOT rebuild the tree or return a replacement.
- A pass MUST be deterministic: for a fixed input and epoch, it produces the same mutations in the same order regardless of thread count.
- A pass MUST emit a trigger for every successful rewrite (see the triggers section).
- A pass MUST respect the step budgets in the budgets section. On exhaustion it MUST stop and leave the tree in a consistent, verifiable state.
- A pass MUST NOT read a trait whose epoch is not the node's current epoch (see the epoch-based staleness section).
5. Triggers
Successful rewrites emit triggers, which the scheduler uses to enqueue dependent work:
| Trigger | Meaning |
|---|---|
ConstantUnfolded |
A node's value became compile-time known. |
UsesReplaced |
Uses of a node were replaced, potentially making the node or its users constant or dead. |
NodeDeleted |
A node was removed; its former users may now be foldable. |
TraitChanged |
A trait changed and dependent traits may be stale. |
StaticDiscovered |
A frontend is_static claim was confirmed or a new compile-time value was found. |
A rewrite that changes no fact MUST NOT emit a trigger. Emitting a trigger for a no-op risks a non-terminating cascade and is a correctness defect.
6. Worklist scheduler and cascade
6.1 Scheduler
- The scheduler collects
(pass, node)pairs into a worklist, deduplicates them, and runs until the worklist drains to a fixpoint: a state in which no scheduled work produces a new trigger. - Order of processing within one function MUST be deterministic, so cascade order is reproducible.
- A per-function step budget guarantees termination even if triggers keep firing. When the budget is exhausted the scheduler MUST stop scheduling further passes for that function and report the exhaustion.
- The scheduler MUST NOT schedule work for a node whose governing function is not yet ready in the current phase.
6.2 Canonical cascade
The reference cascade for a fold:
4 * 16
-> const 64
-> mark is_comptime / is_constant / const_value = 64
-> prove not addressed / not runtime-mutable
-> replace all uses with literal 64
-> node dead -> delete
-> downstream nodes now constant -> enqueue
Each hop emits a trigger. The worklist drains until nothing new is provable. The cascade is the mechanism by which one fold can make a whole subtree constant; it MUST reach the same fixpoint regardless of the order in which independent items are processed.
7. Parallelism and determinism
- HIR passes MUST parallelize per function. Independent function trees may be processed concurrently.
- Within one function, passes MUST run sequentially, so cascade order is deterministic.
- Cross-function effects, such as cross-module constant propagation and requests that depend on
callees, MUST use a module-level fixpoint:
- run per-function passes in parallel;
- run a deterministic module phase;
- re-enqueue only the functions whose inputs changed.
- The module phase MUST be deterministic: it processes functions in a stable order and folds their results in a stable order.
- The determinism guarantee of 000 — Overview binds the HIR optimizer: for a
fixed
Configand input, output MUST be byte-identical independent of thread count and of-j.
8. Epoch-based staleness
- Each node carries an epoch counter. A trait is valid only for the node's current epoch.
- A rewrite that changes a trait MUST advance the affected node's epoch.
- A rewrite that can affect a related node MUST mark the relationship dirty and propagate invalidation along the relationship edge.
- A pass in a parallel worker MUST NOT consume a trait that became stale after the worker read it. Epoch checks MUST detect this.
- A stale derived trait MUST be recomputed before it is consumed. Recomputing it MUST be part of the cascade, not a silent fallback to a default.
This model is what makes parallel HIR passes safe without a global lock.
9. Comptime evaluator
The comptime evaluator is an interpreter over HIR subgraphs. It computes compile-time values and marks them on the nodes.
9.1 Eligibility
An HIR subgraph is eligible for comptime evaluation only if all of the following hold:
- It has no side effects.
- It involves primitives only (no aggregate or opaque operations that cannot be evaluated element-wise).
- It performs no global mutation.
- It performs no I/O.
- It stays within the loop and recursion budgets (see the budgets section below).
If any condition fails, the evaluator MUST bail out cleanly and defer the computation to VIR. A bailout MUST NOT mark any partial result as compile-time known.
9.2 Outputs
On successful evaluation, the evaluator writes, in order:
const_value: the computed value.is_comptimeandis_constant: the proven properties.
const_value MUST be present if and only if is_comptime holds.
9.3 Seeding
The frontend is_static attribute (the frontend-facing static concept) seeds the evaluator:
HIR treats is_static as the claim "this is compile-time" and MUST validate the claim before
setting is_comptime. A false is_static claim is handled by the attribute validation rule of
002 — Traits: warning, overwrite, mark derived.
10. defer expansion
- When HIR optimization is enabled, the optimizer MUST expand every
deferbody at every scope-exit path and erase thedefermarker. This leaves zero runtime residue. - Expansion MUST obey the ordering and determinism rules of 003 — HIR.
- When HIR optimization is disabled (
-O0), the optimizer does not expanddefer; lowering performs the same expansion. Either way, noDefernode reaches VIR.
11. Optimization-level behavior
| Level | HIR behavior | VIR |
|---|---|---|
-O0 |
Traits validated only. | off |
-O1 |
Builtin folds and discovery. | off |
-O2 |
Full cascade plus comptime evaluation. | on |
-O3 |
-O2 plus special AST transforms (loop-unfold/vectorize, then constant-unfold, then comptime). |
on |
-Ofast |
-O3 plus -march=native and fast-math. |
on |
-Oz |
Size-tuned discovery and folds. | on |
Compiler-friendly C is emitted at all optimization levels. -O3 only adds more aggressive AST
transforms; it does not change the trait contract.
12. HIR never inlines
HIR MUST NOT inline code. It proves that a call is small and eligible, then issues an Inline
request (see 002 — Traits). Inlining is a VIR/IPA transform (see
007 — VIR Optimizer). This separation keeps HIR's work cheap and
parallelizable.
13. Default budgets
All budgets are configurable through the API and CLI. The defaults are:
| Budget | Default |
|---|---|
comptime.max_iterations |
4096 |
comptime.max_recursion_depth |
256 |
comptime.max_steps |
1000000 |
comptime.max_aggregate_elements |
65536 |
hir.max_rewrites_per_function |
100000 |
hir.max_rewrites_per_module |
1000000 |
A budget exhaustion is a normal, reported outcome; it MUST NOT be treated as a crash and MUST NOT produce partially annotated output that lowering would accept as complete.
14. Invariants
- The worklist drains to a fixpoint or stops at an explicit budget: the scheduler never loops forever.
- Every successful rewrite emits exactly the triggers needed to re-enqueue affected work.
- A no-op rewrite emits no trigger.
- Every pass validates the epochs of traits it reads.
- After HIR optimization, every trait consumed by lowering is complete and epoch-valid.
- HIR performs no inlining.
- No
Defernode survives HIR optimization when HIR optimization is enabled. - Output is deterministic for a fixed
Configand input, independent of thread count.
15. Example
For y = x + 4 with x = 4:
- The comptime evaluator folds
xtoconst_value = 4, markingis_comptimeandis_constant. ConstantUnfoldedtriggers the fold ofyto8.foo(x, y)is proven small; HIR issues a softInlinerequest instead of inlining it.- The
sqrtcall folds to a floating-point constant. - The worklist drains; the module reaches a fixpoint with
printlnleft as the only real call.
The normative end-to-end walkthrough is 014 — Worked Example.