Skip to main content

polydat_core/dsl/
traversal.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Compile-time support for the `for` construct (SRD 113 steps 2 and
5//! 3): element typing, body-to-child-program lowering, and the
6//! metadata a parent program carries for each traversal and producer.
7//!
8//! A `for` body compiles exactly once, at parent compile time, into a
9//! child [`PolydatProgram`] keyed by the statement's lexical position.
10//! Its element names become `IterationExtern` inputs typed from the
11//! comprehension's sources; outer wires it references become cascade
12//! externs typed from the parent's manifest. Activation (step 4) only
13//! ever allocates state over that program.
14
15use std::collections::{BTreeSet, HashMap};
16use std::sync::{Arc, Mutex};
17
18use crate::ast::PortType;
19use crate::iteration::comprehension::flatten::flatten_static_sources;
20use crate::iteration::comprehension::source::{LiteralValue, Source};
21use crate::iteration::comprehension::{
22    Comprehension, Mode as ValidationMode, StreamerValue, ValidationWarning,
23};
24use crate::kernel::PolydatProgram;
25use crate::kernel::interp::{Lookup, NoScope};
26
27use super::ast::{
28    Arg, Binding, BindingModifier, CallExpr, Expr, ExternPort, ForSource, ForSourceKind, ForStmt,
29    InputDecl, PolydatFile, Statement,
30};
31use super::lexer::Span;
32
33/// A compiled traversal: one `for` statement and the child program its
34/// body lowered to.
35#[derive(Debug, Clone)]
36pub struct Traversal {
37    /// Lexical position of the `for` statement in its parent.
38    pub span: Span,
39    /// The text after `for`, as written.
40    pub source_text: String,
41    /// The comprehension traversed, with a producer reference already
42    /// resolved to the producer's comprehension.
43    pub comprehension: Comprehension,
44    /// Element names and their compile-time types, in tuple order.
45    pub elements: Vec<(String, PortType)>,
46    /// Outer wires the body references, with the parent's types. Bound
47    /// from the parent at activation.
48    pub cascade: Vec<(String, PortType)>,
49    /// The body's program on the interpreter. Compiled once; every
50    /// interpreter activation shares it.
51    pub program: Arc<PolydatProgram>,
52    /// The body as the parent compiled it, for activations on the other
53    /// engines (engine parity, step 8): compiled once per engine, on the
54    /// first activation that asks.
55    pub body: Arc<BodySource>,
56}
57
58/// A traversal body as its parent compiled it: the child file and the
59/// compiler settings the parent used, so the same body compiles on any
60/// engine, once, keyed by the engine as the interpreter's program is
61/// keyed by the body's position (SRD 113 §5.1).
62pub struct BodySource {
63    pub(crate) file: PolydatFile,
64    pub(crate) source_text: String,
65    pub(crate) source_dir: Option<std::path::PathBuf>,
66    pub(crate) lib_paths: Vec<std::path::PathBuf>,
67    pub(crate) strict: bool,
68    pub(crate) context_label: String,
69    pub(crate) cursor_limit: Option<u64>,
70    pub(crate) pragmas: super::pragmas::PragmaSet,
71    /// The modules the parent program had resolved when the body was
72    /// lowered, its own definitions included, so the body sees them
73    /// wherever it compiles, as the parent did.
74    pub(super) modules: HashMap<String, super::modules::ResolvedModule>,
75    /// The body's program per engine, built on first use.
76    pub(crate) programs: Mutex<HashMap<crate::Engine, Arc<dyn crate::kernel::KernelProgram>>>,
77    /// The compile ledger of the tree, for the body's programs on every
78    /// engine.
79    pub(crate) ledger: Arc<crate::kernel::CompileLedger>,
80}
81
82impl BodySource {
83    /// The body's source as the parent lowered it: the implicit `cycle`
84    /// input, one extern per element and per cascaded wire, then the
85    /// body's statements.
86    pub fn source_text(&self) -> &str {
87        &self.source_text
88    }
89
90    /// The body's program on `engine`, compiled on the first call for
91    /// that engine and shared by every use after it.
92    ///
93    /// One body, one carrier. A `for` body and a tile's projection
94    /// body are the same thing — a statement list compiled once per
95    /// engine against the settings the parent compiled under — and
96    /// they reach it through here. The tile path used to compile each
97    /// body twice at node construction, once for the interpreter and
98    /// once on `Engine::default()`, and then render on the default
99    /// engine whatever engine the kernel was running.
100    pub fn program_on(
101        &self,
102        engine: crate::Engine,
103    ) -> Result<Arc<dyn crate::kernel::KernelProgram>, crate::KernelError> {
104        let mut programs = self
105            .programs
106            .lock()
107            .unwrap_or_else(|poisoned| poisoned.into_inner());
108        if let Some(program) = programs.get(&engine) {
109            return Ok(program.clone());
110        }
111        let program = super::compile::Compiler::compile_body_on(self, engine)?.into_program();
112        programs.insert(engine, program.clone());
113        Ok(program)
114    }
115
116    /// A body from the parts the compiler that lowered it holds.
117    #[allow(clippy::too_many_arguments)]
118    pub(super) fn from_parts(
119        file: PolydatFile,
120        source_text: String,
121        source_dir: Option<std::path::PathBuf>,
122        lib_paths: Vec<std::path::PathBuf>,
123        strict: bool,
124        context_label: String,
125        cursor_limit: Option<u64>,
126        pragmas: super::pragmas::PragmaSet,
127        modules: HashMap<String, super::modules::ResolvedModule>,
128        ledger: Arc<crate::kernel::CompileLedger>,
129    ) -> Self {
130        BodySource {
131            file,
132            source_text,
133            source_dir,
134            lib_paths,
135            strict,
136            context_label,
137            cursor_limit,
138            pragmas,
139            modules,
140            programs: Mutex::new(HashMap::new()),
141            ledger,
142        }
143    }
144
145    /// A body from its source text, under the default settings.
146    ///
147    /// The route for a body that reaches the runtime as text rather
148    /// than from the compiler that lowered it — a tile skeleton's
149    /// projection body, which travels in the `tile_render` node's
150    /// serialized spec. `context_label` names it in a diagnostic.
151    pub fn from_source(source: &str, context_label: &str) -> Result<Self, String> {
152        let tokens = super::lexer::lex(source)?;
153        let file = super::parser::parse(tokens)?;
154        Ok(BodySource {
155            file,
156            source_text: source.to_string(),
157            source_dir: None,
158            lib_paths: Vec::new(),
159            strict: false,
160            context_label: context_label.to_string(),
161            cursor_limit: None,
162            pragmas: super::pragmas::PragmaSet::default(),
163            modules: HashMap::new(),
164            programs: Mutex::new(HashMap::new()),
165            ledger: Arc::new(crate::kernel::CompileLedger::default()),
166        })
167    }
168}
169
170impl std::fmt::Debug for BodySource {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.debug_struct("BodySource")
173            .field("context", &self.context_label)
174            .field("statements", &self.file.statements.len())
175            .finish()
176    }
177}
178
179impl Traversal {
180    /// The body's program on `engine`, compiled on the first call for
181    /// that engine and shared by every activation after it, as the
182    /// interpreter's program is (SRD 113 §5.1, §5.2), its own `for`
183    /// statements included: an activation on any engine opens them.
184    pub fn program_on(
185        &self,
186        engine: crate::Engine,
187    ) -> Result<Arc<dyn crate::kernel::KernelProgram>, crate::KernelError> {
188        if matches!(engine, crate::Engine::Interpreter(_)) {
189            return Ok(self.program.clone());
190        }
191        self.body.program_on(engine)
192    }
193}
194
195/// A producer binding, `name := for ...`, recorded on the program that
196/// declares it so traversals over `name` resolve at compile time.
197#[derive(Debug, Clone)]
198pub struct Producer {
199    /// The wire the producer binds.
200    pub name: String,
201    /// Where the binding appears.
202    pub span: Span,
203    /// The text after `for`, as written.
204    pub source_text: String,
205    /// The comprehension, with derivations resolved.
206    pub comprehension: Comprehension,
207}
208
209/// Split a parsed file into the statements the parent compiles directly,
210/// the `for` statements to lower into child programs, and the producer
211/// bindings to record. Order within each group is preserved.
212///
213/// Producer bindings stay in the parent as `const name := streamer(...)`
214/// calls carrying the resolved comprehension, so the wire exists with a
215/// `Streamer` value on it (SRD 113 §3.1). Derivations resolve against
216/// producers bound earlier in the file, in document order.
217pub fn strip_for_forms(
218    file: &PolydatFile,
219    mode: ValidationMode,
220    scope: &dyn Lookup,
221    events: &mut Vec<super::events::CompileEvent>,
222) -> Result<(PolydatFile, Vec<ForStmt>, Vec<Producer>), String> {
223    let mut parent = Vec::with_capacity(file.statements.len());
224    let mut fors = Vec::new();
225    let mut producers: Vec<Producer> = Vec::new();
226    for stmt in &file.statements {
227        match stmt {
228            Statement::For(f) => fors.push(f.clone()),
229            Statement::Binding(b) if matches!(b.value, Expr::For(_)) => {
230                let Expr::For(source) = &b.value else {
231                    unreachable!()
232                };
233                let (comprehension, warnings) =
234                    resolve_source_with(source, &producers, mode, scope)?;
235                events.extend(warning_events(source, &warnings));
236                let name = b.targets.join(",");
237                let value = StreamerValue::new(source.to_text(), comprehension.clone());
238                parent.push(Statement::Binding(Binding {
239                    targets: b.targets.clone(),
240                    value: Expr::Call(CallExpr {
241                        func: "streamer".into(),
242                        args: vec![Arg::Positional(Expr::StringLit(value.to_json(), b.span))],
243                        span: b.span,
244                    }),
245                    modifier: BindingModifier::CONST,
246                    type_annotation: None,
247                    span: b.span,
248                }));
249                producers.push(Producer {
250                    name,
251                    span: b.span,
252                    source_text: source.to_text(),
253                    comprehension,
254                });
255            }
256            other => parent.push(other.clone()),
257        }
258    }
259    Ok((PolydatFile { statements: parent }, fors, producers))
260}
261
262/// Resolve a traversal's source to a comprehension: inline text as is, a
263/// producer reference to the producer bound in the same scope, and a
264/// derivation to the base producer with its filter and order applied.
265pub fn resolve_source(source: &ForSource, producers: &[Producer]) -> Result<Comprehension, String> {
266    resolve_source_with(
267        source,
268        producers,
269        ValidationMode::Permissive,
270        &NoScope::new(),
271    )
272    .map(|(c, _)| c)
273}
274
275/// The compile events for the validator's warnings on `source`, each
276/// placed at the statement that names the comprehension.
277pub(super) fn warning_events(
278    source: &ForSource,
279    warnings: &[ValidationWarning],
280) -> Vec<super::events::CompileEvent> {
281    warnings
282        .iter()
283        .map(|w| super::events::CompileEvent::ComprehensionWarning {
284            source: source.to_text(),
285            line: source.span.line,
286            col: source.span.col,
287            warning: w.to_string(),
288        })
289        .collect()
290}
291
292/// [`resolve_source`] under a validation mode (comprehension_forms.md
293/// §5.8): a strict compile refuses a degenerate composition, a
294/// permissive one returns it as a warning for the compile event log.
295pub fn resolve_source_with(
296    source: &ForSource,
297    producers: &[Producer],
298    mode: ValidationMode,
299    scope: &dyn Lookup,
300) -> Result<(Comprehension, Vec<ValidationWarning>), String> {
301    let find = |name: &str| -> Result<Comprehension, String> {
302        producers
303            .iter()
304            .rev()
305            .find(|p| p.name == name)
306            .map(|p| p.comprehension.clone())
307            .ok_or_else(|| {
308                let known: Vec<&str> = producers.iter().map(|p| p.name.as_str()).collect();
309                format!(
310                    "`for {}` at line {}, col {}: no producer named '{name}' is bound in this scope{}",
311                    source.to_text(),
312                    source.span.line,
313                    source.span.col,
314                    if known.is_empty() { String::new() } else { format!("; producers here: {}", known.join(", ")) }
315                )
316            })
317    };
318    let at = |e: &dyn std::fmt::Display| {
319        format!(
320            "`for {}` at line {}, col {}: {e}",
321            source.to_text(),
322            source.span.line,
323            source.span.col
324        )
325    };
326    let comprehension = match &source.kind {
327        ForSourceKind::Comprehension(c) => {
328            // A source has one comprehension and renders its text from
329            // it, so there is no longer a text that could disagree.
330            // What remains worth checking is the pair that makes the
331            // rendering trustworthy: writing this tree and reading it
332            // back must give this tree. A failure here is a gap between
333            // the comprehension's renderer and its parser, not anything
334            // the program did, and says so.
335            let written = source.to_text();
336            let from_text =
337                crate::iteration::comprehension::spec::parse_comprehension_algebra(&written)
338                    .map_err(|e| at(&e))?;
339            if from_text != *c {
340                return Err(at(&format!(
341                    "this comprehension does not survive being written and read back: it writes \
342                     as `{written}`, which reads as a different comprehension. That is an \
343                     inconsistency between the comprehension renderer and parser, not an error \
344                     in this program"
345                )));
346            }
347            c.clone()
348        }
349        ForSourceKind::Producer(name) => find(name)?,
350        ForSourceKind::Derived {
351            base,
352            filter,
353            order,
354        } => {
355            let mut c = find(base)?;
356            if let Some(pred) = filter {
357                c = Comprehension::filter(c, pred.clone());
358            }
359            if let Some(spec) = order {
360                let (strategy, truncation, seed) = parse_order(spec).map_err(|e| at(&e))?;
361                c = Comprehension::order_seeded(c, strategy, truncation, seed);
362            }
363            c
364        }
365    };
366    // Context-free sources are flattened first (comprehension_forms.md
367    // §10.7.0): a generator that references no name is evaluated here,
368    // charged to the program tree, and traversed as a literal.
369    let comprehension = flatten_static_sources(&comprehension, scope);
370    // Validation is a stage of the compile (comprehension_forms.md §5):
371    // a comprehension that violates a V-axiom is refused at the
372    // statement that names it, whether written inline or derived.
373    let report =
374        crate::iteration::comprehension::validate(&comprehension, mode).map_err(|e| at(&e))?;
375    Ok((comprehension, report.warnings))
376}
377
378/// Parse an `order` spec such as `halton/5` into the algebra's
379/// strategy, truncation, and seed.
380fn parse_order(
381    spec: &str,
382) -> Result<
383    (
384        crate::iteration::comprehension::StrategyName,
385        Option<u64>,
386        Option<u64>,
387    ),
388    String,
389> {
390    crate::iteration::comprehension::spec::parse_order(spec)
391}
392
393/// Type each element name of a comprehension from its source, per SRD
394/// 113 §3.3. `probe` types a generator call expression the way the
395/// enclosing compiler would.
396pub fn element_types(
397    comprehension: &Comprehension,
398    probe: &mut dyn FnMut(&str) -> Result<PortType, String>,
399) -> Result<Vec<(String, PortType)>, String> {
400    let mut out = Vec::new();
401    collect_element_types(comprehension, probe, &mut out)?;
402    Ok(out)
403}
404
405fn collect_element_types(
406    c: &Comprehension,
407    probe: &mut dyn FnMut(&str) -> Result<PortType, String>,
408    out: &mut Vec<(String, PortType)>,
409) -> Result<(), String> {
410    match c {
411        Comprehension::Clause { name, source } => {
412            if out.iter().any(|(n, _)| n == name) {
413                return Ok(());
414            }
415            let ty = source_type(name, source, probe)?;
416            out.push((name.clone(), ty));
417            Ok(())
418        }
419        Comprehension::Cartesian { children } | Comprehension::Zip { children, .. } => {
420            for child in children {
421                collect_element_types(child, probe, out)?;
422            }
423            Ok(())
424        }
425        Comprehension::Union { children } => {
426            // Union children share one tuple shape; the first child's
427            // types stand for all of them.
428            if let Some(first) = children.first() {
429                collect_element_types(first, probe, out)?;
430            }
431            Ok(())
432        }
433        Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
434            collect_element_types(child, probe, out)
435        }
436    }
437}
438
439fn source_type(
440    name: &str,
441    source: &Source,
442    probe: &mut dyn FnMut(&str) -> Result<PortType, String>,
443) -> Result<PortType, String> {
444    match source {
445        Source::Literal { values } => {
446            let mut ty: Option<PortType> = None;
447            for v in values {
448                let t = match v {
449                    LiteralValue::Int(_) => PortType::U64,
450                    LiteralValue::Float(_) => PortType::F64,
451                    LiteralValue::String(_) => PortType::Str,
452                    LiteralValue::Bool(_) => PortType::Bool,
453                    LiteralValue::Json(_) => PortType::Json,
454                };
455                match ty {
456                    None => ty = Some(t),
457                    // An int among floats widens; anything else is mixed.
458                    Some(PortType::F64) if t == PortType::U64 => {}
459                    Some(PortType::U64) if t == PortType::F64 => ty = Some(PortType::F64),
460                    Some(prev) if prev != t => {
461                        return Err(format!(
462                            "element '{name}': literal list mixes {prev:?} and {t:?} values; a comprehension element has one type"
463                        ));
464                    }
465                    Some(_) => {}
466                }
467            }
468            ty.ok_or_else(|| format!("element '{name}': literal list is empty"))
469        }
470        Source::IntRange { .. } => Ok(PortType::U64),
471        Source::ContinuousInterval { .. } | Source::Distribution { .. } => Ok(PortType::F64),
472        // Workload parameter lists are text until a host types them.
473        Source::WorkloadParamList { .. } => Ok(PortType::Str),
474        Source::Generator { expr, .. } => {
475            let head = expr.trim();
476            if head.starts_with("partitions(")
477                || head.starts_with("subdivide(")
478                || head.ends_with(".partitions")
479            {
480                return Ok(PortType::Ext);
481            }
482            probe(head)
483                .map_err(|e| format!("element '{name}': cannot type generator `{head}`: {e}"))
484        }
485    }
486}
487
488/// Names a body declares itself, so they are not cascaded from the
489/// parent: binding targets, element names, declared inputs and externs,
490/// module names, and cursors with their projection sources.
491fn body_declared(body: &[Statement], elements: &[(String, PortType)]) -> BTreeSet<String> {
492    let mut names: BTreeSet<String> = elements.iter().map(|(n, _)| n.clone()).collect();
493    names.insert("cycle".to_string());
494    for stmt in body {
495        match stmt {
496            Statement::Binding(b) => names.extend(b.targets.iter().cloned()),
497            Statement::InputDecl(d) => {
498                names.insert(d.name.clone());
499            }
500            Statement::ExternPort(p) => {
501                names.insert(p.name.clone());
502            }
503            Statement::ModuleDef(m) => {
504                names.insert(m.name.clone());
505            }
506            Statement::Cursor(c) => {
507                names.insert(c.name.clone());
508            }
509            Statement::Pragma { .. } => {}
510            Statement::For(f) => {
511                // A nested traversal's own elements are its business,
512                // but a producer it binds is visible after it.
513                let _ = f;
514            }
515            Statement::Tile(t) => {
516                names.insert(t.name.clone());
517            }
518        }
519    }
520    names
521}
522
523/// Wires a tile's holes and branch conditions reference.
524fn tile_references(pieces: &[super::ast::TilePiece], out: &mut BTreeSet<String>) {
525    use super::ast::TilePiece;
526    use super::refs::collect_expr_refs;
527    for piece in pieces {
528        match piece {
529            TilePiece::Static(_) => {}
530            TilePiece::Hole(h) => collect_expr_refs(&h.expr, out),
531            TilePiece::Projection { body, .. } => tile_references(body, out),
532            TilePiece::Branch {
533                cond,
534                then,
535                otherwise,
536                ..
537            } => {
538                collect_expr_refs(cond, out);
539                tile_references(then, out);
540                if let Some(o) = otherwise {
541                    tile_references(o, out);
542                }
543            }
544        }
545    }
546}
547
548/// Names a body references, gathered from binding expressions, cursor
549/// constructors and `over` clauses, extern defaults, and nested `for`
550/// bodies. Nested bodies contribute their outer references so the
551/// cascade reaches through every level.
552fn body_references(body: &[Statement], out: &mut BTreeSet<String>) {
553    use super::refs::collect_expr_refs;
554    for stmt in body {
555        match stmt {
556            Statement::Binding(b) => collect_expr_refs(&b.value, out),
557            Statement::ExternPort(p) => {
558                if let Some(d) = &p.default {
559                    collect_expr_refs(d, out);
560                }
561            }
562            Statement::Cursor(c) => {
563                collect_expr_refs(&c.constructor, out);
564                if let Some(over) = &c.over {
565                    collect_expr_refs(over, out);
566                }
567            }
568            Statement::For(f) => {
569                let mut inner = BTreeSet::new();
570                body_references(&f.body, &mut inner);
571                let own = body_declared(&f.body, &[]);
572                let elems: BTreeSet<String> = f.source.element_names().into_iter().collect();
573                for n in inner {
574                    if !own.contains(&n) && !elems.contains(&n) {
575                        out.insert(n);
576                    }
577                }
578            }
579            Statement::Tile(t) => tile_references(&t.pieces, out),
580            Statement::InputDecl(_) | Statement::ModuleDef(_) | Statement::Pragma { .. } => {}
581        }
582    }
583}
584
585/// Build the child file for a traversal body: an implicit `cycle`
586/// input, one extern per element, one cascade extern per outer wire
587/// the body references and the parent exposes, then the body itself.
588/// Returns the file and the cascade list.
589pub fn child_file(
590    f: &ForStmt,
591    comprehension: &Comprehension,
592    elements: &[(String, PortType)],
593    type_of: &dyn Fn(&str) -> Option<PortType>,
594) -> Result<(PolydatFile, Vec<(String, PortType)>), String> {
595    // §7: a body may not declare a coordinate other than `cycle`.
596    for stmt in &f.body {
597        if let Statement::InputDecl(d) = stmt
598            && d.name != "cycle"
599        {
600            return Err(format!(
601                "`for {}` at line {}, col {}: a traversal body cannot declare input '{}'; only `cycle` is a coordinate inside a body, and the comprehension supplies the rest",
602                f.source.to_text(),
603                f.span.line,
604                f.span.col,
605                d.name
606            ));
607        }
608    }
609    let declared = body_declared(&f.body, elements);
610    let mut referenced = BTreeSet::new();
611    body_references(&f.body, &mut referenced);
612    // Outer wires the comprehension's own sources reference through
613    // `{name}` also cascade, so the tuple evaluation sees them.
614    referenced.extend(comprehension.referenced_source_names());
615
616    let mut cascade = Vec::new();
617    for name in referenced {
618        if declared.contains(&name) {
619            continue;
620        }
621        let ty = type_of(&name);
622        if let Some(ty) = ty {
623            cascade.push((name, ty));
624        }
625        // Names the parent does not expose stay unresolved; the child
626        // compile reports them as unknown wires with the body's spans.
627    }
628
629    let span = f.span;
630    let mut statements = Vec::with_capacity(f.body.len() + elements.len() + cascade.len() + 1);
631    if !f.body.iter().any(|s| matches!(s, Statement::InputDecl(_))) {
632        statements.push(Statement::InputDecl(InputDecl {
633            name: "cycle".into(),
634            ty: Some("u64".into()),
635            span,
636        }));
637    }
638    for (name, ty) in elements {
639        statements.push(Statement::ExternPort(ExternPort {
640            name: name.clone(),
641            typ: ty.to_keyword().to_string(),
642            default: None,
643            span,
644        }));
645    }
646    for (name, ty) in &cascade {
647        statements.push(Statement::ExternPort(ExternPort {
648            name: name.clone(),
649            typ: ty.to_keyword().to_string(),
650            default: None,
651            span,
652        }));
653    }
654    statements.extend(f.body.iter().cloned());
655    Ok((PolydatFile { statements }, cascade))
656}