1use std::collections::HashMap;
43use std::sync::Arc;
44
45use crate::ast::Value;
46use crate::kernel::PolydatKernel;
47use crate::kernel::interp::Lookup;
48use crate::kernel::interp::{interpolate_via_kernel, interpolate_with_lookup};
49
50pub fn evaluate_spec(
70 spec_text: &str,
71 kernel: &dyn Lookup,
72) -> Result<Vec<Value>, crate::dsl::compile::EmbeddingError> {
73 evaluate_spec_internal(spec_text, kernel).map_err(|msg| {
74 if let Some(rest) = msg.strip_prefix("interpolation: unresolved placeholder '{")
75 && let Some(end) = rest.find('}')
76 {
77 let name = rest[..end].to_string();
78 return crate::dsl::compile::EmbeddingError::UnresolvedPlaceholder {
79 name,
80 source: spec_text.to_string(),
81 };
82 }
83 crate::dsl::compile::EmbeddingError::Parse {
84 source: spec_text.to_string(),
85 message: msg,
86 position: None,
87 }
88 })
89}
90
91fn evaluate_spec_internal(spec_text: &str, kernel: &dyn Lookup) -> Result<Vec<Value>, String> {
92 if let Some(values) = try_eval_all_cursor(spec_text, kernel)? {
93 return Ok(values);
94 }
95 if is_single_bare_ident(spec_text) {
109 return match kernel.lookup(spec_text.trim()) {
113 Some(v) => Ok(
114 match crate::iteration::comprehension::source_values::iteration_interior(&v) {
115 Some(interior) => interior,
116 None => vec![v],
117 },
118 ),
119 None => Err(format!(
120 "comprehension source `{src}` did not resolve to a value — no \
121 wire, const, param, or outer iter-var by that name is in scope \
122 here. If you meant the literal string \"{src}\", quote it: \
123 `\"{src}\"`.",
124 src = spec_text.trim(),
125 )),
126 };
127 }
128 let interpolated = crate::kernel::interp::interpolate_with_lookup(spec_text, |name| {
129 kernel.lookup(name).map(|v| v.to_display_string())
130 })?;
131 if let Some(values) = try_eval_bracket_list(&interpolated, kernel)? {
136 return Ok(values);
137 }
138 if let Some(values) = try_eval_range(&interpolated)? {
143 return Ok(values);
144 }
145 if let Some(values) = try_eval_generator(&interpolated)? {
147 return Ok(values);
148 }
149 if let Some(values) = try_eval_setop(&interpolated, kernel)? {
151 return Ok(values);
152 }
153 if let Some(values) = try_eval_sequencer(&interpolated, kernel)? {
156 return Ok(values);
157 }
158 if let Some(values) = try_eval_partition_call(&interpolated, kernel)? {
162 return Ok(values);
163 }
164 if let Some(values) = try_eval_param_partitions(&interpolated, kernel)? {
167 return Ok(values);
168 }
169 match crate::dsl::compile::eval_const_expr(&interpolated) {
170 Ok(v) => Ok(
179 match crate::iteration::comprehension::source_values::iteration_interior(&v) {
180 Some(interior) => interior,
181 None => vec![v],
182 },
183 ),
184 Err(eval_err) => {
197 if looks_like_literal_list(&interpolated) {
208 Ok(
211 crate::iteration::comprehension::source_values::strip_string_tokens(
212 &interpolated,
213 ),
214 )
215 } else {
216 Err(format!(
217 "for_each clause expression failed to evaluate: {eval_err}\n\
218 spec: {interpolated}\n\
219 If this was meant as a literal list (e.g. `1, 10, 100`), \
220 it should contain only literal values separated by commas. \
221 If it was meant as an expression, fix the underlying \
222 evaluation error."
223 ))
224 }
225 }
226 }
227}
228
229fn try_eval_bracket_list(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
259 let t = text.trim();
260 if !(t.starts_with('[') && t.ends_with(']') && t.len() >= 2) {
261 return Ok(None);
262 }
263 let inner = &t[1..t.len() - 1];
264 if inner.trim().is_empty() {
265 return Ok(Some(Vec::new()));
266 }
267 let mut out = Vec::new();
268 for elem in split_args_top_level(inner) {
269 let elem = elem.trim();
270 let (expr, spread) = if let Some(stripped) = elem.strip_suffix('…') {
272 (stripped.trim(), true)
273 } else if let Some(stripped) = elem.strip_suffix("...") {
274 (stripped.trim(), true)
275 } else {
276 (elem, false)
277 };
278 if expr.is_empty() {
279 return Err("empty element in list comprehension `[...]`".to_string());
280 }
281 let value = eval_element_value(expr, kernel)?;
282 if spread {
283 match crate::iteration::comprehension::source_values::iteration_interior(&value) {
284 Some(interior) => out.extend(interior),
285 None => {
286 return Err(format!(
287 "list comprehension spread `{expr}…` requires an iterable \
288 source, but `{expr}` resolved to a scalar \
289 {ty:?}. Use `[{expr}]` to pass it as a single element, \
290 or supply a list.",
291 ty = value.port_type(),
292 ));
293 }
294 }
295 } else {
296 out.push(value);
297 }
298 }
299 Ok(Some(out))
300}
301
302fn eval_element_value(expr: &str, kernel: &dyn Lookup) -> Result<Value, String> {
309 let e = expr.trim();
310 if is_single_bare_ident(e) {
311 return kernel.lookup(e).ok_or_else(|| {
312 format!(
313 "list element `{e}` did not resolve to a value — no wire, const, \
314 param, or outer iter-var by that name is in scope here. \
315 If you meant the literal string \"{e}\", quote it: `\"{e}\"`."
316 )
317 });
318 }
319 crate::dsl::compile::eval_const_expr(e)
320 .map_err(|err| format!("list element `{e}` failed to evaluate: {err}"))
321}
322
323fn is_single_bare_ident(text: &str) -> bool {
328 let t = text.trim();
329 if t == "true" || t == "false" {
330 return false;
331 }
332 let mut chars = t.chars();
333 match chars.next() {
334 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
335 _ => return false,
336 }
337 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
338}
339
340fn looks_like_literal_list(text: &str) -> bool {
341 let trimmed = text.trim();
342 if trimmed.is_empty() {
343 return false;
344 }
345 !trimmed.chars().any(|c| {
346 matches!(
347 c,
348 '(' | ')'
349 | '['
350 | ']'
351 | '{'
352 | '}'
353 | '\''
354 | '"'
355 | '+'
356 | '*'
357 | '/'
358 | '%'
359 | '='
360 | '<'
361 | '>'
362 | '!'
363 | '&'
364 | '|'
365 | '~'
366 | '^'
367 | '?'
368 )
369 })
370}
371
372pub fn pre_evaluate_clause(
383 spec_text: &str,
384 parent_kernel: &dyn Lookup,
385 workload_params: &HashMap<String, String>,
386 probes: &HashMap<String, String>,
387) -> Result<Vec<Value>, String> {
388 if let Some(values) = try_eval_all_cursor(spec_text, parent_kernel)? {
393 return Ok(values);
394 }
395 if is_single_bare_ident(spec_text) {
405 let name = spec_text.trim();
406 if let Some(pv) = probes.get(name) {
407 return Ok(crate::iteration::comprehension::source_values::strip_string_tokens(pv));
408 }
409 if let Some(v) = parent_kernel.lookup(name) {
410 return Ok(
411 match crate::iteration::comprehension::source_values::iteration_interior(&v) {
412 Some(interior) => interior,
413 None => vec![v],
414 },
415 );
416 }
417 if let Some(s) = workload_params.get(name) {
418 return Ok(crate::iteration::comprehension::source_values::strip_string_tokens(s));
419 }
420 return Err(format!(
421 "comprehension source `{name}` did not resolve to a value — no wire, \
422 const, param, or outer iter-var by that name is in scope here. \
423 If you meant the literal string \"{name}\", quote it: `\"{name}\"`."
424 ));
425 }
426 let mut text = spec_text.to_string();
427 for (var, probe_value) in probes {
428 text = text.replace(&format!("{{{var}}}"), probe_value);
429 }
430
431 let interpolated = interpolate_with_lookup(&text, |name| {
432 parent_kernel
433 .lookup(name)
434 .map(|v| v.to_display_string())
435 .or_else(|| workload_params.get(name).cloned())
436 })?;
437
438 if let Some(values) = try_eval_range(&interpolated)? {
440 return Ok(values);
441 }
442 if let Some(values) = try_eval_generator(&interpolated)? {
445 return Ok(values);
446 }
447 if let Some(values) = try_eval_setop(&interpolated, parent_kernel)? {
448 return Ok(values);
449 }
450 if let Some(values) = try_eval_sequencer(&interpolated, parent_kernel)? {
451 return Ok(values);
452 }
453 if let Some(values) = try_eval_partition_call(&interpolated, parent_kernel)? {
459 return Ok(values);
460 }
461 if let Some(values) = try_eval_param_partitions(&interpolated, parent_kernel)? {
464 return Ok(values);
465 }
466 let value_str = match crate::dsl::compile::eval_const_expr(&interpolated) {
467 Ok(Value::Str(s)) => s.to_string(),
468 Ok(ref v) if v.as_partition_list().is_some() => {
474 let list = v.as_partition_list().unwrap();
475 return Ok(list
476 .as_slice()
477 .iter()
478 .map(|p| Value::from_partition(*p))
479 .collect());
480 }
481 Ok(other) => return Ok(vec![other]),
482 Err(eval_err) => {
487 if looks_like_literal_list(&interpolated) {
488 interpolated
489 } else {
490 return Err(format!(
491 "for_each clause expression failed to evaluate: {eval_err}\n\
492 spec: {interpolated}\n\
493 If this was meant as a literal list (e.g. `1, 10, 100`), \
494 it should contain only literal values separated by commas. \
495 If it was meant as an expression, fix the underlying \
496 evaluation error."
497 ));
498 }
499 }
500 };
501 Ok(parse_list_with_types(&value_str))
502}
503
504pub fn parse_list_with_types(text: &str) -> Vec<Value> {
509 text.split(',')
510 .map(str::trim)
511 .filter(|s| !s.is_empty())
512 .map(|s| {
513 if let Ok(n) = s.parse::<u64>() {
514 Value::U64(n)
515 } else if let Ok(n) = s.parse::<f64>() {
516 Value::F64(n)
517 } else if s == "true" {
518 Value::Bool(true)
519 } else if s == "false" {
520 Value::Bool(false)
521 } else {
522 Value::Str(s.to_string().into())
523 }
524 })
525 .collect()
526}
527
528fn try_eval_all_cursor(spec_text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
548 let trimmed = spec_text.trim();
549 let Some(stripped) = trimmed.strip_prefix("all(") else {
550 return Ok(None);
551 };
552 let Some(arg) = stripped.strip_suffix(')') else {
553 return Ok(None);
554 };
555 let cursor_name = arg.trim();
556 if cursor_name.is_empty() || !is_valid_ident(cursor_name) {
557 return Ok(None);
558 }
559
560 let start_key = format!("__cursor_extent_{cursor_name}_start");
561 let end_key = format!("__cursor_extent_{cursor_name}_end");
562 let start = kernel
563 .lookup(&start_key)
564 .and_then(|v| match v {
565 Value::U64(n) => Some(n),
566 _ => None,
567 })
568 .ok_or_else(|| {
569 format!(
570 "all({cursor_name}): cursor '{cursor_name}' has no resolvable extent — \
571 check that the cursor is declared at or above this scope and that \
572 its range arguments are init-resolvable. Looked for output '{start_key}'."
573 )
574 })?;
575 let end = kernel
576 .lookup(&end_key)
577 .and_then(|v| match v {
578 Value::U64(n) => Some(n),
579 _ => None,
580 })
581 .ok_or_else(|| {
582 format!(
583 "all({cursor_name}): missing auxiliary output '{end_key}' on the parent kernel."
584 )
585 })?;
586
587 if end < start {
588 return Err(format!(
589 "all({cursor_name}): cursor extent end={end} is less than start={start} — \
590 cannot enumerate a negative-extent range."
591 ));
592 }
593 Ok(Some((start..end).map(Value::U64).collect()))
594}
595
596fn is_valid_ident(s: &str) -> bool {
597 let mut chars = s.chars();
598 match chars.next() {
599 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
600 _ => return false,
601 }
602 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
603}
604
605fn try_eval_range(text: &str) -> Result<Option<Vec<Value>>, String> {
628 let trimmed = text.trim();
629 let chars: Vec<char> = trimmed.chars().collect();
630
631 let mut splits: Vec<(usize, bool)> = Vec::new();
635 let mut depth: i32 = 0;
636 let mut i = 0;
637 while i < chars.len() {
638 let c = chars[i];
639 match c {
640 '(' | '[' | '{' => depth += 1,
641 ')' | ']' | '}' => depth -= 1,
642 '"' | '\'' => {
643 let q = c;
645 i += 1;
646 while i < chars.len() && chars[i] != q {
647 i += 1;
648 }
649 }
650 '.' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '.' => {
651 let inclusive = i + 2 < chars.len() && chars[i + 2] == '=';
652 splits.push((i, inclusive));
653 i += if inclusive { 3 } else { 2 };
654 continue;
655 }
656 _ => {}
657 }
658 i += 1;
659 }
660
661 if splits.is_empty() {
662 return Ok(None);
663 }
664 if splits.len() > 2 {
665 return Err(format!(
666 "range expression '{trimmed}': more than two `..` operators \
667 at top level — expected one of `a..b`, `a..=b`, `a..b..s`, \
668 or `a..=b..s`"
669 ));
670 }
671 if splits.len() == 2 && splits[1].1 {
672 return Err(format!(
673 "range expression '{trimmed}': step delimiter cannot be \
674 `..=` — only the bound separator may be inclusive"
675 ));
676 }
677
678 let inclusive = splits[0].1;
680 let first_end = splits[0].0;
681 let after_first = first_end + if inclusive { 3 } else { 2 };
682 let (start_text, mid_text, step_text) = match splits.len() {
683 1 => {
684 let start_s: String = chars[..first_end].iter().collect();
685 let end_s: String = chars[after_first..].iter().collect();
686 (start_s, end_s, None)
687 }
688 2 => {
689 let mid_end = splits[1].0;
690 let after_mid = mid_end + 2; let start_s: String = chars[..first_end].iter().collect();
692 let mid_s: String = chars[after_first..mid_end].iter().collect();
693 let step_s: String = chars[after_mid..].iter().collect();
694 (start_s, mid_s, Some(step_s))
695 }
696 _ => unreachable!(),
697 };
698
699 let start_val = eval_range_segment(&start_text, "range start")?;
700 let end_val = eval_range_segment(&mid_text, "range end")?;
701 let step_val = match step_text {
702 Some(s) => Some(eval_range_segment(&s, "range step")?),
703 None => None,
704 };
705
706 Ok(Some(expand_range(
707 start_val, end_val, step_val, inclusive, trimmed,
708 )?))
709}
710
711fn eval_range_segment(text: &str, what: &str) -> Result<Value, String> {
712 let trimmed = text.trim();
713 if trimmed.is_empty() {
714 return Err(format!("range expression: {what} is empty"));
715 }
716 crate::dsl::compile::eval_const_expr(trimmed)
717 .map_err(|e| format!("range expression: {what} '{trimmed}' did not const-fold — {e}"))
718}
719
720fn expand_range(
724 start: Value,
725 end: Value,
726 step: Option<Value>,
727 inclusive: bool,
728 src: &str,
729) -> Result<Vec<Value>, String> {
730 let any_float = matches!(start, Value::F64(_))
731 || matches!(end, Value::F64(_))
732 || matches!(step, Some(Value::F64(_)));
733
734 let to_f64 = |v: &Value| -> Result<f64, String> {
735 match v {
736 Value::U64(n) => Ok(*n as f64),
737 Value::F64(f) => Ok(*f),
738 other => Err(format!(
739 "range expression '{src}': bound has non-numeric value {other:?}"
740 )),
741 }
742 };
743 let to_i64 = |v: &Value| -> Result<i64, String> {
744 match v {
745 Value::U64(n) => i64::try_from(*n).map_err(|_| {
746 format!("range expression '{src}': bound {n} exceeds signed 64-bit range")
747 }),
748 Value::F64(f) => {
749 if f.fract() == 0.0 && *f >= i64::MIN as f64 && *f <= i64::MAX as f64 {
750 Ok(*f as i64)
751 } else {
752 Err(format!(
753 "range expression '{src}': float bound {f} is not integral; \
754 mix with an explicit float step (e.g. `1.0..10..0.5`) for a float range"
755 ))
756 }
757 }
758 other => Err(format!(
759 "range expression '{src}': bound has non-numeric value {other:?}"
760 )),
761 }
762 };
763
764 if any_float {
765 let s = to_f64(&start)?;
766 let e = to_f64(&end)?;
767 let st = match step.as_ref() {
768 Some(v) => to_f64(v)?,
769 None => 1.0,
770 };
771 if st == 0.0 {
772 return Err(format!("range expression '{src}': step is zero"));
773 }
774 if (e - s).is_sign_positive() && st < 0.0 {
776 return Ok(Vec::new());
777 }
778 if (e - s).is_sign_negative() && st > 0.0 {
779 return Ok(Vec::new());
780 }
781 let mut out = Vec::new();
782 let mut cur = s;
783 let cmp = |x: f64| -> bool {
784 if st > 0.0 {
785 if inclusive {
786 x <= e + 1e-12
787 } else {
788 x < e - 1e-12
789 }
790 } else if inclusive {
791 x >= e - 1e-12
792 } else {
793 x > e + 1e-12
794 }
795 };
796 while cmp(cur) {
797 out.push(Value::F64(cur));
798 cur += st;
799 }
800 return Ok(out);
801 }
802
803 let s = to_i64(&start)?;
805 let e = to_i64(&end)?;
806 let st = match step.as_ref() {
807 Some(v) => to_i64(v)?,
808 None => 1,
809 };
810 if st == 0 {
811 return Err(format!("range expression '{src}': step is zero"));
812 }
813 if st > 0 && s > e {
814 return Ok(Vec::new());
815 }
816 if st < 0 && s < e {
817 return Ok(Vec::new());
818 }
819 let mut out = Vec::new();
820 let mut cur = s;
821 let cmp = |x: i64| -> bool {
822 if st > 0 {
823 if inclusive { x <= e } else { x < e }
824 } else if inclusive {
825 x >= e
826 } else {
827 x > e
828 }
829 };
830 while cmp(cur) {
831 if cur < 0 {
832 return Err(format!(
833 "range expression '{src}': negative value {cur} can't be \
834 represented as Value::U64; use a float range \
835 (mix any bound or step with `.0`) for signed walks"
836 ));
837 }
838 out.push(Value::U64(cur as u64));
839 cur = cur.saturating_add(st);
840 if (st > 0 && cur < s) || (st < 0 && cur > s) {
841 break;
843 }
844 }
845 Ok(out)
846}
847
848fn parse_func_call(text: &str) -> Option<(&str, &str)> {
858 let trimmed = text.trim();
859 if !trimmed.ends_with(')') {
860 return None;
861 }
862 let open = trimmed.find('(')?;
863 let name = trimmed[..open].trim();
864 if name.is_empty() || !is_valid_ident(name) {
865 return None;
866 }
867 let chars: Vec<char> = trimmed.chars().collect();
870 let mut depth = 0i32;
871 let mut in_quote: Option<char> = None;
872 for (i, &c) in chars.iter().enumerate().skip(open) {
873 match (c, in_quote) {
874 ('"' | '\'', None) => in_quote = Some(c),
875 (q, Some(open_q)) if q == open_q => in_quote = None,
876 ('(', None) => depth += 1,
877 (')', None) => {
878 depth -= 1;
879 if depth == 0 {
880 if i != chars.len() - 1 {
881 return None; }
883 let args: String = chars[open + 1..i].iter().collect();
884 let _ = args;
890 let name_slice = &trimmed[..open];
891 let args_slice = &trimmed[open + 1..trimmed.len() - 1];
892 return Some((name_slice.trim(), args_slice));
893 }
894 }
895 _ => {}
896 }
897 }
898 None
899}
900
901fn split_args_top_level(args: &str) -> Vec<&str> {
904 let mut out: Vec<&str> = Vec::new();
905 let chars: Vec<char> = args.chars().collect();
906 let bytes_per_char: Vec<usize> = chars.iter().map(|c| c.len_utf8()).collect();
907 let mut start_byte = 0usize;
908 let mut byte = 0usize;
909 let mut depth = 0i32;
910 let mut in_quote: Option<char> = None;
911 for (i, &c) in chars.iter().enumerate() {
912 match (c, in_quote) {
913 ('"' | '\'', None) => in_quote = Some(c),
914 (q, Some(open_q)) if q == open_q => in_quote = None,
915 ('(' | '[' | '{', None) => depth += 1,
916 (')' | ']' | '}', None) => depth -= 1,
917 (',', None) if depth == 0 => {
918 let seg = &args[start_byte..byte];
919 out.push(seg.trim());
920 start_byte = byte + bytes_per_char[i];
921 }
922 _ => {}
923 }
924 byte += bytes_per_char[i];
925 }
926 let last = &args[start_byte..];
927 if !last.trim().is_empty() || !out.is_empty() {
928 out.push(last.trim());
929 }
930 out
931}
932
933fn parse_u64_arg(text: &str, what: &str) -> Result<u64, String> {
936 let trimmed = text.trim();
937 trimmed
938 .parse::<u64>()
939 .map_err(|_| format!("{what}: expected non-negative integer, got '{trimmed}'"))
940}
941
942fn parse_num_arg(text: &str, what: &str) -> Result<f64, String> {
946 let trimmed = text.trim();
947 trimmed
948 .parse::<f64>()
949 .map_err(|_| format!("{what}: expected numeric, got '{trimmed}'"))
950}
951
952fn try_eval_generator(text: &str) -> Result<Option<Vec<Value>>, String> {
961 let Some((name, args)) = parse_func_call(text) else {
962 return Ok(None);
963 };
964 let arg_list = split_args_top_level(args);
965 match name {
966 "fib" => {
967 if arg_list.len() != 1 {
968 return Err(format!(
969 "fib(n): expected 1 argument, got {}",
970 arg_list.len()
971 ));
972 }
973 let n = parse_u64_arg(arg_list[0], "fib(n)")?;
974 Ok(Some(generate_fib_n(n)))
975 }
976 "fib_until" => {
977 if arg_list.len() != 1 {
978 return Err(format!(
979 "fib_until(max): expected 1 argument, got {}",
980 arg_list.len()
981 ));
982 }
983 let max = parse_u64_arg(arg_list[0], "fib_until(max)")?;
984 Ok(Some(generate_fib_until(max)))
985 }
986 "pow2" => {
987 if arg_list.len() != 1 {
988 return Err(format!(
989 "pow2(n): expected 1 argument, got {}",
990 arg_list.len()
991 ));
992 }
993 let n = parse_u64_arg(arg_list[0], "pow2(n)")?;
994 Ok(Some(generate_pow2_n(n)))
995 }
996 "pow2_until" => {
997 if arg_list.len() != 1 {
998 return Err(format!(
999 "pow2_until(max): expected 1 argument, got {}",
1000 arg_list.len()
1001 ));
1002 }
1003 let max = parse_u64_arg(arg_list[0], "pow2_until(max)")?;
1004 Ok(Some(generate_pow2_until(max)))
1005 }
1006 "binomial" => {
1007 if arg_list.len() != 1 {
1008 return Err(format!(
1009 "binomial(n): expected 1 argument, got {}",
1010 arg_list.len()
1011 ));
1012 }
1013 let n = parse_u64_arg(arg_list[0], "binomial(n)")?;
1014 Ok(Some(generate_binomial(n)))
1015 }
1016 "geometric" => {
1017 if arg_list.len() != 3 {
1018 return Err(format!(
1019 "geometric(start, factor, n): expected 3 args, got {}",
1020 arg_list.len()
1021 ));
1022 }
1023 let start = parse_num_arg(arg_list[0], "geometric.start")?;
1024 let factor = parse_num_arg(arg_list[1], "geometric.factor")?;
1025 let n = parse_u64_arg(arg_list[2], "geometric.n")?;
1026 Ok(Some(generate_geometric(start, factor, n)))
1027 }
1028 "geometric_until" => {
1029 if arg_list.len() != 3 {
1030 return Err(format!(
1031 "geometric_until(start, factor, max): expected 3 args, got {}",
1032 arg_list.len()
1033 ));
1034 }
1035 let start = parse_num_arg(arg_list[0], "geometric_until.start")?;
1036 let factor = parse_num_arg(arg_list[1], "geometric_until.factor")?;
1037 let max = parse_num_arg(arg_list[2], "geometric_until.max")?;
1038 Ok(Some(generate_geometric_until(start, factor, max)))
1039 }
1040 "linear_starts" => {
1041 if arg_list.len() != 3 {
1042 return Err(format!(
1043 "linear_starts(start, end, n): expected 3 args, got {}",
1044 arg_list.len()
1045 ));
1046 }
1047 let start = parse_num_arg(arg_list[0], "linear_starts.start")?;
1048 let end = parse_num_arg(arg_list[1], "linear_starts.end")?;
1049 let n = parse_u64_arg(arg_list[2], "linear_starts.n")?;
1050 Ok(Some(generate_linear_points(start, end, n, false)))
1051 }
1052 "linear_steps" => {
1053 if arg_list.len() != 3 {
1054 return Err(format!(
1055 "linear_steps(start, end, n): expected 3 args, got {}",
1056 arg_list.len()
1057 ));
1058 }
1059 let start = parse_num_arg(arg_list[0], "linear_steps.start")?;
1060 let end = parse_num_arg(arg_list[1], "linear_steps.end")?;
1061 let n = parse_u64_arg(arg_list[2], "linear_steps.n")?;
1062 Ok(Some(generate_linear_points(start, end, n, true)))
1063 }
1064 "log_steps" => {
1065 if arg_list.len() != 3 {
1066 return Err(format!(
1067 "log_steps(start, end, n): expected 3 args, got {}",
1068 arg_list.len()
1069 ));
1070 }
1071 let start = parse_num_arg(arg_list[0], "log_steps.start")?;
1072 let end = parse_num_arg(arg_list[1], "log_steps.end")?;
1073 let n = parse_u64_arg(arg_list[2], "log_steps.n")?;
1074 Ok(Some(generate_log_steps(start, end, n)?))
1075 }
1076 _ => Ok(None),
1077 }
1078}
1079
1080fn generate_fib_n(n: u64) -> Vec<Value> {
1082 if n == 0 {
1083 return Vec::new();
1084 }
1085 let mut out = Vec::with_capacity(n as usize);
1086 let (mut a, mut b): (u64, u64) = (1, 1);
1087 for _ in 0..n {
1088 out.push(Value::U64(a));
1089 let next = a.saturating_add(b);
1090 a = b;
1091 b = next;
1092 }
1093 out
1094}
1095
1096fn generate_fib_until(max: u64) -> Vec<Value> {
1098 let mut out = Vec::new();
1099 let (mut a, mut b): (u64, u64) = (1, 1);
1100 while a <= max {
1101 out.push(Value::U64(a));
1102 let next = a.checked_add(b);
1103 a = b;
1104 match next {
1105 Some(v) => b = v,
1106 None => break,
1107 }
1108 }
1109 out
1110}
1111
1112fn generate_pow2_n(n: u64) -> Vec<Value> {
1114 let mut out = Vec::with_capacity(n as usize);
1115 for i in 0..n {
1116 if i >= 64 {
1117 break;
1118 } out.push(Value::U64(1u64 << i));
1120 }
1121 out
1122}
1123
1124fn generate_pow2_until(max: u64) -> Vec<Value> {
1126 let mut out = Vec::new();
1127 let mut v: u64 = 1;
1128 loop {
1129 if v > max {
1130 break;
1131 }
1132 out.push(Value::U64(v));
1133 v = match v.checked_mul(2) {
1134 Some(x) => x,
1135 None => break,
1136 };
1137 }
1138 out
1139}
1140
1141fn generate_geometric(start: f64, factor: f64, n: u64) -> Vec<Value> {
1143 let mut out = Vec::with_capacity(n as usize);
1144 let mut v = start;
1145 for _ in 0..n {
1146 out.push(Value::F64(v));
1147 v *= factor;
1148 }
1149 out
1150}
1151
1152fn generate_geometric_until(start: f64, factor: f64, max: f64) -> Vec<Value> {
1154 let mut out = Vec::new();
1155 let mut v = start;
1156 if factor <= 1.0 || start <= 0.0 || max <= 0.0 {
1157 return out;
1160 }
1161 while v <= max {
1162 out.push(Value::F64(v));
1163 v *= factor;
1164 }
1165 out
1166}
1167
1168fn generate_binomial(n: u64) -> Vec<Value> {
1170 let mut out = Vec::with_capacity(n as usize + 1);
1171 let mut c: u128 = 1;
1172 out.push(Value::U64(1));
1173 for k in 1..=n {
1174 c = c * (n - k + 1) as u128 / k as u128;
1175 if c > u64::MAX as u128 {
1176 break;
1177 }
1178 out.push(Value::U64(c as u64));
1179 }
1180 out
1181}
1182
1183fn try_eval_partition_call(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
1207 let Some((name, args)) = parse_func_call(text) else {
1208 return Ok(None);
1209 };
1210 let arg_list = split_args_top_level(args);
1211 match name {
1212 "subdivide" => {
1213 if arg_list.len() != 2 {
1214 return Err(format!(
1215 "subdivide(p, n): expected 2 arguments (a partition and a count), got {}",
1216 arg_list.len()
1217 ));
1218 }
1219 let src = arg_list[0].trim();
1220 let n = parse_u64_arg(arg_list[1], "subdivide.n")?;
1221 let Some(value) = kernel.lookup(src) else {
1222 let placeholder = crate::iteration::cursor_partition::Partition {
1227 idx: 0,
1228 count: 1,
1229 start_ord: 0,
1230 end_ord: 1,
1231 start_pct: 0.0,
1232 end_pct: 100.0,
1233 base_extent: 1,
1234 };
1235 return Ok(Some(vec![Value::from_partition(placeholder)]));
1236 };
1237 let Some(p) = value.as_partition().copied() else {
1238 return Err(format!(
1239 "subdivide({src}, {n}): `{src}` resolved to {} — expected a \
1240 Partition value (an iter-var from `for: \"p in partitions(...)\"` \
1241 or a cursor's `.cursor` projection)",
1242 value.to_display_string(),
1243 ));
1244 };
1245 let subs = crate::iteration::cursor_partition::subdivide_partition(&p, n)?;
1246 Ok(Some(subs.into_iter().map(Value::from_partition).collect()))
1247 }
1248 "partitions" => {
1259 if arg_list.is_empty() || arg_list.len() > 2 {
1260 return Err(format!(
1261 "partitions(spec, [extent]): expected 1 or 2 arguments, got {}",
1262 arg_list.len(),
1263 ));
1264 }
1265 let spec = resolve_partition_spec_arg(arg_list[0], kernel)?;
1266 let extent = match arg_list.get(1) {
1267 Some(a) => parse_u64_arg(a, "partitions.extent")?,
1268 None => 100,
1269 };
1270 desugar_partition_spec(&spec, extent, "comprehension source `partitions(...)`")
1271 .map(Some)
1272 }
1273 "profile_partitions" => {
1284 #[cfg(not(feature = "vectordata"))]
1285 {
1286 Err("profile_partitions requires the `vectordata` Cargo feature".to_string())
1287 }
1288
1289 #[cfg(feature = "vectordata")]
1290 {
1291 if arg_list.len() != 2 {
1292 return Err(format!(
1293 "profile_partitions(dataset, pattern): expected 2 arguments, got {}",
1294 arg_list.len()
1295 ));
1296 }
1297 let strip = |s: &str| -> String {
1300 let s = s.trim();
1301 let b = s.as_bytes();
1302 if b.len() >= 2 && (b[0] == b'\'' || b[0] == b'"') && b[b.len() - 1] == b[0] {
1303 s[1..s.len() - 1].to_string()
1304 } else {
1305 s.to_string()
1306 }
1307 };
1308 let dataset = strip(arg_list[0]);
1309 let pattern = strip(arg_list[1]);
1310 match crate::library::vectors::load_dataset_group(&dataset) {
1311 Ok(group) => {
1312 let parts =
1313 crate::library::vectors::build_profile_partitions(&group, &pattern);
1314 Ok(Some(parts.into_iter().map(Value::from_partition).collect()))
1315 }
1316 Err(_) => {
1317 let placeholder = crate::iteration::cursor_partition::Partition {
1321 idx: 0,
1322 count: 1,
1323 start_ord: 0,
1324 end_ord: 1,
1325 start_pct: 0.0,
1326 end_pct: 100.0,
1327 base_extent: 1,
1328 };
1329 Ok(Some(vec![Value::from_partition(placeholder)]))
1330 }
1331 }
1332 }
1333 }
1334 _ => Ok(None),
1335 }
1336}
1337
1338fn try_eval_param_partitions(
1361 text: &str,
1362 kernel: &dyn Lookup,
1363) -> Result<Option<Vec<Value>>, String> {
1364 let Some(ident) = text.trim().strip_suffix(".partitions") else {
1365 return Ok(None);
1366 };
1367 let ident = ident.trim();
1368 if !is_single_bare_ident(ident) {
1369 return Ok(None);
1370 }
1371 let Some(value) = kernel.lookup(ident) else {
1372 let placeholder = crate::iteration::cursor_partition::Partition {
1375 idx: 0,
1376 count: 1,
1377 start_ord: 0,
1378 end_ord: 1,
1379 start_pct: 0.0,
1380 end_pct: 100.0,
1381 base_extent: 1,
1382 };
1383 return Ok(Some(vec![Value::from_partition(placeholder)]));
1384 };
1385 if let Some(list) = value.as_partition_list() {
1387 return Ok(Some(
1388 list.as_slice()
1389 .iter()
1390 .map(|p| Value::from_partition(*p))
1391 .collect(),
1392 ));
1393 }
1394 let Value::Str(spec) = &value else {
1396 return Err(format!(
1397 "comprehension source `{ident}.partitions`: `{ident}` resolved to \
1398 {} — expected a partition-spec string (a workload param such as \
1399 `cursor=linear:4`) or a PartitionList.",
1400 value.to_display_string(),
1401 ));
1402 };
1403 desugar_partition_spec(
1404 spec,
1405 100,
1406 &format!("comprehension source `{ident}.partitions`"),
1407 )
1408 .map(Some)
1409}
1410
1411fn desugar_partition_spec(spec: &str, extent: u64, ctx: &str) -> Result<Vec<Value>, String> {
1418 let parsed = crate::iteration::cursor_partition::parse(spec)
1419 .map_err(|e| format!("{ctx}: bad spec `{spec}`: {e}"))?;
1420 let parts = crate::iteration::cursor_partition::resolve(&parsed, 0, extent)
1421 .map_err(|e| format!("{ctx}: resolve failed for `{spec}`: {e}"))?;
1422 Ok(parts.into_iter().map(Value::from_partition).collect())
1423}
1424
1425fn resolve_partition_spec_arg(arg: &str, kernel: &dyn Lookup) -> Result<String, String> {
1430 let a = arg.trim();
1431 if a.len() >= 2
1432 && ((a.starts_with('"') && a.ends_with('"')) || (a.starts_with('\'') && a.ends_with('\'')))
1433 {
1434 return Ok(a[1..a.len() - 1].to_string());
1435 }
1436 if is_single_bare_ident(a) {
1437 return match kernel.lookup(a) {
1438 Some(Value::Str(s)) => Ok(s.to_string()),
1439 Some(other) => Err(format!(
1440 "partitions(...): `{a}` resolved to {} — expected a spec string",
1441 other.to_display_string(),
1442 )),
1443 None => Err(format!(
1444 "partitions(...): `{a}` did not resolve to a spec string in scope"
1445 )),
1446 };
1447 }
1448 Ok(a.to_string())
1449}
1450
1451fn generate_linear_points(start: f64, end: f64, n: u64, inclusive: bool) -> Vec<Value> {
1462 if n == 0 {
1463 return Vec::new();
1464 }
1465 let denom = if inclusive {
1466 (n.saturating_sub(1)).max(1) as f64
1467 } else {
1468 n as f64
1469 };
1470 let step = (end - start) / denom;
1471 (0..n)
1472 .map(|i| Value::F64(start + step * i as f64))
1473 .collect()
1474}
1475
1476fn generate_log_steps(start: f64, end: f64, n: u64) -> Result<Vec<Value>, String> {
1479 if start <= 0.0 || end <= 0.0 {
1480 return Err(format!(
1481 "log_steps: bounds must be positive, got start={start}, end={end}"
1482 ));
1483 }
1484 if n == 0 {
1485 return Ok(Vec::new());
1486 }
1487 if n == 1 {
1488 return Ok(vec![Value::F64(start)]);
1489 }
1490 let log_s = start.ln();
1491 let log_e = end.ln();
1492 let step = (log_e - log_s) / (n - 1) as f64;
1493 Ok((0..n)
1494 .map(|i| Value::F64((log_s + step * i as f64).exp()))
1495 .collect())
1496}
1497
1498fn try_eval_setop(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
1507 let Some((name, args)) = parse_func_call(text) else {
1508 return Ok(None);
1509 };
1510 let arg_texts = split_args_top_level(args);
1511 let recursively_evaluate = |t: &str| -> Result<Vec<Value>, String> {
1512 evaluate_spec(t, kernel).map_err(|e| e.to_string())
1513 };
1514 match name {
1515 "concat" => {
1516 let mut out = Vec::new();
1517 for a in &arg_texts {
1518 out.extend(recursively_evaluate(a)?);
1519 }
1520 Ok(Some(out))
1521 }
1522 "unique" => {
1523 let mut out: Vec<Value> = Vec::new();
1524 for a in &arg_texts {
1525 for v in recursively_evaluate(a)? {
1526 if !out.contains(&v) {
1527 out.push(v);
1528 }
1529 }
1530 }
1531 Ok(Some(out))
1532 }
1533 "intersect" => {
1534 if arg_texts.is_empty() {
1535 return Ok(Some(Vec::new()));
1536 }
1537 let first = recursively_evaluate(arg_texts[0])?;
1538 let mut out: Vec<Value> = Vec::new();
1539 for v in first {
1540 let mut in_all = true;
1541 for a in &arg_texts[1..] {
1542 let other = recursively_evaluate(a)?;
1543 if !other.contains(&v) {
1544 in_all = false;
1545 break;
1546 }
1547 }
1548 if in_all && !out.contains(&v) {
1549 out.push(v);
1550 }
1551 }
1552 Ok(Some(out))
1553 }
1554 "subtract" => {
1555 if arg_texts.len() != 2 {
1556 return Err(format!(
1557 "subtract(a, b): expected 2 args, got {}",
1558 arg_texts.len()
1559 ));
1560 }
1561 let a = recursively_evaluate(arg_texts[0])?;
1562 let b = recursively_evaluate(arg_texts[1])?;
1563 Ok(Some(a.into_iter().filter(|v| !b.contains(v)).collect()))
1564 }
1565 "interleave" => {
1566 let lists: Result<Vec<Vec<Value>>, String> =
1567 arg_texts.iter().map(|a| recursively_evaluate(a)).collect();
1568 let lists = lists?;
1569 let mut out = Vec::new();
1570 let max_len = lists.iter().map(|l| l.len()).max().unwrap_or(0);
1571 for i in 0..max_len {
1572 for l in &lists {
1573 if let Some(v) = l.get(i) {
1574 out.push(v.clone());
1575 }
1576 }
1577 }
1578 Ok(Some(out))
1579 }
1580 "cycle" => {
1581 if arg_texts.len() != 2 {
1582 return Err(format!(
1583 "cycle(a, n): expected 2 args, got {}",
1584 arg_texts.len()
1585 ));
1586 }
1587 let a = recursively_evaluate(arg_texts[0])?;
1588 let n = parse_u64_arg(arg_texts[1], "cycle.n")?;
1589 let mut out = Vec::with_capacity(a.len() * n as usize);
1590 for _ in 0..n {
1591 out.extend(a.iter().cloned());
1592 }
1593 Ok(Some(out))
1594 }
1595 "reverse" => {
1596 if arg_texts.len() != 1 {
1597 return Err(format!(
1598 "reverse(a): expected 1 arg, got {}",
1599 arg_texts.len()
1600 ));
1601 }
1602 let mut a = recursively_evaluate(arg_texts[0])?;
1603 a.reverse();
1604 Ok(Some(a))
1605 }
1606 "take" => {
1607 if arg_texts.len() != 2 {
1608 return Err(format!(
1609 "take(a, n): expected 2 args, got {}",
1610 arg_texts.len()
1611 ));
1612 }
1613 let a = recursively_evaluate(arg_texts[0])?;
1614 let n = parse_u64_arg(arg_texts[1], "take.n")?;
1615 Ok(Some(a.into_iter().take(n as usize).collect()))
1616 }
1617 "skip" => {
1618 if arg_texts.len() != 2 {
1619 return Err(format!(
1620 "skip(a, n): expected 2 args, got {}",
1621 arg_texts.len()
1622 ));
1623 }
1624 let a = recursively_evaluate(arg_texts[0])?;
1625 let n = parse_u64_arg(arg_texts[1], "skip.n")?;
1626 Ok(Some(a.into_iter().skip(n as usize).collect()))
1627 }
1628 _ => Ok(None),
1629 }
1630}
1631
1632fn try_eval_sequencer(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
1648 let Some((name, args)) = parse_func_call(text) else {
1649 return Ok(None);
1650 };
1651 if !matches!(name, "bucket" | "concat_seq" | "interval_seq") {
1652 return Ok(None);
1653 }
1654 let arg_texts = split_args_top_level(args);
1655
1656 let (items, ratios): (Vec<Value>, Vec<usize>) = match arg_texts.len() {
1661 1 => parse_ratio_prefix_shorthand(arg_texts[0])?,
1662 2 => {
1663 let items = evaluate_spec(arg_texts[0], kernel)?;
1664 let raw_ratios = evaluate_spec(arg_texts[1], kernel)?;
1665 let ratios: Result<Vec<usize>, String> = raw_ratios
1666 .iter()
1667 .map(|v| match v {
1668 Value::U64(n) => Ok(*n as usize),
1669 other => Err(format!(
1670 "{name}: ratio must be non-negative integer, got {other:?}"
1671 )),
1672 })
1673 .collect();
1674 (items, ratios?)
1675 }
1676 _ => {
1677 return Err(format!(
1678 "{name}: expected `(items, ratios)` or `(\"r1:item1, r2:item2, ...\")`; got {} args",
1679 arg_texts.len()
1680 ));
1681 }
1682 };
1683
1684 if items.len() != ratios.len() {
1685 return Err(format!(
1686 "{name}: items.len() ({}) != ratios.len() ({})",
1687 items.len(),
1688 ratios.len(),
1689 ));
1690 }
1691 Ok(Some(match name {
1692 "bucket" => seq_bucket(&items, &ratios),
1693 "concat_seq" => seq_concat(&items, &ratios),
1694 "interval_seq" => seq_interval(&items, &ratios),
1695 _ => unreachable!(),
1696 }))
1697}
1698
1699fn parse_ratio_prefix_shorthand(text: &str) -> Result<(Vec<Value>, Vec<usize>), String> {
1704 let stripped = text
1707 .trim()
1708 .trim_start_matches(['"', '\''])
1709 .trim_end_matches(['"', '\'']);
1710 let mut items = Vec::new();
1711 let mut ratios = Vec::new();
1712 for part in stripped.split(',') {
1713 let part = part.trim();
1714 if part.is_empty() {
1715 continue;
1716 }
1717 let (r, i) = part
1718 .split_once(':')
1719 .ok_or_else(|| format!("ratio-prefix shorthand: missing ':' in '{part}'"))?;
1720 let ratio: usize = r.trim().parse().map_err(|_| {
1721 format!("ratio-prefix shorthand: ratio '{r}' is not a non-negative integer")
1722 })?;
1723 ratios.push(ratio);
1724 items.push(parse_one_value(i.trim()));
1725 }
1726 Ok((items, ratios))
1727}
1728
1729fn parse_one_value(s: &str) -> Value {
1730 if let Ok(n) = s.parse::<u64>() {
1731 return Value::U64(n);
1732 }
1733 if let Ok(f) = s.parse::<f64>() {
1734 return Value::F64(f);
1735 }
1736 if s == "true" {
1737 return Value::Bool(true);
1738 }
1739 if s == "false" {
1740 return Value::Bool(false);
1741 }
1742 Value::Str(s.to_string().into())
1743}
1744
1745fn seq_bucket(items: &[Value], ratios: &[usize]) -> Vec<Value> {
1748 let total: usize = ratios.iter().sum();
1749 let mut out = Vec::with_capacity(total);
1750 let mut remaining: Vec<usize> = ratios.to_vec();
1751 while out.len() < total {
1752 let mut emitted_any = false;
1753 for (i, item) in items.iter().enumerate() {
1754 if remaining[i] > 0 {
1755 out.push(item.clone());
1756 remaining[i] -= 1;
1757 emitted_any = true;
1758 }
1759 }
1760 if !emitted_any {
1761 break;
1762 }
1763 }
1764 out
1765}
1766
1767fn seq_concat(items: &[Value], ratios: &[usize]) -> Vec<Value> {
1770 let total: usize = ratios.iter().sum();
1771 let mut out = Vec::with_capacity(total);
1772 for (item, &r) in items.iter().zip(ratios.iter()) {
1773 for _ in 0..r {
1774 out.push(item.clone());
1775 }
1776 }
1777 out
1778}
1779
1780fn seq_interval(items: &[Value], ratios: &[usize]) -> Vec<Value> {
1786 let total: usize = ratios.iter().sum();
1787 if total == 0 {
1788 return Vec::new();
1789 }
1790 let mut emitted: Vec<usize> = vec![0; items.len()];
1791 let mut out = Vec::with_capacity(total);
1792 for slot in 0..total {
1793 let mut best = 0usize;
1796 let mut best_deficit: f64 = f64::NEG_INFINITY;
1797 for i in 0..items.len() {
1798 let target = ratios[i] as f64 * (slot + 1) as f64 / total as f64;
1799 let deficit = target - emitted[i] as f64;
1800 if deficit > best_deficit {
1801 best_deficit = deficit;
1802 best = i;
1803 }
1804 }
1805 out.push(items[best].clone());
1806 emitted[best] += 1;
1807 }
1808 out
1809}
1810
1811pub fn value_to_polydat_type_name(v: &Value) -> &'static str {
1821 v.port_type().to_keyword()
1822}
1823
1824pub fn enumerate_tuples<F>(
1850 canonical: &Arc<PolydatKernel>,
1851 parent: &Arc<PolydatKernel>,
1852 clauses: &[super::ast_legacy::Clause],
1853 filter: Option<&str>,
1854 mut on_empty_clause: F,
1855) -> Result<Vec<Vec<(String, Value)>>, String>
1856where
1857 F: FnMut(&super::ast_legacy::Clause) -> Result<(), String>,
1858{
1859 let mut out = Vec::new();
1860 enumerate_into(
1861 canonical,
1862 parent,
1863 clauses,
1864 filter,
1865 0,
1866 &Vec::new(),
1867 &mut out,
1868 &mut on_empty_clause,
1869 )?;
1870 Ok(out)
1871}
1872
1873#[allow(clippy::too_many_arguments)]
1874fn enumerate_into<F>(
1875 canonical: &Arc<PolydatKernel>,
1876 parent: &Arc<PolydatKernel>,
1877 clauses: &[super::ast_legacy::Clause],
1878 filter: Option<&str>,
1879 idx: usize,
1880 prefix: &[(String, Value)],
1881 out: &mut Vec<Vec<(String, Value)>>,
1882 on_empty_clause: &mut F,
1883) -> Result<(), String>
1884where
1885 F: FnMut(&super::ast_legacy::Clause) -> Result<(), String>,
1886{
1887 use super::ast_legacy::ClauseSource;
1888
1889 if idx == clauses.len() {
1890 if let Some(predicate) = filter {
1895 let bindings_owned: Vec<(String, Value)> = prefix
1900 .iter()
1901 .map(|(v, val)| ((*v).to_string(), val.clone()))
1902 .collect();
1903 let kernel = parent.materialize_subscope(canonical.program().clone(), &bindings_owned);
1904 let interpolated = interpolate_via_kernel(predicate, &kernel)
1905 .map_err(|e| format!("comprehension filter '{predicate}': {e}"))?;
1906 let result = crate::dsl::compile::eval_const_expr(&interpolated)
1907 .map_err(|e| format!("comprehension filter '{predicate}': {e}"))?;
1908 let keep = match result {
1912 Value::Bool(b) => b,
1913 Value::U64(n) => n != 0,
1914 Value::F64(n) => n != 0.0,
1915 other => {
1916 return Err(format!(
1917 "comprehension filter '{predicate}': expected bool/u64/f64, got {other:?}"
1918 ));
1919 }
1920 };
1921 if keep {
1922 out.push(prefix.to_vec());
1923 }
1924 } else {
1925 out.push(prefix.to_vec());
1926 }
1927 return Ok(());
1928 }
1929 let bindings_owned: Vec<(String, Value)> = prefix
1930 .iter()
1931 .map(|(v, val)| ((*v).to_string(), val.clone()))
1932 .collect();
1933 let kernel = parent.materialize_subscope(canonical.program().clone(), &bindings_owned);
1934
1935 let clause = &clauses[idx];
1936 match &clause.source {
1937 ClauseSource::Single(spec_text) => {
1938 let var = clause.var();
1939 let values = evaluate_spec(spec_text, &kernel)
1940 .map_err(|e| format!("for_each clause '{var} in {spec_text}': {e}"))?;
1941
1942 if values.is_empty() {
1943 on_empty_clause(clause)?;
1944 return Ok(());
1945 }
1946
1947 for value in values {
1948 let mut next_prefix = prefix.to_vec();
1949 next_prefix.push((var.to_string(), value));
1950 enumerate_into(
1951 canonical,
1952 parent,
1953 clauses,
1954 filter,
1955 idx + 1,
1956 &next_prefix,
1957 out,
1958 on_empty_clause,
1959 )?;
1960 }
1961 }
1962 ClauseSource::Parallel { mode, exprs } => {
1963 use super::ast_legacy::ZipMode;
1968 let group_label = format!(
1969 "({}) in {}({})",
1970 clause.vars.join(", "),
1971 match mode {
1972 ZipMode::Strict => "",
1973 ZipMode::Truncate => "zip_truncate",
1974 ZipMode::Cycle => "zip_cycle",
1975 },
1976 exprs.join(", "),
1977 );
1978 let mut columns: Vec<Vec<Value>> = Vec::with_capacity(exprs.len());
1979 for expr in exprs {
1980 let values = evaluate_spec(expr, &kernel)
1981 .map_err(|e| format!("for_each parallel clause '{group_label}': {e}"))?;
1982 columns.push(values);
1983 }
1984 let lens: Vec<usize> = columns.iter().map(|c| c.len()).collect();
1985 let len = match mode {
1986 ZipMode::Strict => {
1987 let len0 = lens[0];
1988 for (i, &l) in lens.iter().enumerate().skip(1) {
1989 if l != len0 {
1990 return Err(format!(
1991 "for_each parallel clause '{group_label}': \
1992 length mismatch — expr 0 produced {len0} values, \
1993 expr {i} produced {l} (use zip_truncate(...) or \
1994 zip_cycle(...) to opt into truncate/cycle semantics)"
1995 ));
1996 }
1997 }
1998 len0
1999 }
2000 ZipMode::Truncate => *lens.iter().min().unwrap(),
2001 ZipMode::Cycle => {
2002 if lens.contains(&0) {
2006 0
2007 } else {
2008 *lens.iter().max().unwrap()
2009 }
2010 }
2011 };
2012 if len == 0 {
2013 on_empty_clause(clause)?;
2014 return Ok(());
2015 }
2016 for step in 0..len {
2017 let mut next_prefix = prefix.to_vec();
2018 for (var, col) in clause.vars.iter().zip(columns.iter()) {
2019 let i = if matches!(mode, ZipMode::Cycle) {
2022 step % col.len()
2023 } else {
2024 step
2025 };
2026 next_prefix.push((var.clone(), col[i].clone()));
2027 }
2028 enumerate_into(
2029 canonical,
2030 parent,
2031 clauses,
2032 filter,
2033 idx + 1,
2034 &next_prefix,
2035 out,
2036 on_empty_clause,
2037 )?;
2038 }
2039 }
2040 }
2041 Ok(())
2042}
2043
2044#[cfg(test)]
2055mod tests {
2056 use super::*;
2057
2058 fn h(pairs: &[(&str, &str)]) -> HashMap<String, String> {
2059 pairs
2060 .iter()
2061 .map(|(k, v)| (k.to_string(), v.to_string()))
2062 .collect()
2063 }
2064
2065 fn interpolate(
2066 text: &str,
2067 bindings: &HashMap<String, String>,
2068 workload_params: &HashMap<String, String>,
2069 ) -> Result<String, String> {
2070 interpolate_with_lookup(text, |name| {
2071 bindings
2072 .get(name)
2073 .or_else(|| workload_params.get(name))
2074 .cloned()
2075 })
2076 }
2077
2078 #[test]
2079 fn flat_substitution() {
2080 let params = h(&[("dataset", "example"), ("prefix", "label")]);
2081 let out = interpolate("matching('{dataset}', '{prefix}')", &h(&[]), ¶ms).unwrap();
2082 assert_eq!(out, "matching('example', 'label')");
2083 }
2084
2085 #[test]
2086 fn bindings_shadow_params() {
2087 let params = h(&[("profile", "default")]);
2088 let bindings = h(&[("profile", "label_07")]);
2089 let out = interpolate("vec_{profile}", &bindings, ¶ms).unwrap();
2090 assert_eq!(out, "vec_label_07");
2091 }
2092
2093 #[test]
2094 fn nested_placeholder_resolves_inside_out() {
2095 let params = h(&[("k_1_limits", "1,2,4,8"), ("k_10_limits", "10,20,30")]);
2096 let bindings = h(&[("k", "1")]);
2097 let out = interpolate("{k_{k}_limits}", &bindings, ¶ms).unwrap();
2098 assert_eq!(out, "1,2,4,8");
2099 }
2100
2101 #[test]
2102 fn deeply_nested() {
2103 let params = h(&[("a_b_c", "WIN")]);
2104 let bindings = h(&[("x", "a"), ("y", "b"), ("z", "c")]);
2105 let out = interpolate("{{x}_{y}_{z}}", &bindings, ¶ms).unwrap();
2106 assert_eq!(out, "WIN");
2107 }
2108
2109 #[test]
2110 fn escape_emits_literal_brace() {
2111 let out = interpolate("\\{not_a_var\\}", &h(&[]), &h(&[])).unwrap();
2112 assert_eq!(out, "{not_a_var}");
2113 }
2114
2115 #[test]
2116 fn escape_inside_otherwise_resolved_text() {
2117 let params = h(&[("x", "1")]);
2118 let out = interpolate("a={x} literal=\\{x\\}", &h(&[]), ¶ms).unwrap();
2119 assert_eq!(out, "a=1 literal={x}");
2120 }
2121
2122 #[test]
2123 fn unresolved_is_hard_error() {
2124 let err = interpolate("hello {nope}", &h(&[]), &h(&[])).unwrap_err();
2125 assert!(err.contains("unresolved"));
2126 assert!(err.contains("nope"));
2127 }
2128
2129 #[test]
2130 fn empty_placeholder_rejected() {
2131 let err = interpolate("a{}b", &h(&[]), &h(&[])).unwrap_err();
2132 assert!(err.contains("empty"));
2133 }
2134
2135 #[test]
2136 fn unmatched_brace_rejected() {
2137 let err = interpolate("a {x", &h(&[]), &h(&[])).unwrap_err();
2138 assert!(err.contains("unmatched"));
2139 }
2140
2141 #[test]
2142 fn idempotent_when_no_placeholders() {
2143 let out = interpolate("plain text", &h(&[]), &h(&[])).unwrap();
2144 assert_eq!(out, "plain text");
2145 }
2146
2147 #[test]
2148 fn resolved_value_with_braces_does_not_re_expand() {
2149 let params = h(&[("greeting", "hello {planet}")]);
2150 let err = interpolate("{greeting}", &h(&[]), ¶ms).unwrap_err();
2151 assert!(err.contains("planet"));
2152 }
2153
2154 #[test]
2155 fn cyclic_placeholders_hit_round_cap() {
2156 let params = h(&[("a", "{b}"), ("b", "{a}")]);
2157 let err = interpolate("{a}", &h(&[]), ¶ms).unwrap_err();
2158 assert!(err.contains("did not stabilize") || err.contains("rounds"));
2159 }
2160
2161 #[test]
2162 fn kernel_resolves_via_get_constant() {
2163 let kernel =
2164 crate::dsl::compile::compile_polydat("const dataset := \"example\"\n").unwrap();
2165 let out = interpolate_via_kernel("path/{dataset}/data", &kernel).unwrap();
2166 assert_eq!(out, "path/example/data");
2167 }
2168
2169 #[test]
2170 fn kernel_resolves_via_get_input() {
2171 let parent = crate::dsl::compile::compile_polydat("const k_values := \"1, 10\"\n").unwrap();
2172 let child_program = crate::dsl::compile::compile_polydat("extern k_values: String\n")
2173 .unwrap()
2174 .program()
2175 .clone();
2176 let child = parent.materialize_subscope(child_program, &[]);
2177 let out = interpolate_via_kernel("values={k_values}", &child).unwrap();
2178 assert_eq!(out, "values=1, 10");
2179 }
2180
2181 #[test]
2182 fn kernel_unresolved_name_errors() {
2183 let kernel = crate::dsl::compile::compile_polydat("const x := 1\n").unwrap();
2184 let err = interpolate_via_kernel("hello {nope}", &kernel)
2185 .unwrap_err()
2186 .to_string();
2187 assert!(err.contains("unresolved"));
2188 assert!(err.contains("nope"));
2189 }
2190
2191 #[test]
2192 fn kernel_nested_template_iterates_to_fixed_point() {
2193 let kernel = crate::dsl::compile::compile_polydat(
2194 "const k := \"1\"\nconst k_1_limits := \"1, 2, 4, 8\"\n",
2195 )
2196 .unwrap();
2197 let out = interpolate_via_kernel("{k_{k}_limits}", &kernel).unwrap();
2198 assert_eq!(out, "1, 2, 4, 8");
2199 }
2200
2201 #[test]
2202 fn parse_list_native_types() {
2203 let v = parse_list_with_types("1, 10, 100");
2204 assert_eq!(v, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2205 }
2206
2207 #[test]
2208 fn parse_list_mixed_types() {
2209 let v = parse_list_with_types("1, 1.5, true, hello");
2210 assert_eq!(
2211 v,
2212 vec![
2213 Value::U64(1),
2214 Value::F64(1.5),
2215 Value::Bool(true),
2216 Value::Str("hello".to_string().into()),
2217 ]
2218 );
2219 }
2220
2221 #[test]
2222 fn all_cursor_returns_extent_range() {
2223 let kernel = crate::dsl::compile::compile_polydat(
2229 "const __cursor_extent_row_start := 0\n\
2230 const __cursor_extent_row_end := 5\n",
2231 )
2232 .unwrap();
2233 let values = evaluate_spec("all(row)", &kernel).unwrap();
2234 assert_eq!(
2235 values,
2236 vec![
2237 Value::U64(0),
2238 Value::U64(1),
2239 Value::U64(2),
2240 Value::U64(3),
2241 Value::U64(4),
2242 ]
2243 );
2244 }
2245
2246 #[test]
2247 fn all_cursor_non_zero_start() {
2248 let kernel = crate::dsl::compile::compile_polydat(
2249 "const __cursor_extent_data_start := 100\n\
2250 const __cursor_extent_data_end := 103\n",
2251 )
2252 .unwrap();
2253 let values = evaluate_spec("all(data)", &kernel).unwrap();
2254 assert_eq!(
2255 values,
2256 vec![Value::U64(100), Value::U64(101), Value::U64(102)]
2257 );
2258 }
2259
2260 #[test]
2261 fn all_cursor_missing_extent_errors() {
2262 let kernel = crate::dsl::compile::compile_polydat("const unrelated := 1\n").unwrap();
2263 let err = evaluate_spec("all(no_such_cursor)", &kernel)
2264 .unwrap_err()
2265 .to_string();
2266 assert!(err.contains("all(no_such_cursor)"));
2267 assert!(err.contains("no resolvable extent"));
2268 }
2269
2270 #[test]
2271 fn all_cursor_only_matches_exact_shape() {
2272 let kernel = crate::dsl::compile::compile_polydat(
2283 "const __cursor_extent_row_start := 0\n\
2284 const __cursor_extent_row_end := 5\n",
2285 )
2286 .unwrap();
2287 let err = evaluate_spec("all(row, 5)", &kernel)
2288 .unwrap_err()
2289 .to_string();
2290 assert!(
2291 err.contains("all(row, 5)"),
2292 "error must mention the failing spec, got: {err}"
2293 );
2294 assert!(
2295 err.contains("failed to evaluate") || err.contains("unknown function"),
2296 "error must explain the eval failure, got: {err}"
2297 );
2298 }
2299
2300 #[test]
2301 fn missing_dataset_surface_as_clean_error_not_garbage() {
2302 let kernel = crate::dsl::compile::compile_polydat("const unrelated := 1\n").unwrap();
2324 let result = evaluate_spec(
2325 "matching_profiles('nonexistent_dataset_xyz_qqq', 'label_')",
2326 &kernel,
2327 );
2328 let err = result
2329 .expect_err("missing dataset must surface as Err, not silent literal-list fallback")
2330 .to_string();
2331 assert!(
2337 err.contains("nonexistent_dataset_xyz_qqq")
2338 || err.contains("matching_profiles")
2339 || err.contains("dataset"),
2340 "error must point at the actual fault, got: {err}"
2341 );
2342 }
2343
2344 #[test]
2345 fn function_call_eval_failure_is_not_silently_split() {
2346 let kernel = crate::dsl::compile::compile_polydat("const unrelated := 1\n").unwrap();
2352 let err = evaluate_spec("nonexistent_func('a', 'b', 'c')", &kernel)
2353 .unwrap_err()
2354 .to_string();
2355 assert!(
2356 err.contains("failed to evaluate") || err.contains("unknown"),
2357 "expected a clean eval-failure error, got: {err}"
2358 );
2359 }
2360
2361 #[test]
2362 fn literal_list_path_still_works() {
2363 let kernel = crate::dsl::compile::compile_polydat("const unrelated := 1\n").unwrap();
2370 let values = evaluate_spec("1, 10, 100", &kernel).unwrap();
2371 assert_eq!(values, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2372
2373 let names = evaluate_spec("foo, bar, baz", &kernel).unwrap();
2374 assert_eq!(
2375 names,
2376 vec![
2377 Value::Str("foo".into()),
2378 Value::Str("bar".into()),
2379 Value::Str("baz".into()),
2380 ]
2381 );
2382 }
2383
2384 #[test]
2385 fn literal_cursor_exposes_extent_auxiliaries() {
2386 let kernel = crate::dsl::compile::compile_polydat("cursor row = range(0, 50)\n").unwrap();
2391 let start = kernel.lookup("__cursor_extent_row_start");
2392 let end = kernel.lookup("__cursor_extent_row_end");
2393 assert_eq!(
2394 start,
2395 Some(Value::U64(0)),
2396 "expected start=0, got {start:?}"
2397 );
2398 assert_eq!(end, Some(Value::U64(50)), "expected end=50, got {end:?}");
2399 }
2400
2401 #[test]
2402 fn all_cursor_with_real_cursor_decl_works() {
2403 let kernel = crate::dsl::compile::compile_polydat("cursor row = range(0, 5)\n").unwrap();
2404 let values = evaluate_spec("all(row)", &kernel).unwrap();
2405 assert_eq!(
2406 values,
2407 vec![
2408 Value::U64(0),
2409 Value::U64(1),
2410 Value::U64(2),
2411 Value::U64(3),
2412 Value::U64(4),
2413 ]
2414 );
2415 }
2416
2417 #[test]
2418 fn all_cursor_ignores_whitespace() {
2419 let kernel = crate::dsl::compile::compile_polydat(
2420 "const __cursor_extent_row_start := 0\n\
2421 const __cursor_extent_row_end := 3\n",
2422 )
2423 .unwrap();
2424 let values = evaluate_spec(" all( row ) ", &kernel).unwrap();
2425 assert_eq!(values.len(), 3);
2426 }
2427
2428 #[test]
2429 fn evaluate_spec_resolves_against_kernel() {
2430 let kernel =
2431 crate::dsl::compile::compile_polydat("const k_values := \"1, 10, 100\"\n").unwrap();
2432 let v = evaluate_spec("{k_values}", &kernel).unwrap();
2433 assert_eq!(v, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2434 }
2435
2436 #[test]
2437 fn evaluate_spec_bare_ident_resolves_like_braced() {
2438 let kernel =
2442 crate::dsl::compile::compile_polydat("const k_values := \"1, 10, 100\"\n").unwrap();
2443 let bare = evaluate_spec("k_values", &kernel).unwrap();
2444 let braced = evaluate_spec("{k_values}", &kernel).unwrap();
2445 assert_eq!(bare, braced);
2446 assert_eq!(bare, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2447 }
2448
2449 #[test]
2450 fn evaluate_spec_unresolved_bare_is_error_with_quoting_hint() {
2451 let kernel = crate::dsl::compile::compile_polydat("\n").unwrap();
2455 let err = evaluate_spec("nonexistent", &kernel)
2456 .unwrap_err()
2457 .to_string();
2458 assert!(err.contains("did not resolve"), "got: {err}");
2459 assert!(err.contains("quote it"), "should hint quoting: {err}");
2460 }
2461
2462 #[test]
2463 fn bracket_list_spread_and_no_peel() {
2464 let kernel = crate::dsl::compile::compile_polydat("const xs := \"1, 2, 3\"\n").unwrap();
2467 let spread = evaluate_spec("[xs…]", &kernel).unwrap();
2469 assert_eq!(spread, vec![Value::U64(1), Value::U64(2), Value::U64(3)]);
2470 let whole = evaluate_spec("[xs]", &kernel).unwrap();
2472 assert_eq!(whole, vec![Value::Str("1, 2, 3".into())]);
2473 }
2474
2475 #[test]
2476 fn bracket_list_mixes_refs_literals_and_spread() {
2477 let kernel = crate::dsl::compile::compile_polydat("const mid := \"7, 8\"\n").unwrap();
2478 let v = evaluate_spec("[1, mid…, \"x\"]", &kernel).unwrap();
2479 assert_eq!(
2480 v,
2481 vec![
2482 Value::U64(1),
2483 Value::U64(7),
2484 Value::U64(8),
2485 Value::Str("x".into()),
2486 ]
2487 );
2488 }
2489
2490 #[test]
2493 fn evaluate_spec_unpacks_partition_list_into_partition_values() {
2494 let kernel = empty_kernel();
2500 let v = evaluate_spec("partitions(\"linear:3\")", &kernel).unwrap();
2501 assert_eq!(v.len(), 3, "expected 3 partitions, got {}", v.len());
2502 for value in &v {
2503 assert!(
2504 value.as_partition().is_some(),
2505 "every iter value should be a Partition, got {value:?}"
2506 );
2507 }
2508 }
2509
2510 #[test]
2511 fn evaluate_spec_unpacks_partition_list_with_explicit_extent() {
2512 let kernel = empty_kernel();
2513 let v = evaluate_spec("partitions(\"fib:5\", 1000)", &kernel).unwrap();
2514 assert_eq!(v.len(), 5);
2515 for (i, value) in v.iter().enumerate() {
2517 let p = value.as_partition().unwrap();
2518 assert_eq!(p.idx, i as u64);
2519 assert_eq!(p.base_extent, 1000);
2520 }
2521 }
2522
2523 #[test]
2524 fn pre_evaluate_clause_returns_partition_values_for_partitions_call() {
2525 let kernel = empty_kernel();
2528 let v = pre_evaluate_clause(
2529 "partitions(\"linear:4\")",
2530 &kernel,
2531 &HashMap::new(),
2532 &HashMap::new(),
2533 )
2534 .unwrap();
2535 assert_eq!(v.len(), 4);
2536 for value in &v {
2537 assert!(
2538 value.as_partition().is_some(),
2539 "pre_evaluate_clause must unpack PartitionList, got {value:?}"
2540 );
2541 }
2542 }
2543
2544 #[test]
2545 fn value_to_polydat_type_name_returns_ext_for_partition_value() {
2546 let p = crate::iteration::cursor_partition::Partition {
2553 idx: 0,
2554 count: 1,
2555 start_ord: 0,
2556 end_ord: 10,
2557 start_pct: 0.0,
2558 end_pct: 100.0,
2559 base_extent: 10,
2560 };
2561 let v = Value::from_partition(p);
2562 assert_eq!(value_to_polydat_type_name(&v), "ext");
2563 }
2564
2565 fn empty_kernel() -> PolydatKernel {
2568 crate::dsl::compile::compile_polydat("\n").unwrap()
2569 }
2570
2571 #[test]
2572 fn range_half_open_integer() {
2573 let v = evaluate_spec("1..5", &empty_kernel()).unwrap();
2574 assert_eq!(
2575 v,
2576 vec![Value::U64(1), Value::U64(2), Value::U64(3), Value::U64(4),]
2577 );
2578 }
2579
2580 #[test]
2581 fn range_inclusive_integer() {
2582 let v = evaluate_spec("1..=5", &empty_kernel()).unwrap();
2583 assert_eq!(
2584 v,
2585 vec![
2586 Value::U64(1),
2587 Value::U64(2),
2588 Value::U64(3),
2589 Value::U64(4),
2590 Value::U64(5),
2591 ]
2592 );
2593 }
2594
2595 #[test]
2596 fn range_with_step() {
2597 let v = evaluate_spec("0..100..10", &empty_kernel()).unwrap();
2598 assert_eq!(
2599 v,
2600 vec![
2601 Value::U64(0),
2602 Value::U64(10),
2603 Value::U64(20),
2604 Value::U64(30),
2605 Value::U64(40),
2606 Value::U64(50),
2607 Value::U64(60),
2608 Value::U64(70),
2609 Value::U64(80),
2610 Value::U64(90),
2611 ]
2612 );
2613 }
2614
2615 #[test]
2616 fn range_inclusive_with_step() {
2617 let v = evaluate_spec("0..=100..25", &empty_kernel()).unwrap();
2618 assert_eq!(
2619 v,
2620 vec![
2621 Value::U64(0),
2622 Value::U64(25),
2623 Value::U64(50),
2624 Value::U64(75),
2625 Value::U64(100),
2626 ]
2627 );
2628 }
2629
2630 #[test]
2631 fn range_float_step() {
2632 let v = evaluate_spec("0.0..=1.0..0.25", &empty_kernel()).unwrap();
2633 assert_eq!(v.len(), 5, "got {v:?}");
2634 if let [
2635 Value::F64(a),
2636 Value::F64(b),
2637 Value::F64(c),
2638 Value::F64(d),
2639 Value::F64(e),
2640 ] = v.as_slice()
2641 {
2642 assert!((a - 0.0).abs() < 1e-12);
2643 assert!((b - 0.25).abs() < 1e-12);
2644 assert!((c - 0.5).abs() < 1e-12);
2645 assert!((d - 0.75).abs() < 1e-12);
2646 assert!((e - 1.0).abs() < 1e-12);
2647 } else {
2648 panic!("expected 5 floats, got {v:?}");
2649 }
2650 }
2651
2652 #[test]
2653 fn range_empty_when_start_equals_end_half_open() {
2654 let v = evaluate_spec("5..5", &empty_kernel()).unwrap();
2655 assert!(v.is_empty(), "got {v:?}");
2656 }
2657
2658 #[test]
2659 fn range_inclusive_with_equal_bounds_emits_one() {
2660 let v = evaluate_spec("5..=5", &empty_kernel()).unwrap();
2661 assert_eq!(v, vec![Value::U64(5)]);
2662 }
2663
2664 #[test]
2665 fn range_with_si_suffix_bounds() {
2666 let v = evaluate_spec("1K..1K..200", &empty_kernel()).unwrap();
2669 assert!(v.is_empty(), "1K..1K with positive step → empty");
2670
2671 let v = evaluate_spec("0..1K..200", &empty_kernel()).unwrap();
2672 assert_eq!(
2673 v,
2674 vec![
2675 Value::U64(0),
2676 Value::U64(200),
2677 Value::U64(400),
2678 Value::U64(600),
2679 Value::U64(800),
2680 ]
2681 );
2682 }
2683
2684 #[test]
2685 fn range_zero_step_errors() {
2686 let err = evaluate_spec("1..10..0", &empty_kernel())
2687 .unwrap_err()
2688 .to_string();
2689 assert!(err.contains("step is zero"), "{err}");
2690 }
2691
2692 #[test]
2693 fn range_too_many_dotdot_errors() {
2694 let err = evaluate_spec("1..2..3..4", &empty_kernel())
2695 .unwrap_err()
2696 .to_string();
2697 assert!(err.contains("more than two `..`"), "{err}");
2698 }
2699
2700 #[test]
2701 fn range_inside_parens_doesnt_split() {
2702 let v = evaluate_spec("(1)..(5)", &empty_kernel()).unwrap();
2709 assert_eq!(v.len(), 4); }
2711
2712 #[test]
2713 fn range_step_with_inclusive_separator_errors() {
2714 let err = evaluate_spec("1..10..=2", &empty_kernel())
2715 .unwrap_err()
2716 .to_string();
2717 assert!(err.contains("step delimiter cannot be `..=`"), "{err}");
2718 }
2719
2720 #[test]
2721 fn range_with_kernel_referenced_bounds() {
2722 let kernel =
2723 crate::dsl::compile::compile_polydat("const lo := 5\nconst hi := 12\n").unwrap();
2724 let v = evaluate_spec("{lo}..{hi}", &kernel).unwrap();
2725 assert_eq!(
2726 v,
2727 vec![
2728 Value::U64(5),
2729 Value::U64(6),
2730 Value::U64(7),
2731 Value::U64(8),
2732 Value::U64(9),
2733 Value::U64(10),
2734 Value::U64(11),
2735 ]
2736 );
2737 }
2738
2739 #[test]
2742 fn fib_n_first_eight() {
2743 let v = evaluate_spec("fib(8)", &empty_kernel()).unwrap();
2744 assert_eq!(
2745 v,
2746 vec![
2747 Value::U64(1),
2748 Value::U64(1),
2749 Value::U64(2),
2750 Value::U64(3),
2751 Value::U64(5),
2752 Value::U64(8),
2753 Value::U64(13),
2754 Value::U64(21),
2755 ]
2756 );
2757 }
2758
2759 #[test]
2760 fn fib_until_50() {
2761 let v = evaluate_spec("fib_until(50)", &empty_kernel()).unwrap();
2762 assert_eq!(
2763 v,
2764 vec![
2765 Value::U64(1),
2766 Value::U64(1),
2767 Value::U64(2),
2768 Value::U64(3),
2769 Value::U64(5),
2770 Value::U64(8),
2771 Value::U64(13),
2772 Value::U64(21),
2773 Value::U64(34),
2774 ]
2775 );
2776 }
2777
2778 #[test]
2779 fn pow2_n_six() {
2780 let v = evaluate_spec("pow2(6)", &empty_kernel()).unwrap();
2781 assert_eq!(
2782 v,
2783 vec![
2784 Value::U64(1),
2785 Value::U64(2),
2786 Value::U64(4),
2787 Value::U64(8),
2788 Value::U64(16),
2789 Value::U64(32),
2790 ]
2791 );
2792 }
2793
2794 #[test]
2795 fn pow2_until_100() {
2796 let v = evaluate_spec("pow2_until(100)", &empty_kernel()).unwrap();
2797 assert_eq!(
2798 v,
2799 vec![
2800 Value::U64(1),
2801 Value::U64(2),
2802 Value::U64(4),
2803 Value::U64(8),
2804 Value::U64(16),
2805 Value::U64(32),
2806 Value::U64(64),
2807 ]
2808 );
2809 }
2810
2811 #[test]
2812 fn binomial_n_5() {
2813 let v = evaluate_spec("binomial(5)", &empty_kernel()).unwrap();
2815 assert_eq!(
2816 v,
2817 vec![
2818 Value::U64(1),
2819 Value::U64(5),
2820 Value::U64(10),
2821 Value::U64(10),
2822 Value::U64(5),
2823 Value::U64(1),
2824 ]
2825 );
2826 }
2827
2828 #[test]
2829 fn geometric_2_doubles_4_terms() {
2830 let v = evaluate_spec("geometric(1, 2, 4)", &empty_kernel()).unwrap();
2831 if let [Value::F64(a), Value::F64(b), Value::F64(c), Value::F64(d)] = v.as_slice() {
2833 assert!((a - 1.0).abs() < 1e-12);
2834 assert!((b - 2.0).abs() < 1e-12);
2835 assert!((c - 4.0).abs() < 1e-12);
2836 assert!((d - 8.0).abs() < 1e-12);
2837 } else {
2838 panic!("expected 4 f64 values, got {v:?}");
2839 }
2840 }
2841
2842 #[test]
2843 fn linear_starts_half_open_5_points() {
2844 let v = evaluate_spec("linear_starts(0, 100, 5)", &empty_kernel()).unwrap();
2845 if let [
2847 Value::F64(a),
2848 Value::F64(b),
2849 Value::F64(c),
2850 Value::F64(d),
2851 Value::F64(e),
2852 ] = v.as_slice()
2853 {
2854 assert!((a - 0.0).abs() < 1e-12);
2855 assert!((b - 20.0).abs() < 1e-12);
2856 assert!((c - 40.0).abs() < 1e-12);
2857 assert!((d - 60.0).abs() < 1e-12);
2858 assert!((e - 80.0).abs() < 1e-12);
2859 } else {
2860 panic!("got {v:?}");
2861 }
2862 }
2863
2864 #[test]
2865 fn linear_steps_inclusive_5_points() {
2866 let v = evaluate_spec("linear_steps(0, 100, 5)", &empty_kernel()).unwrap();
2867 if let [
2869 Value::F64(a),
2870 Value::F64(b),
2871 Value::F64(c),
2872 Value::F64(d),
2873 Value::F64(e),
2874 ] = v.as_slice()
2875 {
2876 assert!((a - 0.0).abs() < 1e-12);
2877 assert!((b - 25.0).abs() < 1e-12);
2878 assert!((c - 50.0).abs() < 1e-12);
2879 assert!((d - 75.0).abs() < 1e-12);
2880 assert!((e - 100.0).abs() < 1e-12);
2881 } else {
2882 panic!("got {v:?}");
2883 }
2884 }
2885
2886 #[test]
2887 fn log_steps_3_decades() {
2888 let v = evaluate_spec("log_steps(1, 1000, 4)", &empty_kernel()).unwrap();
2889 if let [Value::F64(a), Value::F64(b), Value::F64(c), Value::F64(d)] = v.as_slice() {
2891 assert!((a - 1.0).abs() < 1e-9);
2892 assert!((b - 10.0).abs() < 1e-9);
2893 assert!((c - 100.0).abs() < 1e-9);
2894 assert!((d - 1000.0).abs() < 1e-9);
2895 } else {
2896 panic!("got {v:?}");
2897 }
2898 }
2899
2900 #[test]
2901 fn log_steps_rejects_non_positive_bounds() {
2902 let err = evaluate_spec("log_steps(0, 100, 5)", &empty_kernel())
2903 .unwrap_err()
2904 .to_string();
2905 assert!(err.contains("must be positive"), "{err}");
2906 }
2907
2908 #[test]
2911 fn concat_two_ranges() {
2912 let v = evaluate_spec("concat(1..4, 10..13)", &empty_kernel()).unwrap();
2913 assert_eq!(
2914 v,
2915 vec![
2916 Value::U64(1),
2917 Value::U64(2),
2918 Value::U64(3),
2919 Value::U64(10),
2920 Value::U64(11),
2921 Value::U64(12),
2922 ]
2923 );
2924 }
2925
2926 #[test]
2927 fn unique_dedupes_first_occurrence() {
2928 let v = evaluate_spec("unique(1..4, 3..6)", &empty_kernel()).unwrap();
2929 assert_eq!(
2931 v,
2932 vec![
2933 Value::U64(1),
2934 Value::U64(2),
2935 Value::U64(3),
2936 Value::U64(4),
2937 Value::U64(5),
2938 ]
2939 );
2940 }
2941
2942 #[test]
2943 fn intersect_keeps_only_common_values() {
2944 let v = evaluate_spec("intersect(1..10, 5..15)", &empty_kernel()).unwrap();
2945 assert_eq!(
2946 v,
2947 vec![
2948 Value::U64(5),
2949 Value::U64(6),
2950 Value::U64(7),
2951 Value::U64(8),
2952 Value::U64(9),
2953 ]
2954 );
2955 }
2956
2957 #[test]
2958 fn subtract_drops_values_in_b() {
2959 let v = evaluate_spec("subtract(1..6, 3..5)", &empty_kernel()).unwrap();
2960 assert_eq!(v, vec![Value::U64(1), Value::U64(2), Value::U64(5)]);
2962 }
2963
2964 #[test]
2965 fn interleave_round_robin_two_lists() {
2966 let v = evaluate_spec("interleave(1..4, 10..13)", &empty_kernel()).unwrap();
2967 assert_eq!(
2968 v,
2969 vec![
2970 Value::U64(1),
2971 Value::U64(10),
2972 Value::U64(2),
2973 Value::U64(11),
2974 Value::U64(3),
2975 Value::U64(12),
2976 ]
2977 );
2978 }
2979
2980 #[test]
2981 fn cycle_repeats_n_times() {
2982 let v = evaluate_spec("cycle(1..3, 3)", &empty_kernel()).unwrap();
2983 assert_eq!(
2984 v,
2985 vec![
2986 Value::U64(1),
2987 Value::U64(2),
2988 Value::U64(1),
2989 Value::U64(2),
2990 Value::U64(1),
2991 Value::U64(2),
2992 ]
2993 );
2994 }
2995
2996 #[test]
2997 fn reverse_inverts_list() {
2998 let v = evaluate_spec("reverse(1..5)", &empty_kernel()).unwrap();
2999 assert_eq!(
3000 v,
3001 vec![Value::U64(4), Value::U64(3), Value::U64(2), Value::U64(1),]
3002 );
3003 }
3004
3005 #[test]
3006 fn take_n_takes_prefix() {
3007 let v = evaluate_spec("take(1..10, 3)", &empty_kernel()).unwrap();
3008 assert_eq!(v, vec![Value::U64(1), Value::U64(2), Value::U64(3)]);
3009 }
3010
3011 #[test]
3012 fn skip_n_drops_prefix() {
3013 let v = evaluate_spec("skip(1..6, 2)", &empty_kernel()).unwrap();
3014 assert_eq!(v, vec![Value::U64(3), Value::U64(4), Value::U64(5)]);
3015 }
3016
3017 #[test]
3018 fn unique_composes_with_pow2_and_range() {
3019 let v = evaluate_spec("unique(pow2(8), 1..1000..100)", &empty_kernel()).unwrap();
3020 assert_eq!(v.len(), 17);
3024 assert_eq!(v[0], Value::U64(1));
3025 assert_eq!(v[7], Value::U64(128));
3026 assert_eq!(v[8], Value::U64(101));
3027 }
3028
3029 #[test]
3032 fn bucket_round_robin_3_1_2() {
3033 let v = evaluate_spec(
3035 "bucket(concat('ann', 'scan', 'fetch'), concat(3, 1, 2))",
3036 &empty_kernel(),
3037 )
3038 .unwrap();
3039 let _ = v;
3042 }
3043
3044 #[test]
3045 fn bucket_ratio_prefix_shorthand_round_robin() {
3046 let v = evaluate_spec("bucket(\"3:ann, 1:scan, 2:fetch\")", &empty_kernel()).unwrap();
3047 assert_eq!(v.len(), 6);
3050 let strs: Vec<&str> = v
3051 .iter()
3052 .filter_map(|v| match v {
3053 Value::Str(s) => Some(&**s),
3054 _ => None,
3055 })
3056 .collect();
3057 let counts = strs.iter().fold(
3061 std::collections::HashMap::<&str, usize>::new(),
3062 |mut m, s| {
3063 *m.entry(s).or_insert(0) += 1;
3064 m
3065 },
3066 );
3067 assert_eq!(counts.get("ann"), Some(&3));
3068 assert_eq!(counts.get("scan"), Some(&1));
3069 assert_eq!(counts.get("fetch"), Some(&2));
3070 }
3071
3072 #[test]
3073 fn concat_seq_emits_contiguous_runs() {
3074 let v = evaluate_spec(
3075 "concat_seq(\"2:warmup, 3:bench, 1:cooldown\")",
3076 &empty_kernel(),
3077 )
3078 .unwrap();
3079 let strs: Vec<String> = v
3080 .iter()
3081 .filter_map(|v| match v {
3082 Value::Str(s) => Some(s.to_string()),
3083 _ => None,
3084 })
3085 .collect();
3086 assert_eq!(
3087 strs,
3088 vec!["warmup", "warmup", "bench", "bench", "bench", "cooldown",]
3089 );
3090 }
3091
3092 #[test]
3093 fn interval_seq_evenly_spreads_higher_ratio() {
3094 let v = evaluate_spec("interval_seq(\"3:read, 1:write\")", &empty_kernel()).unwrap();
3095 let strs: Vec<String> = v
3098 .iter()
3099 .filter_map(|v| match v {
3100 Value::Str(s) => Some(s.to_string()),
3101 _ => None,
3102 })
3103 .collect();
3104 assert_eq!(strs.len(), 4);
3105 let writes: Vec<usize> = strs
3106 .iter()
3107 .enumerate()
3108 .filter(|(_, s)| *s == "write")
3109 .map(|(i, _)| i)
3110 .collect();
3111 assert_eq!(writes.len(), 1, "expected exactly one write: {strs:?}");
3112 }
3113
3114 #[test]
3115 fn parse_func_call_recognises_simple_call() {
3116 let (n, a) = parse_func_call("fib(8)").unwrap();
3117 assert_eq!(n, "fib");
3118 assert_eq!(a, "8");
3119 }
3120
3121 #[test]
3122 fn parse_func_call_rejects_non_calls() {
3123 assert!(parse_func_call("1..10").is_none());
3124 assert!(parse_func_call("foo + bar").is_none());
3125 assert!(parse_func_call("f(a) + g(b)").is_none()); }
3127
3128 #[test]
3129 fn split_args_top_level_skips_inner_commas() {
3130 let args = split_args_top_level("a, f(b, c), \"x, y\", 3");
3131 assert_eq!(args, vec!["a", "f(b, c)", "\"x, y\"", "3"]);
3132 }
3133}