Skip to main content

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}