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