nmbrs_runtime/scope_synth/cascade_emit.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Cascade-extern emission helpers — building blocks the scope
5//! builders use to push individual lines of Polydat source into a
6//! string that's later folded into a [`polydat::kernel::subcontext::PolydatMatter`]
7//! and handed to the builder.
8//!
9//! These are the "build" half of walk + build + synthesize: the
10//! walker has decided that a particular name should be cascaded;
11//! these helpers do the per-name source-line emission consistent
12//! with nmbrs-runtime's Polydat conventions (chain-aware lookup, type
13//! widening rules, escape handling).
14
15use std::collections::HashSet;
16
17use crate::scope_kernel::ScopeKernel;
18
19use super::helpers::{format_workload_param_as_polydat_literal, value_to_param_string};
20
21/// Emit `const NAME := <literal>` for one workload param into
22/// `source`, choosing the literal value via the canonical
23/// chain-aware lookup order:
24///
25/// 1. `parent_kernel.lookup(name)` — if `Some`, this is the
26/// chain-resolved value. A `bindings:` / `set:` scope above
27/// us may have shadowed the workload-param default; whichever
28/// `const NAME := <override>` is closest in the chain wins
29/// via the local-final transit-suppression rule
30/// (SRD-13f §"Local-authoritative shadow"). If the lookup is
31/// non-scalar (vectors etc.), skip the chain path and fall
32/// back.
33/// 2. `hashmap_default` — the workload-load-time default from
34/// `params:` plus CLI overrides. Used when nothing in the
35/// chain has a value for `name`.
36///
37/// Pre-marks `name` in `emitted` so subsequent cascade passes in
38/// the caller skip it. Optionally appends to `inherited_names`
39/// for the inherited-output bookkeeping consumers also do.
40///
41/// **Why this helper exists**: pre-helper, seven distinct
42/// synthesizer sites each rolled their own `const NAME := <hashmap>`
43/// emission, reading the HashMap directly without consulting the
44/// chain. Any name a scenario-tree `set:` or `bindings:` scope
45/// shadowed kept the stale HashMap default inside any sub-scope
46/// synthesized via one of those sites — breaking the SRD-21 /
47/// SRD-18 contract that the kernel chain is the single
48/// resolution surface. Routing through this helper closes that
49/// hole categorically.
50pub fn emit_workload_param_chain_aware(
51 name: &str,
52 hashmap_default: &str,
53 parent_kernel: &ScopeKernel,
54 source: &mut String,
55 emitted: &mut HashSet<String>,
56 inherited_names: Option<&mut Vec<String>>,
57) {
58 if emitted.contains(name) {
59 return;
60 }
61 let value_str = parent_kernel
62 .lookup(name)
63 .and_then(|v| value_to_param_string(&v))
64 .unwrap_or_else(|| hashmap_default.to_string());
65 let literal = format_workload_param_as_polydat_literal(&value_str);
66 source.push_str(&format!("const {name} := {literal}\n"));
67 emitted.insert(name.to_string());
68 if let Some(inh) = inherited_names {
69 inh.push(name.to_string());
70 }
71}