1use std::collections::HashMap;
44
45use crate::ast::Value;
46use crate::kernel::interp::Lookup;
47use crate::kernel::interp::interpolate_with_lookup;
48
49pub 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, kernel.ledger())? {
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_for(&interpolated, kernel.ledger()) {
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_for(e, kernel.ledger())
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, parent_kernel.ledger())? {
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 =
467 match crate::dsl::compile::eval_const_expr_for(&interpolated, parent_kernel.ledger()) {
468 Ok(Value::Str(s)) => s.to_string(),
469 Ok(ref v) if v.as_partition_list().is_some() => {
475 let list = v.as_partition_list().unwrap();
476 return Ok(list
477 .as_slice()
478 .iter()
479 .map(|p| Value::from_partition(*p))
480 .collect());
481 }
482 Ok(other) => return Ok(vec![other]),
483 Err(eval_err) => {
488 if looks_like_literal_list(&interpolated) {
489 interpolated
490 } else {
491 return Err(format!(
492 "for_each clause expression failed to evaluate: {eval_err}\n\
493 spec: {interpolated}\n\
494 If this was meant as a literal list (e.g. `1, 10, 100`), \
495 it should contain only literal values separated by commas. \
496 If it was meant as an expression, fix the underlying \
497 evaluation error."
498 ));
499 }
500 }
501 };
502 Ok(parse_list_with_types(&value_str))
503}
504
505pub fn parse_list_with_types(text: &str) -> Vec<Value> {
510 text.split(',')
511 .map(str::trim)
512 .filter(|s| !s.is_empty())
513 .map(|s| {
514 if let Ok(n) = s.parse::<u64>() {
515 Value::U64(n)
516 } else if let Ok(n) = s.parse::<f64>() {
517 Value::F64(n)
518 } else if s == "true" {
519 Value::Bool(true)
520 } else if s == "false" {
521 Value::Bool(false)
522 } else {
523 Value::Str(s.to_string().into())
524 }
525 })
526 .collect()
527}
528
529fn try_eval_all_cursor(spec_text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
549 let trimmed = spec_text.trim();
550 let Some(stripped) = trimmed.strip_prefix("all(") else {
551 return Ok(None);
552 };
553 let Some(arg) = stripped.strip_suffix(')') else {
554 return Ok(None);
555 };
556 let cursor_name = arg.trim();
557 if cursor_name.is_empty() || !is_valid_ident(cursor_name) {
558 return Ok(None);
559 }
560
561 let start_key = format!("__cursor_extent_{cursor_name}_start");
562 let end_key = format!("__cursor_extent_{cursor_name}_end");
563 let start = kernel
564 .lookup(&start_key)
565 .and_then(|v| match v {
566 Value::U64(n) => Some(n),
567 _ => None,
568 })
569 .ok_or_else(|| {
570 format!(
571 "all({cursor_name}): cursor '{cursor_name}' has no resolvable extent — \
572 check that the cursor is declared at or above this scope and that \
573 its range arguments are init-resolvable. Looked for output '{start_key}'."
574 )
575 })?;
576 let end = kernel
577 .lookup(&end_key)
578 .and_then(|v| match v {
579 Value::U64(n) => Some(n),
580 _ => None,
581 })
582 .ok_or_else(|| {
583 format!(
584 "all({cursor_name}): missing auxiliary output '{end_key}' on the parent kernel."
585 )
586 })?;
587
588 if end < start {
589 return Err(format!(
590 "all({cursor_name}): cursor extent end={end} is less than start={start} — \
591 cannot enumerate a negative-extent range."
592 ));
593 }
594 Ok(Some((start..end).map(Value::U64).collect()))
595}
596
597fn is_valid_ident(s: &str) -> bool {
598 let mut chars = s.chars();
599 match chars.next() {
600 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
601 _ => return false,
602 }
603 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
604}
605
606fn try_eval_range(
629 text: &str,
630 ledger: &std::sync::Arc<crate::kernel::CompileLedger>,
631) -> Result<Option<Vec<Value>>, String> {
632 let trimmed = text.trim();
633 let chars: Vec<char> = trimmed.chars().collect();
634
635 let mut splits: Vec<(usize, bool)> = Vec::new();
639 let mut depth: i32 = 0;
640 let mut i = 0;
641 while i < chars.len() {
642 let c = chars[i];
643 match c {
644 '(' | '[' | '{' => depth += 1,
645 ')' | ']' | '}' => depth -= 1,
646 '"' | '\'' => {
647 let q = c;
649 i += 1;
650 while i < chars.len() && chars[i] != q {
651 i += 1;
652 }
653 }
654 '.' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '.' => {
655 let inclusive = i + 2 < chars.len() && chars[i + 2] == '=';
656 splits.push((i, inclusive));
657 i += if inclusive { 3 } else { 2 };
658 continue;
659 }
660 _ => {}
661 }
662 i += 1;
663 }
664
665 if splits.is_empty() {
666 return Ok(None);
667 }
668 if splits.len() > 2 {
669 return Err(format!(
670 "range expression '{trimmed}': more than two `..` operators \
671 at top level — expected one of `a..b`, `a..=b`, `a..b..s`, \
672 or `a..=b..s`"
673 ));
674 }
675 if splits.len() == 2 && splits[1].1 {
676 return Err(format!(
677 "range expression '{trimmed}': step delimiter cannot be \
678 `..=` — only the bound separator may be inclusive"
679 ));
680 }
681
682 let inclusive = splits[0].1;
684 let first_end = splits[0].0;
685 let after_first = first_end + if inclusive { 3 } else { 2 };
686 let (start_text, mid_text, step_text) = match splits.len() {
687 1 => {
688 let start_s: String = chars[..first_end].iter().collect();
689 let end_s: String = chars[after_first..].iter().collect();
690 (start_s, end_s, None)
691 }
692 2 => {
693 let mid_end = splits[1].0;
694 let after_mid = mid_end + 2; let start_s: String = chars[..first_end].iter().collect();
696 let mid_s: String = chars[after_first..mid_end].iter().collect();
697 let step_s: String = chars[after_mid..].iter().collect();
698 (start_s, mid_s, Some(step_s))
699 }
700 _ => unreachable!(),
701 };
702
703 let start_val = eval_range_segment(&start_text, "range start", ledger)?;
704 let end_val = eval_range_segment(&mid_text, "range end", ledger)?;
705 let step_val = match step_text {
706 Some(s) => Some(eval_range_segment(&s, "range step", ledger)?),
707 None => None,
708 };
709
710 Ok(Some(expand_range(
711 start_val, end_val, step_val, inclusive, trimmed,
712 )?))
713}
714
715fn eval_range_segment(
716 text: &str,
717 what: &str,
718 ledger: &std::sync::Arc<crate::kernel::CompileLedger>,
719) -> Result<Value, String> {
720 let trimmed = text.trim();
721 if trimmed.is_empty() {
722 return Err(format!("range expression: {what} is empty"));
723 }
724 crate::dsl::compile::eval_const_expr_for(trimmed, ledger)
725 .map_err(|e| format!("range expression: {what} '{trimmed}' did not const-fold — {e}"))
726}
727
728fn expand_range(
732 start: Value,
733 end: Value,
734 step: Option<Value>,
735 inclusive: bool,
736 src: &str,
737) -> Result<Vec<Value>, String> {
738 let any_float = matches!(start, Value::F64(_))
739 || matches!(end, Value::F64(_))
740 || matches!(step, Some(Value::F64(_)));
741
742 let to_f64 = |v: &Value| -> Result<f64, String> {
743 match v {
744 Value::U64(n) => Ok(*n as f64),
745 Value::F64(f) => Ok(*f),
746 other => Err(format!(
747 "range expression '{src}': bound has non-numeric value {other:?}"
748 )),
749 }
750 };
751 let to_i64 = |v: &Value| -> Result<i64, String> {
752 match v {
753 Value::U64(n) => i64::try_from(*n).map_err(|_| {
754 format!("range expression '{src}': bound {n} exceeds signed 64-bit range")
755 }),
756 Value::F64(f) => {
757 if f.fract() == 0.0 && *f >= i64::MIN as f64 && *f <= i64::MAX as f64 {
758 Ok(*f as i64)
759 } else {
760 Err(format!(
761 "range expression '{src}': float bound {f} is not integral; \
762 mix with an explicit float step (e.g. `1.0..10..0.5`) for a float range"
763 ))
764 }
765 }
766 other => Err(format!(
767 "range expression '{src}': bound has non-numeric value {other:?}"
768 )),
769 }
770 };
771
772 if any_float {
773 let s = to_f64(&start)?;
774 let e = to_f64(&end)?;
775 let st = match step.as_ref() {
776 Some(v) => to_f64(v)?,
777 None => 1.0,
778 };
779 if st == 0.0 {
780 return Err(format!("range expression '{src}': step is zero"));
781 }
782 if (e - s).is_sign_positive() && st < 0.0 {
784 return Ok(Vec::new());
785 }
786 if (e - s).is_sign_negative() && st > 0.0 {
787 return Ok(Vec::new());
788 }
789 let mut out = Vec::new();
790 let mut cur = s;
791 let cmp = |x: f64| -> bool {
792 if st > 0.0 {
793 if inclusive {
794 x <= e + 1e-12
795 } else {
796 x < e - 1e-12
797 }
798 } else if inclusive {
799 x >= e - 1e-12
800 } else {
801 x > e + 1e-12
802 }
803 };
804 while cmp(cur) {
805 out.push(Value::F64(cur));
806 cur += st;
807 }
808 return Ok(out);
809 }
810
811 let s = to_i64(&start)?;
813 let e = to_i64(&end)?;
814 let st = match step.as_ref() {
815 Some(v) => to_i64(v)?,
816 None => 1,
817 };
818 if st == 0 {
819 return Err(format!("range expression '{src}': step is zero"));
820 }
821 if st > 0 && s > e {
822 return Ok(Vec::new());
823 }
824 if st < 0 && s < e {
825 return Ok(Vec::new());
826 }
827 let mut out = Vec::new();
828 let mut cur = s;
829 let cmp = |x: i64| -> bool {
830 if st > 0 {
831 if inclusive { x <= e } else { x < e }
832 } else if inclusive {
833 x >= e
834 } else {
835 x > e
836 }
837 };
838 while cmp(cur) {
839 if cur < 0 {
840 return Err(format!(
841 "range expression '{src}': negative value {cur} can't be \
842 represented as Value::U64; use a float range \
843 (mix any bound or step with `.0`) for signed walks"
844 ));
845 }
846 out.push(Value::U64(cur as u64));
847 cur = cur.saturating_add(st);
848 if (st > 0 && cur < s) || (st < 0 && cur > s) {
849 break;
851 }
852 }
853 Ok(out)
854}
855
856fn parse_func_call(text: &str) -> Option<(&str, &str)> {
866 let trimmed = text.trim();
867 if !trimmed.ends_with(')') {
868 return None;
869 }
870 let open = trimmed.find('(')?;
871 let name = trimmed[..open].trim();
872 if name.is_empty() || !is_valid_ident(name) {
873 return None;
874 }
875 let chars: Vec<char> = trimmed.chars().collect();
878 let mut depth = 0i32;
879 let mut in_quote: Option<char> = None;
880 for (i, &c) in chars.iter().enumerate().skip(open) {
881 match (c, in_quote) {
882 ('"' | '\'', None) => in_quote = Some(c),
883 (q, Some(open_q)) if q == open_q => in_quote = None,
884 ('(', None) => depth += 1,
885 (')', None) => {
886 depth -= 1;
887 if depth == 0 {
888 if i != chars.len() - 1 {
889 return None; }
891 let args: String = chars[open + 1..i].iter().collect();
892 let _ = args;
898 let name_slice = &trimmed[..open];
899 let args_slice = &trimmed[open + 1..trimmed.len() - 1];
900 return Some((name_slice.trim(), args_slice));
901 }
902 }
903 _ => {}
904 }
905 }
906 None
907}
908
909fn split_args_top_level(args: &str) -> Vec<&str> {
912 let mut out: Vec<&str> = Vec::new();
913 let chars: Vec<char> = args.chars().collect();
914 let bytes_per_char: Vec<usize> = chars.iter().map(|c| c.len_utf8()).collect();
915 let mut start_byte = 0usize;
916 let mut byte = 0usize;
917 let mut depth = 0i32;
918 let mut in_quote: Option<char> = None;
919 for (i, &c) in chars.iter().enumerate() {
920 match (c, in_quote) {
921 ('"' | '\'', None) => in_quote = Some(c),
922 (q, Some(open_q)) if q == open_q => in_quote = None,
923 ('(' | '[' | '{', None) => depth += 1,
924 (')' | ']' | '}', None) => depth -= 1,
925 (',', None) if depth == 0 => {
926 let seg = &args[start_byte..byte];
927 out.push(seg.trim());
928 start_byte = byte + bytes_per_char[i];
929 }
930 _ => {}
931 }
932 byte += bytes_per_char[i];
933 }
934 let last = &args[start_byte..];
935 if !last.trim().is_empty() || !out.is_empty() {
936 out.push(last.trim());
937 }
938 out
939}
940
941fn parse_u64_arg(text: &str, what: &str) -> Result<u64, String> {
944 let trimmed = text.trim();
945 trimmed
946 .parse::<u64>()
947 .map_err(|_| format!("{what}: expected non-negative integer, got '{trimmed}'"))
948}
949
950fn parse_num_arg(text: &str, what: &str) -> Result<f64, String> {
954 let trimmed = text.trim();
955 trimmed
956 .parse::<f64>()
957 .map_err(|_| format!("{what}: expected numeric, got '{trimmed}'"))
958}
959
960fn try_eval_generator(text: &str) -> Result<Option<Vec<Value>>, String> {
969 let Some((name, args)) = parse_func_call(text) else {
970 return Ok(None);
971 };
972 let arg_list = split_args_top_level(args);
973 match name {
974 "fib" => {
975 if arg_list.len() != 1 {
976 return Err(format!(
977 "fib(n): expected 1 argument, got {}",
978 arg_list.len()
979 ));
980 }
981 let n = parse_u64_arg(arg_list[0], "fib(n)")?;
982 Ok(Some(generate_fib_n(n)?))
983 }
984 "fib_until" => {
985 if arg_list.len() != 1 {
986 return Err(format!(
987 "fib_until(max): expected 1 argument, got {}",
988 arg_list.len()
989 ));
990 }
991 let max = parse_u64_arg(arg_list[0], "fib_until(max)")?;
992 Ok(Some(generate_fib_until(max)))
993 }
994 "pow2" => {
995 if arg_list.len() != 1 {
996 return Err(format!(
997 "pow2(n): expected 1 argument, got {}",
998 arg_list.len()
999 ));
1000 }
1001 let n = parse_u64_arg(arg_list[0], "pow2(n)")?;
1002 Ok(Some(generate_pow2_n(n)))
1003 }
1004 "pow2_until" => {
1005 if arg_list.len() != 1 {
1006 return Err(format!(
1007 "pow2_until(max): expected 1 argument, got {}",
1008 arg_list.len()
1009 ));
1010 }
1011 let max = parse_u64_arg(arg_list[0], "pow2_until(max)")?;
1012 Ok(Some(generate_pow2_until(max)))
1013 }
1014 "binomial" => {
1015 if arg_list.len() != 1 {
1016 return Err(format!(
1017 "binomial(n): expected 1 argument, got {}",
1018 arg_list.len()
1019 ));
1020 }
1021 let n = parse_u64_arg(arg_list[0], "binomial(n)")?;
1022 Ok(Some(generate_binomial(n)))
1023 }
1024 "geometric" => {
1025 if arg_list.len() != 3 {
1026 return Err(format!(
1027 "geometric(start, factor, n): expected 3 args, got {}",
1028 arg_list.len()
1029 ));
1030 }
1031 let start = parse_num_arg(arg_list[0], "geometric.start")?;
1032 let factor = parse_num_arg(arg_list[1], "geometric.factor")?;
1033 let n = parse_u64_arg(arg_list[2], "geometric.n")?;
1034 Ok(Some(generate_geometric(start, factor, n)?))
1035 }
1036 "geometric_until" => {
1037 if arg_list.len() != 3 {
1038 return Err(format!(
1039 "geometric_until(start, factor, max): expected 3 args, got {}",
1040 arg_list.len()
1041 ));
1042 }
1043 let start = parse_num_arg(arg_list[0], "geometric_until.start")?;
1044 let factor = parse_num_arg(arg_list[1], "geometric_until.factor")?;
1045 let max = parse_num_arg(arg_list[2], "geometric_until.max")?;
1046 Ok(Some(generate_geometric_until(start, factor, max)))
1047 }
1048 "linear_starts" => {
1049 if arg_list.len() != 3 {
1050 return Err(format!(
1051 "linear_starts(start, end, n): expected 3 args, got {}",
1052 arg_list.len()
1053 ));
1054 }
1055 let start = parse_num_arg(arg_list[0], "linear_starts.start")?;
1056 let end = parse_num_arg(arg_list[1], "linear_starts.end")?;
1057 let n = parse_u64_arg(arg_list[2], "linear_starts.n")?;
1058 Ok(Some(generate_linear_points(start, end, n, false)?))
1059 }
1060 "linear_steps" => {
1061 if arg_list.len() != 3 {
1062 return Err(format!(
1063 "linear_steps(start, end, n): expected 3 args, got {}",
1064 arg_list.len()
1065 ));
1066 }
1067 let start = parse_num_arg(arg_list[0], "linear_steps.start")?;
1068 let end = parse_num_arg(arg_list[1], "linear_steps.end")?;
1069 let n = parse_u64_arg(arg_list[2], "linear_steps.n")?;
1070 Ok(Some(generate_linear_points(start, end, n, true)?))
1071 }
1072 "log_steps" => {
1073 if arg_list.len() != 3 {
1074 return Err(format!(
1075 "log_steps(start, end, n): expected 3 args, got {}",
1076 arg_list.len()
1077 ));
1078 }
1079 let start = parse_num_arg(arg_list[0], "log_steps.start")?;
1080 let end = parse_num_arg(arg_list[1], "log_steps.end")?;
1081 let n = parse_u64_arg(arg_list[2], "log_steps.n")?;
1082 Ok(Some(generate_log_steps(start, end, n)?))
1083 }
1084 _ => Ok(None),
1085 }
1086}
1087
1088fn generate_fib_n(n: u64) -> Result<Vec<Value>, String> {
1094 let mut out = crate::derive_support::try_buffer_for(n, "fib(n)")?;
1095 let (mut a, mut b): (u64, u64) = (1, 1);
1096 for _ in 0..n {
1097 out.push(Value::U64(a));
1098 let next = a.saturating_add(b);
1099 a = b;
1100 b = next;
1101 }
1102 Ok(out)
1103}
1104
1105fn generate_fib_until(max: u64) -> Vec<Value> {
1107 let mut out = Vec::new();
1108 let (mut a, mut b): (u64, u64) = (1, 1);
1109 while a <= max {
1110 out.push(Value::U64(a));
1111 let next = a.checked_add(b);
1112 a = b;
1113 match next {
1114 Some(v) => b = v,
1115 None => break,
1116 }
1117 }
1118 out
1119}
1120
1121fn generate_pow2_n(n: u64) -> Vec<Value> {
1123 let mut out = Vec::with_capacity(n.min(64) 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) -> Result<Vec<Value>, String> {
1153 let mut out = crate::derive_support::try_buffer_for(n, "geometric(start, factor, n)")?;
1154 let mut v = start;
1155 for _ in 0..n {
1156 out.push(Value::F64(v));
1157 v *= factor;
1158 }
1159 Ok(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.min(67) as usize + 1);
1185 let mut c: u128 = 1;
1186 out.push(Value::U64(1));
1187 for k in 1..=n {
1188 c = c * (n - k + 1) as u128 / k as u128;
1189 if c > u64::MAX as u128 {
1190 break;
1191 }
1192 out.push(Value::U64(c as u64));
1193 }
1194 out
1195}
1196
1197fn try_eval_partition_call(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
1221 let Some((name, args)) = parse_func_call(text) else {
1222 return Ok(None);
1223 };
1224 let arg_list = split_args_top_level(args);
1225 match name {
1226 "subdivide" => {
1227 if arg_list.len() != 2 {
1228 return Err(format!(
1229 "subdivide(p, n): expected 2 arguments (a partition and a count), got {}",
1230 arg_list.len()
1231 ));
1232 }
1233 let src = arg_list[0].trim();
1234 let n = parse_u64_arg(arg_list[1], "subdivide.n")?;
1235 let Some(value) = kernel.lookup(src) else {
1236 let placeholder = crate::iteration::cursor_partition::Partition {
1241 idx: 0,
1242 count: 1,
1243 start_ord: 0,
1244 end_ord: 1,
1245 start_pct: 0.0,
1246 end_pct: 100.0,
1247 base_extent: 1,
1248 };
1249 return Ok(Some(vec![Value::from_partition(placeholder)]));
1250 };
1251 let Some(p) = value.as_partition().copied() else {
1252 return Err(format!(
1253 "subdivide({src}, {n}): `{src}` resolved to {} — expected a \
1254 Partition value (an iter-var from `for: \"p in partitions(...)\"` \
1255 or a cursor's `.cursor` projection)",
1256 value.to_display_string(),
1257 ));
1258 };
1259 let subs = crate::iteration::cursor_partition::subdivide_partition(&p, n)?;
1260 Ok(Some(subs.into_iter().map(Value::from_partition).collect()))
1261 }
1262 "partitions" => {
1273 if arg_list.is_empty() || arg_list.len() > 2 {
1274 return Err(format!(
1275 "partitions(spec, [extent]): expected 1 or 2 arguments, got {}",
1276 arg_list.len(),
1277 ));
1278 }
1279 let spec = resolve_partition_spec_arg(arg_list[0], kernel)?;
1280 let extent = match arg_list.get(1) {
1281 Some(a) => parse_u64_arg(a, "partitions.extent")?,
1282 None => 100,
1283 };
1284 desugar_partition_spec(&spec, extent, "comprehension source `partitions(...)`")
1285 .map(Some)
1286 }
1287 "profile_partitions" => {
1298 #[cfg(not(feature = "vectordata"))]
1299 {
1300 Err("profile_partitions requires the `vectordata` Cargo feature".to_string())
1301 }
1302
1303 #[cfg(feature = "vectordata")]
1304 {
1305 if arg_list.len() != 2 {
1306 return Err(format!(
1307 "profile_partitions(dataset, pattern): expected 2 arguments, got {}",
1308 arg_list.len()
1309 ));
1310 }
1311 let strip = |s: &str| -> String {
1314 let s = s.trim();
1315 let b = s.as_bytes();
1316 if b.len() >= 2 && (b[0] == b'\'' || b[0] == b'"') && b[b.len() - 1] == b[0] {
1317 s[1..s.len() - 1].to_string()
1318 } else {
1319 s.to_string()
1320 }
1321 };
1322 let dataset = strip(arg_list[0]);
1323 let pattern = strip(arg_list[1]);
1324 match crate::library::vectors::load_dataset_group(&dataset) {
1325 Ok(group) => {
1326 let parts =
1327 crate::library::vectors::build_profile_partitions(&group, &pattern);
1328 Ok(Some(parts.into_iter().map(Value::from_partition).collect()))
1329 }
1330 Err(_) => {
1331 let placeholder = crate::iteration::cursor_partition::Partition {
1335 idx: 0,
1336 count: 1,
1337 start_ord: 0,
1338 end_ord: 1,
1339 start_pct: 0.0,
1340 end_pct: 100.0,
1341 base_extent: 1,
1342 };
1343 Ok(Some(vec![Value::from_partition(placeholder)]))
1344 }
1345 }
1346 }
1347 }
1348 _ => Ok(None),
1349 }
1350}
1351
1352fn try_eval_param_partitions(
1375 text: &str,
1376 kernel: &dyn Lookup,
1377) -> Result<Option<Vec<Value>>, String> {
1378 let Some(ident) = text.trim().strip_suffix(".partitions") else {
1379 return Ok(None);
1380 };
1381 let ident = ident.trim();
1382 if !is_single_bare_ident(ident) {
1383 return Ok(None);
1384 }
1385 let Some(value) = kernel.lookup(ident) else {
1386 let placeholder = crate::iteration::cursor_partition::Partition {
1389 idx: 0,
1390 count: 1,
1391 start_ord: 0,
1392 end_ord: 1,
1393 start_pct: 0.0,
1394 end_pct: 100.0,
1395 base_extent: 1,
1396 };
1397 return Ok(Some(vec![Value::from_partition(placeholder)]));
1398 };
1399 if let Some(list) = value.as_partition_list() {
1401 return Ok(Some(
1402 list.as_slice()
1403 .iter()
1404 .map(|p| Value::from_partition(*p))
1405 .collect(),
1406 ));
1407 }
1408 let Value::Str(spec) = &value else {
1410 return Err(format!(
1411 "comprehension source `{ident}.partitions`: `{ident}` resolved to \
1412 {} — expected a partition-spec string (a workload param such as \
1413 `cursor=linear:4`) or a PartitionList.",
1414 value.to_display_string(),
1415 ));
1416 };
1417 desugar_partition_spec(
1418 spec,
1419 100,
1420 &format!("comprehension source `{ident}.partitions`"),
1421 )
1422 .map(Some)
1423}
1424
1425fn desugar_partition_spec(spec: &str, extent: u64, ctx: &str) -> Result<Vec<Value>, String> {
1432 let parsed = crate::iteration::cursor_partition::parse(spec)
1433 .map_err(|e| format!("{ctx}: bad spec `{spec}`: {e}"))?;
1434 let parts = crate::iteration::cursor_partition::resolve(&parsed, 0, extent)
1435 .map_err(|e| format!("{ctx}: resolve failed for `{spec}`: {e}"))?;
1436 Ok(parts.into_iter().map(Value::from_partition).collect())
1437}
1438
1439fn resolve_partition_spec_arg(arg: &str, kernel: &dyn Lookup) -> Result<String, String> {
1444 let a = arg.trim();
1445 if a.len() >= 2
1446 && ((a.starts_with('"') && a.ends_with('"')) || (a.starts_with('\'') && a.ends_with('\'')))
1447 {
1448 return Ok(a[1..a.len() - 1].to_string());
1449 }
1450 if is_single_bare_ident(a) {
1451 return match kernel.lookup(a) {
1452 Some(Value::Str(s)) => Ok(s.to_string()),
1453 Some(other) => Err(format!(
1454 "partitions(...): `{a}` resolved to {} — expected a spec string",
1455 other.to_display_string(),
1456 )),
1457 None => Err(format!(
1458 "partitions(...): `{a}` did not resolve to a spec string in scope"
1459 )),
1460 };
1461 }
1462 Ok(a.to_string())
1463}
1464
1465fn generate_linear_points(
1476 start: f64,
1477 end: f64,
1478 n: u64,
1479 inclusive: bool,
1480) -> Result<Vec<Value>, String> {
1481 let denom = if inclusive {
1482 (n.saturating_sub(1)).max(1) as f64
1483 } else {
1484 n as f64
1485 };
1486 let step = (end - start) / denom;
1487 let mut out = crate::derive_support::try_buffer_for(n, "linear points")?;
1491 out.extend((0..n).map(|i| Value::F64(start + step * i as f64)));
1492 Ok(out)
1493}
1494
1495fn generate_log_steps(start: f64, end: f64, n: u64) -> Result<Vec<Value>, String> {
1498 if start <= 0.0 || end <= 0.0 {
1499 return Err(format!(
1500 "log_steps: bounds must be positive, got start={start}, end={end}"
1501 ));
1502 }
1503 if n == 0 {
1504 return Ok(Vec::new());
1505 }
1506 if n == 1 {
1507 return Ok(vec![Value::F64(start)]);
1508 }
1509 let log_s = start.ln();
1510 let log_e = end.ln();
1511 let step = (log_e - log_s) / (n - 1) as f64;
1512 let mut out = crate::derive_support::try_buffer_for(n, "log_steps(start, end, n)")?;
1513 out.extend((0..n).map(|i| Value::F64((log_s + step * i as f64).exp())));
1514 Ok(out)
1515}
1516
1517fn try_eval_setop(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
1526 let Some((name, args)) = parse_func_call(text) else {
1527 return Ok(None);
1528 };
1529 let arg_texts = split_args_top_level(args);
1530 let recursively_evaluate = |t: &str| -> Result<Vec<Value>, String> {
1531 evaluate_spec(t, kernel).map_err(|e| e.to_string())
1532 };
1533 match name {
1534 "concat" => {
1535 let mut out = Vec::new();
1536 for a in &arg_texts {
1537 out.extend(recursively_evaluate(a)?);
1538 }
1539 Ok(Some(out))
1540 }
1541 "unique" => {
1542 let mut out: Vec<Value> = Vec::new();
1543 for a in &arg_texts {
1544 for v in recursively_evaluate(a)? {
1545 if !out.contains(&v) {
1546 out.push(v);
1547 }
1548 }
1549 }
1550 Ok(Some(out))
1551 }
1552 "intersect" => {
1553 if arg_texts.is_empty() {
1554 return Ok(Some(Vec::new()));
1555 }
1556 let first = recursively_evaluate(arg_texts[0])?;
1557 let mut out: Vec<Value> = Vec::new();
1558 for v in first {
1559 let mut in_all = true;
1560 for a in &arg_texts[1..] {
1561 let other = recursively_evaluate(a)?;
1562 if !other.contains(&v) {
1563 in_all = false;
1564 break;
1565 }
1566 }
1567 if in_all && !out.contains(&v) {
1568 out.push(v);
1569 }
1570 }
1571 Ok(Some(out))
1572 }
1573 "subtract" => {
1574 if arg_texts.len() != 2 {
1575 return Err(format!(
1576 "subtract(a, b): expected 2 args, got {}",
1577 arg_texts.len()
1578 ));
1579 }
1580 let a = recursively_evaluate(arg_texts[0])?;
1581 let b = recursively_evaluate(arg_texts[1])?;
1582 Ok(Some(a.into_iter().filter(|v| !b.contains(v)).collect()))
1583 }
1584 "interleave" => {
1585 let lists: Result<Vec<Vec<Value>>, String> =
1586 arg_texts.iter().map(|a| recursively_evaluate(a)).collect();
1587 let lists = lists?;
1588 let mut out = Vec::new();
1589 let max_len = lists.iter().map(|l| l.len()).max().unwrap_or(0);
1590 for i in 0..max_len {
1591 for l in &lists {
1592 if let Some(v) = l.get(i) {
1593 out.push(v.clone());
1594 }
1595 }
1596 }
1597 Ok(Some(out))
1598 }
1599 "cycle" => {
1600 if arg_texts.len() != 2 {
1601 return Err(format!(
1602 "cycle(a, n): expected 2 args, got {}",
1603 arg_texts.len()
1604 ));
1605 }
1606 let a = recursively_evaluate(arg_texts[0])?;
1607 let n = parse_u64_arg(arg_texts[1], "cycle.n")?;
1608 let total = (a.len() as u64).checked_mul(n).ok_or_else(|| {
1609 format!(
1610 "cycle(a, n): {} values repeated {n} times is more than can be counted",
1611 a.len()
1612 )
1613 })?;
1614 let mut out = crate::derive_support::try_buffer_for(total, "cycle(a, n)")?;
1615 for _ in 0..n {
1616 out.extend(a.iter().cloned());
1617 }
1618 Ok(Some(out))
1619 }
1620 "reverse" => {
1621 if arg_texts.len() != 1 {
1622 return Err(format!(
1623 "reverse(a): expected 1 arg, got {}",
1624 arg_texts.len()
1625 ));
1626 }
1627 let mut a = recursively_evaluate(arg_texts[0])?;
1628 a.reverse();
1629 Ok(Some(a))
1630 }
1631 "take" => {
1632 if arg_texts.len() != 2 {
1633 return Err(format!(
1634 "take(a, n): expected 2 args, got {}",
1635 arg_texts.len()
1636 ));
1637 }
1638 let a = recursively_evaluate(arg_texts[0])?;
1639 let n = parse_u64_arg(arg_texts[1], "take.n")?;
1640 Ok(Some(a.into_iter().take(n as usize).collect()))
1641 }
1642 "skip" => {
1643 if arg_texts.len() != 2 {
1644 return Err(format!(
1645 "skip(a, n): expected 2 args, got {}",
1646 arg_texts.len()
1647 ));
1648 }
1649 let a = recursively_evaluate(arg_texts[0])?;
1650 let n = parse_u64_arg(arg_texts[1], "skip.n")?;
1651 Ok(Some(a.into_iter().skip(n as usize).collect()))
1652 }
1653 _ => Ok(None),
1654 }
1655}
1656
1657fn try_eval_sequencer(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
1673 let Some((name, args)) = parse_func_call(text) else {
1674 return Ok(None);
1675 };
1676 if !matches!(name, "bucket" | "concat_seq" | "interval_seq") {
1677 return Ok(None);
1678 }
1679 let arg_texts = split_args_top_level(args);
1680
1681 let (items, ratios): (Vec<Value>, Vec<usize>) = match arg_texts.len() {
1686 1 => parse_ratio_prefix_shorthand(arg_texts[0])?,
1687 2 => {
1688 let items = evaluate_spec(arg_texts[0], kernel)?;
1689 let raw_ratios = evaluate_spec(arg_texts[1], kernel)?;
1690 let ratios: Result<Vec<usize>, String> = raw_ratios
1691 .iter()
1692 .map(|v| match v {
1693 Value::U64(n) => Ok(*n as usize),
1694 other => Err(format!(
1695 "{name}: ratio must be non-negative integer, got {other:?}"
1696 )),
1697 })
1698 .collect();
1699 (items, ratios?)
1700 }
1701 _ => {
1702 return Err(format!(
1703 "{name}: expected `(items, ratios)` or `(\"r1:item1, r2:item2, ...\")`; got {} args",
1704 arg_texts.len()
1705 ));
1706 }
1707 };
1708
1709 if items.len() != ratios.len() {
1710 return Err(format!(
1711 "{name}: items.len() ({}) != ratios.len() ({})",
1712 items.len(),
1713 ratios.len(),
1714 ));
1715 }
1716 let total = ratios
1720 .iter()
1721 .try_fold(0usize, |acc, &r| acc.checked_add(r))
1722 .ok_or_else(|| format!("{name}: the ratios sum past what can be counted"))?;
1723 let out = crate::derive_support::try_buffer_for(total as u64, name)?;
1724 Ok(Some(match name {
1725 "bucket" => seq_bucket(&items, &ratios, total, out),
1726 "concat_seq" => seq_concat(&items, &ratios, out),
1727 "interval_seq" => seq_interval(&items, &ratios, total, out),
1728 _ => unreachable!(),
1729 }))
1730}
1731
1732fn parse_ratio_prefix_shorthand(text: &str) -> Result<(Vec<Value>, Vec<usize>), String> {
1737 let stripped = text
1740 .trim()
1741 .trim_start_matches(['"', '\''])
1742 .trim_end_matches(['"', '\'']);
1743 let mut items = Vec::new();
1744 let mut ratios = Vec::new();
1745 for part in stripped.split(',') {
1746 let part = part.trim();
1747 if part.is_empty() {
1748 continue;
1749 }
1750 let (r, i) = part
1751 .split_once(':')
1752 .ok_or_else(|| format!("ratio-prefix shorthand: missing ':' in '{part}'"))?;
1753 let ratio: usize = r.trim().parse().map_err(|_| {
1754 format!("ratio-prefix shorthand: ratio '{r}' is not a non-negative integer")
1755 })?;
1756 ratios.push(ratio);
1757 items.push(parse_one_value(i.trim()));
1758 }
1759 Ok((items, ratios))
1760}
1761
1762fn parse_one_value(s: &str) -> Value {
1763 if let Ok(n) = s.parse::<u64>() {
1764 return Value::U64(n);
1765 }
1766 if let Ok(f) = s.parse::<f64>() {
1767 return Value::F64(f);
1768 }
1769 if s == "true" {
1770 return Value::Bool(true);
1771 }
1772 if s == "false" {
1773 return Value::Bool(false);
1774 }
1775 Value::Str(s.to_string().into())
1776}
1777
1778fn seq_bucket(items: &[Value], ratios: &[usize], total: usize, mut out: Vec<Value>) -> Vec<Value> {
1782 let mut remaining: Vec<usize> = ratios.to_vec();
1783 while out.len() < total {
1784 let mut emitted_any = false;
1785 for (i, item) in items.iter().enumerate() {
1786 if remaining[i] > 0 {
1787 out.push(item.clone());
1788 remaining[i] -= 1;
1789 emitted_any = true;
1790 }
1791 }
1792 if !emitted_any {
1793 break;
1794 }
1795 }
1796 out
1797}
1798
1799fn seq_concat(items: &[Value], ratios: &[usize], mut out: Vec<Value>) -> Vec<Value> {
1802 for (item, &r) in items.iter().zip(ratios.iter()) {
1803 for _ in 0..r {
1804 out.push(item.clone());
1805 }
1806 }
1807 out
1808}
1809
1810fn seq_interval(
1816 items: &[Value],
1817 ratios: &[usize],
1818 total: usize,
1819 mut out: Vec<Value>,
1820) -> Vec<Value> {
1821 if total == 0 {
1822 return out;
1823 }
1824 let mut emitted: Vec<usize> = vec![0; items.len()];
1825 for slot in 0..total {
1826 let mut best = 0usize;
1829 let mut best_deficit: f64 = f64::NEG_INFINITY;
1830 for i in 0..items.len() {
1831 let target = ratios[i] as f64 * (slot + 1) as f64 / total as f64;
1832 let deficit = target - emitted[i] as f64;
1833 if deficit > best_deficit {
1834 best_deficit = deficit;
1835 best = i;
1836 }
1837 }
1838 out.push(items[best].clone());
1839 emitted[best] += 1;
1840 }
1841 out
1842}
1843
1844pub fn value_to_polydat_type_name(v: &Value) -> &'static str {
1854 v.port_type().to_keyword()
1855}
1856
1857#[cfg(test)]
1868mod tests {
1869 use super::*;
1870 use crate::kernel::PolydatKernel;
1871 use crate::kernel::interp::interpolate_via_kernel;
1872
1873 fn h(pairs: &[(&str, &str)]) -> HashMap<String, String> {
1874 pairs
1875 .iter()
1876 .map(|(k, v)| (k.to_string(), v.to_string()))
1877 .collect()
1878 }
1879
1880 fn interpolate(
1881 text: &str,
1882 bindings: &HashMap<String, String>,
1883 workload_params: &HashMap<String, String>,
1884 ) -> Result<String, String> {
1885 interpolate_with_lookup(text, |name| {
1886 bindings
1887 .get(name)
1888 .or_else(|| workload_params.get(name))
1889 .cloned()
1890 })
1891 }
1892
1893 #[test]
1894 fn flat_substitution() {
1895 let params = h(&[("dataset", "example"), ("prefix", "label")]);
1896 let out = interpolate("matching('{dataset}', '{prefix}')", &h(&[]), ¶ms).unwrap();
1897 assert_eq!(out, "matching('example', 'label')");
1898 }
1899
1900 #[test]
1901 fn bindings_shadow_params() {
1902 let params = h(&[("profile", "default")]);
1903 let bindings = h(&[("profile", "label_07")]);
1904 let out = interpolate("vec_{profile}", &bindings, ¶ms).unwrap();
1905 assert_eq!(out, "vec_label_07");
1906 }
1907
1908 #[test]
1909 fn nested_placeholder_resolves_inside_out() {
1910 let params = h(&[("k_1_limits", "1,2,4,8"), ("k_10_limits", "10,20,30")]);
1911 let bindings = h(&[("k", "1")]);
1912 let out = interpolate("{k_{k}_limits}", &bindings, ¶ms).unwrap();
1913 assert_eq!(out, "1,2,4,8");
1914 }
1915
1916 #[test]
1917 fn deeply_nested() {
1918 let params = h(&[("a_b_c", "WIN")]);
1919 let bindings = h(&[("x", "a"), ("y", "b"), ("z", "c")]);
1920 let out = interpolate("{{x}_{y}_{z}}", &bindings, ¶ms).unwrap();
1921 assert_eq!(out, "WIN");
1922 }
1923
1924 #[test]
1925 fn escape_emits_literal_brace() {
1926 let out = interpolate("\\{not_a_var\\}", &h(&[]), &h(&[])).unwrap();
1927 assert_eq!(out, "{not_a_var}");
1928 }
1929
1930 #[test]
1931 fn escape_inside_otherwise_resolved_text() {
1932 let params = h(&[("x", "1")]);
1933 let out = interpolate("a={x} literal=\\{x\\}", &h(&[]), ¶ms).unwrap();
1934 assert_eq!(out, "a=1 literal={x}");
1935 }
1936
1937 #[test]
1938 fn unresolved_is_hard_error() {
1939 let err = interpolate("hello {nope}", &h(&[]), &h(&[])).unwrap_err();
1940 assert!(err.contains("unresolved"));
1941 assert!(err.contains("nope"));
1942 }
1943
1944 #[test]
1945 fn empty_placeholder_rejected() {
1946 let err = interpolate("a{}b", &h(&[]), &h(&[])).unwrap_err();
1947 assert!(err.contains("empty"));
1948 }
1949
1950 #[test]
1951 fn unmatched_brace_rejected() {
1952 let err = interpolate("a {x", &h(&[]), &h(&[])).unwrap_err();
1953 assert!(err.contains("unmatched"));
1954 }
1955
1956 #[test]
1957 fn idempotent_when_no_placeholders() {
1958 let out = interpolate("plain text", &h(&[]), &h(&[])).unwrap();
1959 assert_eq!(out, "plain text");
1960 }
1961
1962 #[test]
1963 fn resolved_value_with_braces_does_not_re_expand() {
1964 let params = h(&[("greeting", "hello {planet}")]);
1965 let err = interpolate("{greeting}", &h(&[]), ¶ms).unwrap_err();
1966 assert!(err.contains("planet"));
1967 }
1968
1969 #[test]
1970 fn cyclic_placeholders_hit_round_cap() {
1971 let params = h(&[("a", "{b}"), ("b", "{a}")]);
1972 let err = interpolate("{a}", &h(&[]), ¶ms).unwrap_err();
1973 assert!(err.contains("did not stabilize") || err.contains("rounds"));
1974 }
1975
1976 #[test]
1977 fn kernel_resolves_via_get_constant() {
1978 let kernel =
1979 crate::dsl::compile::compile_polydat_interpreter("const dataset := \"example\"\n")
1980 .unwrap();
1981 let out = interpolate_via_kernel("path/{dataset}/data", &kernel).unwrap();
1982 assert_eq!(out, "path/example/data");
1983 }
1984
1985 #[test]
1986 fn kernel_resolves_via_get_input() {
1987 let parent =
1988 crate::dsl::compile::compile_polydat_interpreter("const k_values := \"1, 10\"\n")
1989 .unwrap();
1990 let child_program =
1991 crate::dsl::compile::compile_polydat_interpreter("extern k_values: String\n")
1992 .unwrap()
1993 .program()
1994 .clone();
1995 let child = parent.materialize_subscope(child_program, &[]);
1996 let out = interpolate_via_kernel("values={k_values}", &child).unwrap();
1997 assert_eq!(out, "values=1, 10");
1998 }
1999
2000 #[test]
2001 fn kernel_unresolved_name_errors() {
2002 let kernel = crate::dsl::compile::compile_polydat_interpreter("const x := 1\n").unwrap();
2003 let err = interpolate_via_kernel("hello {nope}", &kernel)
2004 .unwrap_err()
2005 .to_string();
2006 assert!(err.contains("unresolved"));
2007 assert!(err.contains("nope"));
2008 }
2009
2010 #[test]
2011 fn kernel_nested_template_iterates_to_fixed_point() {
2012 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2013 "const k := \"1\"\nconst k_1_limits := \"1, 2, 4, 8\"\n",
2014 )
2015 .unwrap();
2016 let out = interpolate_via_kernel("{k_{k}_limits}", &kernel).unwrap();
2017 assert_eq!(out, "1, 2, 4, 8");
2018 }
2019
2020 #[test]
2021 fn parse_list_native_types() {
2022 let v = parse_list_with_types("1, 10, 100");
2023 assert_eq!(v, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2024 }
2025
2026 #[test]
2027 fn parse_list_mixed_types() {
2028 let v = parse_list_with_types("1, 1.5, true, hello");
2029 assert_eq!(
2030 v,
2031 vec![
2032 Value::U64(1),
2033 Value::F64(1.5),
2034 Value::Bool(true),
2035 Value::Str("hello".to_string().into()),
2036 ]
2037 );
2038 }
2039
2040 #[test]
2041 fn all_cursor_returns_extent_range() {
2042 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2048 "const __cursor_extent_row_start := 0\n\
2049 const __cursor_extent_row_end := 5\n",
2050 )
2051 .unwrap();
2052 let values = evaluate_spec("all(row)", &kernel).unwrap();
2053 assert_eq!(
2054 values,
2055 vec![
2056 Value::U64(0),
2057 Value::U64(1),
2058 Value::U64(2),
2059 Value::U64(3),
2060 Value::U64(4),
2061 ]
2062 );
2063 }
2064
2065 #[test]
2066 fn all_cursor_non_zero_start() {
2067 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2068 "const __cursor_extent_data_start := 100\n\
2069 const __cursor_extent_data_end := 103\n",
2070 )
2071 .unwrap();
2072 let values = evaluate_spec("all(data)", &kernel).unwrap();
2073 assert_eq!(
2074 values,
2075 vec![Value::U64(100), Value::U64(101), Value::U64(102)]
2076 );
2077 }
2078
2079 #[test]
2080 fn all_cursor_missing_extent_errors() {
2081 let kernel =
2082 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2083 let err = evaluate_spec("all(no_such_cursor)", &kernel)
2084 .unwrap_err()
2085 .to_string();
2086 assert!(err.contains("all(no_such_cursor)"));
2087 assert!(err.contains("no resolvable extent"));
2088 }
2089
2090 #[test]
2091 fn all_cursor_only_matches_exact_shape() {
2092 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2103 "const __cursor_extent_row_start := 0\n\
2104 const __cursor_extent_row_end := 5\n",
2105 )
2106 .unwrap();
2107 let err = evaluate_spec("all(row, 5)", &kernel)
2108 .unwrap_err()
2109 .to_string();
2110 assert!(
2111 err.contains("all(row, 5)"),
2112 "error must mention the failing spec, got: {err}"
2113 );
2114 assert!(
2115 err.contains("failed to evaluate") || err.contains("unknown function"),
2116 "error must explain the eval failure, got: {err}"
2117 );
2118 }
2119
2120 #[test]
2121 fn missing_dataset_surface_as_clean_error_not_garbage() {
2122 let kernel =
2144 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2145 let result = evaluate_spec(
2146 "matching_profiles('nonexistent_dataset_xyz_qqq', 'label_')",
2147 &kernel,
2148 );
2149 let err = result
2150 .expect_err("missing dataset must surface as Err, not silent literal-list fallback")
2151 .to_string();
2152 assert!(
2158 err.contains("nonexistent_dataset_xyz_qqq")
2159 || err.contains("matching_profiles")
2160 || err.contains("dataset"),
2161 "error must point at the actual fault, got: {err}"
2162 );
2163 }
2164
2165 #[test]
2166 fn function_call_eval_failure_is_not_silently_split() {
2167 let kernel =
2173 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2174 let err = evaluate_spec("nonexistent_func('a', 'b', 'c')", &kernel)
2175 .unwrap_err()
2176 .to_string();
2177 assert!(
2178 err.contains("failed to evaluate") || err.contains("unknown"),
2179 "expected a clean eval-failure error, got: {err}"
2180 );
2181 }
2182
2183 #[test]
2184 fn literal_list_path_still_works() {
2185 let kernel =
2192 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2193 let values = evaluate_spec("1, 10, 100", &kernel).unwrap();
2194 assert_eq!(values, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2195
2196 let names = evaluate_spec("foo, bar, baz", &kernel).unwrap();
2197 assert_eq!(
2198 names,
2199 vec![
2200 Value::Str("foo".into()),
2201 Value::Str("bar".into()),
2202 Value::Str("baz".into()),
2203 ]
2204 );
2205 }
2206
2207 #[test]
2208 fn literal_cursor_exposes_extent_auxiliaries() {
2209 let kernel =
2214 crate::dsl::compile::compile_polydat_interpreter("cursor row = range(0, 50)\n")
2215 .unwrap();
2216 let start = kernel.lookup("__cursor_extent_row_start");
2217 let end = kernel.lookup("__cursor_extent_row_end");
2218 assert_eq!(
2219 start,
2220 Some(Value::U64(0)),
2221 "expected start=0, got {start:?}"
2222 );
2223 assert_eq!(end, Some(Value::U64(50)), "expected end=50, got {end:?}");
2224 }
2225
2226 #[test]
2227 fn all_cursor_with_real_cursor_decl_works() {
2228 let kernel =
2229 crate::dsl::compile::compile_polydat_interpreter("cursor row = range(0, 5)\n").unwrap();
2230 let values = evaluate_spec("all(row)", &kernel).unwrap();
2231 assert_eq!(
2232 values,
2233 vec![
2234 Value::U64(0),
2235 Value::U64(1),
2236 Value::U64(2),
2237 Value::U64(3),
2238 Value::U64(4),
2239 ]
2240 );
2241 }
2242
2243 #[test]
2244 fn all_cursor_ignores_whitespace() {
2245 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2246 "const __cursor_extent_row_start := 0\n\
2247 const __cursor_extent_row_end := 3\n",
2248 )
2249 .unwrap();
2250 let values = evaluate_spec(" all( row ) ", &kernel).unwrap();
2251 assert_eq!(values.len(), 3);
2252 }
2253
2254 #[test]
2255 fn evaluate_spec_resolves_against_kernel() {
2256 let kernel =
2257 crate::dsl::compile::compile_polydat_interpreter("const k_values := \"1, 10, 100\"\n")
2258 .unwrap();
2259 let v = evaluate_spec("{k_values}", &kernel).unwrap();
2260 assert_eq!(v, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2261 }
2262
2263 #[test]
2264 fn evaluate_spec_bare_ident_resolves_like_braced() {
2265 let kernel =
2269 crate::dsl::compile::compile_polydat_interpreter("const k_values := \"1, 10, 100\"\n")
2270 .unwrap();
2271 let bare = evaluate_spec("k_values", &kernel).unwrap();
2272 let braced = evaluate_spec("{k_values}", &kernel).unwrap();
2273 assert_eq!(bare, braced);
2274 assert_eq!(bare, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2275 }
2276
2277 #[test]
2278 fn evaluate_spec_unresolved_bare_is_error_with_quoting_hint() {
2279 let kernel = crate::dsl::compile::compile_polydat_interpreter("\n").unwrap();
2283 let err = evaluate_spec("nonexistent", &kernel)
2284 .unwrap_err()
2285 .to_string();
2286 assert!(err.contains("did not resolve"), "got: {err}");
2287 assert!(err.contains("quote it"), "should hint quoting: {err}");
2288 }
2289
2290 #[test]
2291 fn bracket_list_spread_and_no_peel() {
2292 let kernel =
2295 crate::dsl::compile::compile_polydat_interpreter("const xs := \"1, 2, 3\"\n").unwrap();
2296 let spread = evaluate_spec("[xs…]", &kernel).unwrap();
2298 assert_eq!(spread, vec![Value::U64(1), Value::U64(2), Value::U64(3)]);
2299 let whole = evaluate_spec("[xs]", &kernel).unwrap();
2301 assert_eq!(whole, vec![Value::Str("1, 2, 3".into())]);
2302 }
2303
2304 #[test]
2305 fn bracket_list_mixes_refs_literals_and_spread() {
2306 let kernel =
2307 crate::dsl::compile::compile_polydat_interpreter("const mid := \"7, 8\"\n").unwrap();
2308 let v = evaluate_spec("[1, mid…, \"x\"]", &kernel).unwrap();
2309 assert_eq!(
2310 v,
2311 vec![
2312 Value::U64(1),
2313 Value::U64(7),
2314 Value::U64(8),
2315 Value::Str("x".into()),
2316 ]
2317 );
2318 }
2319
2320 #[test]
2323 fn evaluate_spec_unpacks_partition_list_into_partition_values() {
2324 let kernel = empty_kernel();
2330 let v = evaluate_spec("partitions(\"linear:3\")", &kernel).unwrap();
2331 assert_eq!(v.len(), 3, "expected 3 partitions, got {}", v.len());
2332 for value in &v {
2333 assert!(
2334 value.as_partition().is_some(),
2335 "every iter value should be a Partition, got {value:?}"
2336 );
2337 }
2338 }
2339
2340 #[test]
2341 fn evaluate_spec_unpacks_partition_list_with_explicit_extent() {
2342 let kernel = empty_kernel();
2343 let v = evaluate_spec("partitions(\"fib:5\", 1000)", &kernel).unwrap();
2344 assert_eq!(v.len(), 5);
2345 for (i, value) in v.iter().enumerate() {
2347 let p = value.as_partition().unwrap();
2348 assert_eq!(p.idx, i as u64);
2349 assert_eq!(p.base_extent, 1000);
2350 }
2351 }
2352
2353 #[test]
2354 fn pre_evaluate_clause_returns_partition_values_for_partitions_call() {
2355 let kernel = empty_kernel();
2358 let v = pre_evaluate_clause(
2359 "partitions(\"linear:4\")",
2360 &kernel,
2361 &HashMap::new(),
2362 &HashMap::new(),
2363 )
2364 .unwrap();
2365 assert_eq!(v.len(), 4);
2366 for value in &v {
2367 assert!(
2368 value.as_partition().is_some(),
2369 "pre_evaluate_clause must unpack PartitionList, got {value:?}"
2370 );
2371 }
2372 }
2373
2374 #[test]
2375 fn value_to_polydat_type_name_returns_ext_for_partition_value() {
2376 let p = crate::iteration::cursor_partition::Partition {
2383 idx: 0,
2384 count: 1,
2385 start_ord: 0,
2386 end_ord: 10,
2387 start_pct: 0.0,
2388 end_pct: 100.0,
2389 base_extent: 10,
2390 };
2391 let v = Value::from_partition(p);
2392 assert_eq!(value_to_polydat_type_name(&v), "ext");
2393 }
2394
2395 fn empty_kernel() -> PolydatKernel {
2398 crate::dsl::compile::compile_polydat_interpreter("\n").unwrap()
2399 }
2400
2401 #[test]
2402 fn range_half_open_integer() {
2403 let v = evaluate_spec("1..5", &empty_kernel()).unwrap();
2404 assert_eq!(
2405 v,
2406 vec![Value::U64(1), Value::U64(2), Value::U64(3), Value::U64(4),]
2407 );
2408 }
2409
2410 #[test]
2411 fn range_inclusive_integer() {
2412 let v = evaluate_spec("1..=5", &empty_kernel()).unwrap();
2413 assert_eq!(
2414 v,
2415 vec![
2416 Value::U64(1),
2417 Value::U64(2),
2418 Value::U64(3),
2419 Value::U64(4),
2420 Value::U64(5),
2421 ]
2422 );
2423 }
2424
2425 #[test]
2426 fn range_with_step() {
2427 let v = evaluate_spec("0..100..10", &empty_kernel()).unwrap();
2428 assert_eq!(
2429 v,
2430 vec![
2431 Value::U64(0),
2432 Value::U64(10),
2433 Value::U64(20),
2434 Value::U64(30),
2435 Value::U64(40),
2436 Value::U64(50),
2437 Value::U64(60),
2438 Value::U64(70),
2439 Value::U64(80),
2440 Value::U64(90),
2441 ]
2442 );
2443 }
2444
2445 #[test]
2446 fn range_inclusive_with_step() {
2447 let v = evaluate_spec("0..=100..25", &empty_kernel()).unwrap();
2448 assert_eq!(
2449 v,
2450 vec![
2451 Value::U64(0),
2452 Value::U64(25),
2453 Value::U64(50),
2454 Value::U64(75),
2455 Value::U64(100),
2456 ]
2457 );
2458 }
2459
2460 #[test]
2461 fn range_float_step() {
2462 let v = evaluate_spec("0.0..=1.0..0.25", &empty_kernel()).unwrap();
2463 assert_eq!(v.len(), 5, "got {v:?}");
2464 if let [
2465 Value::F64(a),
2466 Value::F64(b),
2467 Value::F64(c),
2468 Value::F64(d),
2469 Value::F64(e),
2470 ] = v.as_slice()
2471 {
2472 assert!((a - 0.0).abs() < 1e-12);
2473 assert!((b - 0.25).abs() < 1e-12);
2474 assert!((c - 0.5).abs() < 1e-12);
2475 assert!((d - 0.75).abs() < 1e-12);
2476 assert!((e - 1.0).abs() < 1e-12);
2477 } else {
2478 panic!("expected 5 floats, got {v:?}");
2479 }
2480 }
2481
2482 #[test]
2483 fn range_empty_when_start_equals_end_half_open() {
2484 let v = evaluate_spec("5..5", &empty_kernel()).unwrap();
2485 assert!(v.is_empty(), "got {v:?}");
2486 }
2487
2488 #[test]
2489 fn range_inclusive_with_equal_bounds_emits_one() {
2490 let v = evaluate_spec("5..=5", &empty_kernel()).unwrap();
2491 assert_eq!(v, vec![Value::U64(5)]);
2492 }
2493
2494 #[test]
2495 fn range_with_si_suffix_bounds() {
2496 let v = evaluate_spec("1K..1K..200", &empty_kernel()).unwrap();
2499 assert!(v.is_empty(), "1K..1K with positive step → empty");
2500
2501 let v = evaluate_spec("0..1K..200", &empty_kernel()).unwrap();
2502 assert_eq!(
2503 v,
2504 vec![
2505 Value::U64(0),
2506 Value::U64(200),
2507 Value::U64(400),
2508 Value::U64(600),
2509 Value::U64(800),
2510 ]
2511 );
2512 }
2513
2514 #[test]
2515 fn range_zero_step_errors() {
2516 let err = evaluate_spec("1..10..0", &empty_kernel())
2517 .unwrap_err()
2518 .to_string();
2519 assert!(err.contains("step is zero"), "{err}");
2520 }
2521
2522 #[test]
2523 fn range_too_many_dotdot_errors() {
2524 let err = evaluate_spec("1..2..3..4", &empty_kernel())
2525 .unwrap_err()
2526 .to_string();
2527 assert!(err.contains("more than two `..`"), "{err}");
2528 }
2529
2530 #[test]
2531 fn range_inside_parens_doesnt_split() {
2532 let v = evaluate_spec("(1)..(5)", &empty_kernel()).unwrap();
2539 assert_eq!(v.len(), 4); }
2541
2542 #[test]
2543 fn range_step_with_inclusive_separator_errors() {
2544 let err = evaluate_spec("1..10..=2", &empty_kernel())
2545 .unwrap_err()
2546 .to_string();
2547 assert!(err.contains("step delimiter cannot be `..=`"), "{err}");
2548 }
2549
2550 #[test]
2551 fn range_with_kernel_referenced_bounds() {
2552 let kernel =
2553 crate::dsl::compile::compile_polydat_interpreter("const lo := 5\nconst hi := 12\n")
2554 .unwrap();
2555 let v = evaluate_spec("{lo}..{hi}", &kernel).unwrap();
2556 assert_eq!(
2557 v,
2558 vec![
2559 Value::U64(5),
2560 Value::U64(6),
2561 Value::U64(7),
2562 Value::U64(8),
2563 Value::U64(9),
2564 Value::U64(10),
2565 Value::U64(11),
2566 ]
2567 );
2568 }
2569
2570 #[test]
2573 fn fib_n_first_eight() {
2574 let v = evaluate_spec("fib(8)", &empty_kernel()).unwrap();
2575 assert_eq!(
2576 v,
2577 vec![
2578 Value::U64(1),
2579 Value::U64(1),
2580 Value::U64(2),
2581 Value::U64(3),
2582 Value::U64(5),
2583 Value::U64(8),
2584 Value::U64(13),
2585 Value::U64(21),
2586 ]
2587 );
2588 }
2589
2590 #[test]
2591 fn fib_until_50() {
2592 let v = evaluate_spec("fib_until(50)", &empty_kernel()).unwrap();
2593 assert_eq!(
2594 v,
2595 vec![
2596 Value::U64(1),
2597 Value::U64(1),
2598 Value::U64(2),
2599 Value::U64(3),
2600 Value::U64(5),
2601 Value::U64(8),
2602 Value::U64(13),
2603 Value::U64(21),
2604 Value::U64(34),
2605 ]
2606 );
2607 }
2608
2609 #[test]
2610 fn pow2_n_six() {
2611 let v = evaluate_spec("pow2(6)", &empty_kernel()).unwrap();
2612 assert_eq!(
2613 v,
2614 vec![
2615 Value::U64(1),
2616 Value::U64(2),
2617 Value::U64(4),
2618 Value::U64(8),
2619 Value::U64(16),
2620 Value::U64(32),
2621 ]
2622 );
2623 }
2624
2625 #[test]
2626 fn pow2_until_100() {
2627 let v = evaluate_spec("pow2_until(100)", &empty_kernel()).unwrap();
2628 assert_eq!(
2629 v,
2630 vec![
2631 Value::U64(1),
2632 Value::U64(2),
2633 Value::U64(4),
2634 Value::U64(8),
2635 Value::U64(16),
2636 Value::U64(32),
2637 Value::U64(64),
2638 ]
2639 );
2640 }
2641
2642 #[test]
2643 fn binomial_n_5() {
2644 let v = evaluate_spec("binomial(5)", &empty_kernel()).unwrap();
2646 assert_eq!(
2647 v,
2648 vec![
2649 Value::U64(1),
2650 Value::U64(5),
2651 Value::U64(10),
2652 Value::U64(10),
2653 Value::U64(5),
2654 Value::U64(1),
2655 ]
2656 );
2657 }
2658
2659 #[test]
2660 fn geometric_2_doubles_4_terms() {
2661 let v = evaluate_spec("geometric(1, 2, 4)", &empty_kernel()).unwrap();
2662 if let [Value::F64(a), Value::F64(b), Value::F64(c), Value::F64(d)] = v.as_slice() {
2664 assert!((a - 1.0).abs() < 1e-12);
2665 assert!((b - 2.0).abs() < 1e-12);
2666 assert!((c - 4.0).abs() < 1e-12);
2667 assert!((d - 8.0).abs() < 1e-12);
2668 } else {
2669 panic!("expected 4 f64 values, got {v:?}");
2670 }
2671 }
2672
2673 #[test]
2674 fn linear_starts_half_open_5_points() {
2675 let v = evaluate_spec("linear_starts(0, 100, 5)", &empty_kernel()).unwrap();
2676 if let [
2678 Value::F64(a),
2679 Value::F64(b),
2680 Value::F64(c),
2681 Value::F64(d),
2682 Value::F64(e),
2683 ] = v.as_slice()
2684 {
2685 assert!((a - 0.0).abs() < 1e-12);
2686 assert!((b - 20.0).abs() < 1e-12);
2687 assert!((c - 40.0).abs() < 1e-12);
2688 assert!((d - 60.0).abs() < 1e-12);
2689 assert!((e - 80.0).abs() < 1e-12);
2690 } else {
2691 panic!("got {v:?}");
2692 }
2693 }
2694
2695 #[test]
2696 fn linear_steps_inclusive_5_points() {
2697 let v = evaluate_spec("linear_steps(0, 100, 5)", &empty_kernel()).unwrap();
2698 if let [
2700 Value::F64(a),
2701 Value::F64(b),
2702 Value::F64(c),
2703 Value::F64(d),
2704 Value::F64(e),
2705 ] = v.as_slice()
2706 {
2707 assert!((a - 0.0).abs() < 1e-12);
2708 assert!((b - 25.0).abs() < 1e-12);
2709 assert!((c - 50.0).abs() < 1e-12);
2710 assert!((d - 75.0).abs() < 1e-12);
2711 assert!((e - 100.0).abs() < 1e-12);
2712 } else {
2713 panic!("got {v:?}");
2714 }
2715 }
2716
2717 #[test]
2718 fn log_steps_3_decades() {
2719 let v = evaluate_spec("log_steps(1, 1000, 4)", &empty_kernel()).unwrap();
2720 if let [Value::F64(a), Value::F64(b), Value::F64(c), Value::F64(d)] = v.as_slice() {
2722 assert!((a - 1.0).abs() < 1e-9);
2723 assert!((b - 10.0).abs() < 1e-9);
2724 assert!((c - 100.0).abs() < 1e-9);
2725 assert!((d - 1000.0).abs() < 1e-9);
2726 } else {
2727 panic!("got {v:?}");
2728 }
2729 }
2730
2731 #[test]
2732 fn log_steps_rejects_non_positive_bounds() {
2733 let err = evaluate_spec("log_steps(0, 100, 5)", &empty_kernel())
2734 .unwrap_err()
2735 .to_string();
2736 assert!(err.contains("must be positive"), "{err}");
2737 }
2738
2739 #[test]
2742 fn concat_two_ranges() {
2743 let v = evaluate_spec("concat(1..4, 10..13)", &empty_kernel()).unwrap();
2744 assert_eq!(
2745 v,
2746 vec![
2747 Value::U64(1),
2748 Value::U64(2),
2749 Value::U64(3),
2750 Value::U64(10),
2751 Value::U64(11),
2752 Value::U64(12),
2753 ]
2754 );
2755 }
2756
2757 #[test]
2758 fn unique_dedupes_first_occurrence() {
2759 let v = evaluate_spec("unique(1..4, 3..6)", &empty_kernel()).unwrap();
2760 assert_eq!(
2762 v,
2763 vec![
2764 Value::U64(1),
2765 Value::U64(2),
2766 Value::U64(3),
2767 Value::U64(4),
2768 Value::U64(5),
2769 ]
2770 );
2771 }
2772
2773 #[test]
2774 fn intersect_keeps_only_common_values() {
2775 let v = evaluate_spec("intersect(1..10, 5..15)", &empty_kernel()).unwrap();
2776 assert_eq!(
2777 v,
2778 vec![
2779 Value::U64(5),
2780 Value::U64(6),
2781 Value::U64(7),
2782 Value::U64(8),
2783 Value::U64(9),
2784 ]
2785 );
2786 }
2787
2788 #[test]
2789 fn subtract_drops_values_in_b() {
2790 let v = evaluate_spec("subtract(1..6, 3..5)", &empty_kernel()).unwrap();
2791 assert_eq!(v, vec![Value::U64(1), Value::U64(2), Value::U64(5)]);
2793 }
2794
2795 #[test]
2796 fn interleave_round_robin_two_lists() {
2797 let v = evaluate_spec("interleave(1..4, 10..13)", &empty_kernel()).unwrap();
2798 assert_eq!(
2799 v,
2800 vec![
2801 Value::U64(1),
2802 Value::U64(10),
2803 Value::U64(2),
2804 Value::U64(11),
2805 Value::U64(3),
2806 Value::U64(12),
2807 ]
2808 );
2809 }
2810
2811 #[test]
2812 fn cycle_repeats_n_times() {
2813 let v = evaluate_spec("cycle(1..3, 3)", &empty_kernel()).unwrap();
2814 assert_eq!(
2815 v,
2816 vec![
2817 Value::U64(1),
2818 Value::U64(2),
2819 Value::U64(1),
2820 Value::U64(2),
2821 Value::U64(1),
2822 Value::U64(2),
2823 ]
2824 );
2825 }
2826
2827 #[test]
2828 fn reverse_inverts_list() {
2829 let v = evaluate_spec("reverse(1..5)", &empty_kernel()).unwrap();
2830 assert_eq!(
2831 v,
2832 vec![Value::U64(4), Value::U64(3), Value::U64(2), Value::U64(1),]
2833 );
2834 }
2835
2836 #[test]
2837 fn take_n_takes_prefix() {
2838 let v = evaluate_spec("take(1..10, 3)", &empty_kernel()).unwrap();
2839 assert_eq!(v, vec![Value::U64(1), Value::U64(2), Value::U64(3)]);
2840 }
2841
2842 #[test]
2843 fn skip_n_drops_prefix() {
2844 let v = evaluate_spec("skip(1..6, 2)", &empty_kernel()).unwrap();
2845 assert_eq!(v, vec![Value::U64(3), Value::U64(4), Value::U64(5)]);
2846 }
2847
2848 #[test]
2849 fn unique_composes_with_pow2_and_range() {
2850 let v = evaluate_spec("unique(pow2(8), 1..1000..100)", &empty_kernel()).unwrap();
2851 assert_eq!(v.len(), 17);
2855 assert_eq!(v[0], Value::U64(1));
2856 assert_eq!(v[7], Value::U64(128));
2857 assert_eq!(v[8], Value::U64(101));
2858 }
2859
2860 #[test]
2863 fn bucket_round_robin_3_1_2() {
2864 let v = evaluate_spec(
2866 "bucket(concat('ann', 'scan', 'fetch'), concat(3, 1, 2))",
2867 &empty_kernel(),
2868 )
2869 .unwrap();
2870 let _ = v;
2873 }
2874
2875 #[test]
2876 fn bucket_ratio_prefix_shorthand_round_robin() {
2877 let v = evaluate_spec("bucket(\"3:ann, 1:scan, 2:fetch\")", &empty_kernel()).unwrap();
2878 assert_eq!(v.len(), 6);
2881 let strs: Vec<&str> = v
2882 .iter()
2883 .filter_map(|v| match v {
2884 Value::Str(s) => Some(&**s),
2885 _ => None,
2886 })
2887 .collect();
2888 let counts = strs.iter().fold(
2892 std::collections::HashMap::<&str, usize>::new(),
2893 |mut m, s| {
2894 *m.entry(s).or_insert(0) += 1;
2895 m
2896 },
2897 );
2898 assert_eq!(counts.get("ann"), Some(&3));
2899 assert_eq!(counts.get("scan"), Some(&1));
2900 assert_eq!(counts.get("fetch"), Some(&2));
2901 }
2902
2903 #[test]
2904 fn concat_seq_emits_contiguous_runs() {
2905 let v = evaluate_spec(
2906 "concat_seq(\"2:warmup, 3:bench, 1:cooldown\")",
2907 &empty_kernel(),
2908 )
2909 .unwrap();
2910 let strs: Vec<String> = v
2911 .iter()
2912 .filter_map(|v| match v {
2913 Value::Str(s) => Some(s.to_string()),
2914 _ => None,
2915 })
2916 .collect();
2917 assert_eq!(
2918 strs,
2919 vec!["warmup", "warmup", "bench", "bench", "bench", "cooldown",]
2920 );
2921 }
2922
2923 #[test]
2924 fn interval_seq_evenly_spreads_higher_ratio() {
2925 let v = evaluate_spec("interval_seq(\"3:read, 1:write\")", &empty_kernel()).unwrap();
2926 let strs: Vec<String> = v
2929 .iter()
2930 .filter_map(|v| match v {
2931 Value::Str(s) => Some(s.to_string()),
2932 _ => None,
2933 })
2934 .collect();
2935 assert_eq!(strs.len(), 4);
2936 let writes: Vec<usize> = strs
2937 .iter()
2938 .enumerate()
2939 .filter(|(_, s)| *s == "write")
2940 .map(|(i, _)| i)
2941 .collect();
2942 assert_eq!(writes.len(), 1, "expected exactly one write: {strs:?}");
2943 }
2944
2945 #[test]
2946 fn parse_func_call_recognises_simple_call() {
2947 let (n, a) = parse_func_call("fib(8)").unwrap();
2948 assert_eq!(n, "fib");
2949 assert_eq!(a, "8");
2950 }
2951
2952 #[test]
2953 fn parse_func_call_rejects_non_calls() {
2954 assert!(parse_func_call("1..10").is_none());
2955 assert!(parse_func_call("foo + bar").is_none());
2956 assert!(parse_func_call("f(a) + g(b)").is_none()); }
2958
2959 #[test]
2960 fn split_args_top_level_skips_inner_commas() {
2961 let args = split_args_top_level("a, f(b, c), \"x, y\", 3");
2962 assert_eq!(args, vec!["a", "f(b, c)", "\"x, y\"", "3"]);
2963 }
2964}