Skip to main content

polydat_core/iteration/comprehension/ir/
interpreter.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Stack-machine IR interpreter — spec §9.1 option (a) +
5//! §9.2 correctness contract.
6//!
7//! Walks an IR `Program` linearly, maintaining a stack of
8//! tuple-stream operands. Each opcode either pushes a new
9//! stream ([`Op::PushClause`]) or combines / wraps the top-N
10//! ([`Op::Cartesian`], `Zip`, `Union`, `Filter`,
11//! `OrderStreaming`, `OrderMaterialize`). `Dispense` marks
12//! the top stream as the output.
13//!
14//! Returns a [`TupleStream`] — a lazy producer the consumer
15//! pulls from. The stream graph is built at
16//! [`interpret`]-time; tuple production happens on-demand.
17
18use crate::iteration::comprehension::source::{LiteralValue, Source};
19use crate::iteration::comprehension::strategies::{Tuple, TupleValue};
20use crate::iteration::comprehension::strategy::{StrategyName, ZipMode};
21
22use super::op::{Op, OrderStreamingKind};
23use super::program::Program;
24
25/// A lazy tuple stream — `advance` returns the next tuple or
26/// `None` when the stream is exhausted.
27pub trait TupleStream {
28    /// The next tuple, or `None` once the stream is exhausted.
29    fn advance(&mut self) -> Option<Tuple>;
30}
31
32/// Boxed stream alias used throughout the interpreter's stack
33/// manipulation.
34type BoxedStream = Box<dyn TupleStream>;
35
36/// Interpret a compiled `Program` and return the result
37/// stream. Per spec §9.1 the final opcode must be
38/// `Op::Dispense`; if it's missing the function panics
39/// (programs not produced by the compiler are caller-error).
40pub fn interpret(program: &Program) -> BoxedStream {
41    let mut stack: Vec<BoxedStream> = Vec::new();
42    for op in program.ops() {
43        match op {
44            Op::PushClause { name, source } => {
45                stack.push(Box::new(ClauseStream::new(name.clone(), source.clone())));
46            }
47            Op::Cartesian { n } => {
48                let children = pop_n(&mut stack, *n);
49                stack.push(Box::new(CartesianStream::new(children)));
50            }
51            Op::Zip { n, mode } => {
52                let children = pop_n(&mut stack, *n);
53                stack.push(Box::new(ZipStream::new(children, *mode)));
54            }
55            Op::Union { n } => {
56                let children = pop_n(&mut stack, *n);
57                stack.push(Box::new(UnionStream::new(children)));
58            }
59            Op::Filter { predicate } => {
60                let inner = stack.pop().expect("Filter on empty stack");
61                stack.push(Box::new(FilterStream::new(inner, predicate.clone())));
62            }
63            Op::OrderStreaming { kind, truncation } => {
64                let inner = stack.pop().expect("OrderStreaming on empty stack");
65                stack.push(Box::new(OrderStreamingStream::new(
66                    inner,
67                    *kind,
68                    *truncation,
69                )));
70            }
71            Op::OrderMaterialize {
72                strategy,
73                truncation,
74                indexed,
75                input_index_fn,
76            } => {
77                let inner = stack.pop().expect("OrderMaterialize on empty stack");
78                stack.push(Box::new(OrderMaterializeStream::new(
79                    inner,
80                    *strategy,
81                    *truncation,
82                    *indexed,
83                    input_index_fn.clone(),
84                )));
85            }
86            Op::Dispense => {
87                // No-op at the interpreter level; the top of
88                // stack is the result.
89            }
90        }
91    }
92    stack.pop().expect("Program produced no result stream")
93}
94
95fn pop_n(stack: &mut Vec<BoxedStream>, n: usize) -> Vec<BoxedStream> {
96    assert!(
97        stack.len() >= n,
98        "stack underflow: needed {n}, have {}",
99        stack.len()
100    );
101    let split_at = stack.len() - n;
102    stack.split_off(split_at)
103}
104
105// ---- ClauseStream ----
106
107/// Streams a Source's values as single-name tuples.
108/// Continuous / Distribution sources are not interpretable
109/// at this layer (they require a sampling order to be
110/// meaningful per V8); attempting to advance one returns
111/// `None` so the interpreter doesn't deadlock.
112struct ClauseStream {
113    name: String,
114    state: ClauseState,
115}
116
117enum ClauseState {
118    LiteralList {
119        values: Vec<LiteralValue>,
120        pos: usize,
121    },
122    IntRange {
123        current: i64,
124        hi: i64,
125        step: i64,
126    },
127    Exhausted,
128}
129
130impl ClauseStream {
131    fn new(name: String, source: Source) -> Self {
132        let state = match source {
133            Source::Literal { values } => ClauseState::LiteralList { values, pos: 0 },
134            Source::IntRange { lo, hi, step } => ClauseState::IntRange {
135                current: lo,
136                hi,
137                step: step.max(1),
138            },
139            // Generator / WorkloadParamList: produce nothing at
140            // this layer (would need runtime evaluator wiring).
141            // ContinuousInterval / Distribution: must be sampled
142            // via an enclosing order; bare clause is not pulled
143            // in valid programs.
144            _ => ClauseState::Exhausted,
145        };
146        Self { name, state }
147    }
148}
149
150impl TupleStream for ClauseStream {
151    fn advance(&mut self) -> Option<Tuple> {
152        let value = match &mut self.state {
153            ClauseState::LiteralList { values, pos } => {
154                if *pos >= values.len() {
155                    return None;
156                }
157                let v = literal_to_tuple_value(&values[*pos]);
158                *pos += 1;
159                v
160            }
161            ClauseState::IntRange { current, hi, step } => {
162                if *current >= *hi {
163                    return None;
164                }
165                let v = TupleValue::I64(*current);
166                *current += *step;
167                v
168            }
169            ClauseState::Exhausted => return None,
170        };
171        Some(Tuple::new().with(self.name.clone(), value))
172    }
173}
174
175fn literal_to_tuple_value(lv: &LiteralValue) -> TupleValue {
176    match lv {
177        LiteralValue::Int(n) => TupleValue::I64(*n),
178        LiteralValue::Float(f) => TupleValue::F64(*f),
179        LiteralValue::String(s) => TupleValue::Str(s.clone()),
180        LiteralValue::Bool(b) => TupleValue::Bool(*b),
181        LiteralValue::Json(j) => TupleValue::Str(j.to_string()),
182    }
183}
184
185// ---- CartesianStream ----
186
187/// Enumerates the cross product of N child streams in Lex
188/// order (rightmost varies fastest). Builds the per-axis
189/// value vectors lazily on demand: the first advance pulls
190/// child 0 once and child 1..N to exhaustion, caching them;
191/// subsequent advances iterate over the cached cross product.
192///
193/// This caches all but the first axis. A fully-streaming
194/// cartesian (no caching) would require child re-iteration,
195/// which the IR layer doesn't currently expose.
196struct CartesianStream {
197    children: Vec<BoxedStream>,
198    /// Cached values for axes 1..N (axis 0 streams).
199    cached: Vec<Vec<Tuple>>,
200    /// Current cursor for axis 0 (lazy pull).
201    current_a0: Option<Tuple>,
202    /// Cursor positions for axes 1..N.
203    cursors: Vec<usize>,
204    /// True once we've initialized — first advance() needs to
205    /// cache children 1..N and pull initial child 0.
206    initialized: bool,
207    done: bool,
208}
209
210impl CartesianStream {
211    fn new(children: Vec<BoxedStream>) -> Self {
212        let n = children.len();
213        Self {
214            children,
215            cached: Vec::with_capacity(n.saturating_sub(1)),
216            current_a0: None,
217            cursors: vec![0; n.saturating_sub(1)],
218            initialized: false,
219            done: false,
220        }
221    }
222
223    fn initialize(&mut self) {
224        if self.children.is_empty() {
225            self.done = true;
226            return;
227        }
228        // Cache all children 1..N to exhaustion.
229        for i in 1..self.children.len() {
230            let mut v = Vec::new();
231            while let Some(t) = self.children[i].advance() {
232                v.push(t);
233            }
234            self.cached.push(v);
235        }
236        // Pull first axis 0 value.
237        self.current_a0 = self.children[0].advance();
238        if self.current_a0.is_none() || self.cached.iter().any(|v| v.is_empty()) {
239            // Any empty axis → empty cartesian.
240            self.done = true;
241        }
242    }
243}
244
245impl TupleStream for CartesianStream {
246    fn advance(&mut self) -> Option<Tuple> {
247        if !self.initialized {
248            self.initialize();
249            self.initialized = true;
250        }
251        if self.done {
252            return None;
253        }
254        // Compose current cursor + current axis-0 value.
255        let mut out = Tuple::new();
256        if let Some(a0) = self.current_a0.as_ref() {
257            for (k, v) in &a0.bindings {
258                out.bindings.push((k.clone(), v.clone()));
259            }
260        }
261        for (i, cursor) in self.cursors.iter().enumerate() {
262            let tup = &self.cached[i][*cursor];
263            for (k, v) in &tup.bindings {
264                out.bindings.push((k.clone(), v.clone()));
265            }
266        }
267
268        // Advance cursors (rightmost-fastest).
269        let n_cached = self.cursors.len();
270        let mut overflow = true;
271        for i in (0..n_cached).rev() {
272            self.cursors[i] += 1;
273            if self.cursors[i] < self.cached[i].len() {
274                overflow = false;
275                break;
276            }
277            self.cursors[i] = 0;
278        }
279        if overflow {
280            // Advance axis 0.
281            self.current_a0 = self.children[0].advance();
282            if self.current_a0.is_none() {
283                self.done = true;
284            }
285        }
286        Some(out)
287    }
288}
289
290// ---- ZipStream ----
291
292struct ZipStream {
293    children: Vec<BoxedStream>,
294    mode: ZipMode,
295    /// For Cycle: cached values of shorter children.
296    cycle_cache: Option<Vec<Vec<Tuple>>>,
297    cycle_cursors: Vec<usize>,
298    cycle_longest_idx: Option<usize>,
299    initialized: bool,
300    done: bool,
301}
302
303impl ZipStream {
304    fn new(children: Vec<BoxedStream>, mode: ZipMode) -> Self {
305        Self {
306            children,
307            mode,
308            cycle_cache: None,
309            cycle_cursors: Vec::new(),
310            cycle_longest_idx: None,
311            initialized: false,
312            done: false,
313        }
314    }
315
316    fn initialize_cycle(&mut self) {
317        // Pull every child to exhaustion (Cycle's barrier).
318        // Identify the longest child; cache the others.
319        let mut all: Vec<Vec<Tuple>> = Vec::with_capacity(self.children.len());
320        for child in &mut self.children {
321            let mut v = Vec::new();
322            while let Some(t) = child.advance() {
323                v.push(t);
324            }
325            all.push(v);
326        }
327        let longest_idx = all
328            .iter()
329            .enumerate()
330            .max_by_key(|(_, v)| v.len())
331            .map(|(i, _)| i)
332            .unwrap_or(0);
333        self.cycle_longest_idx = Some(longest_idx);
334        self.cycle_cursors = vec![0; all.len()];
335        self.cycle_cache = Some(all);
336    }
337}
338
339impl TupleStream for ZipStream {
340    fn advance(&mut self) -> Option<Tuple> {
341        if self.done {
342            return None;
343        }
344        match self.mode {
345            ZipMode::Strict | ZipMode::Truncate => {
346                // Pull one tuple from each child; if any returns
347                // None, this stream is exhausted.
348                let mut out = Tuple::new();
349                for child in &mut self.children {
350                    match child.advance() {
351                        Some(t) => {
352                            for (k, v) in t.bindings {
353                                out.bindings.push((k, v));
354                            }
355                        }
356                        None => {
357                            self.done = true;
358                            return None;
359                        }
360                    }
361                }
362                Some(out)
363            }
364            ZipMode::Cycle => {
365                if !self.initialized {
366                    self.initialize_cycle();
367                    self.initialized = true;
368                }
369                let cache = self.cycle_cache.as_ref().unwrap();
370                let longest = self.cycle_longest_idx.unwrap();
371                if cache[longest].is_empty() {
372                    self.done = true;
373                    return None;
374                }
375                if self.cycle_cursors[longest] >= cache[longest].len() {
376                    self.done = true;
377                    return None;
378                }
379                let mut out = Tuple::new();
380                for (i, v) in cache.iter().enumerate() {
381                    if v.is_empty() {
382                        // Empty child → empty zip.
383                        self.done = true;
384                        return None;
385                    }
386                    let idx = self.cycle_cursors[i] % v.len();
387                    for (k, val) in &v[idx].bindings {
388                        out.bindings.push((k.clone(), val.clone()));
389                    }
390                }
391                // Advance cursors: longest by 1, others wrap.
392                for c in self.cycle_cursors.iter_mut() {
393                    *c += 1;
394                }
395                Some(out)
396            }
397        }
398    }
399}
400
401// ---- UnionStream ----
402
403/// Drain children in order: child 0 fully, then child 1, etc.
404struct UnionStream {
405    children: Vec<BoxedStream>,
406    active_idx: usize,
407}
408
409impl UnionStream {
410    fn new(children: Vec<BoxedStream>) -> Self {
411        Self {
412            children,
413            active_idx: 0,
414        }
415    }
416}
417
418impl TupleStream for UnionStream {
419    fn advance(&mut self) -> Option<Tuple> {
420        loop {
421            if self.active_idx >= self.children.len() {
422                return None;
423            }
424            if let Some(t) = self.children[self.active_idx].advance() {
425                return Some(t);
426            }
427            // Advance to next child.
428            self.active_idx += 1;
429        }
430    }
431}
432
433// ---- FilterStream ----
434
435struct FilterStream {
436    inner: BoxedStream,
437    predicate: String,
438}
439
440impl FilterStream {
441    fn new(inner: BoxedStream, predicate: String) -> Self {
442        Self { inner, predicate }
443    }
444}
445
446impl TupleStream for FilterStream {
447    fn advance(&mut self) -> Option<Tuple> {
448        loop {
449            let candidate = self.inner.advance()?;
450            if evaluate_predicate(&self.predicate, &candidate) {
451                return Some(candidate);
452            }
453        }
454    }
455}
456
457// ---- OrderStreamingStream ----
458
459/// `order(c, Lex, truncation)` — pass-through optionally
460/// capped at truncation tuples.
461struct OrderStreamingStream {
462    inner: BoxedStream,
463    truncation: Option<u64>,
464    emitted: u64,
465}
466
467impl OrderStreamingStream {
468    fn new(inner: BoxedStream, _kind: OrderStreamingKind, truncation: Option<u64>) -> Self {
469        Self {
470            inner,
471            truncation,
472            emitted: 0,
473        }
474    }
475}
476
477impl TupleStream for OrderStreamingStream {
478    fn advance(&mut self) -> Option<Tuple> {
479        if let Some(cap) = self.truncation
480            && self.emitted >= cap
481        {
482            return None;
483        }
484        let t = self.inner.advance()?;
485        self.emitted += 1;
486        Some(t)
487    }
488}
489
490// ---- OrderMaterializeStream ----
491
492/// MATERIALIZATION BARRIER. On first advance, build the
493/// working set per the strategy, then emit permuted tuples
494/// (optionally truncated). Strategy::apply (spec §10.7.8)
495/// dispatches the indexed-vs-naïve path internally using the
496/// `input_index_fn` the compiler propagated from upstream
497/// metadata; falls back to a 1-axis Lattice of observed
498/// length when the upstream metadata couldn't claim a
499/// closed-form addressing function.
500struct OrderMaterializeStream {
501    inner: BoxedStream,
502    strategy: StrategyName,
503    truncation: Option<u64>,
504    #[allow(dead_code)] // R2 path now dispatches through Strategy::apply
505    indexed: bool,
506    input_index_fn: Option<crate::iteration::comprehension::metadata::IndexFn>,
507    materialized: Option<Vec<Tuple>>,
508    pos: usize,
509}
510
511impl OrderMaterializeStream {
512    fn new(
513        inner: BoxedStream,
514        strategy: StrategyName,
515        truncation: Option<u64>,
516        indexed: bool,
517        input_index_fn: Option<crate::iteration::comprehension::metadata::IndexFn>,
518    ) -> Self {
519        Self {
520            inner,
521            strategy,
522            truncation,
523            indexed,
524            input_index_fn,
525            materialized: None,
526            pos: 0,
527        }
528    }
529
530    fn materialize(&mut self) {
531        let mut buf = Vec::new();
532        while let Some(t) = self.inner.advance() {
533            buf.push(t);
534        }
535        let cardinality = buf.len() as u64;
536        let index_fn = self.input_index_fn.clone().unwrap_or(
537            crate::iteration::comprehension::metadata::IndexFn::Lattice {
538                axis_sizes: vec![cardinality],
539            },
540        );
541        let input = crate::iteration::comprehension::strategies::EvaluatedInput {
542            tuples: buf,
543            cardinality,
544            index_fn,
545        };
546        let dispatched = crate::iteration::comprehension::strategies::for_name(self.strategy);
547        let out = dispatched.apply(&input, self.truncation);
548        self.materialized = Some(out);
549    }
550}
551
552impl TupleStream for OrderMaterializeStream {
553    fn advance(&mut self) -> Option<Tuple> {
554        if self.materialized.is_none() {
555            self.materialize();
556        }
557        let buf = self.materialized.as_ref().unwrap();
558        if self.pos >= buf.len() {
559            return None;
560        }
561        let t = buf[self.pos].clone();
562        self.pos += 1;
563        Some(t)
564    }
565}
566
567// ---- Predicate evaluator ----
568
569/// Simple predicate evaluator covering the §10.9.5 catalog.
570/// Returns `true` for unrecognized predicates (the
571/// conservative choice: keep tuples we can't decide on; the
572/// caller's algebra-level predicate analyzer marks unknown
573/// patterns Opaque so the optimizer doesn't push them
574/// down; the IR interpreter then runs them per-tuple here).
575///
576/// Implementations:
577/// - `{name} OP literal` and `literal OP {name}` for the 6
578///   comparison operators.
579/// - `p && q`, `p || q`, `!p` (recursive).
580/// - `{name} in [v1, v2, ...]` discrete-set membership.
581/// - Literal `true` / `false`.
582///
583/// Anything else evaluates to `true` (passes through). This
584/// evaluator serves the IR surfaces; the production `runtime`
585/// walker evaluates richer predicates through the scope.
586fn evaluate_predicate(predicate: &str, tuple: &Tuple) -> bool {
587    let trimmed = predicate.trim();
588    if trimmed.eq_ignore_ascii_case("true") {
589        return true;
590    }
591    if trimmed.eq_ignore_ascii_case("false") {
592        return false;
593    }
594    // Negation.
595    if let Some(inner) = trimmed.strip_prefix('!') {
596        return !evaluate_predicate(inner.trim(), tuple);
597    }
598    // Conjunction.
599    if let Some(parts) = split_top_level(trimmed, "&&") {
600        return parts.iter().all(|p| evaluate_predicate(p, tuple));
601    }
602    // Disjunction.
603    if let Some(parts) = split_top_level(trimmed, "||") {
604        return parts.iter().any(|p| evaluate_predicate(p, tuple));
605    }
606    // `{name} in [v1, v2, ...]`
607    if let Some(in_pos) = trimmed.find(" in ") {
608        let lhs = trimmed[..in_pos].trim();
609        let rhs = trimmed[in_pos + 4..].trim();
610        if let Some(name) = strip_curly(lhs)
611            && let Some(inner) = rhs.strip_prefix('[').and_then(|s| s.strip_suffix(']'))
612        {
613            let needle = lookup(tuple, &name);
614            if needle.is_none() {
615                return true; // Unknown coord — pass through.
616            }
617            return inner.split(',').any(|item| {
618                parse_literal(item.trim())
619                    .map(|v| values_eq(&lit_to_tuple_value(&v), needle.unwrap()))
620                    .unwrap_or(false)
621            });
622        }
623    }
624    // Comparison ops: try longest first.
625    for (op, op_kind) in [
626        ("==", CmpKind::Eq),
627        ("!=", CmpKind::Ne),
628        ("<=", CmpKind::Le),
629        (">=", CmpKind::Ge),
630        ("<", CmpKind::Lt),
631        (">", CmpKind::Gt),
632    ] {
633        if let Some((lhs, rhs)) = split_top_level_op(trimmed, op) {
634            let lhs = lhs.trim();
635            let rhs = rhs.trim();
636            // {name} OP literal
637            if let (Some(name), Some(lit)) = (strip_curly(lhs), parse_literal(rhs)) {
638                let val = lookup(tuple, &name);
639                if val.is_none() {
640                    return true;
641                }
642                return compare(val.unwrap(), op_kind, &lit_to_tuple_value(&lit));
643            }
644            // literal OP {name}
645            if let (Some(name), Some(lit)) = (strip_curly(rhs), parse_literal(lhs)) {
646                let val = lookup(tuple, &name);
647                if val.is_none() {
648                    return true;
649                }
650                // Invert kind: a < b iff b > a.
651                let inv = invert_kind(op_kind);
652                return compare(val.unwrap(), inv, &lit_to_tuple_value(&lit));
653            }
654            // {a} OP {b}
655            if let (Some(a), Some(b)) = (strip_curly(lhs), strip_curly(rhs)) {
656                let va = lookup(tuple, &a);
657                let vb = lookup(tuple, &b);
658                if va.is_none() || vb.is_none() {
659                    return true;
660                }
661                return compare(va.unwrap(), op_kind, vb.unwrap());
662            }
663        }
664    }
665    true
666}
667
668#[derive(Clone, Copy)]
669enum CmpKind {
670    Eq,
671    Ne,
672    Lt,
673    Le,
674    Gt,
675    Ge,
676}
677
678fn invert_kind(k: CmpKind) -> CmpKind {
679    match k {
680        CmpKind::Lt => CmpKind::Gt,
681        CmpKind::Le => CmpKind::Ge,
682        CmpKind::Gt => CmpKind::Lt,
683        CmpKind::Ge => CmpKind::Le,
684        other => other,
685    }
686}
687
688fn lookup<'a>(tuple: &'a Tuple, name: &str) -> Option<&'a TupleValue> {
689    tuple
690        .bindings
691        .iter()
692        .find(|(k, _)| k == name)
693        .map(|(_, v)| v)
694}
695
696fn compare(a: &TupleValue, kind: CmpKind, b: &TupleValue) -> bool {
697    let ord = match (a, b) {
698        (TupleValue::I64(a), TupleValue::I64(b)) => a.cmp(b),
699        (TupleValue::U64(a), TupleValue::U64(b)) => a.cmp(b),
700        (TupleValue::F64(a), TupleValue::F64(b)) => {
701            a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
702        }
703        (TupleValue::I64(a), TupleValue::F64(b)) => (*a as f64)
704            .partial_cmp(b)
705            .unwrap_or(std::cmp::Ordering::Equal),
706        (TupleValue::F64(a), TupleValue::I64(b)) => a
707            .partial_cmp(&(*b as f64))
708            .unwrap_or(std::cmp::Ordering::Equal),
709        (TupleValue::Str(a), TupleValue::Str(b)) => a.cmp(b),
710        (TupleValue::Bool(a), TupleValue::Bool(b)) => a.cmp(b),
711        _ => return false,
712    };
713    match kind {
714        CmpKind::Eq => ord.is_eq(),
715        CmpKind::Ne => !ord.is_eq(),
716        CmpKind::Lt => ord.is_lt(),
717        CmpKind::Le => ord.is_le(),
718        CmpKind::Gt => ord.is_gt(),
719        CmpKind::Ge => ord.is_ge(),
720    }
721}
722
723fn values_eq(a: &TupleValue, b: &TupleValue) -> bool {
724    compare(a, CmpKind::Eq, b)
725}
726
727fn strip_curly(s: &str) -> Option<String> {
728    let s = s.trim();
729    if s.starts_with('{') && s.ends_with('}') {
730        let inner = &s[1..s.len() - 1];
731        let trimmed = inner.trim();
732        if trimmed.chars().all(|c| c.is_alphanumeric() || c == '_') && !trimmed.is_empty() {
733            return Some(trimmed.to_string());
734        }
735    }
736    None
737}
738
739fn parse_literal(s: &str) -> Option<LiteralValue> {
740    let s = s.trim();
741    if s.eq_ignore_ascii_case("true") {
742        return Some(LiteralValue::Bool(true));
743    }
744    if s.eq_ignore_ascii_case("false") {
745        return Some(LiteralValue::Bool(false));
746    }
747    if s.len() >= 2
748        && ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
749    {
750        return Some(LiteralValue::String(s[1..s.len() - 1].to_string()));
751    }
752    if let Ok(n) = s.parse::<i64>() {
753        return Some(LiteralValue::Int(n));
754    }
755    if let Ok(f) = s.parse::<f64>() {
756        return Some(LiteralValue::Float(f));
757    }
758    None
759}
760
761fn lit_to_tuple_value(lv: &LiteralValue) -> TupleValue {
762    literal_to_tuple_value(lv)
763}
764
765fn split_top_level(s: &str, sep: &str) -> Option<Vec<String>> {
766    let mut parts = Vec::new();
767    let mut depth = 0i64;
768    let mut last = 0usize;
769    let bytes = s.as_bytes();
770    let sep_bytes = sep.as_bytes();
771    let mut i = 0;
772    while i < bytes.len() {
773        match bytes[i] {
774            b'(' | b'[' | b'{' => depth += 1,
775            b')' | b']' | b'}' => depth -= 1,
776            _ => {}
777        }
778        if depth == 0
779            && i + sep_bytes.len() <= bytes.len()
780            && &bytes[i..i + sep_bytes.len()] == sep_bytes
781        {
782            parts.push(s[last..i].trim().to_string());
783            last = i + sep_bytes.len();
784            i = last;
785            continue;
786        }
787        i += 1;
788    }
789    if parts.is_empty() {
790        return None;
791    }
792    parts.push(s[last..].trim().to_string());
793    Some(parts)
794}
795
796fn split_top_level_op<'a>(s: &'a str, op: &str) -> Option<(&'a str, &'a str)> {
797    let mut depth = 0i64;
798    let bytes = s.as_bytes();
799    let op_bytes = op.as_bytes();
800    let mut i = 0;
801    while i < bytes.len() {
802        match bytes[i] {
803            b'(' | b'[' | b'{' => depth += 1,
804            b')' | b']' | b'}' => depth -= 1,
805            _ => {}
806        }
807        if depth == 0
808            && i + op_bytes.len() <= bytes.len()
809            && &bytes[i..i + op_bytes.len()] == op_bytes
810        {
811            if op.len() == 1 {
812                let next = bytes.get(i + 1).copied();
813                if next == Some(b'=') {
814                    i += 1;
815                    continue;
816                }
817            }
818            return Some((&s[..i], &s[i + op_bytes.len()..]));
819        }
820        i += 1;
821    }
822    None
823}
824
825#[cfg(test)]
826mod tests {
827    use super::*;
828    use crate::iteration::comprehension::ast::Comprehension;
829    use crate::iteration::comprehension::ir::compile;
830    use crate::iteration::comprehension::source::{LiteralValue, Source};
831
832    fn clause(name: &str, vs: &[i64]) -> Comprehension {
833        Comprehension::clause(
834            name,
835            Source::Literal {
836                values: vs.iter().map(|n| LiteralValue::Int(*n)).collect(),
837            },
838        )
839    }
840
841    fn collect(stream: &mut BoxedStream) -> Vec<Tuple> {
842        let mut out = Vec::new();
843        while let Some(t) = stream.advance() {
844            out.push(t);
845        }
846        out
847    }
848
849    #[test]
850    fn single_clause_dispense() {
851        let ast = clause("k", &[1, 2, 3]);
852        let prog = compile(&ast);
853        let mut stream = interpret(&prog);
854        let tuples = collect(&mut stream);
855        assert_eq!(tuples.len(), 3);
856        assert_eq!(tuples[0].bindings[0].1, TupleValue::I64(1));
857        assert_eq!(tuples[2].bindings[0].1, TupleValue::I64(3));
858    }
859
860    #[test]
861    fn cartesian_2d_lex_order() {
862        let ast = Comprehension::cartesian(vec![clause("a", &[1, 2]), clause("b", &[10, 20])]);
863        let prog = compile(&ast);
864        let mut stream = interpret(&prog);
865        let tuples = collect(&mut stream);
866        assert_eq!(tuples.len(), 4);
867        // Lex: (a=1, b=10), (a=1, b=20), (a=2, b=10), (a=2, b=20)
868        assert_eq!(tuples[0].bindings[0].1, TupleValue::I64(1));
869        assert_eq!(tuples[0].bindings[1].1, TupleValue::I64(10));
870        assert_eq!(tuples[1].bindings[1].1, TupleValue::I64(20));
871        assert_eq!(tuples[2].bindings[0].1, TupleValue::I64(2));
872    }
873
874    #[test]
875    fn zip_strict_3() {
876        let ast = Comprehension::zip(
877            vec![clause("x", &[1, 2, 3]), clause("y", &[10, 20, 30])],
878            ZipMode::Strict,
879        );
880        let prog = compile(&ast);
881        let mut stream = interpret(&prog);
882        let tuples = collect(&mut stream);
883        assert_eq!(tuples.len(), 3);
884        assert_eq!(tuples[0].bindings[0].1, TupleValue::I64(1));
885        assert_eq!(tuples[0].bindings[1].1, TupleValue::I64(10));
886        assert_eq!(tuples[2].bindings[1].1, TupleValue::I64(30));
887    }
888
889    #[test]
890    fn zip_truncate_shortest() {
891        let ast = Comprehension::zip(
892            vec![clause("x", &[1, 2, 3, 4]), clause("y", &[10, 20])],
893            ZipMode::Truncate,
894        );
895        let prog = compile(&ast);
896        let mut stream = interpret(&prog);
897        let tuples = collect(&mut stream);
898        assert_eq!(tuples.len(), 2);
899    }
900
901    #[test]
902    fn union_drains_in_order() {
903        let ast = Comprehension::union(vec![clause("k", &[1, 2]), clause("k", &[10, 20])]);
904        let prog = compile(&ast);
905        let mut stream = interpret(&prog);
906        let tuples = collect(&mut stream);
907        assert_eq!(tuples.len(), 4);
908        assert_eq!(tuples[0].bindings[0].1, TupleValue::I64(1));
909        assert_eq!(tuples[2].bindings[0].1, TupleValue::I64(10));
910    }
911
912    #[test]
913    fn filter_keeps_only_matching() {
914        let cart = Comprehension::cartesian(vec![clause("k", &[1, 2, 3, 4, 5])]);
915        let ast = Comprehension::filter(cart, "{k} > 2");
916        let prog = compile(&ast);
917        let mut stream = interpret(&prog);
918        let tuples = collect(&mut stream);
919        assert_eq!(tuples.len(), 3);
920        for t in &tuples {
921            match t.bindings[0].1 {
922                TupleValue::I64(n) => assert!(n > 2),
923                _ => panic!(),
924            }
925        }
926    }
927
928    #[test]
929    fn order_streaming_lex_truncates() {
930        let ast = Comprehension::order(clause("k", &[1, 2, 3, 4, 5]), StrategyName::Lex, Some(2));
931        let prog = compile(&ast);
932        let mut stream = interpret(&prog);
933        let tuples = collect(&mut stream);
934        assert_eq!(tuples.len(), 2);
935        assert_eq!(tuples[0].bindings[0].1, TupleValue::I64(1));
936        assert_eq!(tuples[1].bindings[0].1, TupleValue::I64(2));
937    }
938
939    #[test]
940    fn order_materialize_shuffle_produces_full_set() {
941        let ast = Comprehension::order(clause("k", &[1, 2, 3, 4, 5]), StrategyName::Shuffle, None);
942        let prog = compile(&ast);
943        let mut stream = interpret(&prog);
944        let tuples = collect(&mut stream);
945        assert_eq!(tuples.len(), 5);
946        // All original values must be present (just permuted).
947        let mut sorted: Vec<i64> = tuples
948            .iter()
949            .map(|t| match t.bindings[0].1 {
950                TupleValue::I64(n) => n,
951                _ => panic!(),
952            })
953            .collect();
954        sorted.sort();
955        assert_eq!(sorted, vec![1, 2, 3, 4, 5]);
956    }
957
958    #[test]
959    fn dispense_sequence_for_section_11_1() {
960        // Spec §11.1: cartesian over (k in 1..=2) × (b in 10..=20 step 10).
961        let ast = Comprehension::cartesian(vec![clause("k", &[1, 2]), clause("b", &[10, 20])]);
962        let prog = compile(&ast);
963        let mut stream = interpret(&prog);
964        let tuples = collect(&mut stream);
965        // 2 × 2 = 4 tuples in Lex.
966        assert_eq!(tuples.len(), 4);
967    }
968}