Files
libvct-spec/spec/004-hir-optimizer.md

254 lines
11 KiB
Markdown

# 004 — HIR Optimizer
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are 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](002-traits.md)), the node model and builder
contract (see [003 — HIR](003-hir.md)), and the HIR-to-VIR translation (see
[005 — Lowering](005-lowering.md)).
## 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:
```d
interface HirPass {
string name();
void run(HirFunction fn, HirContext ctx);
}
```
Normative requirements:
1. A pass **MUST** mutate the annotated AST **in place**. It **MUST NOT** rebuild the tree or
return a replacement.
2. A pass **MUST** be deterministic: for a fixed input and epoch, it produces the same mutations in
the same order regardless of thread count.
3. A pass **MUST** emit a **trigger** for every successful rewrite (see the triggers section).
4. 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.
5. 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
1. 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.
2. Order of processing within one function **MUST** be deterministic, so cascade order is
reproducible.
3. 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.
4. 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
1. HIR passes **MUST** parallelize **per function**. Independent function trees may be processed
concurrently.
2. Within one function, passes **MUST** run **sequentially**, so cascade order is deterministic.
3. 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.
4. The module phase **MUST** be deterministic: it processes functions in a stable order and folds
their results in a stable order.
5. The determinism guarantee of [000 — Overview](000-overview.md) binds the HIR optimizer: for a
fixed `Config` and input, output **MUST** be byte-identical independent of thread count and of
`-j`.
## 8. Epoch-based staleness
1. Each node carries an epoch counter. A trait is valid only for the node's **current epoch**.
2. A rewrite that changes a trait **MUST** advance the affected node's epoch.
3. A rewrite that can affect a related node **MUST** mark the relationship dirty and propagate
invalidation along the relationship edge.
4. A pass in a parallel worker **MUST NOT** consume a trait that became stale after the worker read
it. Epoch checks **MUST** detect this.
5. 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:
1. It has **no side effects**.
2. It involves **primitives only** (no aggregate or opaque operations that cannot be evaluated
element-wise).
3. It performs **no global mutation**.
4. It performs **no I/O**.
5. 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:
1. `const_value`: the computed value.
2. `is_comptime` and `is_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](002-traits.md): warning, overwrite, mark derived.
## 10. `defer` expansion
1. When HIR optimization is enabled, the optimizer **MUST** expand every `defer` body at every
scope-exit path and erase the `defer` marker. This leaves zero runtime residue.
2. Expansion **MUST** obey the ordering and determinism rules of [003 — HIR](003-hir.md).
3. When HIR optimization is disabled (`-O0`), the optimizer does not expand `defer`; lowering
performs the same expansion. Either way, no `Defer` node 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](002-traits.md)). Inlining is a VIR/IPA transform (see
[007 — VIR Optimizer](007-vir-optimizer.md)). 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 `Defer` node survives HIR optimization when HIR optimization is enabled.
- Output is deterministic for a fixed `Config` and input, independent of thread count.
## 15. Example
For `y = x + 4` with `x = 4`:
1. The comptime evaluator folds `x` to `const_value = 4`, marking `is_comptime` and `is_constant`.
2. `ConstantUnfolded` triggers the fold of `y` to `8`.
3. `foo(x, y)` is proven small; HIR issues a soft `Inline` request instead of inlining it.
4. The `sqrt` call folds to a floating-point constant.
5. The worklist drains; the module reaches a fixpoint with `println` left as the only real call.
The normative end-to-end walkthrough is [014 — Worked Example](014-worked-example.md).
## 16. Cross-references
- [000 — Overview](000-overview.md)
- [001 — Architecture](001-architecture.md)
- [002 — Traits](002-traits.md)
- [003 — HIR](003-hir.md)
- [005 — Lowering](005-lowering.md)
- [007 — VIR Optimizer](007-vir-optimizer.md)
- [011 — Diagnostics](011-diagnostics.md)
- [013 — Build & Packaging](013-build-packaging.md)
- [014 — Worked Example](014-worked-example.md)