nmbrs_runtime/scope_synth/for_each.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`build_for_each_scope_kernel`] — synthesize the kernel for
5//! a for-each scope.
6//!
7//! Activity-side replacement for
8//! `polydat::iteration::comprehension::synthesize_for_each_scope`. The
9//! comprehension-specific walking lives here; the broad
10//! parent-program cascade is delegated to the shared
11//! [`super::cascade::cascade_parent_into_source`] walker.
12//!
13//! ## What lives here (comprehension-specific)
14//!
15//! - Probe pre-evaluation of clause spec expressions to detect
16//! each iter-var's native type (so the emitted extern uses
17//! the right `u64` / `f64` / `Str` / `Bool` / `Ext`).
18//! - Cross-clause placeholder discovery — earlier iter-vars'
19//! probe values may substitute into later clauses' spec
20//! expressions, surfacing additional names the cascade
21//! needs to resolve.
22//! - Phase-bindings appendage (SRD-13f Push E) — when a phase
23//! carries both `for_each:` and `bindings:`, the bindings
24//! source lives on the for-each scope's kernel.
25//!
26//! ## What's delegated (generic cascade)
27//!
28//! Everything else — parent-output cascade with provenance-
29//! aware inlining, parent-input cascade, workload-params
30//! cascade, local-inclusion-chain inlining — runs through the
31//! shared walker.
32
33use std::collections::HashSet;
34
35use crate::scope_kernel::ScopeKernel;
36use polydat::iteration::comprehension::pre_evaluate_clause;
37use polydat::kernel::ManifestEntry;
38
39use super::cascade::{CascadeInputs, CascadeOutputs, cascade_parent_into_source};
40use super::helpers::{collect_leaf_placeholders, scan_one};
41
42/// Synthesize and compile a Polydat Kernel for a for-each scope.
43///
44/// `bindings` is `[(iter_var, spec_expr)]` per scalar variable
45/// (parallel-iter clauses contribute one entry per scalar).
46/// `parent_manifest` describes the parent kernel's typed
47/// outputs (use `polydat::kernel::extract_manifest` on the
48/// parent's program). `parent_kernel` provides the in-scope
49/// name space for clause pre-evaluation.
50/// `phase_bindings` is optional Polydat source folded in after the
51/// extern cascade (SRD-13f Push E — when a phase declares both
52/// `for_each:` and `bindings:`, the bindings live on this scope).
53///
54/// Returns a kernel with:
55/// - One extern per iter-var, typed via probe pre-evaluation.
56/// - Cascade-extern declarations for every parent-visible
57/// name the spec expressions reference and the parent
58/// exposes.
59/// - `materialize_wiring_from_outer(parent)` already called.
60/// - Parent input-slot values propagated via
61/// `polydat::kernel::propagate_inputs` ([`ScopeKernel::synthesize_under`]).
62///
63/// The caller's responsibility: per-iteration, install the
64/// tuple's typed values on this kernel's input slots before
65/// evaluating children.
66#[allow(clippy::too_many_arguments)]
67pub fn build_for_each_scope_kernel(
68 bindings: &[(String, String)],
69 parent_manifest: &[ManifestEntry],
70 parent_kernel: &ScopeKernel,
71 workload_params: &std::collections::HashMap<String, String>,
72 polydat_lib_paths: Vec<std::path::PathBuf>,
73 workload_dir: Option<&std::path::Path>,
74 strict: bool,
75 context: &str,
76 phase_bindings: Option<&str>,
77) -> Result<ScopeKernel, String> {
78 let iter_vars: Vec<String> = bindings.iter().map(|(v, _)| v.clone()).collect();
79 let spec_exprs: Vec<String> = bindings.iter().map(|(_, e)| e.clone()).collect();
80
81 let mut source = String::new();
82 let mut emitted: HashSet<String> = HashSet::new();
83 let mut inherited_names: Vec<String> = Vec::new();
84
85 // Probe each clause's spec expression to detect the iter-
86 // var's native Polydat type and discover any additional
87 // placeholders that surface after earlier-iter-vars'
88 // values substitute into later specs.
89 //
90 // `probes` holds the first probe-value per iter-var (used
91 // for cross-clause substitution); `all_referenced` accumulates
92 // every placeholder discovered along the way.
93 let mut probes: std::collections::HashMap<String, String> = std::collections::HashMap::new();
94 let mut all_referenced: HashSet<String> = collect_leaf_placeholders(&spec_exprs);
95 for (idx, var) in iter_vars.iter().enumerate() {
96 if emitted.contains(var) {
97 continue;
98 }
99 let spec_text = spec_exprs.get(idx).map(String::as_str).unwrap_or("");
100 let values = pre_evaluate_clause(spec_text, parent_kernel, workload_params, &probes)
101 .unwrap_or_default();
102 let detected_type = values
103 .first()
104 .map(polydat::iteration::comprehension::value_to_polydat_type_name)
105 .unwrap_or("String");
106 source.push_str(&format!("extern {var}: {detected_type}\n"));
107 emitted.insert(var.clone());
108
109 // Discover placeholders that emerge after substituting
110 // this iter-var's first value into later specs.
111 for v in &values {
112 let v_str = v.to_display_string();
113 for next_spec in &spec_exprs[idx + 1..] {
114 let mut substituted = next_spec.clone();
115 substituted = substituted.replace(&format!("{{{var}}}"), &v_str);
116 let mut emergent = HashSet::new();
117 scan_one(&substituted, &mut emergent);
118 all_referenced.extend(emergent);
119 }
120 }
121 if let Some(first) = values.into_iter().next() {
122 probes.insert(var.clone(), first.to_display_string());
123 }
124 }
125
126 // The iter-vars are pre-emitted; we don't want the cascade
127 // to re-emit them.
128 let pre_emitted: HashSet<String> = iter_vars.iter().cloned().collect();
129 // Drive the shared cascade walker. include_referenced_cascade
130 // is true for for_each — its spec expressions are narrow
131 // Polydat source where every referenced name needs an extern.
132 cascade_parent_into_source(
133 CascadeInputs {
134 parent_kernel,
135 workload_params,
136 parent_manifest,
137 referenced: &all_referenced,
138 pre_emitted: &pre_emitted,
139 shadow_names: &pre_emitted,
140 include_referenced_cascade: true,
141 },
142 CascadeOutputs {
143 source: &mut source,
144 emitted: &mut emitted,
145 inherited_names: &mut inherited_names,
146 },
147 );
148
149 // SRD-13f Push E — append phase-level `bindings:` source
150 // after the extern cascade. Phase bindings can reference
151 // iter vars (now externs above) and any cascaded parent
152 // name; the Polydat compiler resolves both.
153 if let Some(body) = phase_bindings {
154 let trimmed = body.trim();
155 if !trimmed.is_empty() {
156 if !source.ends_with('\n') && !source.is_empty() {
157 source.push('\n');
158 }
159 source.push_str(body);
160 if !source.ends_with('\n') {
161 source.push('\n');
162 }
163 }
164 }
165
166 if source.is_empty() {
167 source.push_str("const __empty := 0\n");
168 }
169
170 // SRD-67 Phase 3 — finalize through the SubcontextBuilder
171 // bridge. The for_each synthesiser threads polydat_lib_paths /
172 // workload_dir / strict through CompileOptions so the
173 // underlying compile invocation matches the legacy call.
174 let compile_options = polydat::kernel::subcontext::CompileOptions {
175 workload_dir: workload_dir.map(|p| p.to_path_buf()),
176 polydat_lib_paths,
177 strict,
178 required_outputs: Vec::new(),
179 context_label: Some(context.to_string()),
180 cursor_limit: None,
181 ..Default::default()
182 };
183 crate::scope_kernel::ScopeKernel::synthesize_under(
184 parent_kernel,
185 crate::scope_kernel::SourceMatter::source(context, source, compile_options)
186 .inherited(inherited_names),
187 )
188 .map_err(|e| format!("{context}: for_each scope synthesis: {e}"))
189}