From 69d20949c0b7f0307e171084a0a03b6e75dd4453 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 8 Jun 2026 16:38:09 +0200 Subject: [PATCH] tidy(aura-engine): single-own alias-validity + shared alias-count anchor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the two correctness-neutral debt items the 0021 fan-in pre-pass left behind (architect drift review at cycle close). Alias-validity: the `BadInteriorIndex` check was raised in two places — the structural pre-pass `check_composite_fan_in` and `inline_composite`. Since `compile_with_params` runs the pre-pass over every composite before any lowering, `inline_composite`'s copy was dead for that error path. The check now lives in one helper `check_alias_indices`, called solely by the pre-pass; `inline_composite` drops its copy (and the now-unused `param_aliases` binding — the overlay is a pure naming layer, unused in lowering, which is driven by the injected scalar vector). Alias-count: the per-node `a.node == node` predicate was re-derived in three spots (signature base, the unaliased-param test, the CLI render base-length). All three now route through one exported anchor `aliases_on`, so an alias-semantics change touches one site. Behaviour-preserving: the full workspace suite is the guard (15+6 cli, 25 core, 69 engine, 6+1+1 ingest/misc, 29 std — 0 failed), `out_of_range_param_alias_rejected` still green (now raised by the pre-pass), clippy -D warnings clean. No ledger change (the C9 refinement landed in 0021). closes #45 --- crates/aura-cli/src/graph.rs | 4 +- crates/aura-engine/src/blueprint.rs | 62 +++++++++++++++++------------ crates/aura-engine/src/lib.rs | 3 +- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/crates/aura-cli/src/graph.rs b/crates/aura-cli/src/graph.rs index 92ad68c..7d35421 100644 --- a/crates/aura-cli/src/graph.rs +++ b/crates/aura-cli/src/graph.rs @@ -16,7 +16,7 @@ use ascii_dag::graph::{Graph, RenderMode}; use ascii_dag::render::colors::Palette; use aura_core::{LeafFactory, Node, ScalarKind}; -use aura_engine::{signature_of, Blueprint, BlueprintNode, Composite, Edge, SourceSpec}; +use aura_engine::{aliases_on, signature_of, Blueprint, BlueprintNode, Composite, Edge, SourceSpec}; /// Edge colouring for a rendered graph. `Plain` is monochrome — golden-stable and /// pipe-safe (no escape codes when redirected to a file). `Ansi` emits per-edge @@ -252,7 +252,7 @@ fn slot_source(c: &Composite, index: usize, slot: usize) -> (String, usize, bool /// rendered identifier never goes below. fn signature_base_len(c: &Composite, node: usize) -> usize { match &c.nodes()[node] { - BlueprintNode::Leaf(_) => 1 + c.params().iter().filter(|a| a.node == node).count(), + BlueprintNode::Leaf(_) => 1 + aliases_on(c.params(), node).count(), BlueprintNode::Composite(_) => 1, } } diff --git a/crates/aura-engine/src/blueprint.rs b/crates/aura-engine/src/blueprint.rs index 9cecd9d..7a9fd5f 100644 --- a/crates/aura-engine/src/blueprint.rs +++ b/crates/aura-engine/src/blueprint.rs @@ -276,7 +276,7 @@ pub fn signature_of( if let Some(ch) = f.label().chars().next() { s.push(ch); } - for a in aliases.iter().filter(|a| a.node == node) { + for a in aliases_on(aliases, node) { if let Some(ch) = a.name.chars().next() { s.push(ch); } @@ -309,6 +309,30 @@ pub fn signature_of( s } +/// The param aliases declared on interior node `node`, in declared order. The single +/// anchor for the `a.node == node` predicate shared by the signature base, the +/// unaliased-param test, and the CLI's render base-length (issue #45) — change the +/// predicate here, not in three places. +pub fn aliases_on(aliases: &[ParamAlias], node: usize) -> impl Iterator { + aliases.iter().filter(move |a| a.node == node) +} + +/// Validate that every param alias names a real interior leaf param slot: a dangling +/// `(node, slot)` is a `BadInteriorIndex` (C23 — names are cosmetic, but a dangling +/// handle is an author error). Sole owner of the alias-index check: the structural +/// pre-pass recurses every composite that lowering reaches, so `inline_composite` +/// trusts this has already run rather than re-checking (issue #45). +fn check_alias_indices(nodes: &[BlueprintNode], aliases: &[ParamAlias]) -> Result<(), CompileError> { + for a in aliases { + let ok = a.node < nodes.len() + && matches!(&nodes[a.node], BlueprintNode::Leaf(f) if a.slot < f.params().len()); + if !ok { + return Err(CompileError::BadInteriorIndex); + } + } + Ok(()) +} + /// Structural validation (param-value-independent): walk every composite in the /// blueprint and reject an indistinguishable fan-in. Recurses into nested /// composites so the check holds at every level. Runs before the arity gate (a @@ -334,16 +358,10 @@ fn check_composite_fan_in( input_roles: &[Role], param_aliases: &[ParamAlias], ) -> Result<(), CompileError> { - // alias-validity first (mirrors `inline_composite`): a bogus alias index is a - // `BadInteriorIndex`, ordered ahead of the fan-in fault so a bad alias surfaces - // as the index error, not as an incidental collision. - for a in param_aliases { - let ok = a.node < nodes.len() - && matches!(&nodes[a.node], BlueprintNode::Leaf(f) if a.slot < f.params().len()); - if !ok { - return Err(CompileError::BadInteriorIndex); - } - } + // alias-validity first: a bogus alias index is a `BadInteriorIndex`, ordered + // ahead of the fan-in fault so a bad alias surfaces as the index error, not as an + // incidental collision. + check_alias_indices(nodes, param_aliases)?; for node in 0..nodes.len() { let mut sources: Vec<(usize, String, bool)> = Vec::new(); // (slot, sig, has_unaliased_param) @@ -380,7 +398,7 @@ fn leaf_has_unaliased_param(nodes: &[BlueprintNode], aliases: &[ParamAlias], nod match &nodes[node] { BlueprintNode::Leaf(f) => { let n_params = f.params().len(); - let aliased = aliases.iter().filter(|a| a.node == node).count(); + let aliased = aliases_on(aliases, node).count(); n_params > aliased } BlueprintNode::Composite(_) => false, @@ -492,21 +510,15 @@ fn inline_composite( ) -> Result { // `name` is the non-load-bearing render symbol (#13); it dissolves at inline // (C23 — the boundary does not reach the compilat), so it is not destructured. - // `params` here are the composite's ParamAlias overlay (renamed to avoid - // shadowing the injected scalar `params: &[Scalar]` arg consumed by - // `lower_items` below). - let Composite { name: _, nodes, edges, input_roles, params: param_aliases, output } = c; + // `params` (the composite's ParamAlias overlay) is a pure naming layer validated + // by the pre-pass and unused in lowering — the injected scalar `params: &[Scalar]` + // arg drives `lower_items` below — so it is dropped here. + let Composite { name: _, nodes, edges, input_roles, params: _, output } = c; let item_count = nodes.len(); - // an alias must name a real interior leaf param slot (C23 — names are cosmetic - // but a dangling handle is an author error). Mirrors the output range-check. - for a in ¶m_aliases { - let ok = a.node < item_count - && matches!(&nodes[a.node], BlueprintNode::Leaf(f) if a.slot < f.params().len()); - if !ok { - return Err(CompileError::BadInteriorIndex); - } - } + // alias-validity (a dangling `(node, slot)` is a `BadInteriorIndex`) is owned by + // the structural pre-pass `check_alias_indices`, which `compile_with_params` runs + // over every composite before any lowering — so it has already fired here (#45). // recursively lower interior items, then rewrite interior edges through them let interior = lower_items(nodes, params, cursor, flat_nodes, flat_edges)?; diff --git a/crates/aura-engine/src/lib.rs b/crates/aura-engine/src/lib.rs index dbec361..eee1fce 100644 --- a/crates/aura-engine/src/lib.rs +++ b/crates/aura-engine/src/lib.rs @@ -36,7 +36,8 @@ mod harness; mod report; pub use blueprint::{ - signature_of, Blueprint, BlueprintNode, CompileError, Composite, OutField, ParamAlias, Role, + aliases_on, signature_of, Blueprint, BlueprintNode, CompileError, Composite, OutField, + ParamAlias, Role, }; pub use harness::{BootstrapError, Edge, Harness, SourceSpec, Target}; pub use report::{f64_field, summarize, RunManifest, RunMetrics, RunReport};