From a8bd52eaff002f8c7102ee7ec1d83896e0881875 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 14 Jun 2026 22:21:15 +0200 Subject: [PATCH] refactor(aura-core): own FieldSpec.name to drop the derive_signature heap leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FieldSpec.name was &'static str, forcing derive_signature to Box::leak every re-exported composite OutField name. Because compile_with_params reruns derive_signature per sweep point, an N-point sweep over an M-output-field composite leaked N×M boundary names unbounded — a scaling wall the moment the World/sweep layer compiles families per point. Pull FieldSpec.name to String, mirroring the already-owned PortSpec.name and ParamSpec.name: a composite re-exports interior fields under runtime boundary names, so ownership is the correct model, not a workaround. FieldSpec loses Copy; the only fallout is one borrow at the render site. leak_name/Box::leak deleted. Behaviour-preserving: signature contents unchanged, all 231 tests green. --- crates/aura-core/src/node.rs | 18 +++++++++++------- crates/aura-engine/src/blueprint.rs | 13 +++---------- crates/aura-engine/src/graph_model.rs | 4 ++-- crates/aura-engine/src/harness.rs | 22 +++++++++++----------- crates/aura-std/src/add.rs | 2 +- crates/aura-std/src/ema.rs | 2 +- crates/aura-std/src/exposure.rs | 2 +- crates/aura-std/src/lincomb.rs | 2 +- crates/aura-std/src/sim_broker.rs | 2 +- crates/aura-std/src/sma.rs | 2 +- crates/aura-std/src/sub.rs | 2 +- 11 files changed, 34 insertions(+), 37 deletions(-) diff --git a/crates/aura-core/src/node.rs b/crates/aura-core/src/node.rs index 5da83c8..dc77080 100644 --- a/crates/aura-core/src/node.rs +++ b/crates/aura-core/src/node.rs @@ -33,8 +33,9 @@ pub enum Firing { /// window = its `length`). The `name` is a **non-load-bearing** debug symbol (C23): /// identity is the positional slot, the name is for tracing / graph rendering /// (#13), never read by bootstrap or the run loop (which wire by index). A -/// `String`, not `&'static str` like `FieldSpec.name`: a variadic node generates -/// per-slot names (`term[0]`), exactly as `ParamSpec.name` does (`weights[0]`). +/// `String` (as `FieldSpec.name` and `ParamSpec.name` are): a variadic node +/// generates per-slot names (`term[0]`), needing ownership, exactly as +/// `ParamSpec.name` does (`weights[0]`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct PortSpec { pub kind: ScalarKind, @@ -45,10 +46,13 @@ pub struct PortSpec { /// One declared output column of a node's record: its name (metadata for sinks / /// the playground, C18) and its scalar kind. The position of a `FieldSpec` in /// `NodeSchema.output` is what an `Edge` binds (`Edge::from_field`); the name is -/// not load-bearing for wiring. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// not load-bearing for wiring. The `name` is a `String` (not `Copy`): a composite +/// re-exports an interior field under a runtime boundary name (`OutField.name`, +/// derived in `derive_signature`), so ownership is required — the same reason +/// `PortSpec.name` and `ParamSpec.name` are owned. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct FieldSpec { - pub name: &'static str, + pub name: String, pub kind: ScalarKind, } @@ -56,8 +60,8 @@ pub struct FieldSpec { /// kind. The name is a **non-load-bearing** debug symbol (path-qualified at /// aggregation, as `FieldSpec.name` already is); a param's identity is its /// positional slot in the blueprint's aggregated param-space (C23 — by index, not -/// by name). Unlike `FieldSpec.name` (`&'static str`), the name is a `String`: a -/// vector knob carries a runtime index (`weights[0]`) and aggregation prefixes the +/// by name). The name is a `String` (as `FieldSpec.name` and `PortSpec.name` are): +/// a vector knob carries a runtime index (`weights[0]`) and aggregation prefixes the /// composite path (`strategy.weights[0]`). Permitted kinds: `I64`/`F64`/`Bool`; /// `Timestamp` is a structural axis (C20), never a numeric knob. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/aura-engine/src/blueprint.rs b/crates/aura-engine/src/blueprint.rs index 394f28f..afa734d 100644 --- a/crates/aura-engine/src/blueprint.rs +++ b/crates/aura-engine/src/blueprint.rs @@ -100,7 +100,7 @@ fn derive_signature(c: &Composite) -> NodeSchema { .get(of.node) .and_then(|n| n.signature().output.get(of.field).map(|f| f.kind)) .unwrap_or(ScalarKind::F64); - FieldSpec { name: leak_name(&of.name), kind } + FieldSpec { name: of.name.clone(), kind } }) .collect(); let mut params = Vec::new(); @@ -120,13 +120,6 @@ fn interior_slot_kind(nodes: &[BlueprintNode], _edges: &[Edge], t: &Target) -> S .unwrap_or(ScalarKind::F64) } -/// Leak a boundary name into a `&'static str` for a derived `FieldSpec`. Acceptable -/// because `derive_signature` is a cold, pre-build, render/validation path (never the -/// hot loop), and the leaked names are bounded by the static blueprint. -fn leak_name(s: &str) -> &'static str { - Box::leak(s.to_string().into_boxed_str()) -} - /// One named input role: role `r` (by position) fans the source value into /// `targets`. The `name` is a non-load-bearing render symbol (C23); identity is /// the role index, which survives lowering — the name does not. @@ -1048,7 +1041,7 @@ mod tests { } /// A one-f64-field output record under name `v`. fn out_v() -> Vec { - vec![FieldSpec { name: "v", kind: ScalarKind::F64 }] + vec![FieldSpec { name: "v".into(), kind: ScalarKind::F64 }] } /// A bound root role of f64 kind, fanning into `targets`. fn root_role(name: &str, targets: Vec) -> Role { @@ -2181,7 +2174,7 @@ mod tests { "Boom", NodeSchema { inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "in".into() }], - output: vec![FieldSpec { name: "v", kind: ScalarKind::I64 }], + output: vec![FieldSpec { name: "v".into(), kind: ScalarKind::I64 }], params: vec![], }, |_| panic!("build must not run when validation fails pre-build"), diff --git a/crates/aura-engine/src/graph_model.rs b/crates/aura-engine/src/graph_model.rs index f31fd46..5b93bdc 100644 --- a/crates/aura-engine/src/graph_model.rs +++ b/crates/aura-engine/src/graph_model.rs @@ -88,7 +88,7 @@ fn prim_record(b: &aura_core::PrimitiveBuilder) -> String { let role = if s.output.is_empty() { "sink" } else { "node" }; let params = join(s.params.iter().map(|p| named_kind(&p.name, p.kind))); let ins = join(s.inputs.iter().map(port_json)); - let outs = join(s.output.iter().map(|f| named_kind(f.name, f.kind))); + let outs = join(s.output.iter().map(|f| named_kind(&f.name, f.kind))); // explicit instance name only — an unnamed node carries no "name" field // (node_name()'s lowercased-type default would be a redundant type-duplicate) let name = match b.instance_name() { @@ -298,7 +298,7 @@ mod tests { PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() }, PortSpec { kind: ScalarKind::F64, firing: Firing::Barrier(0), name: "rhs".into() }, ], - output: vec![FieldSpec { name: "diff", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "diff".into(), kind: ScalarKind::F64 }], params: vec![], }, |_| Box::new(Bare), diff --git a/crates/aura-engine/src/harness.rs b/crates/aura-engine/src/harness.rs index da375c1..981253d 100644 --- a/crates/aura-engine/src/harness.rs +++ b/crates/aura-engine/src/harness.rs @@ -395,7 +395,7 @@ mod tests { fn sig() -> NodeSchema { NodeSchema { inputs: vec![f64_port(Firing::Any), f64_port(Firing::Any)], - output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], } } @@ -424,7 +424,7 @@ mod tests { fn sig() -> NodeSchema { NodeSchema { inputs: vec![f64_port(Firing::Barrier(0)), f64_port(Firing::Barrier(0))], - output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], } } @@ -453,7 +453,7 @@ mod tests { f64_port(Firing::Barrier(0)), f64_port(Firing::Any), ], - output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], } } @@ -486,11 +486,11 @@ mod tests { NodeSchema { inputs: vec![f64_port(Firing::Barrier(0)); 5], output: vec![ - FieldSpec { name: "open", kind: ScalarKind::F64 }, - FieldSpec { name: "high", kind: ScalarKind::F64 }, - FieldSpec { name: "low", kind: ScalarKind::F64 }, - FieldSpec { name: "close", kind: ScalarKind::F64 }, - FieldSpec { name: "volume", kind: ScalarKind::F64 }, + FieldSpec { name: "open".into(), kind: ScalarKind::F64 }, + FieldSpec { name: "high".into(), kind: ScalarKind::F64 }, + FieldSpec { name: "low".into(), kind: ScalarKind::F64 }, + FieldSpec { name: "close".into(), kind: ScalarKind::F64 }, + FieldSpec { name: "volume".into(), kind: ScalarKind::F64 }, ], params: vec![], } @@ -524,8 +524,8 @@ mod tests { NodeSchema { inputs: vec![f64_port(Firing::Any)], output: vec![ - FieldSpec { name: "f", kind: ScalarKind::F64 }, - FieldSpec { name: "i", kind: ScalarKind::I64 }, + FieldSpec { name: "f".into(), kind: ScalarKind::F64 }, + FieldSpec { name: "i".into(), kind: ScalarKind::I64 }, ], params: vec![], } @@ -553,7 +553,7 @@ mod tests { fn sig() -> NodeSchema { NodeSchema { inputs: vec![f64_port(Firing::Any)], - output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], } } diff --git a/crates/aura-std/src/add.rs b/crates/aura-std/src/add.rs index 62a807e..d1df682 100644 --- a/crates/aura-std/src/add.rs +++ b/crates/aura-std/src/add.rs @@ -36,7 +36,7 @@ impl Add { PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() }, PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() }, ], - output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], }, |_| Box::new(Add::new()), diff --git a/crates/aura-std/src/ema.rs b/crates/aura-std/src/ema.rs index 64ef841..a3e1ebc 100644 --- a/crates/aura-std/src/ema.rs +++ b/crates/aura-std/src/ema.rs @@ -61,7 +61,7 @@ impl Ema { "EMA", NodeSchema { inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }], - output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], }, |p| Box::new(Ema::new(p[0].as_i64().expect("length slot is I64") as usize)), diff --git a/crates/aura-std/src/exposure.rs b/crates/aura-std/src/exposure.rs index 7542b38..0d440cb 100644 --- a/crates/aura-std/src/exposure.rs +++ b/crates/aura-std/src/exposure.rs @@ -30,7 +30,7 @@ impl Exposure { "Exposure", NodeSchema { inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "signal".into() }], - output: vec![FieldSpec { name: "exposure", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "exposure".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }], }, |p| Box::new(Exposure::new(p[0].as_f64().expect("scale slot is F64"))), diff --git a/crates/aura-std/src/lincomb.rs b/crates/aura-std/src/lincomb.rs index dd3f1c5..e132f70 100644 --- a/crates/aura-std/src/lincomb.rs +++ b/crates/aura-std/src/lincomb.rs @@ -52,7 +52,7 @@ impl LinComb { .collect(); PrimitiveBuilder::new( "LinComb", - NodeSchema { inputs, output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], params }, + NodeSchema { inputs, output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params }, |p| Box::new(LinComb::new( p.iter().map(|s| s.as_f64().expect("weight slot is F64")).collect(), )), diff --git a/crates/aura-std/src/sim_broker.rs b/crates/aura-std/src/sim_broker.rs index 1e531e4..49dfd44 100644 --- a/crates/aura-std/src/sim_broker.rs +++ b/crates/aura-std/src/sim_broker.rs @@ -66,7 +66,7 @@ impl SimBroker { PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "exposure".into() }, PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "price".into() }, ], - output: vec![FieldSpec { name: "equity", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "equity".into(), kind: ScalarKind::F64 }], params: vec![], }, move |_| Box::new(SimBroker::new(pip_size)), diff --git a/crates/aura-std/src/sma.rs b/crates/aura-std/src/sma.rs index e69578e..96350de 100644 --- a/crates/aura-std/src/sma.rs +++ b/crates/aura-std/src/sma.rs @@ -29,7 +29,7 @@ impl Sma { "SMA", NodeSchema { inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }], - output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], }, |p| Box::new(Sma::new(p[0].as_i64().expect("length slot is I64") as usize)), diff --git a/crates/aura-std/src/sub.rs b/crates/aura-std/src/sub.rs index 1c4a756..4b37d5e 100644 --- a/crates/aura-std/src/sub.rs +++ b/crates/aura-std/src/sub.rs @@ -27,7 +27,7 @@ impl Sub { PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() }, PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() }, ], - output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], + output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![], }, |_| Box::new(Sub::new()),