From 249aafdb0faa9b0e9f1645a86be730bb3d044824 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 11 Jul 2026 06:56:21 +0200 Subject: [PATCH] =?UTF-8?q?refactor(std,composites):=20interned=20cost[k].?= =?UTF-8?q?*=20port=20names=20=E2=80=94=20the=20leaks=20drop=20(#152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One process-global intern pool (intern_port/cost_port, the COL_PORTS LazyLock pattern) in the cost module is now the single source of the cost[k]./cost[k]. names; CostSum::builder and cost_graph consume it and the per-build .leak()s in cost_graph are gone — the prerequisite before cost is rebuilt per member across a sweep. Two unit pins (same pointer for the same pair, dedup for bare names). Verified: aura-std 166/0, cost_graph 4/4, zero .leak() in the two sites, full workspace suite green, clippy -D warnings clean. closes #152 refs #234 --- crates/aura-composites/src/lib.rs | 23 +++++++------ crates/aura-std/src/cost.rs | 54 +++++++++++++++++++++++++++++++ crates/aura-std/src/cost_sum.rs | 7 ++-- crates/aura-std/src/lib.rs | 4 +-- 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/crates/aura-composites/src/lib.rs b/crates/aura-composites/src/lib.rs index 0ab7eac..d025fda 100644 --- a/crates/aura-composites/src/lib.rs +++ b/crates/aura-composites/src/lib.rs @@ -18,8 +18,9 @@ use aura_core::{PrimitiveBuilder, Scalar}; use aura_engine::{Composite, GraphBuilder, NodeHandle}; use aura_std::{ - CostSum, Delay, Ema, FixedStop, LinComb, Mul, PositionManagement, Sizer, Sqrt, Sub, - COST_FIELD_NAMES, GEOMETRY_WIDTH, PM_FIELD_NAMES, + cost_port, intern_port, CostSum, Delay, Ema, FixedStop, LinComb, Mul, + PositionManagement, Sizer, Sqrt, Sub, COST_FIELD_NAMES, GEOMETRY_WIDTH, + PM_FIELD_NAMES, }; /// The volatility stop as a composition of primitives: @@ -149,11 +150,11 @@ pub fn risk_executor_vol_open(risk_budget: f64) -> Composite { /// schema is geometry-prefix-first then the factor's extras (slot `GEOMETRY_WIDTH` /// onward); `cost_graph` reads `schema().inputs[GEOMETRY_WIDTH..]` to discover the /// extras and names them `cost[k].` — mirroring `CostSum`'s own `cost[k].*` -/// input vocabulary. Runtime-computed port names are `.leak()`ed to `&'static str` -/// to satisfy the `NodeHandle::input`/`output` bound — fine while cost is run-path -/// only (the graph is built once, a one-shot leak), but to be interned (the -/// `COL_PORTS` production pattern) before cost reaches the per-member sweep path, -/// where a per-build leak would accumulate. See #152. +/// input vocabulary. Runtime-computed port names are interned (`cost_port` / +/// `intern_port`, the `COL_PORTS` production pattern — #152): one allocation +/// per DISTINCT name for the process lifetime, so rebuilding the graph per +/// member across a sweep reuses the same strings instead of leaking fresh +/// ones per build. pub fn cost_graph(cost_nodes: Vec) -> Composite { assert!(!cost_nodes.is_empty(), "cost_graph needs at least one cost node"); let n = cost_nodes.len(); @@ -183,14 +184,12 @@ pub fn cost_graph(cost_nodes: Vec) -> Composite { stop_t.push(h.input("stop_price")); // Each extra input becomes a `cost[k].` composite role. for name in &extra_names { - let role = g.input_role(&format!("cost[{k}].{name}")); - let port: &'static str = name.clone().leak(); - g.feed(role, [h.input(port)]); + let role = g.input_role(cost_port(k, name)); + g.feed(role, [h.input(intern_port(name))]); } // The node's 3 cost fields -> CostSum's `cost[k].` inputs. for field in COST_FIELD_NAMES { - let agg_in: &'static str = format!("cost[{k}].{field}").leak(); - g.connect(h.output(field), agg.input(agg_in)); + g.connect(h.output(field), agg.input(cost_port(k, field))); } } g.feed(closed, closed_t); diff --git a/crates/aura-std/src/cost.rs b/crates/aura-std/src/cost.rs index 93d6ccb..967b387 100644 --- a/crates/aura-std/src/cost.rs +++ b/crates/aura-std/src/cost.rs @@ -15,6 +15,8 @@ use aura_core::{ Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, ScalarKind, }; +use std::collections::HashSet; +use std::sync::{LazyLock, Mutex}; /// The 3-field cost-in-R record every cost node emits, in slot order — one source /// of truth for the producer schema and the `CostSum` aggregator. @@ -27,6 +29,35 @@ pub const COST_FIELD_NAMES: [&str; COST_WIDTH] = /// these, beginning at slot `GEOMETRY_WIDTH`. pub const GEOMETRY_WIDTH: usize = 4; +/// Process-global port-name intern pool (#152): ONE `&'static str` allocation +/// per distinct name for the process lifetime — the `COL_PORTS` `LazyLock` +/// pattern generalized. Rebuilding a cost graph per member across a sweep +/// reuses these strings instead of `.leak()`ing fresh ones per build. +static PORT_NAMES: LazyLock>> = + LazyLock::new(|| Mutex::new(HashSet::new())); + +/// Intern an arbitrary port name: the same input yields the same +/// `&'static str` allocation on every call. The one leak per DISTINCT name is +/// the pool's deliberate, bounded cost; per-build callers never leak. +pub fn intern_port(name: &str) -> &'static str { + let mut pool = PORT_NAMES.lock().expect("port-name intern pool lock"); + match pool.get(name) { + Some(interned) => interned, + None => { + let interned: &'static str = Box::leak(name.to_string().into_boxed_str()); + pool.insert(interned); + interned + } + } +} + +/// The interned `cost[{k}].{name}` port/agg name — the single source of the +/// cost-vocabulary name shape, consumed by `CostSum::builder` and `cost_graph` +/// (aura-composites) (#152). +pub fn cost_port(k: usize, name: &str) -> &'static str { + intern_port(&format!("cost[{k}].{name}")) +} + fn geometry_input_ports() -> Vec { vec![ PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "closed".into() }, @@ -474,6 +505,29 @@ mod tests { assert_eq!(schema.params, params); } + #[test] + fn cost_port_interns_one_static_name_per_distinct_pair() { + // Same (k, name) -> the SAME allocation (pointer equality), process-wide; + // a distinct pair -> a distinct interned name. The no-per-build-leak + // property #152 asks for: rebuilding a cost graph per member reuses these. + let a = cost_port(0, "volatility"); + let b = cost_port(0, "volatility"); + assert!(std::ptr::eq(a, b), "same pair must return the same interned str"); + assert_eq!(a, "cost[0].volatility"); + let c = cost_port(1, "volatility"); + assert_eq!(c, "cost[1].volatility"); + let d = cost_port(0, "cost_in_r"); + assert_eq!(d, "cost[0].cost_in_r"); + } + + #[test] + fn intern_port_dedups_bare_names() { + let a = intern_port("volatility"); + let b = intern_port("volatility"); + assert!(std::ptr::eq(a, b), "the pool must dedup bare names too"); + assert_eq!(a, "volatility"); + } + #[test] fn producer_and_aggregator_share_the_triple() { // The structural lockstep: producer output and aggregator output/inputs all diff --git a/crates/aura-std/src/cost_sum.rs b/crates/aura-std/src/cost_sum.rs index 645a1b2..cc90f8b 100644 --- a/crates/aura-std/src/cost_sum.rs +++ b/crates/aura-std/src/cost_sum.rs @@ -15,7 +15,7 @@ use aura_core::{ /// both the producer side (`cost_node_builder`) and this aggregator. The input-name /// loop, the output schema, the lookback vector, and the eval accumulator all read /// it, so the producer↔aggregator field match is structural, not by-convention. -use crate::cost::{COST_FIELD_NAMES, COST_WIDTH}; +use crate::cost::{cost_port, COST_FIELD_NAMES, COST_WIDTH}; /// Per-field sum of `n_costs` cost-in-R records. Inputs are /// `cost[k].{cost_in_r,cum_cost_in_r,open_cost_in_r}` for `k in 0..n_costs`, in @@ -42,7 +42,10 @@ impl CostSum { inputs.push(PortSpec { kind: ScalarKind::F64, firing: Firing::Any, - name: format!("cost[{k}].{field}"), + // Owned String (PortSpec.name is String); the NAME comes from + // the shared cost_port helper so the aggregator's input names + // and cost_graph's agg-side names share ONE shape definition. + name: cost_port(k, field).to_string(), }); } } diff --git a/crates/aura-std/src/lib.rs b/crates/aura-std/src/lib.rs index 465046a..9dcbb08 100644 --- a/crates/aura-std/src/lib.rs +++ b/crates/aura-std/src/lib.rs @@ -62,8 +62,8 @@ pub use carry_cost::CarryCost; pub use const_node::Const; pub use constant_cost::ConstantCost; pub use cost::{ - cost_node_builder, ChargeMode, CostNode, CostRunner, COST_FIELD_NAMES, COST_WIDTH, - GEOMETRY_WIDTH, + cost_node_builder, cost_port, intern_port, ChargeMode, CostNode, CostRunner, + COST_FIELD_NAMES, COST_WIDTH, GEOMETRY_WIDTH, }; pub use cost_sum::CostSum; pub use delay::Delay;