1use std::collections::HashMap;
65#[cfg(test)]
66use std::sync::Arc;
67
68use crate::ast::Value;
69use crate::dsl::compile::eval_const_expr;
70use crate::iteration::comprehension::ast::Comprehension;
71use crate::iteration::comprehension::eval_source::{EvalContext, SourceEval};
72use crate::iteration::comprehension::metadata::IndexFn;
73use crate::iteration::comprehension::source::Source;
74use crate::iteration::comprehension::strategies::{EvaluatedInput, Tuple, TupleValue};
75use crate::iteration::comprehension::strategy::StrategyName;
76#[cfg(test)]
77use crate::kernel::PolydatKernel;
78use crate::kernel::interp::{Layered, Lookup, interpolate_via_kernel};
79
80pub type RuntimeTuple = Vec<(String, Value)>;
87
88struct EvaluatedNode {
98 tuples: Vec<RuntimeTuple>,
99 index_fn: Option<IndexFn>,
100}
101
102#[derive(Debug)]
105pub struct EmptyClause<'a> {
106 pub var: &'a str,
108 pub spec_expr: Option<&'a str>,
110}
111
112#[derive(Debug, Clone)]
114pub enum RuntimeError {
115 SourceEval {
118 var: String,
120 source: String,
122 message: String,
124 },
125 FilterEval {
127 predicate: String,
129 message: String,
131 },
132 OrderEval {
134 strategy: StrategyName,
136 message: String,
138 },
139 StrategyRejectsInput {
142 strategy: StrategyName,
144 index_fn: Option<IndexFn>,
146 },
147 UnsupportedShape(String),
150 EmptyPolicy(String),
152}
153
154impl std::fmt::Display for RuntimeError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 RuntimeError::SourceEval {
158 var,
159 source,
160 message,
161 } => {
162 write!(f, "for_each clause '{var} in {source}': {message}")
163 }
164 RuntimeError::FilterEval { predicate, message } => {
165 write!(f, "comprehension filter '{predicate}': {message}")
166 }
167 RuntimeError::OrderEval { strategy, message } => {
168 write!(f, "order strategy {strategy:?}: {message}")
169 }
170 RuntimeError::StrategyRejectsInput { strategy, index_fn } => write!(
171 f,
172 "order strategy {strategy:?} rejects input shape {index_fn:?} \
173 (V4: per-strategy IndexFn contract; see spec §3.6's strategy table)"
174 ),
175 RuntimeError::UnsupportedShape(msg) => write!(f, "{msg}"),
176 RuntimeError::EmptyPolicy(msg) => write!(f, "{msg}"),
177 }
178 }
179}
180
181impl std::error::Error for RuntimeError {}
182
183pub fn evaluate_for_iteration<F>(
199 comp: &Comprehension,
200 scope: &dyn Lookup,
201 workload_params: &HashMap<String, String>,
202 on_empty: F,
203) -> Result<Vec<RuntimeTuple>, RuntimeError>
204where
205 F: FnMut(EmptyClause<'_>) -> Result<(), String>,
206{
207 let mut state = EvalState {
208 scope,
209 workload_params,
210 on_empty,
211 };
212 state.evaluate_node(comp, &[]).map(|n| n.tuples)
213}
214
215fn fast_predicate(predicate: &str, tuple: &RuntimeTuple) -> Option<bool> {
220 let p = predicate.trim();
221 if p.eq_ignore_ascii_case("true") {
222 return Some(true);
223 }
224 if p.eq_ignore_ascii_case("false") {
225 return Some(false);
226 }
227 if let Some(inner) = p.strip_prefix('!') {
228 return fast_predicate(inner, tuple).map(|b| !b);
229 }
230 if let Some(parts) = split_top(p, "||") {
231 let mut any = false;
232 for part in parts {
233 any |= fast_predicate(&part, tuple)?;
234 }
235 return Some(any);
236 }
237 if let Some(parts) = split_top(p, "&&") {
238 let mut all = true;
239 for part in parts {
240 all &= fast_predicate(&part, tuple)?;
241 }
242 return Some(all);
243 }
244 if let Some(pos) = p.find(" in ") {
245 let name = curly(p[..pos].trim())?;
246 let list = p[pos + 4..].trim().strip_prefix('[')?.strip_suffix(']')?;
247 let needle = tuple_scalar(tuple, &name)?;
248 let mut hit = false;
249 for item in list.split(',') {
250 let lit = literal(item.trim())?;
251 hit |= scalar_eq(&needle, &lit);
252 }
253 return Some(hit);
254 }
255 for op in ["==", "!=", "<=", ">=", "<", ">"] {
256 if let Some((lhs, rhs)) = split_op(p, op) {
257 let lhs = lhs.trim();
258 let rhs = rhs.trim();
259 let a = operand(tuple, lhs)?;
260 let b = operand(tuple, rhs)?;
261 return Some(match op {
262 "==" => scalar_eq(&a, &b),
263 "!=" => !scalar_eq(&a, &b),
264 "<" => scalar_cmp(&a, &b)? == std::cmp::Ordering::Less,
265 ">" => scalar_cmp(&a, &b)? == std::cmp::Ordering::Greater,
266 "<=" => scalar_cmp(&a, &b)? != std::cmp::Ordering::Greater,
267 _ => scalar_cmp(&a, &b)? != std::cmp::Ordering::Less,
268 });
269 }
270 }
271 None
272}
273
274#[derive(Debug, Clone, PartialEq)]
275enum Scalar {
276 Int(i128),
277 Float(f64),
278 Str(String),
279 Bool(bool),
280}
281
282fn operand(tuple: &RuntimeTuple, text: &str) -> Option<Scalar> {
283 match curly(text) {
284 Some(name) => tuple_scalar(tuple, &name),
285 None => literal(text),
286 }
287}
288
289fn tuple_scalar(tuple: &RuntimeTuple, name: &str) -> Option<Scalar> {
290 let (_, v) = tuple.iter().find(|(n, _)| n == name)?;
291 match v {
292 Value::U64(n) => Some(Scalar::Int(*n as i128)),
293 Value::F64(f) => Some(Scalar::Float(*f)),
294 Value::Str(s) => Some(Scalar::Str(s.to_string())),
295 Value::Bool(b) => Some(Scalar::Bool(*b)),
296 Value::Json(j) => match j.as_ref() {
298 serde_json::Value::Number(n) if n.is_i64() => Some(Scalar::Int(n.as_i64()? as i128)),
299 serde_json::Value::Number(n) if n.is_u64() => Some(Scalar::Int(n.as_u64()? as i128)),
300 serde_json::Value::Number(n) => Some(Scalar::Float(n.as_f64()?)),
301 serde_json::Value::String(s) => Some(Scalar::Str(s.clone())),
302 serde_json::Value::Bool(b) => Some(Scalar::Bool(*b)),
303 _ => None,
304 },
305 _ => None,
306 }
307}
308
309fn literal(text: &str) -> Option<Scalar> {
310 if let Some(s) = text.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
311 return Some(Scalar::Str(s.to_string()));
312 }
313 if let Some(s) = text.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
314 return Some(Scalar::Str(s.to_string()));
315 }
316 match text {
317 "true" => return Some(Scalar::Bool(true)),
318 "false" => return Some(Scalar::Bool(false)),
319 _ => {}
320 }
321 if let Ok(i) = text.parse::<i128>() {
322 return Some(Scalar::Int(i));
323 }
324 if let Ok(f) = text.parse::<f64>() {
325 return Some(Scalar::Float(f));
326 }
327 if !text.is_empty()
330 && text
331 .chars()
332 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
333 {
334 return Some(Scalar::Str(text.to_string()));
335 }
336 None
337}
338
339fn scalar_eq(a: &Scalar, b: &Scalar) -> bool {
340 match (a, b) {
341 (Scalar::Int(x), Scalar::Float(y)) | (Scalar::Float(y), Scalar::Int(x)) => {
342 (*x as f64) == *y
343 }
344 _ => a == b,
345 }
346}
347
348fn scalar_cmp(a: &Scalar, b: &Scalar) -> Option<std::cmp::Ordering> {
349 match (a, b) {
350 (Scalar::Int(x), Scalar::Int(y)) => Some(x.cmp(y)),
351 (Scalar::Float(x), Scalar::Float(y)) => x.partial_cmp(y),
352 (Scalar::Int(x), Scalar::Float(y)) => (*x as f64).partial_cmp(y),
353 (Scalar::Float(x), Scalar::Int(y)) => x.partial_cmp(&(*y as f64)),
354 (Scalar::Str(x), Scalar::Str(y)) => Some(x.cmp(y)),
355 _ => None,
356 }
357}
358
359fn curly(text: &str) -> Option<String> {
360 let inner = text.strip_prefix('{')?.strip_suffix('}')?;
361 (!inner.is_empty() && inner.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
362 .then(|| inner.to_string())
363}
364
365fn split_top(s: &str, sep: &str) -> Option<Vec<String>> {
367 let mut parts = Vec::new();
368 let mut depth = 0i32;
369 let mut quote: Option<char> = None;
370 let mut start = 0;
371 let bytes: Vec<char> = s.chars().collect();
372 let sepc: Vec<char> = sep.chars().collect();
373 let mut i = 0;
374 while i < bytes.len() {
375 let c = bytes[i];
376 if let Some(q) = quote {
377 if c == q {
378 quote = None;
379 }
380 } else {
381 match c {
382 '"' | '\'' => quote = Some(c),
383 '(' | '[' | '{' => depth += 1,
384 ')' | ']' | '}' => depth -= 1,
385 _ => {}
386 }
387 if depth == 0 && bytes[i..].starts_with(&sepc) {
388 parts.push(bytes[start..i].iter().collect::<String>());
389 i += sepc.len();
390 start = i;
391 continue;
392 }
393 }
394 i += 1;
395 }
396 if parts.is_empty() {
397 return None;
398 }
399 parts.push(bytes[start..].iter().collect::<String>());
400 Some(parts)
401}
402
403fn split_op<'a>(s: &'a str, op: &str) -> Option<(&'a str, &'a str)> {
404 let mut depth = 0i32;
405 let mut quote: Option<char> = None;
406 let chars: Vec<(usize, char)> = s.char_indices().collect();
407 for (k, &(idx, c)) in chars.iter().enumerate() {
408 if let Some(q) = quote {
409 if c == q {
410 quote = None;
411 }
412 continue;
413 }
414 match c {
415 '"' | '\'' => {
416 quote = Some(c);
417 continue;
418 }
419 '(' | '[' | '{' => depth += 1,
420 ')' | ']' | '}' => depth -= 1,
421 _ => {}
422 }
423 if depth == 0 && s[idx..].starts_with(op) {
424 let next = chars.get(k + op.len()).map(|(_, c)| *c);
426 if (op == "<" || op == ">") && next == Some('=') {
427 continue;
428 }
429 let prev = if k > 0 { Some(chars[k - 1].1) } else { None };
430 if (op == "<" || op == ">") && matches!(prev, Some('<') | Some('>')) {
431 continue;
432 }
433 return Some((&s[..idx], &s[idx + op.len()..]));
434 }
435 }
436 None
437}
438
439struct EvalState<'a, F> {
443 scope: &'a dyn Lookup,
446 #[allow(dead_code)]
455 workload_params: &'a HashMap<String, String>,
456 on_empty: F,
457}
458
459impl<F> EvalState<'_, F>
460where
461 F: FnMut(EmptyClause<'_>) -> Result<(), String>,
462{
463 fn evaluate_node(
464 &mut self,
465 node: &Comprehension,
466 prefix: &[(String, Value)],
467 ) -> Result<EvaluatedNode, RuntimeError> {
468 match node {
469 Comprehension::Clause { name, source } => self.evaluate_clause(name, source, prefix),
470 Comprehension::Cartesian { children } => self.evaluate_cartesian(children, prefix),
471 Comprehension::Zip { children, mode } => self.evaluate_zip(children, *mode, prefix),
472 Comprehension::Union { children } => self.evaluate_union(children, prefix),
473 Comprehension::Filter { child, predicate } => {
474 let inner = self.evaluate_node(child, prefix)?;
475 self.apply_filter(inner, predicate)
476 }
477 Comprehension::Order {
478 child,
479 strategy,
480 truncation,
481 } => {
482 let inner = self.evaluate_node(child, prefix)?;
483 if inner.tuples.is_empty()
487 && let Some(intervals) = continuous_axes(child)
488 {
489 let names = child.coordinate_names();
490 return Self::sample_continuous(&names, &intervals, *strategy, *truncation);
491 }
492 self.apply_order(inner, *strategy, *truncation)
493 }
494 }
495 }
496
497 fn evaluate_clause(
498 &mut self,
499 name: &str,
500 source: &Source,
501 prefix: &[(String, Value)],
502 ) -> Result<EvaluatedNode, RuntimeError> {
503 let ctx = EvalContext {
504 var_name: name,
505 scope: self.scope,
506 prefix,
507 };
508 let evaluated = source.evaluate(Some(&ctx)).map_err(|e| match e {
509 crate::iteration::comprehension::eval_source::EvalError::EvalFailed {
510 var,
511 source,
512 message,
513 } => RuntimeError::SourceEval {
514 var,
515 source,
516 message,
517 },
518 crate::iteration::comprehension::eval_source::EvalError::NeedsContext => {
519 RuntimeError::UnsupportedShape(format!(
520 "clause '{name}': source requires kernel context but evaluator \
521 provided none — internal bug in runtime walker"
522 ))
523 }
524 })?;
525
526 if evaluated.values.is_empty() {
527 let spec_text = source_display_text(source);
528 (self.on_empty)(EmptyClause {
529 var: name,
530 spec_expr: spec_text.as_deref(),
531 })
532 .map_err(RuntimeError::EmptyPolicy)?;
533 return Ok(EvaluatedNode {
534 tuples: Vec::new(),
535 index_fn: Some(evaluated.index_fn),
536 });
537 }
538 let tuples: Vec<RuntimeTuple> = evaluated
539 .values
540 .into_iter()
541 .map(|v| vec![(name.to_string(), v)])
542 .collect();
543 Ok(EvaluatedNode {
544 tuples,
545 index_fn: Some(evaluated.index_fn),
546 })
547 }
548
549 fn evaluate_cartesian(
550 &mut self,
551 children: &[Comprehension],
552 prefix: &[(String, Value)],
553 ) -> Result<EvaluatedNode, RuntimeError> {
554 if children.is_empty() {
555 return Ok(EvaluatedNode {
556 tuples: vec![Vec::new()],
557 index_fn: Some(IndexFn::Lattice {
558 axis_sizes: vec![1],
559 }),
560 });
561 }
562 let mut child_index_fns: Vec<Option<IndexFn>> = Vec::with_capacity(children.len());
563 let mut dependent_observed = false;
564 let result_tuples = self.evaluate_cartesian_rec(
565 children,
566 prefix,
567 &mut child_index_fns,
568 &mut dependent_observed,
569 )?;
570
571 let combined = if dependent_observed {
577 None
578 } else {
579 combine_cartesian_index_fn(&child_index_fns)
580 };
581 Ok(EvaluatedNode {
582 tuples: result_tuples,
583 index_fn: combined,
584 })
585 }
586
587 fn evaluate_cartesian_rec(
588 &mut self,
589 children: &[Comprehension],
590 prefix: &[(String, Value)],
591 child_index_fns: &mut Vec<Option<IndexFn>>,
592 dependent_observed: &mut bool,
593 ) -> Result<Vec<RuntimeTuple>, RuntimeError> {
594 if children.is_empty() {
595 return Ok(vec![Vec::new()]);
596 }
597 let (head, tail) = children.split_first().unwrap();
598 let head_eval = self.evaluate_node(head, prefix)?;
599 let head_axis_len = head_eval.tuples.len() as u64;
600 if child_index_fns.len() <= prefix_depth(prefix, child_index_fns) {
602 child_index_fns.push(head_eval.index_fn.clone());
603 } else if let Some(prev) = child_index_fns
604 .get(prefix_depth(prefix, child_index_fns))
605 .cloned()
606 .flatten()
607 {
608 if axis_size_of(&prev) != Some(head_axis_len) {
612 *dependent_observed = true;
613 }
614 }
615
616 if tail.is_empty() {
617 return Ok(head_eval.tuples);
618 }
619 let mut out = Vec::new();
620 for head_tuple in head_eval.tuples {
621 let mut extended_prefix: Vec<(String, Value)> = prefix.to_vec();
622 extended_prefix.extend(head_tuple.iter().cloned());
623 let tail_tuples = self.evaluate_cartesian_rec(
624 tail,
625 &extended_prefix,
626 child_index_fns,
627 dependent_observed,
628 )?;
629 for tail_tuple in tail_tuples {
630 let mut merged = head_tuple.clone();
631 merged.extend(tail_tuple);
632 out.push(merged);
633 }
634 }
635 Ok(out)
636 }
637
638 fn evaluate_zip(
639 &mut self,
640 children: &[Comprehension],
641 mode: crate::iteration::comprehension::strategy::ZipMode,
642 prefix: &[(String, Value)],
643 ) -> Result<EvaluatedNode, RuntimeError> {
644 use crate::iteration::comprehension::strategy::ZipMode;
645 if children.is_empty() {
646 return Ok(EvaluatedNode {
647 tuples: vec![Vec::new()],
648 index_fn: Some(IndexFn::Lockstep { length: 1 }),
649 });
650 }
651 let per_child: Vec<EvaluatedNode> = children
652 .iter()
653 .map(|c| self.evaluate_node(c, prefix))
654 .collect::<Result<_, _>>()?;
655 let lengths: Vec<usize> = per_child.iter().map(|n| n.tuples.len()).collect();
656 let iter_count = match mode {
657 ZipMode::Strict => {
658 let first = lengths.first().copied().unwrap_or(0);
659 if lengths.iter().any(|&n| n != first) {
660 return Err(RuntimeError::UnsupportedShape(format!(
661 "zip strict: child lengths differ ({lengths:?})"
662 )));
663 }
664 first
665 }
666 ZipMode::Truncate => lengths.iter().copied().min().unwrap_or(0),
667 ZipMode::Cycle => lengths.iter().copied().max().unwrap_or(0),
668 };
669 let mut tuples = Vec::with_capacity(iter_count);
670 for i in 0..iter_count {
671 let mut bindings: RuntimeTuple = Vec::new();
672 for (child, &len) in per_child.iter().zip(lengths.iter()) {
673 if len == 0 {
674 continue;
675 }
676 let idx = match mode {
677 ZipMode::Cycle => i % len,
678 _ => i,
679 };
680 bindings.extend(child.tuples[idx].iter().cloned());
681 }
682 tuples.push(bindings);
683 }
684 let index_fn = match mode {
685 ZipMode::Strict | ZipMode::Truncate => Some(IndexFn::Lockstep {
686 length: iter_count as u64,
687 }),
688 ZipMode::Cycle => Some(IndexFn::Modular {
689 axis_sizes: lengths.iter().map(|n| *n as u64).collect(),
690 }),
691 };
692 Ok(EvaluatedNode { tuples, index_fn })
693 }
694
695 fn evaluate_union(
696 &mut self,
697 children: &[Comprehension],
698 prefix: &[(String, Value)],
699 ) -> Result<EvaluatedNode, RuntimeError> {
700 let mut tuples = Vec::new();
701 let mut segment_sizes = Vec::with_capacity(children.len());
702 let mut all_segments_addressable = true;
703 for child in children {
704 let sub = self.evaluate_node(child, prefix)?;
705 segment_sizes.push(sub.tuples.len() as u64);
706 if sub.index_fn.is_none() {
707 all_segments_addressable = false;
708 }
709 tuples.extend(sub.tuples);
710 }
711 let index_fn = if all_segments_addressable {
712 Some(IndexFn::Concatenation { segment_sizes })
713 } else {
714 None
715 };
716 Ok(EvaluatedNode { tuples, index_fn })
717 }
718
719 fn apply_filter(
720 &mut self,
721 input: EvaluatedNode,
722 predicate: &str,
723 ) -> Result<EvaluatedNode, RuntimeError> {
724 let mut out = Vec::with_capacity(input.tuples.len());
725 for tuple in input.tuples {
726 if let Some(keep) = fast_predicate(predicate, &tuple) {
732 if keep {
733 out.push(tuple);
734 }
735 continue;
736 }
737 let scope = Layered {
738 prefix: &tuple,
739 inner: self.scope,
740 };
741 let interpolated = interpolate_via_kernel(predicate, &scope).map_err(|e| {
742 RuntimeError::FilterEval {
743 predicate: predicate.to_string(),
744 message: e.to_string(),
745 }
746 })?;
747 let result = eval_const_expr(&interpolated).map_err(|e| RuntimeError::FilterEval {
748 predicate: predicate.to_string(),
749 message: e.to_string(),
750 })?;
751 let keep = match result {
752 Value::Bool(b) => b,
753 Value::U64(n) => n != 0,
754 Value::F64(n) => n != 0.0,
755 other => {
756 return Err(RuntimeError::FilterEval {
757 predicate: predicate.to_string(),
758 message: format!("expected bool/u64/f64, got {other:?}"),
759 });
760 }
761 };
762 if keep {
763 out.push(tuple);
764 }
765 }
766 Ok(EvaluatedNode {
768 tuples: out,
769 index_fn: None,
770 })
771 }
772
773 fn sample_continuous(
778 names: &[String],
779 intervals: &[crate::iteration::comprehension::cardinality::Interval],
780 strategy: StrategyName,
781 truncation: Option<u64>,
782 ) -> Result<EvaluatedNode, RuntimeError> {
783 use crate::iteration::comprehension::strategies::{
784 halton::halton_multi_indices, lhs::lhs_multi_indices, shuffle::shuffle_multi_indices,
785 sobol::sobol_multi_indices,
786 };
787 let Some(n) = truncation else {
788 return Err(RuntimeError::OrderEval {
789 strategy,
790 message: "a continuous source has no finite tuple set; give the order a count, as in `order halton/16`".into(),
791 });
792 };
793 let index_fn = IndexFn::Continuous {
794 intervals: intervals.to_vec(),
795 measure: crate::iteration::comprehension::cardinality::ProductMeasure::Uniform,
796 };
797 let points = match strategy {
798 StrategyName::Halton => halton_multi_indices(&index_fn, Some(n)),
799 StrategyName::Sobol => sobol_multi_indices(&index_fn, Some(n)),
800 StrategyName::Lhs => lhs_multi_indices(&index_fn, Some(n)),
801 StrategyName::Shuffle => shuffle_multi_indices(&index_fn, Some(n)),
802 other => return Err(RuntimeError::OrderEval {
803 strategy: other,
804 message:
805 "a continuous source needs a sampling strategy: halton, sobol, lhs, or shuffle"
806 .into(),
807 }),
808 };
809 let scale = (1u64 << 53) as f64;
810 let tuples = points
811 .into_iter()
812 .map(|mi| {
813 mi.iter()
814 .enumerate()
815 .map(|(axis, u)| {
816 let iv = &intervals[axis.min(intervals.len().saturating_sub(1))];
817 let frac = (*u as f64) / scale;
818 let x = iv.lo + frac * (iv.hi - iv.lo);
819 let name = names
820 .get(axis)
821 .cloned()
822 .unwrap_or_else(|| format!("axis{axis}"));
823 (name, Value::F64(x))
824 })
825 .collect::<RuntimeTuple>()
826 })
827 .collect();
828 Ok(EvaluatedNode {
829 tuples,
830 index_fn: None,
831 })
832 }
833
834 fn apply_order(
835 &mut self,
836 input: EvaluatedNode,
837 strategy: StrategyName,
838 truncation: Option<u64>,
839 ) -> Result<EvaluatedNode, RuntimeError> {
840 use crate::iteration::comprehension::strategies::{
841 Strategy, antidiagonal::Antidiagonal, diagonal::Diagonal, extrema::Extrema,
842 halton::Halton, lex::Lex, lhs::Lhs, reverse_lex::ReverseLex, shells::Shells,
843 shuffle::Shuffle, sobol::Sobol,
844 };
845 use crate::iteration::comprehension::surfaces::polydat_value_to_tuple_value;
846
847 let dispatch: Box<dyn Strategy> = match strategy {
848 StrategyName::Lex => Box::new(Lex),
849 StrategyName::ReverseLex => Box::new(ReverseLex),
850 StrategyName::Diagonal => Box::new(Diagonal),
851 StrategyName::Antidiagonal => Box::new(Antidiagonal),
852 StrategyName::Extrema => Box::new(Extrema),
853 StrategyName::Shells => Box::new(Shells),
854 StrategyName::Halton => Box::new(Halton),
855 StrategyName::Sobol => Box::new(Sobol),
856 StrategyName::Lhs => Box::new(Lhs),
857 StrategyName::Shuffle => Box::new(Shuffle),
858 };
859
860 if !dispatch.accepts_input(input.index_fn.as_ref()) {
862 return Err(RuntimeError::StrategyRejectsInput {
863 strategy,
864 index_fn: input.index_fn.clone(),
865 });
866 }
867
868 let algebra_tuples: Vec<Tuple> = input
875 .tuples
876 .iter()
877 .map(|rt| Tuple {
878 bindings: rt
879 .iter()
880 .map(|(n, v)| {
881 let tv = polydat_value_to_tuple_value(v)
882 .unwrap_or(TupleValue::Str(v.to_display_string()));
883 (n.clone(), tv)
884 })
885 .collect(),
886 })
887 .collect();
888
889 let index_fn = input.index_fn.clone().unwrap_or(IndexFn::Lattice {
898 axis_sizes: vec![algebra_tuples.len() as u64],
899 });
900 let cardinality = algebra_tuples.len() as u64;
901 let evaluated_input = EvaluatedInput {
902 tuples: algebra_tuples.clone(),
903 cardinality,
904 index_fn,
905 };
906
907 let ordered = dispatch.apply(&evaluated_input, truncation);
908
909 let mut consumed = vec![false; algebra_tuples.len()];
912 let mut out = Vec::with_capacity(ordered.len());
913 for ordered_tuple in &ordered {
914 let idx = algebra_tuples
915 .iter()
916 .enumerate()
917 .find(|(i, at)| !consumed[*i] && *at == ordered_tuple)
918 .map(|(i, _)| i)
919 .ok_or_else(|| RuntimeError::OrderEval {
920 strategy,
921 message: "ordered tuple lost reference to runtime source — \
922 Strategy::apply must return tuples drawn from \
923 EvaluatedInput.tuples (per spec §10.7.8)"
924 .into(),
925 })?;
926 consumed[idx] = true;
927 out.push(input.tuples[idx].clone());
928 }
929 Ok(EvaluatedNode {
933 tuples: out,
934 index_fn: None,
935 })
936 }
937}
938
939pub(crate) fn continuous_axes(
943 c: &Comprehension,
944) -> Option<Vec<crate::iteration::comprehension::cardinality::Interval>> {
945 match c {
946 Comprehension::Clause {
947 source: Source::ContinuousInterval { interval, .. },
948 ..
949 } => Some(vec![interval.clone()]),
950 Comprehension::Clause {
951 source: Source::Distribution { support, .. },
952 ..
953 } => Some(vec![support.clone()]),
954 Comprehension::Clause { .. } => None,
955 Comprehension::Cartesian { children } => {
956 let mut out = Vec::new();
957 for ch in children {
958 out.extend(continuous_axes(ch)?);
959 }
960 Some(out)
961 }
962 Comprehension::Filter { child, .. } => continuous_axes(child),
963 Comprehension::Zip { .. } | Comprehension::Union { .. } | Comprehension::Order { .. } => {
964 None
965 }
966 }
967}
968
969fn prefix_depth(prefix: &[(String, Value)], _recorded: &[Option<IndexFn>]) -> usize {
978 prefix.len()
979}
980
981fn combine_cartesian_index_fn(children: &[Option<IndexFn>]) -> Option<IndexFn> {
982 let mut axis_sizes = Vec::new();
983 for opt in children {
984 match opt {
985 Some(IndexFn::Lattice { axis_sizes: a }) => axis_sizes.extend(a.iter().copied()),
986 Some(IndexFn::Lockstep { length }) => axis_sizes.push(*length),
987 _ => return None,
990 }
991 }
992 Some(IndexFn::Lattice { axis_sizes })
993}
994
995fn axis_size_of(idx: &IndexFn) -> Option<u64> {
996 match idx {
997 IndexFn::Lattice { axis_sizes } if axis_sizes.len() == 1 => Some(axis_sizes[0]),
998 IndexFn::Lockstep { length } => Some(*length),
999 _ => None,
1000 }
1001}
1002
1003fn source_display_text(source: &Source) -> Option<String> {
1004 match source {
1005 Source::Generator { expr, .. } => Some(expr.clone()),
1006 Source::WorkloadParamList { name, .. } => Some(format!("{{{name}}}")),
1007 _ => None,
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014 use crate::iteration::comprehension::source::LiteralValue;
1015
1016 fn empty_kernel() -> Arc<PolydatKernel> {
1017 Arc::new(crate::dsl::compile_polydat("\n").unwrap())
1018 }
1019
1020 fn canonical_with_k() -> Arc<PolydatKernel> {
1025 Arc::new(crate::dsl::compile_polydat("extern k: u64\n").unwrap())
1026 }
1027
1028 #[test]
1029 fn int_range_yields_values() {
1030 let comp = Comprehension::Clause {
1031 name: "k".into(),
1032 source: Source::IntRange {
1033 lo: 1,
1034 hi: 5,
1035 step: 1,
1036 },
1037 };
1038 let canonical = empty_kernel();
1039 let params = HashMap::new();
1040 let tuples = evaluate_for_iteration(&comp, &*canonical, ¶ms, |_| Ok(())).unwrap();
1041 assert_eq!(tuples.len(), 4);
1042 assert_eq!(tuples[0][0].1, Value::U64(1));
1043 assert_eq!(tuples[3][0].1, Value::U64(4));
1044 }
1045
1046 #[test]
1047 fn literal_list_yields_values() {
1048 let comp = Comprehension::Clause {
1049 name: "x".into(),
1050 source: Source::Literal {
1051 values: vec![LiteralValue::Int(10), LiteralValue::Int(20)],
1052 },
1053 };
1054 let canonical = empty_kernel();
1055 let params = HashMap::new();
1056 let tuples = evaluate_for_iteration(&comp, &*canonical, ¶ms, |_| Ok(())).unwrap();
1057 assert_eq!(tuples.len(), 2);
1058 }
1059
1060 #[test]
1061 fn cartesian_produces_product() {
1062 let comp = Comprehension::cartesian(vec![
1063 Comprehension::Clause {
1064 name: "x".into(),
1065 source: Source::IntRange {
1066 lo: 1,
1067 hi: 3,
1068 step: 1,
1069 },
1070 },
1071 Comprehension::Clause {
1072 name: "y".into(),
1073 source: Source::IntRange {
1074 lo: 10,
1075 hi: 30,
1076 step: 10,
1077 },
1078 },
1079 ]);
1080 let canonical = empty_kernel();
1081 let params = HashMap::new();
1082 let tuples = evaluate_for_iteration(&comp, &*canonical, ¶ms, |_| Ok(())).unwrap();
1083 assert_eq!(tuples.len(), 4);
1085 }
1086
1087 #[test]
1088 fn union_produces_concatenation() {
1089 let comp = Comprehension::union(vec![
1090 Comprehension::Clause {
1091 name: "k".into(),
1092 source: Source::Literal {
1093 values: vec![LiteralValue::Int(1)],
1094 },
1095 },
1096 Comprehension::Clause {
1097 name: "k".into(),
1098 source: Source::Literal {
1099 values: vec![LiteralValue::Int(10), LiteralValue::Int(20)],
1100 },
1101 },
1102 ]);
1103 let canonical = empty_kernel();
1104 let params = HashMap::new();
1105 let tuples = evaluate_for_iteration(&comp, &*canonical, ¶ms, |_| Ok(())).unwrap();
1106 assert_eq!(tuples.len(), 3);
1107 }
1108
1109 #[test]
1110 fn filter_drops_non_matching() {
1111 let comp = Comprehension::filter(
1112 Comprehension::Clause {
1113 name: "k".into(),
1114 source: Source::IntRange {
1115 lo: 1,
1116 hi: 6,
1117 step: 1,
1118 },
1119 },
1120 "{k} > 3",
1121 );
1122 let canonical = canonical_with_k();
1123 let params = HashMap::new();
1124 let tuples = evaluate_for_iteration(&comp, &*canonical, ¶ms, |_| Ok(())).unwrap();
1125 assert_eq!(tuples.len(), 2);
1127 }
1128
1129 #[test]
1130 fn order_lex_truncate() {
1131 let comp = Comprehension::order(
1132 Comprehension::Clause {
1133 name: "k".into(),
1134 source: Source::IntRange {
1135 lo: 1,
1136 hi: 100,
1137 step: 1,
1138 },
1139 },
1140 StrategyName::Lex,
1141 Some(5),
1142 );
1143 let canonical = empty_kernel();
1144 let params = HashMap::new();
1145 let tuples = evaluate_for_iteration(&comp, &*canonical, ¶ms, |_| Ok(())).unwrap();
1146 assert_eq!(tuples.len(), 5);
1147 }
1148
1149 #[test]
1155 fn extrema_over_cartesian_uses_indexed_form() {
1156 let comp = Comprehension::order(
1157 Comprehension::cartesian(vec![
1158 Comprehension::Clause {
1159 name: "k".into(),
1160 source: Source::Literal {
1161 values: vec![
1162 LiteralValue::Int(1),
1163 LiteralValue::Int(2),
1164 LiteralValue::Int(3),
1165 ],
1166 },
1167 },
1168 Comprehension::Clause {
1169 name: "limit".into(),
1170 source: Source::Literal {
1171 values: vec![
1172 LiteralValue::Int(10),
1173 LiteralValue::Int(20),
1174 LiteralValue::Int(30),
1175 ],
1176 },
1177 },
1178 ]),
1179 StrategyName::Extrema,
1180 Some(1),
1184 );
1185 let canonical = empty_kernel();
1186 let params = HashMap::new();
1187 let tuples = evaluate_for_iteration(&comp, &*canonical, ¶ms, |_| Ok(())).unwrap();
1188 assert_eq!(tuples.len(), 4);
1190 for t in &tuples {
1192 assert_eq!(t.len(), 2);
1193 let k = match &t[0].1 {
1194 Value::U64(n) => *n,
1195 other => panic!("expected u64 k, got {other:?}"),
1196 };
1197 let lim = match &t[1].1 {
1198 Value::U64(n) => *n,
1199 other => panic!("expected u64 limit, got {other:?}"),
1200 };
1201 assert!(k == 1 || k == 3, "expected extreme k, got {k}");
1202 assert!(lim == 10 || lim == 30, "expected extreme limit, got {lim}");
1203 }
1204 }
1205}