tidy(aura-engine): single-own alias-validity + shared alias-count anchor

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
This commit is contained in:
2026-06-08 16:38:09 +02:00
parent 04c2280dd8
commit 69d20949c0
3 changed files with 41 additions and 28 deletions
+2 -2
View File
@@ -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,
}
}
+37 -25
View File
@@ -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<Item = &ParamAlias> {
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<ItemLowering, CompileError> {
// `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 &param_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)?;
+2 -1
View File
@@ -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};