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