Skip to main content

polydat_core/iteration/comprehension/
runtime.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Runtime evaluator — walks an algebra [`Comprehension`] AST
5//! against a [`Lookup`] scope (a `PolydatKernel` or a `Layered`
6//! view) to produce typed coordinate tuples.
7//!
8//! ## Why this is separate from the static IR interpreter
9//!
10//! The static IR interpreter (`super::ir::interpreter`) walks
11//! a compiled stack-machine program over fully-statically-
12//! resolvable `Source` variants (`IntRange`, `Literal`). It
13//! has no notion of a runtime parent kernel, which is correct
14//! for the consumption surfaces it serves
15//! (comprehension_forms.md §9.5).
16//!
17//! Runtime comprehension evaluation is fundamentally different:
18//!
19//! - **Source-text evaluation requires the parent kernel.**
20//!   `Source::Generator { expr: "pre_{outer}" }` and
21//!   `Source::WorkloadParamList { name }` resolve against the
22//!   parent kernel's chain — `{outer}` substitution via
23//!   [`interpolate_via_kernel`](crate::kernel::interp::interpolate_via_kernel), `kernel.lookup(name)` for
24//!   workload params.
25//! - **Cartesian is dependent-tuple, not independent.** Clause
26//!   N's spec text may reference iter-vars from clauses
27//!   1..N-1. Prior-axis values are layered in front of the
28//!   scope (`Layered`) so each clause evaluates against the
29//!   correct context: the dependent product of
30//!   comprehension_forms.md §3.2.
31//! - **Filter predicates evaluate against per-tuple scopes.**
32//!   A [`CompiledPredicate`] parses the predicate once and tests
33//!   each tuple: its boolean structure over the tuple's values
34//!   directly, and any richer sub-expression interpolated against
35//!   a `Layered` view of the scope and evaluated as a Polydat
36//!   expression, charged to the scope's ledger.
37//!
38//! All three rest on the kernel's own primitives; this evaluator
39//! is the algebra-typed entry point to them.
40//!
41//! ## What this owns
42//!
43//! [`evaluate_indexed`] is the public surface:
44//! `(algebra AST + scope) → IndexedTuples`, the tuples addressed by
45//! position and computed when asked for; [`evaluate_for_iteration`]
46//! computes them all. The tuples carry polydat [`Value`]s, which the
47//! caller binds into a kernel over the body's program, one per tuple.
48//! [`evaluate_for_iteration_materialized`] is the reference the
49//! index-addressed evaluator is held to: every node materializes.
50//!
51//! Order modifiers route through `Strategy::select`
52//! (comprehension_forms.md §10.7.8): each node returns its tuples
53//! paired with the [`IndexFn`] they satisfy, and the Order node
54//! selects positions from that shape and the tuple count. An order
55//! over a filter selects through `Strategy::select_surviving`, ranking
56//! the survivors by their positions in the filter's input (§5 V5). V4
57//! fires again at this site, against the evaluated shape. An order's
58//! own output is addressed through its selection, one axis as long as
59//! the selection (§3.6), so an order over an order selects from it.
60//!
61//! ## What this does NOT own
62//!
63//! - Per-iteration kernel construction. The evaluator returns
64//!   tuples; the caller binds each into a kernel over the body's
65//!   program — `TraversalStream` for a `for` statement, the
66//!   renderer for a tile's projection.
67
68#[cfg(test)]
69use std::sync::Arc;
70
71use crate::ast::Value;
72use crate::iteration::comprehension::ast::Comprehension;
73use crate::iteration::comprehension::cardinality::{Interval, ProductMeasure};
74use crate::iteration::comprehension::eval_source::{EvalContext, NoneRead, evaluate_reading};
75use crate::iteration::comprehension::measure::AxisMeasure;
76use crate::iteration::comprehension::metadata::{IndexFn, cycle_length};
77use crate::iteration::comprehension::predicate::CompiledPredicate;
78use crate::iteration::comprehension::source::Source;
79use crate::iteration::comprehension::strategies::{Selection, ranked_filter, shape_input};
80use crate::iteration::comprehension::strategy::StrategyName;
81#[cfg(test)]
82use crate::kernel::PolydatKernel;
83use crate::kernel::interp::Lookup;
84
85/// Runtime tuple type — polydat-Value-based to preserve Ext
86/// typing (Partition / Json / etc.) through the iteration
87/// pipeline. The algebra layer's
88/// [`Tuple`](crate::iteration::comprehension::strategies::Tuple) uses
89/// [`TupleValue`](crate::iteration::comprehension::strategies::TupleValue)
90/// which is scalar-only; this `RuntimeTuple`
91/// is what the executor actually wants for per-iteration
92/// kernel binding via [`PolydatKernel::for_iteration`](crate::kernel::PolydatKernel::for_iteration).
93pub type RuntimeTuple = Vec<(String, Value)>;
94
95/// Per-node result of the runtime walker.
96///
97/// `tuples` is the materialized stream in source order
98/// (matches the runtime walker's natural enumeration — head
99/// axis varies slowest in cartesian, sequential in union,
100/// lockstep in zip). `index_fn` is the addressing scheme the
101/// stream satisfies; `None` when the stream is non-addressable
102/// (a filter's output, a dependent cartesian whose axes vary, a
103/// truncated `Lex` order over either).
104struct EvaluatedNode {
105    tuples: Vec<RuntimeTuple>,
106    index_fn: Option<IndexFn>,
107}
108
109/// Errors the runtime evaluator surfaces.
110#[derive(Debug, Clone)]
111pub enum RuntimeError {
112    /// Source evaluation failed (interpolation error,
113    /// eval_const_expr error, unsupported source shape, etc.).
114    SourceEval {
115        /// The clause's element name.
116        var: String,
117        /// The source text.
118        source: String,
119        /// The underlying reason.
120        message: String,
121    },
122    /// Filter predicate evaluation failed.
123    FilterEval {
124        /// The predicate text.
125        predicate: String,
126        /// The underlying reason.
127        message: String,
128    },
129    /// Strategy application failed.
130    OrderEval {
131        /// The strategy applied.
132        strategy: StrategyName,
133        /// The underlying reason.
134        message: String,
135    },
136    /// V4 (comprehension_forms.md §5) violation — strategy rejects
137    /// the input's addressing shape at invocation time (§10.7.8).
138    StrategyRejectsInput {
139        /// The strategy applied.
140        strategy: StrategyName,
141        /// The input's addressing scheme, if one was claimed.
142        index_fn: Option<IndexFn>,
143    },
144    /// A shape the runtime evaluator cannot evaluate: a discrete
145    /// clause referencing an axis beside a continuous one in a
146    /// sampled space, a cartesian or union of more than 2^64 tuples,
147    /// or a source asking for a scope the evaluator did not supply.
148    UnsupportedShape(String),
149    /// A strict zip's operands have different lengths. The traversal
150    /// evaluators report it at open; a stream reports it when one
151    /// operand ends before the others, after the tuples before it.
152    ZipLengthMismatch {
153        /// Each operand's tuple count, in operand order.
154        lengths: Vec<u64>,
155    },
156}
157
158impl std::fmt::Display for RuntimeError {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            RuntimeError::SourceEval {
162                var,
163                source,
164                message,
165            } => {
166                write!(f, "for_each clause '{var} in {source}': {message}")
167            }
168            RuntimeError::FilterEval { predicate, message } => {
169                write!(f, "comprehension filter '{predicate}': {message}")
170            }
171            RuntimeError::OrderEval { strategy, message } => {
172                write!(f, "order strategy {strategy:?}: {message}")
173            }
174            RuntimeError::StrategyRejectsInput { strategy, index_fn } => write!(
175                f,
176                "order strategy {strategy:?} rejects input shape {index_fn:?} \
177                 (V4: per-strategy IndexFn contract; see comprehension_forms.md §3.6's \
178                 strategy table)"
179            ),
180            RuntimeError::UnsupportedShape(msg) => write!(f, "{msg}"),
181            RuntimeError::ZipLengthMismatch { lengths } => {
182                write!(f, "zip strict: child lengths differ ({lengths:?})")
183            }
184        }
185    }
186}
187
188impl std::error::Error for RuntimeError {}
189
190/// Evaluate a comprehension against a scope and produce the
191/// typed coordinate-tuple list.
192///
193/// `scope` is where names resolve: the body's kernel with the
194/// parent's cascaded wires, or any other [`Lookup`]. A clause with no
195/// values yields no tuples, which is what an empty clause means.
196///
197/// This is [`evaluate_indexed`] with every tuple computed.
198pub fn evaluate_for_iteration(
199    comp: &Comprehension,
200    scope: &dyn Lookup,
201) -> Result<Vec<RuntimeTuple>, RuntimeError> {
202    evaluate_indexed(comp, scope).map(|t| t.to_vec())
203}
204
205/// [`evaluate_for_iteration`], and what each leaf clause yielded on the
206/// way ([`ClauseYield`]).
207///
208/// The counts come off the evaluation that already happened — the
209/// evaluator stands at each clause holding its name, its source and its
210/// values — so nothing is evaluated twice to produce them, and the
211/// record is one entry per leaf rather than one per tuple.
212///
213/// This is how a host says something about an empty clause. Emptiness
214/// stays a legal value here and the policy stays with the caller: read
215/// the clauses whose `evaluations` is non-zero and whose `values` is
216/// zero, and warn, fail, or ignore as that host requires. A clause the
217/// construction could already count as empty is a validation warning
218/// instead ([`super::validate::ValidationWarning::EmptySource`]).
219pub fn evaluate_for_iteration_reported(
220    comp: &Comprehension,
221    scope: &dyn Lookup,
222) -> Result<EvaluatedIteration, RuntimeError> {
223    evaluate_for_iteration_with_none_reads(comp, scope).map(|(evaluated, _)| evaluated)
224}
225
226/// [`evaluate_for_iteration_reported`], and for each clause the names
227/// whose reads of None made an evaluation of its source yield nothing
228/// ([`NoneReads`]).
229///
230/// Outside `pragma strict`, a source that reads a name nothing binds,
231/// or a name bound to None, yields nothing (comprehension_forms.md §5
232/// V3). A host that holds such a name as an error, naming the clause,
233/// reads the clause's entry here instead of checking the scope again.
234pub fn evaluate_for_iteration_with_none_reads(
235    comp: &Comprehension,
236    scope: &dyn Lookup,
237) -> Result<(EvaluatedIteration, NoneReads), RuntimeError> {
238    let mut state = EvalState::new(comp, scope);
239    let (node, _) = state.index_node(comp, &[])?;
240    let tuples = IndexedTuples { node }.to_vec();
241    Ok(state.finish(tuples))
242}
243
244/// Evaluate a comprehension against a scope to its tuples addressed by
245/// position (comprehension_forms.md §10.2 R2).
246///
247/// Every source is evaluated here, so every source error surfaces
248/// here, but a tuple is computed only when it is asked for. A clause
249/// over a range holds its bounds, a clause over any other source holds
250/// its values, an independent cartesian, a zip, and a union hold their
251/// operands, and an order holds its operand and the positions its
252/// strategy selected from the operand's shape. A node with no closed
253/// form over its operands holds its tuples: a filter's survivors, a
254/// cartesian whose sources reference an earlier axis, and an order
255/// that samples a continuous space. `order halton/100` over a large
256/// product therefore holds the product's axes and 100 positions, and
257/// over a filter of that product it holds the axes and the positions of
258/// the survivors it keeps.
259///
260/// The tuples are exactly those [`evaluate_for_iteration_materialized`]
261/// produces, in the same order, and the clause yields it reports are
262/// the same.
263pub fn evaluate_indexed(
264    comp: &Comprehension,
265    scope: &dyn Lookup,
266) -> Result<IndexedTuples, RuntimeError> {
267    let mut state = EvalState::new(comp, scope);
268    let (node, _) = state.index_node(comp, &[])?;
269    Ok(IndexedTuples { node })
270}
271
272/// The reference evaluator: every node materializes its tuples, and a
273/// cartesian evaluates each later axis once per tuple of the axes
274/// before it.
275///
276/// [`evaluate_indexed`] must yield exactly these tuples in exactly
277/// this order, and report the same clause yields; the equivalence
278/// harness compares the two over every comprehension shape.
279pub fn evaluate_for_iteration_materialized(
280    comp: &Comprehension,
281    scope: &dyn Lookup,
282) -> Result<EvaluatedIteration, RuntimeError> {
283    evaluate_for_iteration_materialized_with_none_reads(comp, scope).map(|(evaluated, _)| evaluated)
284}
285
286/// [`evaluate_for_iteration_materialized`], and the clauses' None reads
287/// as [`evaluate_for_iteration_with_none_reads`] reports them; the
288/// equivalence harness compares the two.
289pub fn evaluate_for_iteration_materialized_with_none_reads(
290    comp: &Comprehension,
291    scope: &dyn Lookup,
292) -> Result<(EvaluatedIteration, NoneReads), RuntimeError> {
293    let mut state = EvalState::new(comp, scope);
294    let tuples = state.evaluate_node(comp, &[])?.tuples;
295    Ok(state.finish(tuples))
296}
297
298/// A comprehension's tuples addressed by position: each tuple is
299/// computed from its position when asked for ([`evaluate_indexed`]).
300#[derive(Debug, Clone)]
301pub struct IndexedTuples {
302    node: Indexed,
303}
304
305impl IndexedTuples {
306    /// How many tuples there are.
307    pub fn len(&self) -> u64 {
308        self.node.len()
309    }
310
311    /// Whether there are no tuples.
312    pub fn is_empty(&self) -> bool {
313        self.len() == 0
314    }
315
316    /// The tuple at position `i`, or `None` past the end.
317    pub fn get(&self, i: u64) -> Option<RuntimeTuple> {
318        if i >= self.len() {
319            return None;
320        }
321        let mut out = RuntimeTuple::new();
322        self.node.append_at(i, &mut out);
323        Some(out)
324    }
325
326    /// Every tuple, in order.
327    pub fn iter(&self) -> impl Iterator<Item = RuntimeTuple> + '_ {
328        (0..self.len()).filter_map(|i| self.get(i))
329    }
330
331    /// Every tuple, computed now.
332    pub fn to_vec(&self) -> Vec<RuntimeTuple> {
333        self.iter().collect()
334    }
335}
336
337/// One node of an evaluated comprehension, answering the tuple at a
338/// position.
339#[derive(Debug, Clone)]
340enum Indexed {
341    /// Materialized tuples: a node with no closed form over its
342    /// operands.
343    Tuples(Vec<RuntimeTuple>),
344    /// One clause's values.
345    Clause { name: String, values: ClauseValues },
346    /// An independent cartesian: the children's tuples at the
347    /// mixed-radix digits of the position, the last child least
348    /// significant.
349    Product {
350        children: Vec<Indexed>,
351        lens: Vec<u64>,
352        len: u64,
353    },
354    /// A strict or truncating zip: every child's tuple at the position.
355    Lockstep { children: Vec<Indexed>, len: u64 },
356    /// A cycle zip: every child's tuple at the position modulo its
357    /// length. An empty child empties the zip, so no position reaches
358    /// one.
359    Cycle { children: Vec<Indexed>, len: u64 },
360    /// A union: the child whose segment holds the position.
361    Concat { children: Vec<Indexed>, len: u64 },
362    /// An order: the child's tuple at the selected position.
363    Select {
364        child: Box<Indexed>,
365        selection: Selection,
366    },
367}
368
369/// A clause's values, each computed from its position.
370#[derive(Debug, Clone)]
371enum ClauseValues {
372    /// `lo, lo + step, …`, `len` of them: an integer range.
373    Range { lo: i64, step: i64, len: u64 },
374    /// Any other source's evaluated values.
375    List(Vec<Value>),
376}
377
378impl ClauseValues {
379    fn len(&self) -> u64 {
380        match self {
381            ClauseValues::Range { len, .. } => *len,
382            ClauseValues::List(values) => values.len() as u64,
383        }
384    }
385
386    fn at(&self, i: u64) -> Value {
387        match self {
388            // A range's values are unsigned coordinates, as its
389            // evaluated source gives them.
390            ClauseValues::Range { lo, step, .. } => {
391                Value::U64((i128::from(*lo) + i128::from(i) * i128::from(*step)) as i64 as u64)
392            }
393            ClauseValues::List(values) => values[i as usize].clone(),
394        }
395    }
396}
397
398impl Indexed {
399    fn len(&self) -> u64 {
400        match self {
401            Indexed::Tuples(tuples) => tuples.len() as u64,
402            Indexed::Clause { values, .. } => values.len(),
403            Indexed::Product { len, .. }
404            | Indexed::Lockstep { len, .. }
405            | Indexed::Cycle { len, .. }
406            | Indexed::Concat { len, .. } => *len,
407            Indexed::Select { selection, .. } => selection.len(),
408        }
409    }
410
411    /// Append the bindings of the tuple at `i`, which is below
412    /// [`Self::len`], to `out`.
413    fn append_at(&self, i: u64, out: &mut RuntimeTuple) {
414        match self {
415            Indexed::Tuples(tuples) => out.extend(tuples[i as usize].iter().cloned()),
416            Indexed::Clause { name, values } => out.push((name.clone(), values.at(i))),
417            Indexed::Product { children, lens, .. } => {
418                let mut digits = vec![0u64; lens.len()];
419                let mut rest = i;
420                for (d, len) in digits.iter_mut().zip(lens).rev() {
421                    *d = rest % len;
422                    rest /= len;
423                }
424                for (child, d) in children.iter().zip(digits) {
425                    child.append_at(d, out);
426                }
427            }
428            Indexed::Lockstep { children, .. } => {
429                for child in children {
430                    child.append_at(i, out);
431                }
432            }
433            Indexed::Cycle { children, .. } => {
434                for child in children {
435                    child.append_at(i % child.len(), out);
436                }
437            }
438            Indexed::Concat { children, .. } => {
439                let mut offset = i;
440                for child in children {
441                    let len = child.len();
442                    if offset < len {
443                        child.append_at(offset, out);
444                        return;
445                    }
446                    offset -= len;
447                }
448            }
449            Indexed::Select { child, selection } => {
450                if let Some(p) = selection.get(i) {
451                    child.append_at(p, out);
452                }
453            }
454        }
455    }
456}
457
458/// Internal walker state — the scope the recursive walker resolves
459/// names against, so it does not thread it through every call.
460/// What one leaf clause yielded over a whole traversal.
461///
462/// A clause under a cartesian is evaluated once per outer tuple, so
463/// emptiness is a property of the evaluations and not of the clause:
464///
465/// - `evaluations == 0` — never reached, because something outside it
466///   was empty first. Not the cause; the cause is an earlier clause.
467/// - `evaluations > 0 && values == 0` — reached and yielded nothing
468///   every time. This is the clause a diagnostic should name.
469/// - `values > 0` — yielded.
470///
471/// An empty stream is a legal value of the algebra, so none of these
472/// is an error. What a host does about one is the host's policy.
473///
474/// Why an evaluation yielded nothing, when the reason is a read of
475/// None, is reported beside these records by
476/// [`evaluate_for_iteration_with_none_reads`] ([`NoneReads`]).
477#[derive(Debug, Clone, PartialEq, Eq)]
478pub struct ClauseYield {
479    /// The clause's element name.
480    pub var: String,
481    /// The source's canonical text, when it has one.
482    pub source: Option<String>,
483    /// How many times this clause was evaluated.
484    pub evaluations: usize,
485    /// How many values it produced, summed over those evaluations.
486    pub values: usize,
487}
488
489/// Per leaf clause, the names whose reads of None made an evaluation of
490/// its source yield nothing (comprehension_forms.md §5 V3,
491/// none_semantics.md Rule 1): the names the source read, after
492/// composition, that nothing binds or that are bound to None, each with
493/// which of the two it was. A host that holds an unbound name in a
494/// source as an error reads it here.
495///
496/// Indexed like [`EvaluatedIteration::clauses`]: entry `i` belongs to
497/// clause `i`. Each list holds a name once over every evaluation, in
498/// [`NoneRead`]'s order, and is empty when no evaluation read None.
499#[derive(Debug, Clone, Default, PartialEq, Eq)]
500pub struct NoneReads {
501    per_clause: Vec<Vec<NoneRead>>,
502}
503
504impl NoneReads {
505    /// The None reads of clause `index` of [`EvaluatedIteration::clauses`];
506    /// empty past the end.
507    pub fn clause(&self, index: usize) -> &[NoneRead] {
508        self.per_clause.get(index).map_or(&[], Vec::as_slice)
509    }
510
511    /// Every clause's None reads, in clause order.
512    pub fn iter(&self) -> impl Iterator<Item = &[NoneRead]> {
513        self.per_clause.iter().map(Vec::as_slice)
514    }
515
516    /// Whether no evaluation of any clause read None.
517    pub fn is_empty(&self) -> bool {
518        self.per_clause.iter().all(Vec::is_empty)
519    }
520}
521
522/// A traversal's tuples, and what each leaf clause yielded reaching
523/// them ([`ClauseYield`]), in the tree order of the comprehension.
524#[derive(Debug, Clone)]
525pub struct EvaluatedIteration {
526    /// The coordinate tuples, as [`evaluate_for_iteration`] returns.
527    pub tuples: Vec<RuntimeTuple>,
528    /// Every leaf clause of the comprehension, evaluated or not.
529    pub clauses: Vec<ClauseYield>,
530}
531
532struct EvalState<'a> {
533    /// Where names resolve: the body's kernel with the parent's cascaded
534    /// wires bound; a tuple's own bindings are layered in front per use.
535    scope: &'a dyn Lookup,
536    /// Per leaf clause, what it yielded. Seeded from the tree before
537    /// evaluation so a clause never reached is present with zero
538    /// evaluations rather than absent.
539    yields: Vec<ClauseYield>,
540    /// Per leaf clause, parallel to `yields`, the names whose reads of
541    /// None made an evaluation yield nothing.
542    none_reads: Vec<Vec<NoneRead>>,
543    /// Leaf identity → index into `yields`. Keyed by the address of the
544    /// clause's own `Source` inside the borrowed tree, which is what
545    /// distinguishes two clauses that share a name across the branches
546    /// of a union.
547    by_leaf: std::collections::HashMap<usize, usize>,
548    /// How many evaluations the node being evaluated stands for. The
549    /// reference evaluator evaluates a cartesian's later axis once per
550    /// tuple of the axes before it; the index-addressed evaluator
551    /// evaluates an independent axis once, which stands for that many.
552    mult: usize,
553}
554
555impl<'a> EvalState<'a> {
556    /// Seed one entry per leaf clause, in tree order.
557    fn new(comp: &Comprehension, scope: &'a dyn Lookup) -> Self {
558        let mut state = EvalState {
559            scope,
560            yields: Vec::new(),
561            none_reads: Vec::new(),
562            by_leaf: std::collections::HashMap::new(),
563            mult: 1,
564        };
565        state.enumerate_leaves(comp);
566        state
567    }
568
569    /// The evaluation's report: the tuples with each clause's yield, and
570    /// each clause's None reads beside it.
571    fn finish(self, tuples: Vec<RuntimeTuple>) -> (EvaluatedIteration, NoneReads) {
572        (
573            EvaluatedIteration {
574                tuples,
575                clauses: self.yields,
576            },
577            NoneReads {
578                per_clause: self.none_reads,
579            },
580        )
581    }
582
583    fn enumerate_leaves(&mut self, node: &Comprehension) {
584        match node {
585            Comprehension::Clause { name, source } => {
586                self.by_leaf
587                    .insert(std::ptr::from_ref(source) as usize, self.yields.len());
588                self.yields.push(ClauseYield {
589                    var: name.clone(),
590                    source: source.to_text(),
591                    evaluations: 0,
592                    values: 0,
593                });
594                self.none_reads.push(Vec::new());
595            }
596            Comprehension::Cartesian { children }
597            | Comprehension::Zip { children, .. }
598            | Comprehension::Union { children } => {
599                for child in children {
600                    self.enumerate_leaves(child);
601                }
602            }
603            Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
604                self.enumerate_leaves(child);
605            }
606        }
607    }
608
609    /// Record the evaluations of a leaf, the values each produced, and
610    /// the names whose reads of None made it yield nothing: one
611    /// evaluation standing for `mult` identical ones.
612    fn record_yield(&mut self, source: &Source, values: usize, reads_none: Vec<NoneRead>) {
613        if let Some(&i) = self.by_leaf.get(&(std::ptr::from_ref(source) as usize)) {
614            let clause = &mut self.yields[i];
615            clause.evaluations = clause.evaluations.saturating_add(self.mult);
616            clause.values = clause
617                .values
618                .saturating_add(values.saturating_mul(self.mult));
619            if !reads_none.is_empty() {
620                let reads = &mut self.none_reads[i];
621                reads.extend(reads_none);
622                reads.sort();
623                reads.dedup();
624            }
625        }
626    }
627}
628
629impl EvalState<'_> {
630    fn evaluate_node(
631        &mut self,
632        node: &Comprehension,
633        prefix: &[(String, Value)],
634    ) -> Result<EvaluatedNode, RuntimeError> {
635        match node {
636            Comprehension::Clause { name, source } => self.evaluate_clause(name, source, prefix),
637            Comprehension::Cartesian { children } => self.evaluate_cartesian(children, prefix),
638            Comprehension::Zip { children, mode } => self.evaluate_zip(children, *mode, prefix),
639            Comprehension::Union { children } => self.evaluate_union(children, prefix),
640            Comprehension::Filter { child, predicate } => {
641                let inner = self.evaluate_node(child, prefix)?;
642                self.apply_filter(inner, predicate)
643            }
644            Comprehension::Order {
645                child,
646                strategy,
647                truncation,
648                seed,
649            } => {
650                // A strategy that selects from the shape reads through
651                // the untruncated orders under it (§7.4 O1).
652                let child = shape_input(child, *strategy);
653                // A continuous axis has no tuples of its own: an order
654                // over one samples the child's space, discrete axes by
655                // position and continuous axes through their measures
656                // (comprehension_forms.md §10.2 R2).
657                if has_continuous_axis(child) {
658                    let sampled =
659                        self.sample_space(child, prefix, *strategy, *truncation, *seed)?;
660                    return Ok(EvaluatedNode {
661                        index_fn: Some(selected(sampled.tuples.len() as u64)),
662                        tuples: sampled.tuples,
663                    });
664                }
665                // A non-`Lex` order over a filter ranks the survivors by
666                // their positions in the filter's input (§5 V5).
667                if let Some((child, predicate)) = ranked_filter(child, *strategy) {
668                    let inner = self.evaluate_node(child, prefix)?;
669                    let predicate = CompiledPredicate::new(predicate);
670                    let mut survivors = Vec::new();
671                    for (p, tuple) in inner.tuples.iter().enumerate() {
672                        if predicate.keeps(tuple, self.scope)? {
673                            survivors.push(p as u64);
674                        }
675                    }
676                    let selection = surviving_selection(
677                        *strategy,
678                        inner.index_fn.as_ref(),
679                        inner.tuples.len() as u64,
680                        *truncation,
681                        *seed,
682                        &survivors,
683                    )?;
684                    return Ok(EvaluatedNode {
685                        tuples: selection
686                            .iter()
687                            .map(|p| inner.tuples[p as usize].clone())
688                            .collect(),
689                        index_fn: Some(selected(selection.len())),
690                    });
691                }
692                let inner = self.evaluate_node(child, prefix)?;
693                self.apply_order(inner, *strategy, *truncation, *seed)
694            }
695        }
696    }
697
698    /// Evaluate one clause's source against the prefix and record what
699    /// it yielded.
700    fn evaluate_source(
701        &mut self,
702        name: &str,
703        source: &Source,
704        prefix: &[(String, Value)],
705    ) -> Result<crate::iteration::comprehension::eval_source::EvaluatedSource, RuntimeError> {
706        let ctx = EvalContext {
707            var_name: name,
708            scope: self.scope,
709            prefix,
710        };
711        let (evaluated, reads_none) =
712            evaluate_reading(source, Some(&ctx)).map_err(|e| match e {
713                crate::iteration::comprehension::eval_source::EvalError::EvalFailed {
714                    var,
715                    source,
716                    message,
717                } => RuntimeError::SourceEval {
718                    var,
719                    source,
720                    message,
721                },
722                crate::iteration::comprehension::eval_source::EvalError::NeedsContext => {
723                    RuntimeError::UnsupportedShape(format!(
724                        "clause '{name}': source requires kernel context but evaluator \
725                             provided none — internal bug in runtime walker"
726                    ))
727                }
728            })?;
729        self.record_yield(source, evaluated.values.len(), reads_none);
730        Ok(evaluated)
731    }
732
733    fn evaluate_clause(
734        &mut self,
735        name: &str,
736        source: &Source,
737        prefix: &[(String, Value)],
738    ) -> Result<EvaluatedNode, RuntimeError> {
739        let evaluated = self.evaluate_source(name, source, prefix)?;
740
741        if evaluated.values.is_empty() {
742            // A clause with no values yields no tuples, which is what
743            // an empty clause means: a legal value of the algebra, not
744            // an error and not a policy decision made here. The
745            // evaluation is counted above, so a host that wants to say
746            // something about it reads `ClauseYield` off the result.
747            return Ok(EvaluatedNode {
748                tuples: Vec::new(),
749                index_fn: Some(evaluated.index_fn),
750            });
751        }
752        let tuples: Vec<RuntimeTuple> = evaluated
753            .values
754            .into_iter()
755            .map(|v| vec![(name.to_string(), v)])
756            .collect();
757        Ok(EvaluatedNode {
758            tuples,
759            index_fn: Some(evaluated.index_fn),
760        })
761    }
762
763    fn evaluate_cartesian(
764        &mut self,
765        children: &[Comprehension],
766        prefix: &[(String, Value)],
767    ) -> Result<EvaluatedNode, RuntimeError> {
768        if children.is_empty() {
769            return Ok(EvaluatedNode {
770                tuples: vec![Vec::new()],
771                index_fn: Some(IndexFn::Lattice {
772                    axis_sizes: vec![1],
773                }),
774            });
775        }
776        let mut child_index_fns: Vec<(Option<IndexFn>, u64)> = Vec::with_capacity(children.len());
777        let mut dependent_observed = false;
778        let result_tuples = self.evaluate_cartesian_rec(
779            children.len(),
780            children,
781            prefix,
782            &mut child_index_fns,
783            &mut dependent_observed,
784        )?;
785
786        // Combined Lattice from observed per-clause cardinalities.
787        // Dependent cartesians produce children whose
788        // per-clause cardinality varies with the prefix — we
789        // can't claim a clean Lattice in that case, so the
790        // combined index_fn is None.
791        let combined = if dependent_observed {
792            None
793        } else {
794            let index_fns: Vec<Option<IndexFn>> =
795                child_index_fns.into_iter().map(|(idx, _)| idx).collect();
796            combine_cartesian_index_fn(&index_fns)
797        };
798        Ok(EvaluatedNode {
799            tuples: result_tuples,
800            index_fn: combined,
801        })
802    }
803
804    fn evaluate_cartesian_rec(
805        &mut self,
806        child_count: usize,
807        children: &[Comprehension],
808        prefix: &[(String, Value)],
809        child_index_fns: &mut Vec<(Option<IndexFn>, u64)>,
810        dependent_observed: &mut bool,
811    ) -> Result<Vec<RuntimeTuple>, RuntimeError> {
812        if children.is_empty() {
813            return Ok(vec![Vec::new()]);
814        }
815        let (head, tail) = children.split_first().unwrap();
816        let head_eval = self.evaluate_node(head, prefix)?;
817        let head_axis_len = head_eval.tuples.len() as u64;
818        // The head's position among the cartesian's children: the
819        // first evaluation at a position records its index_fn and
820        // tuple count, and a later evaluation with another count
821        // (a dependent cartesian) leaves the product no lattice.
822        let depth = child_count - children.len();
823        match child_index_fns.get(depth) {
824            None => child_index_fns.push((head_eval.index_fn.clone(), head_axis_len)),
825            Some((_, first)) if *first != head_axis_len => *dependent_observed = true,
826            Some(_) => {}
827        }
828
829        if tail.is_empty() {
830            return Ok(head_eval.tuples);
831        }
832        let mut out = Vec::new();
833        for head_tuple in head_eval.tuples {
834            let mut extended_prefix: Vec<(String, Value)> = prefix.to_vec();
835            extended_prefix.extend(head_tuple.iter().cloned());
836            let tail_tuples = self.evaluate_cartesian_rec(
837                child_count,
838                tail,
839                &extended_prefix,
840                child_index_fns,
841                dependent_observed,
842            )?;
843            for tail_tuple in tail_tuples {
844                let mut merged = head_tuple.clone();
845                merged.extend(tail_tuple);
846                out.push(merged);
847            }
848        }
849        Ok(out)
850    }
851
852    fn evaluate_zip(
853        &mut self,
854        children: &[Comprehension],
855        mode: crate::iteration::comprehension::strategy::ZipMode,
856        prefix: &[(String, Value)],
857    ) -> Result<EvaluatedNode, RuntimeError> {
858        use crate::iteration::comprehension::strategy::ZipMode;
859        if children.is_empty() {
860            return Ok(EvaluatedNode {
861                tuples: vec![Vec::new()],
862                index_fn: Some(IndexFn::Lockstep { length: 1 }),
863            });
864        }
865        let per_child: Vec<EvaluatedNode> = children
866            .iter()
867            .map(|c| self.evaluate_node(c, prefix))
868            .collect::<Result<_, _>>()?;
869        let lengths: Vec<usize> = per_child.iter().map(|n| n.tuples.len()).collect();
870        let iter_count = match mode {
871            ZipMode::Strict => {
872                let first = lengths.first().copied().unwrap_or(0);
873                if lengths.iter().any(|&n| n != first) {
874                    return Err(RuntimeError::ZipLengthMismatch {
875                        lengths: lengths.iter().map(|&n| n as u64).collect(),
876                    });
877                }
878                first
879            }
880            ZipMode::Truncate => lengths.iter().copied().min().unwrap_or(0),
881            ZipMode::Cycle => {
882                let counts: Vec<u64> = lengths.iter().map(|&n| n as u64).collect();
883                cycle_length(&counts) as usize
884            }
885        };
886        let mut tuples = Vec::with_capacity(iter_count);
887        for i in 0..iter_count {
888            let mut bindings: RuntimeTuple = Vec::new();
889            for (child, &len) in per_child.iter().zip(lengths.iter()) {
890                let idx = match mode {
891                    ZipMode::Cycle => i % len,
892                    _ => i,
893                };
894                bindings.extend(child.tuples[idx].iter().cloned());
895            }
896            tuples.push(bindings);
897        }
898        let index_fn = match mode {
899            ZipMode::Strict | ZipMode::Truncate => Some(IndexFn::Lockstep {
900                length: iter_count as u64,
901            }),
902            ZipMode::Cycle => Some(IndexFn::Modular {
903                axis_sizes: lengths.iter().map(|n| *n as u64).collect(),
904            }),
905        };
906        Ok(EvaluatedNode { tuples, index_fn })
907    }
908
909    fn evaluate_union(
910        &mut self,
911        children: &[Comprehension],
912        prefix: &[(String, Value)],
913    ) -> Result<EvaluatedNode, RuntimeError> {
914        let mut tuples = Vec::new();
915        let mut segment_sizes = Vec::with_capacity(children.len());
916        let mut all_segments_addressable = true;
917        for child in children {
918            let sub = self.evaluate_node(child, prefix)?;
919            segment_sizes.push(sub.tuples.len() as u64);
920            if sub.index_fn.is_none() {
921                all_segments_addressable = false;
922            }
923            tuples.extend(sub.tuples);
924        }
925        let index_fn = if all_segments_addressable {
926            Some(IndexFn::Concatenation { segment_sizes })
927        } else {
928            None
929        };
930        Ok(EvaluatedNode { tuples, index_fn })
931    }
932
933    fn apply_filter(
934        &mut self,
935        input: EvaluatedNode,
936        predicate: &str,
937    ) -> Result<EvaluatedNode, RuntimeError> {
938        let predicate = CompiledPredicate::new(predicate);
939        let mut out = Vec::with_capacity(input.tuples.len());
940        for tuple in input.tuples {
941            if predicate.keeps(&tuple, self.scope)? {
942                out.push(tuple);
943            }
944        }
945        // A filter's output has no index function
946        // (comprehension_forms.md §10.7.2).
947        Ok(EvaluatedNode {
948            tuples: out,
949            index_fn: None,
950        })
951    }
952
953    /// Sample an order over a space with a continuous axis
954    /// (comprehension_forms.md §10.2 R2). The child's clauses are the axes: a discrete
955    /// clause is evaluated to its values, a continuous clause keeps
956    /// its interval and measure. A sampling strategy (Halton, Sobol,
957    /// Lhs, Shuffle) draws `truncation` multi-indices over the
958    /// `Continuous` or `Hybrid` index function of those axes, and
959    /// each continuous coordinate is carried onto its interval by
960    /// its measure; `Extrema` takes the strata of the box, a
961    /// continuous axis contributing its two ends. Filters between
962    /// the order and its clauses apply to the drawn tuples, and a
963    /// sequence strategy keeps drawing until `truncation` tuples
964    /// pass, up to a bounded number of rounds.
965    fn sample_space(
966        &mut self,
967        child: &Comprehension,
968        prefix: &[(String, Value)],
969        strategy: StrategyName,
970        truncation: Option<u64>,
971        seed: Option<u64>,
972    ) -> Result<EvaluatedNode, RuntimeError> {
973        let mut space = SampleSpace::default();
974        self.collect_sample_space(child, prefix, &mut space, &mut Vec::new())?;
975        let discrete_axes: Vec<u64> = space
976            .axes
977            .iter()
978            .filter_map(|a| match a {
979                SampleAxis::Discrete(tuples) => Some(tuples.len() as u64),
980                SampleAxis::Continuous { .. } => None,
981            })
982            .collect();
983        if discrete_axes.contains(&0) {
984            return Ok(EvaluatedNode {
985                tuples: Vec::new(),
986                index_fn: None,
987            });
988        }
989        let (intervals, measures): (Vec<Interval>, Vec<ProductMeasure>) = space
990            .axes
991            .iter()
992            .filter_map(|a| match a {
993                SampleAxis::Continuous {
994                    interval, measure, ..
995                } => Some((
996                    interval.clone(),
997                    match measure {
998                        AxisMeasure::Uniform => ProductMeasure::Uniform,
999                        AxisMeasure::Named { name, .. } => ProductMeasure::Named(*name),
1000                    },
1001                )),
1002                SampleAxis::Discrete(_) => None,
1003            })
1004            .unzip();
1005        let sequence = !matches!(strategy, StrategyName::Extrema);
1006        // A sampling strategy lays a hybrid out discrete axes first
1007        // (`IndexFn::Hybrid`); Extrema's strata are lex in clause
1008        // order, so it takes a lattice in that order, a continuous
1009        // axis being its two ends.
1010        let index_fn = if !sequence {
1011            IndexFn::Lattice {
1012                axis_sizes: space
1013                    .axes
1014                    .iter()
1015                    .map(|a| match a {
1016                        SampleAxis::Discrete(tuples) => tuples.len() as u64,
1017                        SampleAxis::Continuous { .. } => 2,
1018                    })
1019                    .collect(),
1020            }
1021        } else if discrete_axes.is_empty() {
1022            IndexFn::Continuous {
1023                intervals,
1024                measure: ProductMeasure::Product(measures),
1025            }
1026        } else {
1027            IndexFn::Hybrid {
1028                discrete_axes,
1029                continuous_axes: intervals,
1030                measure: ProductMeasure::Product(measures),
1031            }
1032        };
1033
1034        let mut want = truncation;
1035        let mut rounds = 0;
1036        loop {
1037            let multi_indices = draw_sample(&index_fn, strategy, want, seed)?;
1038            let drawn = multi_indices.len() as u64;
1039            let tuples = multi_indices
1040                .iter()
1041                .map(|mi| space.realize(mi, strategy))
1042                .collect();
1043            let mut node = EvaluatedNode {
1044                tuples,
1045                index_fn: None,
1046            };
1047            for predicate in &space.predicates {
1048                node = self.apply_filter(node, predicate)?;
1049            }
1050            let (Some(n), Some(asked)) = (truncation, want) else {
1051                return Ok(node);
1052            };
1053            let enough = node.tuples.len() as u64 >= n;
1054            let exhausted = drawn < asked;
1055            if !sequence {
1056                return Ok(node);
1057            }
1058            if enough || exhausted || rounds >= SAMPLE_ROUNDS {
1059                node.tuples.truncate(n as usize);
1060                return Ok(node);
1061            }
1062
1063            want = Some(asked.saturating_mul(2));
1064            rounds += 1;
1065        }
1066    }
1067
1068    /// Walk `c` into `space`: clauses become axes in order, filters
1069    /// contribute their predicates, and any other node (a zip, union,
1070    /// or inner order) is evaluated and becomes one discrete axis of
1071    /// its tuples. `bound` is the names bound so far; a discrete
1072    /// clause may not reference one (a sampled cartesian is
1073    /// independent: comprehension_forms.md §5, V4 over a dependent
1074    /// cartesian).
1075    fn collect_sample_space(
1076        &mut self,
1077        c: &Comprehension,
1078        prefix: &[(String, Value)],
1079        space: &mut SampleSpace,
1080        bound: &mut Vec<String>,
1081    ) -> Result<(), RuntimeError> {
1082        let measure_error = |name: &str, message: String| RuntimeError::SourceEval {
1083            var: name.to_string(),
1084            source: "<continuous>".to_string(),
1085            message,
1086        };
1087        match c {
1088            Comprehension::Clause {
1089                name,
1090                source: Source::ContinuousInterval { interval, measure },
1091            } => {
1092                let measure =
1093                    AxisMeasure::from_product(measure, 0).map_err(|m| measure_error(name, m))?;
1094                space.axes.push(SampleAxis::Continuous {
1095                    name: name.clone(),
1096                    interval: interval.clone(),
1097                    measure,
1098                });
1099                bound.push(name.clone());
1100            }
1101            Comprehension::Clause {
1102                name,
1103                source:
1104                    Source::Distribution {
1105                        distribution,
1106                        support,
1107                        params,
1108                    },
1109            } => {
1110                let measure = AxisMeasure::named(*distribution, params)
1111                    .map_err(|m| measure_error(name, m))?;
1112                space.axes.push(SampleAxis::Continuous {
1113                    name: name.clone(),
1114                    interval: support.clone(),
1115                    measure,
1116                });
1117                bound.push(name.clone());
1118            }
1119            Comprehension::Clause { name, source } => {
1120                let references = c.source_names_read();
1121                if let Some(dep) = bound.iter().find(|b| references.contains(*b)) {
1122                    return Err(RuntimeError::UnsupportedShape(format!(
1123                        "clause '{name}' references '{dep}' beside a continuous axis; \
1124                         a sampled cartesian is independent (comprehension_forms.md §5, V4)"
1125                    )));
1126                }
1127                let node = self.evaluate_clause(name, source, prefix)?;
1128                space.axes.push(SampleAxis::Discrete(node.tuples));
1129                bound.push(name.clone());
1130            }
1131            Comprehension::Cartesian { children } => {
1132                for child in children {
1133                    self.collect_sample_space(child, prefix, space, bound)?;
1134                }
1135            }
1136            Comprehension::Filter { child, predicate } => {
1137                self.collect_sample_space(child, prefix, space, bound)?;
1138                space.predicates.push(predicate.clone());
1139            }
1140            Comprehension::Zip { .. }
1141            | Comprehension::Union { .. }
1142            | Comprehension::Order { .. } => {
1143                let node = self.evaluate_node(c, prefix)?;
1144                bound.extend(c.coordinate_names());
1145                space.axes.push(SampleAxis::Discrete(node.tuples));
1146            }
1147        }
1148        Ok(())
1149    }
1150
1151    fn apply_order(
1152        &mut self,
1153        input: EvaluatedNode,
1154        strategy: StrategyName,
1155        truncation: Option<u64>,
1156        seed: Option<u64>,
1157    ) -> Result<EvaluatedNode, RuntimeError> {
1158        // The strategy selects positions from the input's shape
1159        // alone (its `IndexFn` and tuple count), so the chosen tuples
1160        // are the input's tuples at those positions.
1161        let selection = order_selection(
1162            strategy,
1163            input.index_fn.as_ref(),
1164            input.tuples.len() as u64,
1165            truncation,
1166            seed,
1167        )?;
1168        let out = selection
1169            .iter()
1170            .map(|p| input.tuples[p as usize].clone())
1171            .collect();
1172        Ok(EvaluatedNode {
1173            tuples: out,
1174            index_fn: order_output(strategy, truncation, input.index_fn, selection.len()),
1175        })
1176    }
1177}
1178
1179/// An order's output addressed through its selection of `len` positions:
1180/// position `i` is the input's tuple at the `i`-th selected position, one
1181/// axis as long as the selection (comprehension_forms.md §3.6).
1182fn selected(len: u64) -> IndexFn {
1183    IndexFn::Lattice {
1184        axis_sizes: vec![len],
1185    }
1186}
1187
1188/// The addressing an order's output has, as the metadata algebra says
1189/// (§10.7.2): an untruncated `Lex` passes its input's through, a
1190/// truncated `Lex` selects a prefix of an addressable input's positions,
1191/// and any other order is addressed through its selection.
1192fn order_output(
1193    strategy: StrategyName,
1194    truncation: Option<u64>,
1195    input: Option<IndexFn>,
1196    len: u64,
1197) -> Option<IndexFn> {
1198    match (strategy, truncation) {
1199        (StrategyName::Lex, None) => input,
1200        (StrategyName::Lex, Some(_)) => input.map(|_| selected(len)),
1201        _ => Some(selected(len)),
1202    }
1203}
1204
1205/// The index-addressed evaluator ([`evaluate_indexed`]). Each node
1206/// returns its tuples addressed by position together with the
1207/// `IndexFn` the reference evaluator claims for it, which is what an
1208/// enclosing order's strategy routes on.
1209impl EvalState<'_> {
1210    fn index_node(
1211        &mut self,
1212        node: &Comprehension,
1213        prefix: &[(String, Value)],
1214    ) -> Result<(Indexed, Option<IndexFn>), RuntimeError> {
1215        match node {
1216            Comprehension::Clause { name, source } => self.index_clause(name, source, prefix),
1217            Comprehension::Cartesian { children } => self.index_cartesian(children, prefix),
1218            Comprehension::Zip { children, mode } => self.index_zip(children, *mode, prefix),
1219            Comprehension::Union { children } => self.index_union(children, prefix),
1220            Comprehension::Filter { child, predicate } => {
1221                // A filter keeps the tuples that pass; which ones is
1222                // known only by testing each, so its output holds them.
1223                let (inner, _) = self.index_node(child, prefix)?;
1224                let predicate = CompiledPredicate::new(predicate);
1225                let mut kept = Vec::new();
1226                let mut tuple = RuntimeTuple::new();
1227                for i in 0..inner.len() {
1228                    tuple.clear();
1229                    inner.append_at(i, &mut tuple);
1230                    if predicate.keeps(&tuple, self.scope)? {
1231                        kept.push(tuple.clone());
1232                    }
1233                }
1234                Ok((Indexed::Tuples(kept), None))
1235            }
1236            Comprehension::Order {
1237                child,
1238                strategy,
1239                truncation,
1240                seed,
1241            } => {
1242                let child = shape_input(child, *strategy);
1243                if has_continuous_axis(child) {
1244                    let sampled =
1245                        self.sample_space(child, prefix, *strategy, *truncation, *seed)?;
1246                    let len = sampled.tuples.len() as u64;
1247                    return Ok((Indexed::Tuples(sampled.tuples), Some(selected(len))));
1248                }
1249                // A non-`Lex` order over a filter holds the filter's
1250                // input and the survivors' positions in it, which the
1251                // strategy ranks by those positions (§5 V5): a barrier
1252                // sized by the survivors.
1253                if let Some((child, predicate)) = ranked_filter(child, *strategy) {
1254                    let (inner, index_fn) = self.index_node(child, prefix)?;
1255                    let predicate = CompiledPredicate::new(predicate);
1256                    let mut survivors = Vec::new();
1257                    let mut tuple = RuntimeTuple::new();
1258                    for p in 0..inner.len() {
1259                        tuple.clear();
1260                        inner.append_at(p, &mut tuple);
1261                        if predicate.keeps(&tuple, self.scope)? {
1262                            survivors.push(p);
1263                        }
1264                    }
1265                    let selection = surviving_selection(
1266                        *strategy,
1267                        index_fn.as_ref(),
1268                        inner.len(),
1269                        *truncation,
1270                        *seed,
1271                        &survivors,
1272                    )?;
1273                    let len = selection.len();
1274                    return Ok((
1275                        Indexed::Select {
1276                            child: Box::new(inner),
1277                            selection,
1278                        },
1279                        Some(selected(len)),
1280                    ));
1281                }
1282                let (inner, index_fn) = self.index_node(child, prefix)?;
1283                let selection = order_selection(
1284                    *strategy,
1285                    index_fn.as_ref(),
1286                    inner.len(),
1287                    *truncation,
1288                    *seed,
1289                )?;
1290                let len = selection.len();
1291                Ok((
1292                    Indexed::Select {
1293                        child: Box::new(inner),
1294                        selection,
1295                    },
1296                    order_output(*strategy, *truncation, index_fn, len),
1297                ))
1298            }
1299        }
1300    }
1301
1302    fn index_clause(
1303        &mut self,
1304        name: &str,
1305        source: &Source,
1306        prefix: &[(String, Value)],
1307    ) -> Result<(Indexed, Option<IndexFn>), RuntimeError> {
1308        let values = match source {
1309            // A range's values are its bounds; the source's own
1310            // evaluation would hold every value.
1311            Source::IntRange { lo, hi, step } => {
1312                let step = (*step).max(1);
1313                let len = if hi <= lo {
1314                    0
1315                } else {
1316                    ((i128::from(*hi) - i128::from(*lo)) as u128).div_ceil(step as u128) as u64
1317                };
1318                self.record_yield(source, len as usize, Vec::new());
1319                ClauseValues::Range { lo: *lo, step, len }
1320            }
1321            _ => {
1322                let evaluated = self.evaluate_source(name, source, prefix)?;
1323                let index_fn = evaluated.index_fn;
1324                return Ok((
1325                    Indexed::Clause {
1326                        name: name.to_string(),
1327                        values: ClauseValues::List(evaluated.values),
1328                    },
1329                    Some(index_fn),
1330                ));
1331            }
1332        };
1333        let len = values.len();
1334        Ok((
1335            Indexed::Clause {
1336                name: name.to_string(),
1337                values,
1338            },
1339            Some(IndexFn::Lattice {
1340                axis_sizes: vec![len],
1341            }),
1342        ))
1343    }
1344
1345    /// An independent cartesian evaluates each axis once, the
1346    /// evaluation standing for one per tuple of the axes before it,
1347    /// and stops at an empty axis as the reference evaluator does. A
1348    /// cartesian whose sources reference an earlier axis is evaluated
1349    /// by the reference evaluator and holds its tuples.
1350    fn index_cartesian(
1351        &mut self,
1352        children: &[Comprehension],
1353        prefix: &[(String, Value)],
1354    ) -> Result<(Indexed, Option<IndexFn>), RuntimeError> {
1355        if children.is_empty() || references_an_earlier_axis(children) {
1356            let node = self.evaluate_cartesian(children, prefix)?;
1357            return Ok((Indexed::Tuples(node.tuples), node.index_fn));
1358        }
1359        let base = self.mult;
1360        let mut parts = Vec::with_capacity(children.len());
1361        let mut index_fns = Vec::with_capacity(children.len());
1362        let mut lens = Vec::with_capacity(children.len());
1363        let mut len: u64 = 1;
1364        for child in children {
1365            let evaluated = self.index_node(child, prefix);
1366            let (part, index_fn) = match evaluated {
1367                Ok(done) => done,
1368                Err(e) => {
1369                    self.mult = base;
1370                    return Err(e);
1371                }
1372            };
1373            let part_len = part.len();
1374            parts.push(part);
1375            index_fns.push(index_fn);
1376            lens.push(part_len);
1377            len = match len.checked_mul(part_len) {
1378                Some(n) => n,
1379                None => {
1380                    self.mult = base;
1381                    return Err(RuntimeError::UnsupportedShape(format!(
1382                        "cartesian of {lens:?} tuples exceeds 2^64"
1383                    )));
1384                }
1385            };
1386            if part_len == 0 {
1387                break;
1388            }
1389            self.mult = self
1390                .mult
1391                .saturating_mul(usize::try_from(part_len).unwrap_or(usize::MAX));
1392        }
1393        self.mult = base;
1394        let index_fn = combine_cartesian_index_fn(&index_fns);
1395        if len == 0 {
1396            return Ok((Indexed::Tuples(Vec::new()), index_fn));
1397        }
1398        Ok((
1399            Indexed::Product {
1400                children: parts,
1401                lens,
1402                len,
1403            },
1404            index_fn,
1405        ))
1406    }
1407
1408    fn index_zip(
1409        &mut self,
1410        children: &[Comprehension],
1411        mode: crate::iteration::comprehension::strategy::ZipMode,
1412        prefix: &[(String, Value)],
1413    ) -> Result<(Indexed, Option<IndexFn>), RuntimeError> {
1414        use crate::iteration::comprehension::strategy::ZipMode;
1415        if children.is_empty() {
1416            let node = self.evaluate_zip(children, mode, prefix)?;
1417            return Ok((Indexed::Tuples(node.tuples), node.index_fn));
1418        }
1419        let mut parts = Vec::with_capacity(children.len());
1420        for child in children {
1421            parts.push(self.index_node(child, prefix)?.0);
1422        }
1423        let lengths: Vec<u64> = parts.iter().map(Indexed::len).collect();
1424        let len = match mode {
1425            ZipMode::Strict => {
1426                let first = lengths[0];
1427                if lengths.iter().any(|&n| n != first) {
1428                    return Err(RuntimeError::ZipLengthMismatch { lengths });
1429                }
1430                first
1431            }
1432            ZipMode::Truncate => lengths.iter().copied().min().unwrap_or(0),
1433            ZipMode::Cycle => cycle_length(&lengths),
1434        };
1435        Ok(match mode {
1436            ZipMode::Strict | ZipMode::Truncate => (
1437                Indexed::Lockstep {
1438                    children: parts,
1439                    len,
1440                },
1441                Some(IndexFn::Lockstep { length: len }),
1442            ),
1443            ZipMode::Cycle => (
1444                Indexed::Cycle {
1445                    children: parts,
1446                    len,
1447                },
1448                Some(IndexFn::Modular {
1449                    axis_sizes: lengths,
1450                }),
1451            ),
1452        })
1453    }
1454
1455    fn index_union(
1456        &mut self,
1457        children: &[Comprehension],
1458        prefix: &[(String, Value)],
1459    ) -> Result<(Indexed, Option<IndexFn>), RuntimeError> {
1460        let mut parts = Vec::with_capacity(children.len());
1461        let mut segment_sizes = Vec::with_capacity(children.len());
1462        let mut all_segments_addressable = true;
1463        for child in children {
1464            let (part, index_fn) = self.index_node(child, prefix)?;
1465            all_segments_addressable &= index_fn.is_some();
1466            segment_sizes.push(part.len());
1467            parts.push(part);
1468        }
1469        let len = segment_sizes
1470            .iter()
1471            .try_fold(0u64, |acc, n| acc.checked_add(*n))
1472            .ok_or_else(|| {
1473                RuntimeError::UnsupportedShape(format!(
1474                    "union of {segment_sizes:?} tuples exceeds 2^64"
1475                ))
1476            })?;
1477        let index_fn = all_segments_addressable.then_some(IndexFn::Concatenation { segment_sizes });
1478        Ok((
1479            Indexed::Concat {
1480                children: parts,
1481                len,
1482            },
1483            index_fn,
1484        ))
1485    }
1486}
1487
1488/// `true` when a child of a cartesian reads, in its sources, a name that
1489/// an earlier child binds, a composed name's leaf included: the child's
1490/// tuples then depend on the tuple before it (comprehension_forms.md
1491/// §3.2).
1492fn references_an_earlier_axis(children: &[Comprehension]) -> bool {
1493    let mut bound: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
1494    for child in children {
1495        if child.source_names_read().iter().any(|n| bound.contains(n)) {
1496            return true;
1497        }
1498        collect_clause_names(child, &mut bound);
1499    }
1500    false
1501}
1502
1503/// Every clause name in `c`, in every branch of a union.
1504fn collect_clause_names(c: &Comprehension, out: &mut std::collections::BTreeSet<String>) {
1505    match c {
1506        Comprehension::Clause { name, .. } => {
1507            out.insert(name.clone());
1508        }
1509        Comprehension::Cartesian { children }
1510        | Comprehension::Zip { children, .. }
1511        | Comprehension::Union { children } => {
1512            for child in children {
1513                collect_clause_names(child, out);
1514            }
1515        }
1516        Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
1517            collect_clause_names(child, out);
1518        }
1519    }
1520}
1521
1522/// The positions `strategy` selects over an input of `cardinality`
1523/// tuples addressed by `index_fn`, after V4 (comprehension_forms.md
1524/// §10.7.8). An input the walker could not address (a dependent
1525/// cartesian, a filter) is ordered as a one-axis lattice of its
1526/// tuples: V4 admits only `Lex` over one, and `Lex` reads nothing but
1527/// the count. A non-`Lex` order over a filter selects through
1528/// [`surviving_selection`] instead.
1529fn order_selection(
1530    strategy: StrategyName,
1531    index_fn: Option<&IndexFn>,
1532    cardinality: u64,
1533    truncation: Option<u64>,
1534    seed: Option<u64>,
1535) -> Result<Selection, RuntimeError> {
1536    let dispatch = crate::iteration::comprehension::strategies::for_name(strategy);
1537    if !dispatch.accepts_input(index_fn) {
1538        return Err(RuntimeError::StrategyRejectsInput {
1539            strategy,
1540            index_fn: index_fn.cloned(),
1541        });
1542    }
1543    let fallback;
1544    let index_fn = match index_fn {
1545        Some(idx) => idx,
1546        None => {
1547            fallback = IndexFn::Lattice {
1548                axis_sizes: vec![cardinality],
1549            };
1550            &fallback
1551        }
1552    };
1553    Ok(dispatch.select(index_fn, cardinality, truncation, seed))
1554}
1555
1556/// The positions `strategy` selects among `survivors`, the positions of
1557/// a filter's input its predicate keeps, ranked by their positions in
1558/// that input of `cardinality` tuples addressed by `index_fn`, after V4
1559/// against that input (comprehension_forms.md §5 V5).
1560fn surviving_selection(
1561    strategy: StrategyName,
1562    index_fn: Option<&IndexFn>,
1563    cardinality: u64,
1564    truncation: Option<u64>,
1565    seed: Option<u64>,
1566    survivors: &[u64],
1567) -> Result<Selection, RuntimeError> {
1568    let dispatch = crate::iteration::comprehension::strategies::for_name(strategy);
1569    let Some(index_fn) = index_fn.filter(|idx| dispatch.accepts_input(Some(idx))) else {
1570        return Err(RuntimeError::StrategyRejectsInput {
1571            strategy,
1572            index_fn: index_fn.cloned(),
1573        });
1574    };
1575    Ok(dispatch.select_surviving(index_fn, cardinality, truncation, seed, survivors))
1576}
1577
1578/// How many times a sampled order redraws, doubling the count each
1579/// time, when its filters leave fewer tuples than asked.
1580const SAMPLE_ROUNDS: u32 = 6;
1581
1582/// The scale of a continuous code: the strategies encode a point of
1583/// `[0, 1)` as a 53-bit fraction.
1584const UNIT_SCALE: f64 = (1u64 << 53) as f64;
1585
1586/// One axis of a sampled space (comprehension_forms.md §10.2 R2): a
1587/// discrete child
1588/// evaluated to its tuples, or a continuous clause's interval and
1589/// measure.
1590enum SampleAxis {
1591    Discrete(Vec<RuntimeTuple>),
1592    Continuous {
1593        name: String,
1594        interval: Interval,
1595        measure: AxisMeasure,
1596    },
1597}
1598
1599/// The space an order over a continuous axis samples: its axes in
1600/// clause order, and the predicates of the filters between the
1601/// order and its clauses.
1602#[derive(Default)]
1603struct SampleSpace {
1604    axes: Vec<SampleAxis>,
1605    predicates: Vec<String>,
1606}
1607
1608impl SampleSpace {
1609    /// The tuple at a multi-index, its coordinates in clause order.
1610    /// Under a sampling strategy the multi-index lays the discrete
1611    /// positions first and the continuous codes after them
1612    /// (`IndexFn::Hybrid`), a code being a 53-bit fraction of the
1613    /// unit interval; under `Extrema` it is in clause order over the
1614    /// lattice, a continuous code being `0` or `1` for an end of the
1615    /// interval.
1616    fn realize(&self, mi: &[u64], strategy: StrategyName) -> RuntimeTuple {
1617        let extrema = matches!(strategy, StrategyName::Extrema);
1618        let discrete_count = self
1619            .axes
1620            .iter()
1621            .filter(|a| matches!(a, SampleAxis::Discrete(_)))
1622            .count();
1623        let (mut d, mut c) = (0, if extrema { 0 } else { discrete_count });
1624        let mut out = RuntimeTuple::new();
1625        for axis in &self.axes {
1626            match axis {
1627                SampleAxis::Discrete(tuples) => {
1628                    let pos = mi.get(d).copied().unwrap_or(0) as usize;
1629                    d += 1;
1630                    if extrema {
1631                        c += 1;
1632                    }
1633                    if let Some(t) = tuples.get(pos) {
1634                        out.extend(t.iter().cloned());
1635                    }
1636                }
1637                SampleAxis::Continuous {
1638                    name,
1639                    interval,
1640                    measure,
1641                } => {
1642                    let code = mi.get(c).copied().unwrap_or(0);
1643                    c += 1;
1644                    if extrema {
1645                        d += 1;
1646                    }
1647                    let x = if extrema {
1648                        measure.endpoint(interval, code == 1)
1649                    } else {
1650                        measure.map_unit(code as f64 / UNIT_SCALE, interval)
1651                    };
1652                    out.push((name.clone(), Value::F64(x)));
1653                }
1654            }
1655        }
1656        out
1657    }
1658}
1659
1660/// The multi-indices a strategy draws over a `Continuous` or
1661/// `Hybrid` index function: a sampling strategy needs a count, and
1662/// `Extrema` takes `count` strata (every stratum for `None`). `seed`
1663/// is the authored seed of a seeded strategy.
1664fn draw_sample(
1665    index_fn: &IndexFn,
1666    strategy: StrategyName,
1667    count: Option<u64>,
1668    seed: Option<u64>,
1669) -> Result<Vec<Vec<u64>>, RuntimeError> {
1670    use crate::iteration::comprehension::strategies::{
1671        extrema::extrema_multi_indices, halton::try_halton_multi_indices,
1672        lhs::try_lhs_multi_indices, shuffle::try_shuffle_multi_indices,
1673        sobol::try_sobol_multi_indices,
1674    };
1675    if matches!(strategy, StrategyName::Extrema) {
1676        return Ok(extrema_multi_indices(index_fn, count));
1677    }
1678    let Some(n) = count else {
1679        return Err(RuntimeError::OrderEval {
1680            strategy,
1681            message: "a continuous source has no finite tuple set; give the order a count, \
1682                      as in `order halton/16`"
1683                .into(),
1684        });
1685    };
1686    // The count is the order's own (`order halton/16`), from the spec
1687    // text, so a count no machine can hold is refused here as the
1688    // order's error rather than aborting in the allocator.
1689    let drawn = match strategy {
1690        StrategyName::Halton => try_halton_multi_indices(index_fn, Some(n)),
1691        StrategyName::Sobol => try_sobol_multi_indices(index_fn, Some(n)),
1692        StrategyName::Lhs => try_lhs_multi_indices(index_fn, Some(n), seed),
1693        StrategyName::Shuffle => try_shuffle_multi_indices(index_fn, Some(n), seed),
1694        other => {
1695            return Err(RuntimeError::OrderEval {
1696                strategy: other,
1697                message: "a continuous source needs a sampling strategy: halton, sobol, lhs, \
1698                          shuffle, or extrema"
1699                    .into(),
1700            });
1701        }
1702    };
1703    drawn.map_err(|message| RuntimeError::OrderEval { strategy, message })
1704}
1705
1706/// `true` when an order over `c` samples: a continuous clause is
1707/// reachable through cartesians and filters alone. Under a zip,
1708/// union, or inner order the continuous axis is that node's to
1709/// discharge.
1710pub(crate) fn has_continuous_axis(c: &Comprehension) -> bool {
1711    match c {
1712        Comprehension::Clause { source, .. } => matches!(
1713            source,
1714            Source::ContinuousInterval { .. } | Source::Distribution { .. }
1715        ),
1716        Comprehension::Cartesian { children } => children.iter().any(has_continuous_axis),
1717        Comprehension::Filter { child, .. } => has_continuous_axis(child),
1718        Comprehension::Zip { .. } | Comprehension::Union { .. } | Comprehension::Order { .. } => {
1719            false
1720        }
1721    }
1722}
1723
1724fn combine_cartesian_index_fn(children: &[Option<IndexFn>]) -> Option<IndexFn> {
1725    let mut axis_sizes = Vec::new();
1726    for opt in children {
1727        match opt {
1728            Some(IndexFn::Lattice { axis_sizes: a }) => axis_sizes.extend(a.iter().copied()),
1729            Some(IndexFn::Lockstep { length }) => axis_sizes.push(*length),
1730            // Other shapes don't combine as cartesian axes
1731            // cleanly — fall back to None.
1732            _ => return None,
1733        }
1734    }
1735    Some(IndexFn::Lattice { axis_sizes })
1736}
1737
1738#[cfg(test)]
1739mod tests {
1740    use super::*;
1741    use crate::iteration::comprehension::source::LiteralValue;
1742
1743    fn empty_kernel() -> Arc<PolydatKernel> {
1744        Arc::new(crate::dsl::compile_polydat_interpreter("\n").unwrap())
1745    }
1746
1747    /// Canonical kernel with `extern k: u64` so the runtime
1748    /// evaluator can install per-clause `k` values via
1749    /// materialize_subscope — the shape the traversal lowering
1750    /// produces.
1751    fn canonical_with_k() -> Arc<PolydatKernel> {
1752        Arc::new(crate::dsl::compile_polydat_interpreter("extern k: u64\n").unwrap())
1753    }
1754
1755    fn clause(name: &str, source: Source) -> Comprehension {
1756        Comprehension::Clause {
1757            name: name.into(),
1758            source,
1759        }
1760    }
1761
1762    fn empty_literal() -> Source {
1763        Source::Literal { values: Vec::new() }
1764    }
1765
1766    /// The count is what the clause produced, per leaf, in tree order.
1767    #[test]
1768    fn every_leaf_reports_what_it_yielded() {
1769        let comp = Comprehension::Cartesian {
1770            children: vec![
1771                clause(
1772                    "a",
1773                    Source::IntRange {
1774                        lo: 0,
1775                        hi: 3,
1776                        step: 1,
1777                    },
1778                ),
1779                clause(
1780                    "b",
1781                    Source::Literal {
1782                        values: vec![LiteralValue::Int(7), LiteralValue::Int(8)],
1783                    },
1784                ),
1785            ],
1786        };
1787        let scope = empty_kernel();
1788
1789        let out = evaluate_for_iteration_reported(&comp, &*scope).unwrap();
1790        assert_eq!(out.tuples.len(), 6, "3 x 2");
1791        assert_eq!(out.clauses.len(), 2, "one entry per leaf, in tree order");
1792        assert_eq!(out.clauses[0].var, "a");
1793        assert_eq!(out.clauses[0].values, 3);
1794        assert_eq!(out.clauses[1].var, "b");
1795        // `b` is evaluated once per tuple of `a`, so its values sum over
1796        // those evaluations: emptiness is a property of the evaluations.
1797        assert_eq!(out.clauses[1].evaluations, 3);
1798        assert_eq!(out.clauses[1].values, 6);
1799    }
1800
1801    /// The clause a diagnostic should name is the one that was reached
1802    /// and still produced nothing.
1803    #[test]
1804    fn an_empty_clause_is_reached_and_yields_nothing() {
1805        let comp = Comprehension::Cartesian {
1806            children: vec![
1807                clause(
1808                    "a",
1809                    Source::IntRange {
1810                        lo: 0,
1811                        hi: 2,
1812                        step: 1,
1813                    },
1814                ),
1815                clause("b", empty_literal()),
1816            ],
1817        };
1818        let scope = empty_kernel();
1819
1820        let out = evaluate_for_iteration_reported(&comp, &*scope).unwrap();
1821        assert!(out.tuples.is_empty(), "an empty clause empties the product");
1822        let culprits: Vec<&str> = out
1823            .clauses
1824            .iter()
1825            .filter(|c| c.evaluations > 0 && c.values == 0)
1826            .map(|c| c.var.as_str())
1827            .collect();
1828        assert_eq!(culprits, ["b"], "only the empty clause is named");
1829    }
1830
1831    /// A clause behind an empty one is never reached, so it is not the
1832    /// cause and must not read as one.
1833    #[test]
1834    fn a_clause_behind_an_empty_one_is_never_reached() {
1835        let comp = Comprehension::Cartesian {
1836            children: vec![
1837                clause("outer", empty_literal()),
1838                clause(
1839                    "inner",
1840                    Source::IntRange {
1841                        lo: 0,
1842                        hi: 9,
1843                        step: 1,
1844                    },
1845                ),
1846            ],
1847        };
1848        let scope = empty_kernel();
1849
1850        let out = evaluate_for_iteration_reported(&comp, &*scope).unwrap();
1851        assert!(out.tuples.is_empty());
1852        let by = |v: &str| {
1853            out.clauses
1854                .iter()
1855                .find(|c| c.var == v)
1856                .expect("every leaf is present whether reached or not")
1857        };
1858        assert_eq!(by("outer").evaluations, 1);
1859        assert_eq!(by("outer").values, 0);
1860        assert_eq!(
1861            by("inner").evaluations,
1862            0,
1863            "never reached: the cause is `outer`, not this"
1864        );
1865        assert_eq!(by("inner").values, 0);
1866    }
1867
1868    /// Two clauses can share a name across the branches of a union;
1869    /// they are still two leaves and are counted apart.
1870    #[test]
1871    fn clauses_sharing_a_name_across_a_union_are_counted_apart() {
1872        let comp = Comprehension::Union {
1873            children: vec![
1874                clause(
1875                    "k",
1876                    Source::Literal {
1877                        values: vec![LiteralValue::Int(1)],
1878                    },
1879                ),
1880                clause("k", empty_literal()),
1881            ],
1882        };
1883        let scope = empty_kernel();
1884
1885        let out = evaluate_for_iteration_reported(&comp, &*scope).unwrap();
1886        assert_eq!(out.clauses.len(), 2, "two leaves, one name");
1887        assert_eq!(out.clauses[0].values, 1);
1888        assert_eq!(out.clauses[1].values, 0);
1889        assert_eq!(out.clauses[1].evaluations, 1, "reached, and empty");
1890    }
1891
1892    /// The counts ride the evaluation that already happened, so the
1893    /// plain entry point is the reported one without its record.
1894    #[test]
1895    fn the_plain_entry_point_agrees_with_the_reported_one() {
1896        let comp = Comprehension::Cartesian {
1897            children: vec![
1898                clause(
1899                    "a",
1900                    Source::IntRange {
1901                        lo: 1,
1902                        hi: 4,
1903                        step: 1,
1904                    },
1905                ),
1906                clause(
1907                    "b",
1908                    Source::Literal {
1909                        values: vec![LiteralValue::Int(5)],
1910                    },
1911                ),
1912            ],
1913        };
1914        let scope = empty_kernel();
1915
1916        let plain = evaluate_for_iteration(&comp, &*scope).unwrap();
1917        let reported = evaluate_for_iteration_reported(&comp, &*scope).unwrap();
1918        assert_eq!(plain, reported.tuples);
1919    }
1920
1921    #[test]
1922    fn int_range_yields_values() {
1923        let comp = Comprehension::Clause {
1924            name: "k".into(),
1925            source: Source::IntRange {
1926                lo: 1,
1927                hi: 5,
1928                step: 1,
1929            },
1930        };
1931        let canonical = empty_kernel();
1932
1933        let tuples = evaluate_for_iteration(&comp, &*canonical).unwrap();
1934        assert_eq!(tuples.len(), 4);
1935        assert_eq!(tuples[0][0].1, Value::U64(1));
1936        assert_eq!(tuples[3][0].1, Value::U64(4));
1937    }
1938
1939    #[test]
1940    fn literal_list_yields_values() {
1941        let comp = Comprehension::Clause {
1942            name: "x".into(),
1943            source: Source::Literal {
1944                values: vec![LiteralValue::Int(10), LiteralValue::Int(20)],
1945            },
1946        };
1947        let canonical = empty_kernel();
1948
1949        let tuples = evaluate_for_iteration(&comp, &*canonical).unwrap();
1950        assert_eq!(tuples.len(), 2);
1951    }
1952
1953    #[test]
1954    fn cartesian_produces_product() {
1955        let comp = Comprehension::cartesian(vec![
1956            Comprehension::Clause {
1957                name: "x".into(),
1958                source: Source::IntRange {
1959                    lo: 1,
1960                    hi: 3,
1961                    step: 1,
1962                },
1963            },
1964            Comprehension::Clause {
1965                name: "y".into(),
1966                source: Source::IntRange {
1967                    lo: 10,
1968                    hi: 30,
1969                    step: 10,
1970                },
1971            },
1972        ]);
1973        let canonical = empty_kernel();
1974
1975        let tuples = evaluate_for_iteration(&comp, &*canonical).unwrap();
1976        // 2 × 2 = 4
1977        assert_eq!(tuples.len(), 4);
1978    }
1979
1980    #[test]
1981    fn union_produces_concatenation() {
1982        let comp = Comprehension::union(vec![
1983            Comprehension::Clause {
1984                name: "k".into(),
1985                source: Source::Literal {
1986                    values: vec![LiteralValue::Int(1)],
1987                },
1988            },
1989            Comprehension::Clause {
1990                name: "k".into(),
1991                source: Source::Literal {
1992                    values: vec![LiteralValue::Int(10), LiteralValue::Int(20)],
1993                },
1994            },
1995        ]);
1996        let canonical = empty_kernel();
1997
1998        let tuples = evaluate_for_iteration(&comp, &*canonical).unwrap();
1999        assert_eq!(tuples.len(), 3);
2000    }
2001
2002    #[test]
2003    fn filter_drops_non_matching() {
2004        let comp = Comprehension::filter(
2005            Comprehension::Clause {
2006                name: "k".into(),
2007                source: Source::IntRange {
2008                    lo: 1,
2009                    hi: 6,
2010                    step: 1,
2011                },
2012            },
2013            "{k} > 3",
2014        );
2015        let canonical = canonical_with_k();
2016
2017        let tuples = evaluate_for_iteration(&comp, &*canonical).unwrap();
2018        // 1..6 = [1,2,3,4,5]; filter > 3 keeps [4, 5]
2019        assert_eq!(tuples.len(), 2);
2020    }
2021
2022    #[test]
2023    fn order_lex_truncate() {
2024        let comp = Comprehension::order(
2025            Comprehension::Clause {
2026                name: "k".into(),
2027                source: Source::IntRange {
2028                    lo: 1,
2029                    hi: 100,
2030                    step: 1,
2031                },
2032            },
2033            StrategyName::Lex,
2034            Some(5),
2035        );
2036        let canonical = empty_kernel();
2037
2038        let tuples = evaluate_for_iteration(&comp, &*canonical).unwrap();
2039        assert_eq!(tuples.len(), 5);
2040    }
2041
2042    /// A cartesian's lattice has one axis per child, however many
2043    /// names a child binds: a two-name zip at the head is one axis, so
2044    /// `halton/6` over the 2 x 3 product draws all six tuples.
2045    #[test]
2046    fn a_multi_name_head_is_one_lattice_axis() {
2047        use crate::iteration::comprehension::strategy::ZipMode;
2048        let lit = |name: &str, vs: &[i64]| Comprehension::Clause {
2049            name: name.into(),
2050            source: Source::Literal {
2051                values: vs.iter().map(|v| LiteralValue::Int(*v)).collect(),
2052            },
2053        };
2054        let comp = Comprehension::order(
2055            Comprehension::cartesian(vec![
2056                Comprehension::zip(vec![lit("a", &[1, 2]), lit("b", &[3, 4])], ZipMode::Strict),
2057                lit("c", &[5, 6, 7]),
2058            ]),
2059            StrategyName::Halton,
2060            Some(6),
2061        );
2062        let tuples = evaluate_for_iteration(&comp, &*empty_kernel()).unwrap();
2063        assert_eq!(
2064            tuples.len(),
2065            6,
2066            "every tuple of the 2 x 3 product: {tuples:?}"
2067        );
2068    }
2069
2070    /// A Generator-evaluated source claims an IndexFn::Lattice
2071    /// through SourceEval, so Extrema's indexed path fires and the
2072    /// 2-D Lattice case
2073    /// (cartesian of two clauses) gives the 2x2 corners, not
2074    /// just first/last of the cartesian product.
2075    #[test]
2076    fn extrema_over_cartesian_uses_indexed_form() {
2077        let comp = Comprehension::order(
2078            Comprehension::cartesian(vec![
2079                Comprehension::Clause {
2080                    name: "k".into(),
2081                    source: Source::Literal {
2082                        values: vec![
2083                            LiteralValue::Int(1),
2084                            LiteralValue::Int(2),
2085                            LiteralValue::Int(3),
2086                        ],
2087                    },
2088                },
2089                Comprehension::Clause {
2090                    name: "limit".into(),
2091                    source: Source::Literal {
2092                        values: vec![
2093                            LiteralValue::Int(10),
2094                            LiteralValue::Int(20),
2095                            LiteralValue::Int(30),
2096                        ],
2097                    },
2098                },
2099            ]),
2100            StrategyName::Extrema,
2101            // `extrema/1` is the corner stratum (comprehension_forms.md
2102            // §3.6); bare `extrema` is the whole 9-tuple space ordered
2103            // corners first.
2104            Some(1),
2105        );
2106        let canonical = empty_kernel();
2107
2108        let tuples = evaluate_for_iteration(&comp, &*canonical).unwrap();
2109        // 3x3 lattice → 4 corners (interior count 0) via the indexed form.
2110        assert_eq!(tuples.len(), 4);
2111        // Each corner pairs an extreme k with an extreme limit.
2112        for t in &tuples {
2113            assert_eq!(t.len(), 2);
2114            let k = match &t[0].1 {
2115                Value::U64(n) => *n,
2116                other => panic!("expected u64 k, got {other:?}"),
2117            };
2118            let lim = match &t[1].1 {
2119                Value::U64(n) => *n,
2120                other => panic!("expected u64 limit, got {other:?}"),
2121            };
2122            assert!(k == 1 || k == 3, "expected extreme k, got {k}");
2123            assert!(lim == 10 || lim == 30, "expected extreme limit, got {lim}");
2124        }
2125    }
2126}