A single run recorded into `runs/traces/<render-name>/`, so a second run of the same blueprint overwrote the first and could leave a stale tap file beside an index that no longer listed it. The directory is now `runs/traces/<render-name>-<id8>/`, where the id is a digest over the run's own manifest: two runs differing in any identity-bearing input land in two directories, two runs differing in nothing land in one. The digest hashes the manifest with its two provenance fields removed and its parameter vectors merged. - `commit` (the aura binary's build sha) and `project.commit` (the project repo's HEAD plus a `-dirty` marker, re-derived on every invocation) record who built or checked out the code, not what the run was. The latter would have minted a fresh directory for any uncommitted file anywhere in the project worktree — the authoring loop's normal state. - `params` and `defaults` are merged name-sorted before hashing: how a value was supplied is not what the run is, so a no-op `--override` no longer splits one run across two directories. The two vectors are disjoint by construction. `project.dylib_sha256` stays in. It is what the C13 hot-reload comparison varies — an unchanged blueprint against a rebuilt node crate — which the campaign leg structurally cannot express, since its axes vary params, instrument and window, never code. The manifest is now assembled before the tap bind rather than after the run. Every input it needs was already resolved at that point, and the one value both keys the directory and enters the report, so a handle can never name one identity while the record describes another. `aura chart <render-name>` no longer names a directory; it now lists the trace handles beginning with that name instead of refusing blankly. `TraceStoreError::NameTaken` stops prescribing `--trace`, retired in #319. C18, C22, C23 and C27 record the new identity, each superseded clause moved verbatim to its history sidecar; C23 gains its first. closes #311
5.7 KiB
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 prefixes the
trace directory (traces/<name>-<id8>/, #311 — the run's own identity digest
completes it); 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 — the bit-identity correctness invariant that licences every rewrite.
- C8 —
FieldSpec.nameas the non-load-bearing precedent and the per-node param declarations. - C9 — inlining vs runtime sub-engine.
- C11, C12 — recorded-stream sharing and the sweep family.
- C19 — the flat-graph representation this optimises.
History: c23-graph-compilation.history.md