Files
Aura/docs/design/contracts/c23-graph-compilation.md
T
claude 4ff85b94e5 audit: #331 cycle close — every authored intake gates the root name, cli_fixed_cost re-pinned
Architect drift review (drift_found) resolved fix-path on all three
items. [high] The sweep/walkforward/mc family builders and
validate_and_register_axes read authored envelope files and
put_blueprint'd them ungated — falsifying the C29 exemption's premise
that the store is populated through gated intakes; all four sites now
gate the root name before the family/axis build (placing the gate
after the build produced a misleading unknown-axis error instead of
the gate refusal — RED-verified), with an e2e pin on the synthetic
sweep route. The decision was gate-the-routes, not weaken-the-prose:
the exemption's premise is the invariant, not a wording choice.
[medium] C24 now states the gate rule as the class it is (the op
intake plus every CLI intake reading an authored envelope from a
file; store read-back exempt, C29). [low] C23 reconciles
names-as-debug-symbols with the render name's one operational role
(trace-directory key — never compilation, identity, or execution
semantics).

Ratify: aura-bench cli_fixed_cost baseline re-pinned
(run_line_fnv 6bb0d796f760d140 -> 9bdbc3acf7b2926a). The moved metric
was caused by the #328 tidy (4474814), which switched single-run
manifest defaults from wrapped to raw axis names after that cycle's
bench rerun had already passed; bbac29d and this cycle's HEAD produce
fingerprint-identical record lines (verified with both binaries on
one scratch project), so this cycle only inherits the stale pin. All
five surfaces fingerprint OK after the re-pin.

refs #331, #328
2026-07-25 04:55:12 +02:00

83 lines
5.6 KiB
Markdown

# C23 — Graph compilation and behaviour-preserving optimisation
**Guarantee.** The bootstrap (C19) is a **compilation**: it lowers a param-generic,
named **blueprint** (the authoring source — nodes, composites, strategy, harness;
C8/C9/C20) into a flat, type-erased **`FlatGraph`** — the frozen runnable instance
(C7) — **wired by raw index, not by name** (`Edge { from, to, slot, from_field }`).
Composites are **inlined** at this step (C9): the composite *boundary* dissolves
entirely (there is no composite in the flat graph). The blueprint's field /
input-role *names* are **non-load-bearing** — the wiring resolves by index, and names
survive at most as **informative debug symbols** (exactly as `FieldSpec.name` already
is, C8), kept for tracing / rendering (C9 graph-as-data) but carrying no run
semantics. (Reconciling with the root composite's own render name, #331: that
claim stands unweakened — the name never influences compilation, identity, or
execution semantics — but at *run* time, outside compilation, it keys the trace
directory (`traces/<name>/`); that operational role, not any load-bearing
weight inside the flat graph, is exactly why the authored-blueprint intakes
shape-gate it.) The flat graph is then the target of **behaviour-preserving optimisation**
— any transform that leaves every observable sink trace bit-identical (C1 is the
correctness invariant) — on two levels:
- **intra-graph** (within one graph): **common-subexpression elimination** — two
identical nodes (same type, same bound params, same input source) merge to one with
output fan-out, so fractal composition (C9) costs no redundant compute — and
**dead-node elimination** — a node on no path to a sink is dropped. Pure-consumer
sinks (C8, no output) are never CSE candidates, so their out-of-graph side effects
are preserved by construction.
- **across the sweep family** (over the family of flat graphs one blueprint yields
under a param sweep, C12): the sub-graph whose nodes carry **no swept param** and
whose inputs are **all sweep-invariant** (transitively from the sources — same data
window) is **loop-invariant** w.r.t. the sweep and is computed **once**; its output
is materialised as a recorded stream (C11) shared read-only across the sweep
instances (`Arc<[T]>`, C12). Only the parameter-dependent suffix re-runs per sweep
point — loop-invariant code motion over the sweep loop.
The compilation also carries one structural gate that **is** load-bearing:
**param-namespace injectivity**. `check_param_namespace_injective` runs over the
`param_space()` name projection before name resolution, reads the **boundary** name
projection — the by-name authoring address space — and rejects a duplicated path
(`CompileError::DuplicateParamPath`). This makes the authoring address space
injective **without** making names load-bearing in the flat graph: node names qualify
the param path at construction and are dropped at lowering (the flat graph stays
wired by raw index).
**Forbids.** Any "optimisation" that changes an observable trace (it breaks C1);
optimising over a representation that is not graph-as-data — an opaque trait-object
interior (the rejected nested-composite reading of C9) cannot be analysed or
rewritten across its boundary, so it forecloses both levels.
**Why.** Determinism (C1) is not merely an audit property; it is the **licence** for
these rewrites — a behaviour-preserving transform is only meaningful because "same
input → bit-identical run" makes "same result" decidable, and a sweep-invariant
sub-graph is reproducible enough to compute once and share. The flat graph
representation (C19) is the necessary condition: only a flat, inspectable
graph-as-data — with each node's declared param-ranges (C8) — lets the compiler
identify identical sub-expressions, dead nodes, and the sweep-invariant frontier.
This is precisely why a composite is an inlining (C9) and not a runtime sub-engine:
the flat graph is what makes the whole graph optimisable.
## Current state
The flat-graph **representation** — blueprint → inline / compile → flat instance — is
the load-bearing substrate, built **first**, and is realized: `Composite::compile`
emits the first-class `FlatGraph` (C19). The per-node param declarations the
sweep-level pass presupposes carry `name` + `kind` (C8); the swept *range* is still
pending (#32/C20), as is the sweep orchestration itself (C12/C21).
The **optimisation passes** — intra-graph CSE/DCE and sweep-invariant hoisting — are
**deferred, behaviour-preserving follow-on work**; no pass exists yet
(`Composite::compile` in `crates/aura-engine/src/blueprint.rs` adds none). Each is
gated by a "flat graph with pass ≡ flat graph without pass, bit-identical run" test
(C1). The one compilation-time structural check that is **live** today is the
param-namespace injectivity gate above — `check_param_namespace_injective`, defined in
`crates/aura-engine/src/blueprint.rs` and applied at that file's own compile entry
points (`compile`, `compile_with_params`, and the `bind` paths) as well as from
`construction.rs`'s `finish`, raising `CompileError::DuplicateParamPath`.
## See also
- [C1](c01-determinism.md) — the bit-identity correctness invariant that licences every rewrite.
- [C8](c08-node-contract.md) — `FieldSpec.name` as the non-load-bearing precedent and the per-node param declarations.
- [C9](c09-fractal-composition.md) — inlining vs runtime sub-engine.
- [C11](c11-sources-record-replay.md), [C12](c12-atomic-sim-unit.md) — recorded-stream sharing and the sweep family.
- [C19](c19-bootstrap.md) — the flat-graph representation this optimises.