1use std::collections::HashMap;
41
42use crate::ast::Value;
43use crate::kernel::interp::Lookup;
44use crate::kernel::interp::interpolate_with_lookup;
45
46pub fn evaluate_spec(
67 spec_text: &str,
68 kernel: &dyn Lookup,
69) -> Result<Vec<Value>, crate::dsl::compile::EmbeddingError> {
70 evaluate_spec_internal(spec_text, kernel).map_err(|msg| {
71 if let Some(rest) = msg.strip_prefix("interpolation: unresolved placeholder '{")
72 && let Some(end) = rest.find('}')
73 {
74 let name = rest[..end].to_string();
75 return crate::dsl::compile::EmbeddingError::UnresolvedPlaceholder {
76 name,
77 source: spec_text.to_string(),
78 };
79 }
80 crate::dsl::compile::EmbeddingError::Parse {
81 source: spec_text.to_string(),
82 message: msg,
83 position: None,
84 }
85 })
86}
87
88fn evaluate_spec_internal(spec_text: &str, kernel: &dyn Lookup) -> Result<Vec<Value>, String> {
89 if let Some(values) = try_eval_all_cursor(spec_text, kernel)? {
90 return Ok(values);
91 }
92 if is_single_bare_ident(spec_text) {
101 return match kernel.lookup(spec_text.trim()) {
102 Some(v) => Ok(
103 match crate::iteration::comprehension::source_values::iteration_interior(&v) {
104 Some(interior) => interior,
105 None => vec![v],
106 },
107 ),
108 None => Err(format!(
109 "comprehension source `{src}` did not resolve to a value — no \
110 wire, const, param, or outer iter-var by that name is in scope \
111 here. If you meant the literal string \"{src}\", quote it: \
112 `\"{src}\"`.",
113 src = spec_text.trim(),
114 )),
115 };
116 }
117 let interpolated = crate::kernel::interp::interpolate_with_lookup(spec_text, |name| {
118 kernel.lookup(name).map(|v| v.to_display_string())
119 })?;
120 if let Some(values) = try_eval_bracket_list(&interpolated, kernel)? {
125 return Ok(values);
126 }
127 if let Some(values) = try_eval_range(&interpolated, kernel.ledger())? {
132 return Ok(values);
133 }
134 if let Some(values) = try_eval_generator(&interpolated)? {
136 return Ok(values);
137 }
138 if let Some(values) = try_eval_setop(&interpolated, kernel)? {
140 return Ok(values);
141 }
142 if let Some(values) = try_eval_sequencer(&interpolated, kernel)? {
145 return Ok(values);
146 }
147 if let Some(values) = try_eval_partition_call(&interpolated, kernel)? {
151 return Ok(values);
152 }
153 if let Some(values) = try_eval_param_partitions(&interpolated, kernel)? {
157 return Ok(values);
158 }
159 match crate::dsl::compile::eval_const_expr_for(&interpolated, kernel.ledger()) {
160 Ok(v) => Ok(
167 match crate::iteration::comprehension::source_values::iteration_interior(&v) {
168 Some(interior) => interior,
169 None => vec![v],
170 },
171 ),
172 Err(eval_err) => {
184 if looks_like_literal_list(&interpolated) {
189 Ok(
192 crate::iteration::comprehension::source_values::strip_string_tokens(
193 &interpolated,
194 ),
195 )
196 } else {
197 Err(format!(
198 "for_each clause expression failed to evaluate: {eval_err}\n\
199 spec: {interpolated}\n\
200 If this was meant as a literal list (e.g. `1, 10, 100`), \
201 it should contain only literal values separated by commas. \
202 If it was meant as an expression, fix the underlying \
203 evaluation error."
204 ))
205 }
206 }
207 }
208}
209
210fn try_eval_bracket_list(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
224 let t = text.trim();
225 if !(t.starts_with('[') && t.ends_with(']') && t.len() >= 2) {
226 return Ok(None);
227 }
228 let inner = &t[1..t.len() - 1];
229 if inner.trim().is_empty() {
230 return Ok(Some(Vec::new()));
231 }
232 let mut out = Vec::new();
233 for elem in split_args_top_level(inner) {
234 let elem = elem.trim();
235 let (expr, spread) = if let Some(stripped) = elem.strip_suffix('…') {
237 (stripped.trim(), true)
238 } else if let Some(stripped) = elem.strip_suffix("...") {
239 (stripped.trim(), true)
240 } else {
241 (elem, false)
242 };
243 if expr.is_empty() {
244 return Err("empty element in list comprehension `[...]`".to_string());
245 }
246 let value = eval_element_value(expr, kernel)?;
247 if spread {
248 match crate::iteration::comprehension::source_values::iteration_interior(&value) {
249 Some(interior) => out.extend(interior),
250 None => {
251 return Err(format!(
252 "list comprehension spread `{expr}…` requires an iterable \
253 source, but `{expr}` resolved to a scalar \
254 {ty:?}. Use `[{expr}]` to pass it as a single element, \
255 or supply a list.",
256 ty = value.port_type(),
257 ));
258 }
259 }
260 } else {
261 out.push(value);
262 }
263 }
264 Ok(Some(out))
265}
266
267fn eval_element_value(expr: &str, kernel: &dyn Lookup) -> Result<Value, String> {
274 let e = expr.trim();
275 if is_single_bare_ident(e) {
276 return kernel.lookup(e).ok_or_else(|| {
277 format!(
278 "list element `{e}` did not resolve to a value — no wire, const, \
279 param, or outer iter-var by that name is in scope here. \
280 If you meant the literal string \"{e}\", quote it: `\"{e}\"`."
281 )
282 });
283 }
284 crate::dsl::compile::eval_const_expr_for(e, kernel.ledger())
285 .map_err(|err| format!("list element `{e}` failed to evaluate: {err}"))
286}
287
288fn is_single_bare_ident(text: &str) -> bool {
294 let t = text.trim();
295 if t == "true" || t == "false" {
296 return false;
297 }
298 let mut chars = t.chars();
299 match chars.next() {
300 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
301 _ => return false,
302 }
303 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
304}
305
306fn looks_like_literal_list(text: &str) -> bool {
322 let trimmed = text.trim();
323 if trimmed.is_empty() {
324 return false;
325 }
326 !trimmed.chars().any(|c| {
327 matches!(
328 c,
329 '(' | ')'
330 | '['
331 | ']'
332 | '{'
333 | '}'
334 | '\''
335 | '"'
336 | '+'
337 | '*'
338 | '/'
339 | '%'
340 | '='
341 | '<'
342 | '>'
343 | '!'
344 | '&'
345 | '|'
346 | '~'
347 | '^'
348 | '?'
349 )
350 })
351}
352
353pub fn pre_evaluate_clause(
364 spec_text: &str,
365 parent_kernel: &dyn Lookup,
366 workload_params: &HashMap<String, String>,
367 probes: &HashMap<String, String>,
368) -> Result<Vec<Value>, String> {
369 if let Some(values) = try_eval_all_cursor(spec_text, parent_kernel)? {
374 return Ok(values);
375 }
376 if is_single_bare_ident(spec_text) {
384 let name = spec_text.trim();
385 if let Some(pv) = probes.get(name) {
386 return Ok(crate::iteration::comprehension::source_values::strip_string_tokens(pv));
387 }
388 if let Some(v) = parent_kernel.lookup(name) {
389 return Ok(
390 match crate::iteration::comprehension::source_values::iteration_interior(&v) {
391 Some(interior) => interior,
392 None => vec![v],
393 },
394 );
395 }
396 if let Some(s) = workload_params.get(name) {
397 return Ok(crate::iteration::comprehension::source_values::strip_string_tokens(s));
398 }
399 return Err(format!(
400 "comprehension source `{name}` did not resolve to a value — no wire, \
401 const, param, or outer iter-var by that name is in scope here. \
402 If you meant the literal string \"{name}\", quote it: `\"{name}\"`."
403 ));
404 }
405 let mut text = spec_text.to_string();
406 for (var, probe_value) in probes {
407 text = text.replace(&format!("{{{var}}}"), probe_value);
408 }
409
410 let interpolated = interpolate_with_lookup(&text, |name| {
411 parent_kernel
412 .lookup(name)
413 .map(|v| v.to_display_string())
414 .or_else(|| workload_params.get(name).cloned())
415 })?;
416
417 if let Some(values) = try_eval_range(&interpolated, parent_kernel.ledger())? {
419 return Ok(values);
420 }
421 if let Some(values) = try_eval_generator(&interpolated)? {
424 return Ok(values);
425 }
426 if let Some(values) = try_eval_setop(&interpolated, parent_kernel)? {
427 return Ok(values);
428 }
429 if let Some(values) = try_eval_sequencer(&interpolated, parent_kernel)? {
430 return Ok(values);
431 }
432 if let Some(values) = try_eval_partition_call(&interpolated, parent_kernel)? {
438 return Ok(values);
439 }
440 if let Some(values) = try_eval_param_partitions(&interpolated, parent_kernel)? {
443 return Ok(values);
444 }
445 let value_str =
446 match crate::dsl::compile::eval_const_expr_for(&interpolated, parent_kernel.ledger()) {
447 Ok(Value::Str(s)) => s.to_string(),
448 Ok(ref v) if v.as_partition_list().is_some() => {
454 let list = v.as_partition_list().unwrap();
455 return Ok(list
456 .as_slice()
457 .iter()
458 .map(|p| Value::from_partition(*p))
459 .collect());
460 }
461 Ok(other) => return Ok(vec![other]),
462 Err(eval_err) => {
467 if looks_like_literal_list(&interpolated) {
468 interpolated
469 } else {
470 return Err(format!(
471 "for_each clause expression failed to evaluate: {eval_err}\n\
472 spec: {interpolated}\n\
473 If this was meant as a literal list (e.g. `1, 10, 100`), \
474 it should contain only literal values separated by commas. \
475 If it was meant as an expression, fix the underlying \
476 evaluation error."
477 ));
478 }
479 }
480 };
481 Ok(parse_list_with_types(&value_str))
482}
483
484pub fn parse_list_with_types(text: &str) -> Vec<Value> {
489 text.split(',')
490 .map(str::trim)
491 .filter(|s| !s.is_empty())
492 .map(|s| {
493 if let Ok(n) = s.parse::<u64>() {
494 Value::U64(n)
495 } else if let Ok(n) = s.parse::<f64>() {
496 Value::F64(n)
497 } else if s == "true" {
498 Value::Bool(true)
499 } else if s == "false" {
500 Value::Bool(false)
501 } else {
502 Value::Str(s.to_string().into())
503 }
504 })
505 .collect()
506}
507
508fn try_eval_all_cursor(spec_text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
528 let trimmed = spec_text.trim();
529 let Some(stripped) = trimmed.strip_prefix("all(") else {
530 return Ok(None);
531 };
532 let Some(arg) = stripped.strip_suffix(')') else {
533 return Ok(None);
534 };
535 let cursor_name = arg.trim();
536 if cursor_name.is_empty() || !is_valid_ident(cursor_name) {
537 return Ok(None);
538 }
539
540 let start_key = format!("__cursor_extent_{cursor_name}_start");
541 let end_key = format!("__cursor_extent_{cursor_name}_end");
542 let start = kernel
543 .lookup(&start_key)
544 .and_then(|v| match v {
545 Value::U64(n) => Some(n),
546 _ => None,
547 })
548 .ok_or_else(|| {
549 format!(
550 "all({cursor_name}): cursor '{cursor_name}' has no resolvable extent — \
551 check that the cursor is declared at or above this scope and that \
552 its range arguments are init-resolvable. Looked for output '{start_key}'."
553 )
554 })?;
555 let end = kernel
556 .lookup(&end_key)
557 .and_then(|v| match v {
558 Value::U64(n) => Some(n),
559 _ => None,
560 })
561 .ok_or_else(|| {
562 format!(
563 "all({cursor_name}): missing auxiliary output '{end_key}' on the parent kernel."
564 )
565 })?;
566
567 if end < start {
568 return Err(format!(
569 "all({cursor_name}): cursor extent end={end} is less than start={start} — \
570 cannot enumerate a negative-extent range."
571 ));
572 }
573 Ok(Some((start..end).map(Value::U64).collect()))
574}
575
576fn is_valid_ident(s: &str) -> bool {
577 let mut chars = s.chars();
578 match chars.next() {
579 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
580 _ => return false,
581 }
582 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
583}
584
585fn try_eval_range(
608 text: &str,
609 ledger: &std::sync::Arc<crate::kernel::CompileLedger>,
610) -> Result<Option<Vec<Value>>, String> {
611 let trimmed = text.trim();
612 let chars: Vec<char> = trimmed.chars().collect();
613
614 let mut splits: Vec<(usize, bool)> = Vec::new();
618 let mut depth: i32 = 0;
619 let mut i = 0;
620 while i < chars.len() {
621 let c = chars[i];
622 match c {
623 '(' | '[' | '{' => depth += 1,
624 ')' | ']' | '}' => depth -= 1,
625 '"' | '\'' => {
626 let q = c;
628 i += 1;
629 while i < chars.len() && chars[i] != q {
630 i += 1;
631 }
632 }
633 '.' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '.' => {
634 let inclusive = i + 2 < chars.len() && chars[i + 2] == '=';
635 splits.push((i, inclusive));
636 i += if inclusive { 3 } else { 2 };
637 continue;
638 }
639 _ => {}
640 }
641 i += 1;
642 }
643
644 if splits.is_empty() {
645 return Ok(None);
646 }
647 if splits.len() > 2 {
648 return Err(format!(
649 "range expression '{trimmed}': more than two `..` operators \
650 at top level — expected one of `a..b`, `a..=b`, `a..b..s`, \
651 or `a..=b..s`"
652 ));
653 }
654 if splits.len() == 2 && splits[1].1 {
655 return Err(format!(
656 "range expression '{trimmed}': step delimiter cannot be \
657 `..=` — only the bound separator may be inclusive"
658 ));
659 }
660
661 let inclusive = splits[0].1;
663 let first_end = splits[0].0;
664 let after_first = first_end + if inclusive { 3 } else { 2 };
665 let (start_text, mid_text, step_text) = match splits.len() {
666 1 => {
667 let start_s: String = chars[..first_end].iter().collect();
668 let end_s: String = chars[after_first..].iter().collect();
669 (start_s, end_s, None)
670 }
671 2 => {
672 let mid_end = splits[1].0;
673 let after_mid = mid_end + 2; let start_s: String = chars[..first_end].iter().collect();
675 let mid_s: String = chars[after_first..mid_end].iter().collect();
676 let step_s: String = chars[after_mid..].iter().collect();
677 (start_s, mid_s, Some(step_s))
678 }
679 _ => unreachable!(),
680 };
681
682 let start_val = eval_range_segment(&start_text, "range start", ledger)?;
683 let end_val = eval_range_segment(&mid_text, "range end", ledger)?;
684 let step_val = match step_text {
685 Some(s) => Some(eval_range_segment(&s, "range step", ledger)?),
686 None => None,
687 };
688
689 Ok(Some(expand_range(
690 start_val, end_val, step_val, inclusive, trimmed,
691 )?))
692}
693
694fn eval_range_segment(
695 text: &str,
696 what: &str,
697 ledger: &std::sync::Arc<crate::kernel::CompileLedger>,
698) -> Result<Value, String> {
699 let trimmed = text.trim();
700 if trimmed.is_empty() {
701 return Err(format!("range expression: {what} is empty"));
702 }
703 crate::dsl::compile::eval_const_expr_for(trimmed, ledger)
704 .map_err(|e| format!("range expression: {what} '{trimmed}' did not const-fold — {e}"))
705}
706
707fn expand_range(
711 start: Value,
712 end: Value,
713 step: Option<Value>,
714 inclusive: bool,
715 src: &str,
716) -> Result<Vec<Value>, String> {
717 let any_float = matches!(start, Value::F64(_))
718 || matches!(end, Value::F64(_))
719 || matches!(step, Some(Value::F64(_)));
720
721 let to_f64 = |v: &Value| -> Result<f64, String> {
722 match v {
723 Value::U64(n) => Ok(*n as f64),
724 Value::F64(f) => Ok(*f),
725 other => Err(format!(
726 "range expression '{src}': bound has non-numeric value {other:?}"
727 )),
728 }
729 };
730 let to_i64 = |v: &Value| -> Result<i64, String> {
731 match v {
732 Value::U64(n) => i64::try_from(*n).map_err(|_| {
733 format!("range expression '{src}': bound {n} exceeds signed 64-bit range")
734 }),
735 Value::F64(f) => {
736 if f.fract() == 0.0 && *f >= i64::MIN as f64 && *f <= i64::MAX as f64 {
737 Ok(*f as i64)
738 } else {
739 Err(format!(
740 "range expression '{src}': float bound {f} is not integral; \
741 mix with an explicit float step (e.g. `1.0..10..0.5`) for a float range"
742 ))
743 }
744 }
745 other => Err(format!(
746 "range expression '{src}': bound has non-numeric value {other:?}"
747 )),
748 }
749 };
750
751 if any_float {
752 let s = to_f64(&start)?;
753 let e = to_f64(&end)?;
754 let st = match step.as_ref() {
755 Some(v) => to_f64(v)?,
756 None => 1.0,
757 };
758 if st == 0.0 {
759 return Err(format!("range expression '{src}': step is zero"));
760 }
761 if (e - s).is_sign_positive() && st < 0.0 {
763 return Ok(Vec::new());
764 }
765 if (e - s).is_sign_negative() && st > 0.0 {
766 return Ok(Vec::new());
767 }
768 let mut out = Vec::new();
769 let mut cur = s;
770 let cmp = |x: f64| -> bool {
771 if st > 0.0 {
772 if inclusive {
773 x <= e + 1e-12
774 } else {
775 x < e - 1e-12
776 }
777 } else if inclusive {
778 x >= e - 1e-12
779 } else {
780 x > e + 1e-12
781 }
782 };
783 while cmp(cur) {
784 out.push(Value::F64(cur));
785 cur += st;
786 }
787 return Ok(out);
788 }
789
790 let s = to_i64(&start)?;
792 let e = to_i64(&end)?;
793 let st = match step.as_ref() {
794 Some(v) => to_i64(v)?,
795 None => 1,
796 };
797 if st == 0 {
798 return Err(format!("range expression '{src}': step is zero"));
799 }
800 if st > 0 && s > e {
801 return Ok(Vec::new());
802 }
803 if st < 0 && s < e {
804 return Ok(Vec::new());
805 }
806 let mut out = Vec::new();
807 let mut cur = s;
808 let cmp = |x: i64| -> bool {
809 if st > 0 {
810 if inclusive { x <= e } else { x < e }
811 } else if inclusive {
812 x >= e
813 } else {
814 x > e
815 }
816 };
817 while cmp(cur) {
818 if cur < 0 {
819 return Err(format!(
820 "range expression '{src}': negative value {cur} can't be \
821 represented as Value::U64; use a float range \
822 (mix any bound or step with `.0`) for signed walks"
823 ));
824 }
825 out.push(Value::U64(cur as u64));
826 cur = cur.saturating_add(st);
827 if (st > 0 && cur < s) || (st < 0 && cur > s) {
828 break;
830 }
831 }
832 Ok(out)
833}
834
835fn parse_func_call(text: &str) -> Option<(&str, &str)> {
845 let trimmed = text.trim();
846 if !trimmed.ends_with(')') {
847 return None;
848 }
849 let open = trimmed.find('(')?;
850 let name = trimmed[..open].trim();
851 if name.is_empty() || !is_valid_ident(name) {
852 return None;
853 }
854 let chars: Vec<char> = trimmed.chars().collect();
857 let mut depth = 0i32;
858 let mut in_quote: Option<char> = None;
859 for (i, &c) in chars.iter().enumerate().skip(open) {
860 match (c, in_quote) {
861 ('"' | '\'', None) => in_quote = Some(c),
862 (q, Some(open_q)) if q == open_q => in_quote = None,
863 ('(', None) => depth += 1,
864 (')', None) => {
865 depth -= 1;
866 if depth == 0 {
867 if i != chars.len() - 1 {
868 return None; }
870 let args: String = chars[open + 1..i].iter().collect();
871 let _ = args;
877 let name_slice = &trimmed[..open];
878 let args_slice = &trimmed[open + 1..trimmed.len() - 1];
879 return Some((name_slice.trim(), args_slice));
880 }
881 }
882 _ => {}
883 }
884 }
885 None
886}
887
888fn split_args_top_level(args: &str) -> Vec<&str> {
891 let mut out: Vec<&str> = Vec::new();
892 let chars: Vec<char> = args.chars().collect();
893 let bytes_per_char: Vec<usize> = chars.iter().map(|c| c.len_utf8()).collect();
894 let mut start_byte = 0usize;
895 let mut byte = 0usize;
896 let mut depth = 0i32;
897 let mut in_quote: Option<char> = None;
898 for (i, &c) in chars.iter().enumerate() {
899 match (c, in_quote) {
900 ('"' | '\'', None) => in_quote = Some(c),
901 (q, Some(open_q)) if q == open_q => in_quote = None,
902 ('(' | '[' | '{', None) => depth += 1,
903 (')' | ']' | '}', None) => depth -= 1,
904 (',', None) if depth == 0 => {
905 let seg = &args[start_byte..byte];
906 out.push(seg.trim());
907 start_byte = byte + bytes_per_char[i];
908 }
909 _ => {}
910 }
911 byte += bytes_per_char[i];
912 }
913 let last = &args[start_byte..];
914 if !last.trim().is_empty() || !out.is_empty() {
915 out.push(last.trim());
916 }
917 out
918}
919
920fn parse_u64_arg(text: &str, what: &str) -> Result<u64, String> {
923 let trimmed = text.trim();
924 trimmed
925 .parse::<u64>()
926 .map_err(|_| format!("{what}: expected non-negative integer, got '{trimmed}'"))
927}
928
929fn parse_num_arg(text: &str, what: &str) -> Result<f64, String> {
933 let trimmed = text.trim();
934 trimmed
935 .parse::<f64>()
936 .map_err(|_| format!("{what}: expected numeric, got '{trimmed}'"))
937}
938
939#[derive(Clone, Copy, Debug, PartialEq, Eq)]
947pub enum NamedGenerator {
948 Fib,
950 FibUntil,
952 Pow2,
954 Pow2Until,
956 Binomial,
958 Geometric,
960 GeometricUntil,
962 LinearStarts,
964 LinearSteps,
966 LogSteps,
968}
969
970impl NamedGenerator {
971 pub fn all() -> impl Iterator<Item = NamedGenerator> {
973 std::iter::successors(Some(Self::Fib), |g| g.after())
974 }
975
976 fn after(self) -> Option<Self> {
980 use NamedGenerator as G;
981 match self {
982 G::Fib => Some(G::FibUntil),
983 G::FibUntil => Some(G::Pow2),
984 G::Pow2 => Some(G::Pow2Until),
985 G::Pow2Until => Some(G::Binomial),
986 G::Binomial => Some(G::Geometric),
987 G::Geometric => Some(G::GeometricUntil),
988 G::GeometricUntil => Some(G::LinearStarts),
989 G::LinearStarts => Some(G::LinearSteps),
990 G::LinearSteps => Some(G::LogSteps),
991 G::LogSteps => None,
992 }
993 }
994
995 pub fn signature(self) -> &'static str {
997 use NamedGenerator as G;
998 match self {
999 G::Fib => "fib(n)",
1000 G::FibUntil => "fib_until(max)",
1001 G::Pow2 => "pow2(n)",
1002 G::Pow2Until => "pow2_until(max)",
1003 G::Binomial => "binomial(n)",
1004 G::Geometric => "geometric(start, factor, n)",
1005 G::GeometricUntil => "geometric_until(start, factor, max)",
1006 G::LinearStarts => "linear_starts(start, end, n)",
1007 G::LinearSteps => "linear_steps(start, end, n)",
1008 G::LogSteps => "log_steps(start, end, n)",
1009 }
1010 }
1011
1012 pub fn name(self) -> &'static str {
1014 let sig = self.signature();
1015 &sig[..sig.find('(').unwrap_or(sig.len())]
1016 }
1017
1018 fn params(self) -> Vec<&'static str> {
1020 let sig = self.signature();
1021 let inner = &sig[self.name().len() + 1..sig.len() - 1];
1022 inner.split(',').map(str::trim).collect()
1023 }
1024
1025 pub fn from_name(name: &str) -> Option<Self> {
1027 Self::all().find(|g| g.name() == name)
1028 }
1029
1030 pub fn of_call(text: &str) -> Option<Self> {
1033 parse_func_call(text).and_then(|(name, _)| Self::from_name(name))
1034 }
1035
1036 pub fn yields_integers(self) -> bool {
1039 use NamedGenerator as G;
1040 match self {
1041 G::Fib | G::FibUntil | G::Pow2 | G::Pow2Until | G::Binomial => true,
1042 G::Geometric | G::GeometricUntil | G::LinearStarts | G::LinearSteps | G::LogSteps => {
1043 false
1044 }
1045 }
1046 }
1047
1048 fn expand(self, args: &[&str]) -> Result<Vec<Value>, String> {
1050 use NamedGenerator as G;
1051 let params = self.params();
1052 if args.len() != params.len() {
1053 return Err(format!(
1054 "{}: expected {} argument{}, got {}",
1055 self.signature(),
1056 params.len(),
1057 if params.len() == 1 { "" } else { "s" },
1058 args.len()
1059 ));
1060 }
1061 let what = |i: usize| format!("{}.{}", self.name(), params[i]);
1062 let int = |i: usize| parse_u64_arg(args[i], &what(i));
1063 let num = |i: usize| parse_num_arg(args[i], &what(i));
1064 let call = format!("{}({})", self.name(), args.join(", "));
1065 match self {
1066 G::Fib => generate_fib_n(int(0)?, &call),
1067 G::FibUntil => Ok(generate_fib_until(int(0)?)),
1068 G::Pow2 => generate_pow2_n(int(0)?, &call),
1069 G::Pow2Until => Ok(generate_pow2_until(int(0)?)),
1070 G::Binomial => generate_binomial(int(0)?, &call),
1071 G::Geometric => {
1072 let (start, factor) = (num(0)?, num(1)?);
1073 if !(factor.is_finite() && factor > 0.0) {
1074 return Err(format!(
1075 "{}: expected a positive, finite number, got {factor}",
1076 what(1)
1077 ));
1078 }
1079 generate_geometric(start, factor, int(2)?)
1080 }
1081 G::GeometricUntil => {
1082 let (start, factor) = (num(0)?, num(1)?);
1083 if !(factor.is_finite() && factor > 1.0) {
1084 return Err(format!(
1085 "{}: expected a finite number greater than 1, got {factor}",
1086 what(1)
1087 ));
1088 }
1089 Ok(generate_geometric_until(start, factor, num(2)?))
1090 }
1091 G::LinearStarts => generate_linear_points(num(0)?, num(1)?, int(2)?, false),
1092 G::LinearSteps => generate_linear_points(num(0)?, num(1)?, int(2)?, true),
1093 G::LogSteps => {
1094 let (start, end) = (num(0)?, num(1)?);
1095 for (i, bound) in [(0, start), (1, end)] {
1096 if bound.is_nan() || bound <= 0.0 {
1097 return Err(format!(
1098 "{}: expected a positive number, got {bound}",
1099 what(i)
1100 ));
1101 }
1102 }
1103 generate_log_steps(start, end, int(2)?)
1104 }
1105 }
1106 }
1107
1108 pub fn largest_valid_argument(self) -> Option<u64> {
1112 use NamedGenerator as G;
1113 match self {
1114 G::Fib => Some(FIB_MAX_N),
1115 G::Pow2 => Some(POW2_MAX_N),
1116 G::Binomial => Some(BINOMIAL_MAX_N),
1117 _ => None,
1118 }
1119 }
1120}
1121
1122const FIB_MAX_N: u64 = 93;
1125const POW2_MAX_N: u64 = 64;
1128const BINOMIAL_MAX_N: u64 = 67;
1132
1133fn try_eval_generator(text: &str) -> Result<Option<Vec<Value>>, String> {
1138 let Some((name, args)) = parse_func_call(text) else {
1139 return Ok(None);
1140 };
1141 let Some(generator) = NamedGenerator::from_name(name) else {
1142 return Ok(None);
1143 };
1144 generator.expand(&split_args_top_level(args)).map(Some)
1145}
1146
1147pub fn refused_generator_call(text: &str, kernel: &dyn Lookup) -> Option<String> {
1156 let interpolated = crate::kernel::interp::interpolate_with_lookup(text, |name| {
1157 kernel.lookup(name).map(|v| v.to_display_string())
1158 })
1159 .ok()?;
1160 first_refused_call(&interpolated)
1161}
1162
1163fn first_refused_call(text: &str) -> Option<String> {
1164 let (name, args) = parse_func_call(text)?;
1165 let args = split_args_top_level(args);
1166 match NamedGenerator::from_name(name) {
1167 Some(generator) => generator.expand(&args).err(),
1168 None => args.iter().find_map(|a| first_refused_call(a)),
1169 }
1170}
1171
1172fn generate_fib_n(n: u64, call: &str) -> Result<Vec<Value>, String> {
1177 if n > FIB_MAX_N {
1178 return Err(format!(
1179 "{call}: term {} is past u64::MAX; fib.n is at most {FIB_MAX_N}",
1180 FIB_MAX_N + 1
1181 ));
1182 }
1183 let mut out = Vec::with_capacity(n as usize);
1184 let (mut a, mut b): (u128, u128) = (1, 1);
1187 for _ in 0..n {
1188 out.push(Value::U64(a as u64));
1189 (a, b) = (b, a + b);
1190 }
1191 Ok(out)
1192}
1193
1194fn generate_fib_until(max: u64) -> Vec<Value> {
1199 let mut out = Vec::new();
1200 let (mut a, mut b): (u128, u128) = (1, 1);
1201 while a <= u128::from(max) {
1202 out.push(Value::U64(a as u64));
1203 (a, b) = (b, a + b);
1204 }
1205 out
1206}
1207
1208fn generate_pow2_n(n: u64, call: &str) -> Result<Vec<Value>, String> {
1211 if n > POW2_MAX_N {
1212 return Err(format!(
1213 "{call}: term {}, 2^64, is past u64::MAX; pow2.n is at most {POW2_MAX_N}",
1214 POW2_MAX_N + 1
1215 ));
1216 }
1217 Ok((0..n).map(|i| Value::U64(1u64 << i)).collect())
1218}
1219
1220fn generate_pow2_until(max: u64) -> Vec<Value> {
1222 let mut out = Vec::new();
1223 let mut v: u64 = 1;
1224 loop {
1225 if v > max {
1226 break;
1227 }
1228 out.push(Value::U64(v));
1229 v = match v.checked_mul(2) {
1230 Some(x) => x,
1231 None => break,
1232 };
1233 }
1234 out
1235}
1236
1237fn generate_geometric(start: f64, factor: f64, n: u64) -> Result<Vec<Value>, String> {
1239 let mut out = crate::derive_support::try_buffer_for(n, "geometric(start, factor, n)")?;
1240 let mut v = start;
1241 for _ in 0..n {
1242 out.push(Value::F64(v));
1243 v *= factor;
1244 }
1245 Ok(out)
1246}
1247
1248fn generate_geometric_until(start: f64, factor: f64, max: f64) -> Vec<Value> {
1252 let mut out = Vec::new();
1253 let mut v = start;
1254 if start.is_nan() || start <= 0.0 {
1255 return out;
1256 }
1257 while v <= max {
1258 out.push(Value::F64(v));
1259 v *= factor;
1260 }
1261 out
1262}
1263
1264fn generate_binomial(n: u64, call: &str) -> Result<Vec<Value>, String> {
1271 let mut out = Vec::with_capacity(n.min(BINOMIAL_MAX_N) as usize + 1);
1272 let mut c: u128 = 1;
1274 out.push(Value::U64(1));
1275 for k in 1..=n {
1276 c = c * u128::from(n - k + 1) / u128::from(k);
1277 if c > u128::from(u64::MAX) {
1278 return Err(format!(
1279 "{call}: term C({n}, {k}) is past u64::MAX; binomial.n is at most \
1280 {BINOMIAL_MAX_N}"
1281 ));
1282 }
1283 out.push(Value::U64(c as u64));
1284 }
1285 Ok(out)
1286}
1287
1288fn try_eval_partition_call(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
1313 let Some((name, args)) = parse_func_call(text) else {
1314 return Ok(None);
1315 };
1316 let arg_list = split_args_top_level(args);
1317 match name {
1318 "subdivide" => {
1319 if arg_list.len() != 2 {
1320 return Err(format!(
1321 "subdivide(p, n): expected 2 arguments (a partition and a count), got {}",
1322 arg_list.len()
1323 ));
1324 }
1325 let src = arg_list[0].trim();
1326 let n = parse_u64_arg(arg_list[1], "subdivide.n")?;
1327 let Some(value) = kernel.lookup(src) else {
1328 let placeholder = crate::iteration::cursor_partition::Partition {
1333 idx: 0,
1334 count: 1,
1335 start_ord: 0,
1336 end_ord: 1,
1337 start_pct: 0.0,
1338 end_pct: 100.0,
1339 base_extent: 1,
1340 };
1341 return Ok(Some(vec![Value::from_partition(placeholder)]));
1342 };
1343 let Some(p) = value.as_partition().copied() else {
1344 return Err(format!(
1345 "subdivide({src}, {n}): `{src}` resolved to {} — expected a \
1346 Partition value (an iter-var from `for: \"p in partitions(...)\"` \
1347 or a cursor's `.cursor` projection)",
1348 value.to_display_string(),
1349 ));
1350 };
1351 let subs = crate::iteration::cursor_partition::subdivide_partition(&p, n)?;
1352 Ok(Some(subs.into_iter().map(Value::from_partition).collect()))
1353 }
1354 "partitions" => {
1365 if arg_list.is_empty() || arg_list.len() > 2 {
1366 return Err(format!(
1367 "partitions(spec, [extent]): expected 1 or 2 arguments, got {}",
1368 arg_list.len(),
1369 ));
1370 }
1371 let spec = resolve_partition_spec_arg(arg_list[0], kernel)?;
1372 let extent = match arg_list.get(1) {
1373 Some(a) => parse_u64_arg(a, "partitions.extent")?,
1374 None => 100,
1375 };
1376 desugar_partition_spec(&spec, extent, "comprehension source `partitions(...)`")
1377 .map(Some)
1378 }
1379 "profile_partitions" => {
1390 #[cfg(not(feature = "vectordata"))]
1391 {
1392 Err("profile_partitions requires the `vectordata` Cargo feature".to_string())
1393 }
1394
1395 #[cfg(feature = "vectordata")]
1396 {
1397 if arg_list.len() != 2 {
1398 return Err(format!(
1399 "profile_partitions(dataset, pattern): expected 2 arguments, got {}",
1400 arg_list.len()
1401 ));
1402 }
1403 let strip = |s: &str| -> String {
1406 let s = s.trim();
1407 let b = s.as_bytes();
1408 if b.len() >= 2 && (b[0] == b'\'' || b[0] == b'"') && b[b.len() - 1] == b[0] {
1409 s[1..s.len() - 1].to_string()
1410 } else {
1411 s.to_string()
1412 }
1413 };
1414 let dataset = strip(arg_list[0]);
1415 let pattern = strip(arg_list[1]);
1416 match crate::library::vectors::load_dataset_group(&dataset) {
1417 Ok(group) => {
1418 let parts =
1419 crate::library::vectors::build_profile_partitions(&group, &pattern);
1420 Ok(Some(parts.into_iter().map(Value::from_partition).collect()))
1421 }
1422 Err(_) => {
1423 let placeholder = crate::iteration::cursor_partition::Partition {
1427 idx: 0,
1428 count: 1,
1429 start_ord: 0,
1430 end_ord: 1,
1431 start_pct: 0.0,
1432 end_pct: 100.0,
1433 base_extent: 1,
1434 };
1435 Ok(Some(vec![Value::from_partition(placeholder)]))
1436 }
1437 }
1438 }
1439 }
1440 _ => Ok(None),
1441 }
1442}
1443
1444fn try_eval_param_partitions(
1467 text: &str,
1468 kernel: &dyn Lookup,
1469) -> Result<Option<Vec<Value>>, String> {
1470 let Some(ident) = text.trim().strip_suffix(".partitions") else {
1471 return Ok(None);
1472 };
1473 let ident = ident.trim();
1474 if !is_single_bare_ident(ident) {
1475 return Ok(None);
1476 }
1477 let Some(value) = kernel.lookup(ident) else {
1478 let placeholder = crate::iteration::cursor_partition::Partition {
1481 idx: 0,
1482 count: 1,
1483 start_ord: 0,
1484 end_ord: 1,
1485 start_pct: 0.0,
1486 end_pct: 100.0,
1487 base_extent: 1,
1488 };
1489 return Ok(Some(vec![Value::from_partition(placeholder)]));
1490 };
1491 if let Some(list) = value.as_partition_list() {
1493 return Ok(Some(
1494 list.as_slice()
1495 .iter()
1496 .map(|p| Value::from_partition(*p))
1497 .collect(),
1498 ));
1499 }
1500 let Value::Str(spec) = &value else {
1503 return Err(format!(
1504 "comprehension source `{ident}.partitions`: `{ident}` resolved to \
1505 {} — expected a partition-spec string (a workload param such as \
1506 `cursor=linear:4`) or a PartitionList.",
1507 value.to_display_string(),
1508 ));
1509 };
1510 desugar_partition_spec(
1511 spec,
1512 100,
1513 &format!("comprehension source `{ident}.partitions`"),
1514 )
1515 .map(Some)
1516}
1517
1518fn desugar_partition_spec(spec: &str, extent: u64, ctx: &str) -> Result<Vec<Value>, String> {
1526 let parsed = crate::iteration::cursor_partition::parse(spec)
1527 .map_err(|e| format!("{ctx}: bad spec `{spec}`: {e}"))?;
1528 let parts = crate::iteration::cursor_partition::resolve(&parsed, 0, extent)
1529 .map_err(|e| format!("{ctx}: resolve failed for `{spec}`: {e}"))?;
1530 Ok(parts.into_iter().map(Value::from_partition).collect())
1531}
1532
1533fn resolve_partition_spec_arg(arg: &str, kernel: &dyn Lookup) -> Result<String, String> {
1538 let a = arg.trim();
1539 if a.len() >= 2
1540 && ((a.starts_with('"') && a.ends_with('"')) || (a.starts_with('\'') && a.ends_with('\'')))
1541 {
1542 return Ok(a[1..a.len() - 1].to_string());
1543 }
1544 if is_single_bare_ident(a) {
1545 return match kernel.lookup(a) {
1546 Some(Value::Str(s)) => Ok(s.to_string()),
1547 Some(other) => Err(format!(
1548 "partitions(...): `{a}` resolved to {} — expected a spec string",
1549 other.to_display_string(),
1550 )),
1551 None => Err(format!(
1552 "partitions(...): `{a}` did not resolve to a spec string in scope"
1553 )),
1554 };
1555 }
1556 Ok(a.to_string())
1557}
1558
1559fn generate_linear_points(
1570 start: f64,
1571 end: f64,
1572 n: u64,
1573 inclusive: bool,
1574) -> Result<Vec<Value>, String> {
1575 let denom = if inclusive {
1576 (n.saturating_sub(1)).max(1) as f64
1577 } else {
1578 n as f64
1579 };
1580 let step = (end - start) / denom;
1581 let mut out = crate::derive_support::try_buffer_for(n, "linear points")?;
1585 out.extend((0..n).map(|i| Value::F64(start + step * i as f64)));
1586 if inclusive && n >= 2 {
1589 out[n as usize - 1] = Value::F64(end);
1590 }
1591 Ok(out)
1592}
1593
1594fn generate_log_steps(start: f64, end: f64, n: u64) -> Result<Vec<Value>, String> {
1598 if n == 0 {
1599 return Ok(Vec::new());
1600 }
1601 if n == 1 {
1602 return Ok(vec![Value::F64(start)]);
1603 }
1604 let log_s = start.ln();
1605 let log_e = end.ln();
1606 let step = (log_e - log_s) / (n - 1) as f64;
1607 let mut out = crate::derive_support::try_buffer_for(n, "log_steps(start, end, n)")?;
1608 out.push(Value::F64(start));
1609 out.extend((1..n - 1).map(|i| Value::F64((log_s + step * i as f64).exp())));
1610 out.push(Value::F64(end));
1611 Ok(out)
1612}
1613
1614fn try_eval_setop(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
1623 let Some((name, args)) = parse_func_call(text) else {
1624 return Ok(None);
1625 };
1626 let arg_texts = split_args_top_level(args);
1627 let recursively_evaluate = |t: &str| -> Result<Vec<Value>, String> {
1628 evaluate_spec(t, kernel).map_err(|e| e.to_string())
1629 };
1630 match name {
1631 "concat" => {
1632 let mut out = Vec::new();
1633 for a in &arg_texts {
1634 out.extend(recursively_evaluate(a)?);
1635 }
1636 Ok(Some(out))
1637 }
1638 "unique" => {
1639 let mut out: Vec<Value> = Vec::new();
1640 for a in &arg_texts {
1641 for v in recursively_evaluate(a)? {
1642 if !out.contains(&v) {
1643 out.push(v);
1644 }
1645 }
1646 }
1647 Ok(Some(out))
1648 }
1649 "intersect" => {
1650 if arg_texts.is_empty() {
1651 return Ok(Some(Vec::new()));
1652 }
1653 let first = recursively_evaluate(arg_texts[0])?;
1654 let mut out: Vec<Value> = Vec::new();
1655 for v in first {
1656 let mut in_all = true;
1657 for a in &arg_texts[1..] {
1658 let other = recursively_evaluate(a)?;
1659 if !other.contains(&v) {
1660 in_all = false;
1661 break;
1662 }
1663 }
1664 if in_all && !out.contains(&v) {
1665 out.push(v);
1666 }
1667 }
1668 Ok(Some(out))
1669 }
1670 "subtract" => {
1671 if arg_texts.len() != 2 {
1672 return Err(format!(
1673 "subtract(a, b): expected 2 args, got {}",
1674 arg_texts.len()
1675 ));
1676 }
1677 let a = recursively_evaluate(arg_texts[0])?;
1678 let b = recursively_evaluate(arg_texts[1])?;
1679 Ok(Some(a.into_iter().filter(|v| !b.contains(v)).collect()))
1680 }
1681 "interleave" => {
1682 let lists: Result<Vec<Vec<Value>>, String> =
1683 arg_texts.iter().map(|a| recursively_evaluate(a)).collect();
1684 let lists = lists?;
1685 let mut out = Vec::new();
1686 let max_len = lists.iter().map(|l| l.len()).max().unwrap_or(0);
1687 for i in 0..max_len {
1688 for l in &lists {
1689 if let Some(v) = l.get(i) {
1690 out.push(v.clone());
1691 }
1692 }
1693 }
1694 Ok(Some(out))
1695 }
1696 "cycle" => {
1697 if arg_texts.len() != 2 {
1698 return Err(format!(
1699 "cycle(a, n): expected 2 args, got {}",
1700 arg_texts.len()
1701 ));
1702 }
1703 let a = recursively_evaluate(arg_texts[0])?;
1704 let n = parse_u64_arg(arg_texts[1], "cycle.n")?;
1705 let total = (a.len() as u64).checked_mul(n).ok_or_else(|| {
1706 format!(
1707 "cycle(a, n): {} values repeated {n} times is more than can be counted",
1708 a.len()
1709 )
1710 })?;
1711 let mut out = crate::derive_support::try_buffer_for(total, "cycle(a, n)")?;
1712 for _ in 0..n {
1713 out.extend(a.iter().cloned());
1714 }
1715 Ok(Some(out))
1716 }
1717 "reverse" => {
1718 if arg_texts.len() != 1 {
1719 return Err(format!(
1720 "reverse(a): expected 1 arg, got {}",
1721 arg_texts.len()
1722 ));
1723 }
1724 let mut a = recursively_evaluate(arg_texts[0])?;
1725 a.reverse();
1726 Ok(Some(a))
1727 }
1728 "take" => {
1729 if arg_texts.len() != 2 {
1730 return Err(format!(
1731 "take(a, n): expected 2 args, got {}",
1732 arg_texts.len()
1733 ));
1734 }
1735 let a = recursively_evaluate(arg_texts[0])?;
1736 let n = parse_u64_arg(arg_texts[1], "take.n")?;
1737 Ok(Some(a.into_iter().take(n as usize).collect()))
1738 }
1739 "skip" => {
1740 if arg_texts.len() != 2 {
1741 return Err(format!(
1742 "skip(a, n): expected 2 args, got {}",
1743 arg_texts.len()
1744 ));
1745 }
1746 let a = recursively_evaluate(arg_texts[0])?;
1747 let n = parse_u64_arg(arg_texts[1], "skip.n")?;
1748 Ok(Some(a.into_iter().skip(n as usize).collect()))
1749 }
1750 _ => Ok(None),
1751 }
1752}
1753
1754fn try_eval_sequencer(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, String> {
1770 let Some((name, args)) = parse_func_call(text) else {
1771 return Ok(None);
1772 };
1773 if !matches!(name, "bucket" | "concat_seq" | "interval_seq") {
1774 return Ok(None);
1775 }
1776 let arg_texts = split_args_top_level(args);
1777
1778 let (items, ratios): (Vec<Value>, Vec<usize>) = match arg_texts.len() {
1783 1 => parse_ratio_prefix_shorthand(arg_texts[0])?,
1784 2 => {
1785 let items = evaluate_spec(arg_texts[0], kernel)?;
1786 let raw_ratios = evaluate_spec(arg_texts[1], kernel)?;
1787 let ratios: Result<Vec<usize>, String> = raw_ratios
1788 .iter()
1789 .map(|v| match v {
1790 Value::U64(n) => Ok(*n as usize),
1791 other => Err(format!(
1792 "{name}: ratio must be non-negative integer, got {other:?}"
1793 )),
1794 })
1795 .collect();
1796 (items, ratios?)
1797 }
1798 _ => {
1799 return Err(format!(
1800 "{name}: expected `(items, ratios)` or `(\"r1:item1, r2:item2, ...\")`; got {} args",
1801 arg_texts.len()
1802 ));
1803 }
1804 };
1805
1806 if items.len() != ratios.len() {
1807 return Err(format!(
1808 "{name}: items.len() ({}) != ratios.len() ({})",
1809 items.len(),
1810 ratios.len(),
1811 ));
1812 }
1813 let total = ratios
1817 .iter()
1818 .try_fold(0usize, |acc, &r| acc.checked_add(r))
1819 .ok_or_else(|| format!("{name}: the ratios sum past what can be counted"))?;
1820 let out = crate::derive_support::try_buffer_for(total as u64, name)?;
1821 Ok(Some(match name {
1822 "bucket" => seq_bucket(&items, &ratios, total, out),
1823 "concat_seq" => seq_concat(&items, &ratios, out),
1824 "interval_seq" => seq_interval(&items, &ratios, total, out),
1825 _ => unreachable!(),
1826 }))
1827}
1828
1829fn parse_ratio_prefix_shorthand(text: &str) -> Result<(Vec<Value>, Vec<usize>), String> {
1834 let stripped = text
1837 .trim()
1838 .trim_start_matches(['"', '\''])
1839 .trim_end_matches(['"', '\'']);
1840 let mut items = Vec::new();
1841 let mut ratios = Vec::new();
1842 for part in stripped.split(',') {
1843 let part = part.trim();
1844 if part.is_empty() {
1845 continue;
1846 }
1847 let (r, i) = part
1848 .split_once(':')
1849 .ok_or_else(|| format!("ratio-prefix shorthand: missing ':' in '{part}'"))?;
1850 let ratio: usize = r.trim().parse().map_err(|_| {
1851 format!("ratio-prefix shorthand: ratio '{r}' is not a non-negative integer")
1852 })?;
1853 ratios.push(ratio);
1854 items.push(parse_one_value(i.trim()));
1855 }
1856 Ok((items, ratios))
1857}
1858
1859fn parse_one_value(s: &str) -> Value {
1860 if let Ok(n) = s.parse::<u64>() {
1861 return Value::U64(n);
1862 }
1863 if let Ok(f) = s.parse::<f64>() {
1864 return Value::F64(f);
1865 }
1866 if s == "true" {
1867 return Value::Bool(true);
1868 }
1869 if s == "false" {
1870 return Value::Bool(false);
1871 }
1872 Value::Str(s.to_string().into())
1873}
1874
1875fn seq_bucket(items: &[Value], ratios: &[usize], total: usize, mut out: Vec<Value>) -> Vec<Value> {
1879 let mut remaining: Vec<usize> = ratios.to_vec();
1880 while out.len() < total {
1881 let mut emitted_any = false;
1882 for (i, item) in items.iter().enumerate() {
1883 if remaining[i] > 0 {
1884 out.push(item.clone());
1885 remaining[i] -= 1;
1886 emitted_any = true;
1887 }
1888 }
1889 if !emitted_any {
1890 break;
1891 }
1892 }
1893 out
1894}
1895
1896fn seq_concat(items: &[Value], ratios: &[usize], mut out: Vec<Value>) -> Vec<Value> {
1899 for (item, &r) in items.iter().zip(ratios.iter()) {
1900 for _ in 0..r {
1901 out.push(item.clone());
1902 }
1903 }
1904 out
1905}
1906
1907fn seq_interval(
1913 items: &[Value],
1914 ratios: &[usize],
1915 total: usize,
1916 mut out: Vec<Value>,
1917) -> Vec<Value> {
1918 if total == 0 {
1919 return out;
1920 }
1921 let mut emitted: Vec<usize> = vec![0; items.len()];
1922 for slot in 0..total {
1923 let mut best = 0usize;
1926 let mut best_deficit: f64 = f64::NEG_INFINITY;
1927 for i in 0..items.len() {
1928 let target = ratios[i] as f64 * (slot + 1) as f64 / total as f64;
1929 let deficit = target - emitted[i] as f64;
1930 if deficit > best_deficit {
1931 best_deficit = deficit;
1932 best = i;
1933 }
1934 }
1935 out.push(items[best].clone());
1936 emitted[best] += 1;
1937 }
1938 out
1939}
1940
1941pub fn value_to_polydat_type_name(v: &Value) -> &'static str {
1951 v.port_type().to_keyword()
1952}
1953
1954#[cfg(test)]
1965mod tests {
1966 use super::*;
1967 use crate::kernel::PolydatKernel;
1968 use crate::kernel::interp::interpolate_via_kernel;
1969
1970 fn h(pairs: &[(&str, &str)]) -> HashMap<String, String> {
1971 pairs
1972 .iter()
1973 .map(|(k, v)| (k.to_string(), v.to_string()))
1974 .collect()
1975 }
1976
1977 fn interpolate(
1978 text: &str,
1979 bindings: &HashMap<String, String>,
1980 workload_params: &HashMap<String, String>,
1981 ) -> Result<String, String> {
1982 interpolate_with_lookup(text, |name| {
1983 bindings
1984 .get(name)
1985 .or_else(|| workload_params.get(name))
1986 .cloned()
1987 })
1988 }
1989
1990 #[test]
1991 fn flat_substitution() {
1992 let params = h(&[("dataset", "example"), ("prefix", "label")]);
1993 let out = interpolate("matching('{dataset}', '{prefix}')", &h(&[]), ¶ms).unwrap();
1994 assert_eq!(out, "matching('example', 'label')");
1995 }
1996
1997 #[test]
1998 fn bindings_shadow_params() {
1999 let params = h(&[("profile", "default")]);
2000 let bindings = h(&[("profile", "label_07")]);
2001 let out = interpolate("vec_{profile}", &bindings, ¶ms).unwrap();
2002 assert_eq!(out, "vec_label_07");
2003 }
2004
2005 #[test]
2006 fn nested_placeholder_resolves_inside_out() {
2007 let params = h(&[("k_1_limits", "1,2,4,8"), ("k_10_limits", "10,20,30")]);
2008 let bindings = h(&[("k", "1")]);
2009 let out = interpolate("{k_{k}_limits}", &bindings, ¶ms).unwrap();
2010 assert_eq!(out, "1,2,4,8");
2011 }
2012
2013 #[test]
2014 fn deeply_nested() {
2015 let params = h(&[("a_b_c", "WIN")]);
2016 let bindings = h(&[("x", "a"), ("y", "b"), ("z", "c")]);
2017 let out = interpolate("{{x}_{y}_{z}}", &bindings, ¶ms).unwrap();
2018 assert_eq!(out, "WIN");
2019 }
2020
2021 #[test]
2022 fn escape_emits_literal_brace() {
2023 let out = interpolate("\\{not_a_var\\}", &h(&[]), &h(&[])).unwrap();
2024 assert_eq!(out, "{not_a_var}");
2025 }
2026
2027 #[test]
2028 fn escape_inside_otherwise_resolved_text() {
2029 let params = h(&[("x", "1")]);
2030 let out = interpolate("a={x} literal=\\{x\\}", &h(&[]), ¶ms).unwrap();
2031 assert_eq!(out, "a=1 literal={x}");
2032 }
2033
2034 #[test]
2035 fn unresolved_is_hard_error() {
2036 let err = interpolate("hello {nope}", &h(&[]), &h(&[])).unwrap_err();
2037 assert!(err.contains("unresolved"));
2038 assert!(err.contains("nope"));
2039 }
2040
2041 #[test]
2042 fn empty_placeholder_rejected() {
2043 let err = interpolate("a{}b", &h(&[]), &h(&[])).unwrap_err();
2044 assert!(err.contains("empty"));
2045 }
2046
2047 #[test]
2048 fn unmatched_brace_rejected() {
2049 let err = interpolate("a {x", &h(&[]), &h(&[])).unwrap_err();
2050 assert!(err.contains("unmatched"));
2051 }
2052
2053 #[test]
2054 fn idempotent_when_no_placeholders() {
2055 let out = interpolate("plain text", &h(&[]), &h(&[])).unwrap();
2056 assert_eq!(out, "plain text");
2057 }
2058
2059 #[test]
2060 fn resolved_value_with_braces_does_not_re_expand() {
2061 let params = h(&[("greeting", "hello {planet}")]);
2062 let err = interpolate("{greeting}", &h(&[]), ¶ms).unwrap_err();
2063 assert!(err.contains("planet"));
2064 }
2065
2066 #[test]
2067 fn cyclic_placeholders_hit_round_cap() {
2068 let params = h(&[("a", "{b}"), ("b", "{a}")]);
2069 let err = interpolate("{a}", &h(&[]), ¶ms).unwrap_err();
2070 assert!(err.contains("did not stabilize") || err.contains("rounds"));
2071 }
2072
2073 #[test]
2074 fn kernel_resolves_via_get_constant() {
2075 let kernel =
2076 crate::dsl::compile::compile_polydat_interpreter("const dataset := \"example\"\n")
2077 .unwrap();
2078 let out = interpolate_via_kernel("path/{dataset}/data", &kernel).unwrap();
2079 assert_eq!(out, "path/example/data");
2080 }
2081
2082 #[test]
2083 fn kernel_resolves_via_get_input() {
2084 let parent =
2085 crate::dsl::compile::compile_polydat_interpreter("const k_values := \"1, 10\"\n")
2086 .unwrap();
2087 let child_program =
2088 crate::dsl::compile::compile_polydat_interpreter("extern k_values: String\n")
2089 .unwrap()
2090 .program()
2091 .clone();
2092 let child = parent.materialize_subscope(child_program, &[]);
2093 let out = interpolate_via_kernel("values={k_values}", &child).unwrap();
2094 assert_eq!(out, "values=1, 10");
2095 }
2096
2097 #[test]
2098 fn kernel_unresolved_name_errors() {
2099 let kernel = crate::dsl::compile::compile_polydat_interpreter("const x := 1\n").unwrap();
2100 let err = interpolate_via_kernel("hello {nope}", &kernel)
2101 .unwrap_err()
2102 .to_string();
2103 assert!(err.contains("unresolved"));
2104 assert!(err.contains("nope"));
2105 }
2106
2107 #[test]
2108 fn kernel_nested_template_iterates_to_fixed_point() {
2109 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2110 "const k := \"1\"\nconst k_1_limits := \"1, 2, 4, 8\"\n",
2111 )
2112 .unwrap();
2113 let out = interpolate_via_kernel("{k_{k}_limits}", &kernel).unwrap();
2114 assert_eq!(out, "1, 2, 4, 8");
2115 }
2116
2117 #[test]
2118 fn parse_list_native_types() {
2119 let v = parse_list_with_types("1, 10, 100");
2120 assert_eq!(v, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2121 }
2122
2123 #[test]
2124 fn parse_list_mixed_types() {
2125 let v = parse_list_with_types("1, 1.5, true, hello");
2126 assert_eq!(
2127 v,
2128 vec![
2129 Value::U64(1),
2130 Value::F64(1.5),
2131 Value::Bool(true),
2132 Value::Str("hello".to_string().into()),
2133 ]
2134 );
2135 }
2136
2137 #[test]
2138 fn all_cursor_returns_extent_range() {
2139 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2145 "const __cursor_extent_row_start := 0\n\
2146 const __cursor_extent_row_end := 5\n",
2147 )
2148 .unwrap();
2149 let values = evaluate_spec("all(row)", &kernel).unwrap();
2150 assert_eq!(
2151 values,
2152 vec![
2153 Value::U64(0),
2154 Value::U64(1),
2155 Value::U64(2),
2156 Value::U64(3),
2157 Value::U64(4),
2158 ]
2159 );
2160 }
2161
2162 #[test]
2163 fn all_cursor_non_zero_start() {
2164 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2165 "const __cursor_extent_data_start := 100\n\
2166 const __cursor_extent_data_end := 103\n",
2167 )
2168 .unwrap();
2169 let values = evaluate_spec("all(data)", &kernel).unwrap();
2170 assert_eq!(
2171 values,
2172 vec![Value::U64(100), Value::U64(101), Value::U64(102)]
2173 );
2174 }
2175
2176 #[test]
2177 fn all_cursor_missing_extent_errors() {
2178 let kernel =
2179 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2180 let err = evaluate_spec("all(no_such_cursor)", &kernel)
2181 .unwrap_err()
2182 .to_string();
2183 assert!(err.contains("all(no_such_cursor)"));
2184 assert!(err.contains("no resolvable extent"));
2185 }
2186
2187 #[test]
2188 fn all_cursor_only_matches_exact_shape() {
2189 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2199 "const __cursor_extent_row_start := 0\n\
2200 const __cursor_extent_row_end := 5\n",
2201 )
2202 .unwrap();
2203 let err = evaluate_spec("all(row, 5)", &kernel)
2204 .unwrap_err()
2205 .to_string();
2206 assert!(
2207 err.contains("all(row, 5)"),
2208 "error must mention the failing spec, got: {err}"
2209 );
2210 assert!(
2211 err.contains("failed to evaluate") || err.contains("unknown function"),
2212 "error must explain the eval failure, got: {err}"
2213 );
2214 }
2215
2216 #[test]
2217 fn missing_dataset_surface_as_clean_error_not_garbage() {
2218 let kernel =
2229 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2230 let result = evaluate_spec(
2231 "matching_profiles('nonexistent_dataset_xyz_qqq', 'label_')",
2232 &kernel,
2233 );
2234 let err = result
2235 .expect_err("missing dataset must surface as Err, not silent literal-list fallback")
2236 .to_string();
2237 assert!(
2243 err.contains("nonexistent_dataset_xyz_qqq")
2244 || err.contains("matching_profiles")
2245 || err.contains("dataset"),
2246 "error must point at the actual fault, got: {err}"
2247 );
2248 }
2249
2250 #[test]
2251 fn function_call_eval_failure_is_not_silently_split() {
2252 let kernel =
2258 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2259 let err = evaluate_spec("nonexistent_func('a', 'b', 'c')", &kernel)
2260 .unwrap_err()
2261 .to_string();
2262 assert!(
2263 err.contains("failed to evaluate") || err.contains("unknown"),
2264 "expected a clean eval-failure error, got: {err}"
2265 );
2266 }
2267
2268 #[test]
2269 fn literal_list_path_still_works() {
2270 let kernel =
2277 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2278 let values = evaluate_spec("1, 10, 100", &kernel).unwrap();
2279 assert_eq!(values, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2280
2281 let names = evaluate_spec("foo, bar, baz", &kernel).unwrap();
2282 assert_eq!(
2283 names,
2284 vec![
2285 Value::Str("foo".into()),
2286 Value::Str("bar".into()),
2287 Value::Str("baz".into()),
2288 ]
2289 );
2290 }
2291
2292 #[test]
2293 fn literal_cursor_exposes_extent_auxiliaries() {
2294 let kernel =
2299 crate::dsl::compile::compile_polydat_interpreter("cursor row = range(0, 50)\n")
2300 .unwrap();
2301 let start = kernel.lookup("__cursor_extent_row_start");
2302 let end = kernel.lookup("__cursor_extent_row_end");
2303 assert_eq!(
2304 start,
2305 Some(Value::U64(0)),
2306 "expected start=0, got {start:?}"
2307 );
2308 assert_eq!(end, Some(Value::U64(50)), "expected end=50, got {end:?}");
2309 }
2310
2311 #[test]
2312 fn all_cursor_with_real_cursor_decl_works() {
2313 let kernel =
2314 crate::dsl::compile::compile_polydat_interpreter("cursor row = range(0, 5)\n").unwrap();
2315 let values = evaluate_spec("all(row)", &kernel).unwrap();
2316 assert_eq!(
2317 values,
2318 vec![
2319 Value::U64(0),
2320 Value::U64(1),
2321 Value::U64(2),
2322 Value::U64(3),
2323 Value::U64(4),
2324 ]
2325 );
2326 }
2327
2328 #[test]
2329 fn all_cursor_ignores_whitespace() {
2330 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2331 "const __cursor_extent_row_start := 0\n\
2332 const __cursor_extent_row_end := 3\n",
2333 )
2334 .unwrap();
2335 let values = evaluate_spec(" all( row ) ", &kernel).unwrap();
2336 assert_eq!(values.len(), 3);
2337 }
2338
2339 #[test]
2340 fn evaluate_spec_resolves_against_kernel() {
2341 let kernel =
2342 crate::dsl::compile::compile_polydat_interpreter("const k_values := \"1, 10, 100\"\n")
2343 .unwrap();
2344 let v = evaluate_spec("{k_values}", &kernel).unwrap();
2345 assert_eq!(v, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2346 }
2347
2348 #[test]
2349 fn evaluate_spec_bare_ident_resolves_like_braced() {
2350 let kernel =
2354 crate::dsl::compile::compile_polydat_interpreter("const k_values := \"1, 10, 100\"\n")
2355 .unwrap();
2356 let bare = evaluate_spec("k_values", &kernel).unwrap();
2357 let braced = evaluate_spec("{k_values}", &kernel).unwrap();
2358 assert_eq!(bare, braced);
2359 assert_eq!(bare, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2360 }
2361
2362 #[test]
2363 fn evaluate_spec_unresolved_bare_is_error_with_quoting_hint() {
2364 let kernel = crate::dsl::compile::compile_polydat_interpreter("\n").unwrap();
2368 let err = evaluate_spec("nonexistent", &kernel)
2369 .unwrap_err()
2370 .to_string();
2371 assert!(err.contains("did not resolve"), "got: {err}");
2372 assert!(err.contains("quote it"), "should hint quoting: {err}");
2373 }
2374
2375 #[test]
2376 fn bracket_list_spread_and_no_peel() {
2377 let kernel =
2380 crate::dsl::compile::compile_polydat_interpreter("const xs := \"1, 2, 3\"\n").unwrap();
2381 let spread = evaluate_spec("[xs…]", &kernel).unwrap();
2383 assert_eq!(spread, vec![Value::U64(1), Value::U64(2), Value::U64(3)]);
2384 let whole = evaluate_spec("[xs]", &kernel).unwrap();
2386 assert_eq!(whole, vec![Value::Str("1, 2, 3".into())]);
2387 }
2388
2389 #[test]
2390 fn bracket_list_mixes_refs_literals_and_spread() {
2391 let kernel =
2392 crate::dsl::compile::compile_polydat_interpreter("const mid := \"7, 8\"\n").unwrap();
2393 let v = evaluate_spec("[1, mid…, \"x\"]", &kernel).unwrap();
2394 assert_eq!(
2395 v,
2396 vec![
2397 Value::U64(1),
2398 Value::U64(7),
2399 Value::U64(8),
2400 Value::Str("x".into()),
2401 ]
2402 );
2403 }
2404
2405 #[test]
2408 fn evaluate_spec_unpacks_partition_list_into_partition_values() {
2409 let kernel = empty_kernel();
2415 let v = evaluate_spec("partitions(\"linear:3\")", &kernel).unwrap();
2416 assert_eq!(v.len(), 3, "expected 3 partitions, got {}", v.len());
2417 for value in &v {
2418 assert!(
2419 value.as_partition().is_some(),
2420 "every iter value should be a Partition, got {value:?}"
2421 );
2422 }
2423 }
2424
2425 #[test]
2426 fn evaluate_spec_unpacks_partition_list_with_explicit_extent() {
2427 let kernel = empty_kernel();
2428 let v = evaluate_spec("partitions(\"fib:5\", 1000)", &kernel).unwrap();
2429 assert_eq!(v.len(), 5);
2430 for (i, value) in v.iter().enumerate() {
2432 let p = value.as_partition().unwrap();
2433 assert_eq!(p.idx, i as u64);
2434 assert_eq!(p.base_extent, 1000);
2435 }
2436 }
2437
2438 #[test]
2439 fn pre_evaluate_clause_returns_partition_values_for_partitions_call() {
2440 let kernel = empty_kernel();
2443 let v = pre_evaluate_clause(
2444 "partitions(\"linear:4\")",
2445 &kernel,
2446 &HashMap::new(),
2447 &HashMap::new(),
2448 )
2449 .unwrap();
2450 assert_eq!(v.len(), 4);
2451 for value in &v {
2452 assert!(
2453 value.as_partition().is_some(),
2454 "pre_evaluate_clause must unpack PartitionList, got {value:?}"
2455 );
2456 }
2457 }
2458
2459 #[test]
2460 fn value_to_polydat_type_name_returns_ext_for_partition_value() {
2461 let p = crate::iteration::cursor_partition::Partition {
2468 idx: 0,
2469 count: 1,
2470 start_ord: 0,
2471 end_ord: 10,
2472 start_pct: 0.0,
2473 end_pct: 100.0,
2474 base_extent: 10,
2475 };
2476 let v = Value::from_partition(p);
2477 assert_eq!(value_to_polydat_type_name(&v), "ext");
2478 }
2479
2480 fn empty_kernel() -> PolydatKernel {
2483 crate::dsl::compile::compile_polydat_interpreter("\n").unwrap()
2484 }
2485
2486 #[test]
2487 fn range_half_open_integer() {
2488 let v = evaluate_spec("1..5", &empty_kernel()).unwrap();
2489 assert_eq!(
2490 v,
2491 vec![Value::U64(1), Value::U64(2), Value::U64(3), Value::U64(4),]
2492 );
2493 }
2494
2495 #[test]
2496 fn range_inclusive_integer() {
2497 let v = evaluate_spec("1..=5", &empty_kernel()).unwrap();
2498 assert_eq!(
2499 v,
2500 vec![
2501 Value::U64(1),
2502 Value::U64(2),
2503 Value::U64(3),
2504 Value::U64(4),
2505 Value::U64(5),
2506 ]
2507 );
2508 }
2509
2510 #[test]
2511 fn range_with_step() {
2512 let v = evaluate_spec("0..100..10", &empty_kernel()).unwrap();
2513 assert_eq!(
2514 v,
2515 vec![
2516 Value::U64(0),
2517 Value::U64(10),
2518 Value::U64(20),
2519 Value::U64(30),
2520 Value::U64(40),
2521 Value::U64(50),
2522 Value::U64(60),
2523 Value::U64(70),
2524 Value::U64(80),
2525 Value::U64(90),
2526 ]
2527 );
2528 }
2529
2530 #[test]
2531 fn range_inclusive_with_step() {
2532 let v = evaluate_spec("0..=100..25", &empty_kernel()).unwrap();
2533 assert_eq!(
2534 v,
2535 vec![
2536 Value::U64(0),
2537 Value::U64(25),
2538 Value::U64(50),
2539 Value::U64(75),
2540 Value::U64(100),
2541 ]
2542 );
2543 }
2544
2545 #[test]
2546 fn range_float_step() {
2547 let v = evaluate_spec("0.0..=1.0..0.25", &empty_kernel()).unwrap();
2548 assert_eq!(v.len(), 5, "got {v:?}");
2549 if let [
2550 Value::F64(a),
2551 Value::F64(b),
2552 Value::F64(c),
2553 Value::F64(d),
2554 Value::F64(e),
2555 ] = v.as_slice()
2556 {
2557 assert!((a - 0.0).abs() < 1e-12);
2558 assert!((b - 0.25).abs() < 1e-12);
2559 assert!((c - 0.5).abs() < 1e-12);
2560 assert!((d - 0.75).abs() < 1e-12);
2561 assert!((e - 1.0).abs() < 1e-12);
2562 } else {
2563 panic!("expected 5 floats, got {v:?}");
2564 }
2565 }
2566
2567 #[test]
2568 fn range_empty_when_start_equals_end_half_open() {
2569 let v = evaluate_spec("5..5", &empty_kernel()).unwrap();
2570 assert!(v.is_empty(), "got {v:?}");
2571 }
2572
2573 #[test]
2574 fn range_inclusive_with_equal_bounds_emits_one() {
2575 let v = evaluate_spec("5..=5", &empty_kernel()).unwrap();
2576 assert_eq!(v, vec![Value::U64(5)]);
2577 }
2578
2579 #[test]
2580 fn range_with_si_suffix_bounds() {
2581 let v = evaluate_spec("1K..1K..200", &empty_kernel()).unwrap();
2584 assert!(v.is_empty(), "1K..1K with positive step → empty");
2585
2586 let v = evaluate_spec("0..1K..200", &empty_kernel()).unwrap();
2587 assert_eq!(
2588 v,
2589 vec![
2590 Value::U64(0),
2591 Value::U64(200),
2592 Value::U64(400),
2593 Value::U64(600),
2594 Value::U64(800),
2595 ]
2596 );
2597 }
2598
2599 #[test]
2600 fn range_zero_step_errors() {
2601 let err = evaluate_spec("1..10..0", &empty_kernel())
2602 .unwrap_err()
2603 .to_string();
2604 assert!(err.contains("step is zero"), "{err}");
2605 }
2606
2607 #[test]
2608 fn range_too_many_dotdot_errors() {
2609 let err = evaluate_spec("1..2..3..4", &empty_kernel())
2610 .unwrap_err()
2611 .to_string();
2612 assert!(err.contains("more than two `..`"), "{err}");
2613 }
2614
2615 #[test]
2616 fn range_inside_parens_doesnt_split() {
2617 let v = evaluate_spec("(1)..(5)", &empty_kernel()).unwrap();
2624 assert_eq!(v.len(), 4); }
2626
2627 #[test]
2628 fn range_step_with_inclusive_separator_errors() {
2629 let err = evaluate_spec("1..10..=2", &empty_kernel())
2630 .unwrap_err()
2631 .to_string();
2632 assert!(err.contains("step delimiter cannot be `..=`"), "{err}");
2633 }
2634
2635 #[test]
2636 fn range_with_kernel_referenced_bounds() {
2637 let kernel =
2638 crate::dsl::compile::compile_polydat_interpreter("const lo := 5\nconst hi := 12\n")
2639 .unwrap();
2640 let v = evaluate_spec("{lo}..{hi}", &kernel).unwrap();
2641 assert_eq!(
2642 v,
2643 vec![
2644 Value::U64(5),
2645 Value::U64(6),
2646 Value::U64(7),
2647 Value::U64(8),
2648 Value::U64(9),
2649 Value::U64(10),
2650 Value::U64(11),
2651 ]
2652 );
2653 }
2654
2655 #[test]
2658 fn fib_n_first_eight() {
2659 let v = evaluate_spec("fib(8)", &empty_kernel()).unwrap();
2660 assert_eq!(
2661 v,
2662 vec![
2663 Value::U64(1),
2664 Value::U64(1),
2665 Value::U64(2),
2666 Value::U64(3),
2667 Value::U64(5),
2668 Value::U64(8),
2669 Value::U64(13),
2670 Value::U64(21),
2671 ]
2672 );
2673 }
2674
2675 #[test]
2676 fn fib_until_50() {
2677 let v = evaluate_spec("fib_until(50)", &empty_kernel()).unwrap();
2678 assert_eq!(
2679 v,
2680 vec![
2681 Value::U64(1),
2682 Value::U64(1),
2683 Value::U64(2),
2684 Value::U64(3),
2685 Value::U64(5),
2686 Value::U64(8),
2687 Value::U64(13),
2688 Value::U64(21),
2689 Value::U64(34),
2690 ]
2691 );
2692 }
2693
2694 #[test]
2695 fn pow2_n_six() {
2696 let v = evaluate_spec("pow2(6)", &empty_kernel()).unwrap();
2697 assert_eq!(
2698 v,
2699 vec![
2700 Value::U64(1),
2701 Value::U64(2),
2702 Value::U64(4),
2703 Value::U64(8),
2704 Value::U64(16),
2705 Value::U64(32),
2706 ]
2707 );
2708 }
2709
2710 #[test]
2711 fn pow2_until_100() {
2712 let v = evaluate_spec("pow2_until(100)", &empty_kernel()).unwrap();
2713 assert_eq!(
2714 v,
2715 vec![
2716 Value::U64(1),
2717 Value::U64(2),
2718 Value::U64(4),
2719 Value::U64(8),
2720 Value::U64(16),
2721 Value::U64(32),
2722 Value::U64(64),
2723 ]
2724 );
2725 }
2726
2727 #[test]
2728 fn binomial_n_5() {
2729 let v = evaluate_spec("binomial(5)", &empty_kernel()).unwrap();
2731 assert_eq!(
2732 v,
2733 vec![
2734 Value::U64(1),
2735 Value::U64(5),
2736 Value::U64(10),
2737 Value::U64(10),
2738 Value::U64(5),
2739 Value::U64(1),
2740 ]
2741 );
2742 }
2743
2744 #[test]
2745 fn geometric_2_doubles_4_terms() {
2746 let v = evaluate_spec("geometric(1, 2, 4)", &empty_kernel()).unwrap();
2747 if let [Value::F64(a), Value::F64(b), Value::F64(c), Value::F64(d)] = v.as_slice() {
2749 assert!((a - 1.0).abs() < 1e-12);
2750 assert!((b - 2.0).abs() < 1e-12);
2751 assert!((c - 4.0).abs() < 1e-12);
2752 assert!((d - 8.0).abs() < 1e-12);
2753 } else {
2754 panic!("expected 4 f64 values, got {v:?}");
2755 }
2756 }
2757
2758 #[test]
2759 fn linear_starts_half_open_5_points() {
2760 let v = evaluate_spec("linear_starts(0, 100, 5)", &empty_kernel()).unwrap();
2761 if let [
2763 Value::F64(a),
2764 Value::F64(b),
2765 Value::F64(c),
2766 Value::F64(d),
2767 Value::F64(e),
2768 ] = v.as_slice()
2769 {
2770 assert!((a - 0.0).abs() < 1e-12);
2771 assert!((b - 20.0).abs() < 1e-12);
2772 assert!((c - 40.0).abs() < 1e-12);
2773 assert!((d - 60.0).abs() < 1e-12);
2774 assert!((e - 80.0).abs() < 1e-12);
2775 } else {
2776 panic!("got {v:?}");
2777 }
2778 }
2779
2780 #[test]
2781 fn linear_steps_inclusive_5_points() {
2782 let v = evaluate_spec("linear_steps(0, 100, 5)", &empty_kernel()).unwrap();
2783 if let [
2785 Value::F64(a),
2786 Value::F64(b),
2787 Value::F64(c),
2788 Value::F64(d),
2789 Value::F64(e),
2790 ] = v.as_slice()
2791 {
2792 assert!((a - 0.0).abs() < 1e-12);
2793 assert!((b - 25.0).abs() < 1e-12);
2794 assert!((c - 50.0).abs() < 1e-12);
2795 assert!((d - 75.0).abs() < 1e-12);
2796 assert!((e - 100.0).abs() < 1e-12);
2797 } else {
2798 panic!("got {v:?}");
2799 }
2800 }
2801
2802 #[test]
2803 fn log_steps_3_decades() {
2804 let v = evaluate_spec("log_steps(1, 1000, 4)", &empty_kernel()).unwrap();
2805 if let [Value::F64(a), Value::F64(b), Value::F64(c), Value::F64(d)] = v.as_slice() {
2807 assert!((a - 1.0).abs() < 1e-9);
2808 assert!((b - 10.0).abs() < 1e-9);
2809 assert!((c - 100.0).abs() < 1e-9);
2810 assert_eq!(*d, 1000.0);
2811 } else {
2812 panic!("got {v:?}");
2813 }
2814 }
2815
2816 #[test]
2817 fn log_steps_rejects_non_positive_bounds() {
2818 let err = evaluate_spec("log_steps(0, 100, 5)", &empty_kernel())
2819 .unwrap_err()
2820 .to_string();
2821 assert!(
2822 err.contains("log_steps.start: expected a positive number, got 0"),
2823 "{err}"
2824 );
2825 let err = evaluate_spec("log_steps(1, -2, 5)", &empty_kernel())
2826 .unwrap_err()
2827 .to_string();
2828 assert!(
2829 err.contains("log_steps.end: expected a positive number, got -2"),
2830 "{err}"
2831 );
2832 }
2833
2834 #[test]
2837 fn inclusive_steps_end_exactly_at_their_bounds() {
2838 let floats = |spec: &str| -> Vec<f64> {
2839 evaluate_spec(spec, &empty_kernel())
2840 .unwrap()
2841 .iter()
2842 .map(|v| match v {
2843 Value::F64(f) => *f,
2844 other => panic!("{spec}: {other:?}"),
2845 })
2846 .collect()
2847 };
2848 for (spec, start, end) in [
2849 ("log_steps(1, 1000, 4)", 1.0, 1000.0),
2850 ("log_steps(3, 7, 9)", 3.0, 7.0),
2851 ("log_steps(0.1, 0.7, 13)", 0.1, 0.7),
2852 ("log_steps(1000, 1, 4)", 1000.0, 1.0),
2853 ("linear_steps(0, 1, 4)", 0.0, 1.0),
2854 ("linear_steps(0.1, 0.7, 13)", 0.1, 0.7),
2855 ("linear_steps(-3, 1e9, 7)", -3.0, 1e9),
2856 ] {
2857 let v = floats(spec);
2858 assert_eq!(v.first(), Some(&start), "{spec}: {v:?}");
2859 assert_eq!(v.last(), Some(&end), "{spec}: {v:?}");
2860 }
2861 assert_eq!(floats("linear_steps(2, 5, 1)"), vec![2.0]);
2862 assert_eq!(floats("log_steps(2, 5, 1)"), vec![2.0]);
2863 }
2864
2865 #[test]
2869 fn overflow_limits_are_where_the_terms_leave_u64() {
2870 let max = u128::from(u64::MAX);
2871 let (mut a, mut b, mut term) = (1u128, 1u128, 1u64);
2873 while a <= max {
2874 (a, b, term) = (b, a + b, term + 1);
2875 }
2876 assert_eq!(term, 94);
2877 assert_eq!(NamedGenerator::Fib.largest_valid_argument(), Some(term - 1));
2878 assert_eq!(1u128 << 64, max + 1);
2880 assert_eq!(NamedGenerator::Pow2.largest_valid_argument(), Some(64));
2881 let row_overflow = |n: u64| -> Option<u64> {
2884 let mut c = 1u128;
2885 (1..=n).find(|&k| {
2886 c = c * u128::from(n - k + 1) / u128::from(k);
2887 c > max
2888 })
2889 };
2890 let first_row = (0..).find(|&n| row_overflow(n).is_some()).unwrap();
2891 assert_eq!(first_row, 68);
2892 assert_eq!(row_overflow(68), Some(31));
2893 assert_eq!(
2894 NamedGenerator::Binomial.largest_valid_argument(),
2895 Some(first_row - 1)
2896 );
2897 assert_eq!(NamedGenerator::Geometric.largest_valid_argument(), None);
2898 }
2899
2900 #[test]
2904 fn a_call_past_its_limit_is_refused_by_its_first_overflowing_term() {
2905 let k = empty_kernel();
2906 let fib = evaluate_spec("fib(93)", &k).unwrap();
2907 assert_eq!(fib.len(), 93);
2908 assert_eq!(fib[92], Value::U64(12_200_160_415_121_876_738));
2909 let pow2 = evaluate_spec("pow2(64)", &k).unwrap();
2910 assert_eq!(pow2.last(), Some(&Value::U64(1 << 63)));
2911 let row = evaluate_spec("binomial(67)", &k).unwrap();
2912 assert_eq!(row.len(), 68);
2913 assert_eq!(row[33], Value::U64(14_226_520_737_620_288_370));
2914 for (spec, message) in [
2915 (
2916 "fib(94)",
2917 "fib(94): term 94 is past u64::MAX; fib.n is at most 93",
2918 ),
2919 (
2920 "fib(18446744073709551615)",
2921 "fib(18446744073709551615): term 94 is past u64::MAX",
2922 ),
2923 (
2924 "pow2(65)",
2925 "pow2(65): term 65, 2^64, is past u64::MAX; pow2.n is at most 64",
2926 ),
2927 (
2928 "binomial(68)",
2929 "binomial(68): term C(68, 31) is past u64::MAX; binomial.n is at most 67",
2930 ),
2931 (
2932 "binomial(70)",
2933 "binomial(70): term C(70, 28) is past u64::MAX",
2934 ),
2935 (
2936 "binomial(1000000000000)",
2937 "binomial(1000000000000): term C(1000000000000, 2) is past u64::MAX",
2938 ),
2939 ] {
2940 let err = evaluate_spec(spec, &k).unwrap_err().to_string();
2941 assert!(err.contains(message), "{spec}: {err}");
2942 }
2943 }
2944
2945 #[test]
2949 fn a_geometric_factor_out_of_range_is_refused_by_name() {
2950 let k = empty_kernel();
2951 for (spec, message) in [
2952 (
2953 "geometric(1, 0, 4)",
2954 "geometric.factor: expected a positive, finite number, got 0",
2955 ),
2956 (
2957 "geometric(1, -2, 4)",
2958 "geometric.factor: expected a positive, finite number, got -2",
2959 ),
2960 (
2961 "geometric(1, inf, 4)",
2962 "geometric.factor: expected a positive, finite number, got inf",
2963 ),
2964 (
2965 "geometric_until(1, 1, 100)",
2966 "geometric_until.factor: expected a finite number greater than 1, got 1",
2967 ),
2968 (
2969 "geometric_until(1, 0.5, 100)",
2970 "geometric_until.factor: expected a finite number greater than 1, got 0.5",
2971 ),
2972 ] {
2973 let err = evaluate_spec(spec, &k).unwrap_err().to_string();
2974 assert!(err.contains(message), "{spec}: {err}");
2975 }
2976 assert_eq!(
2977 evaluate_spec("geometric(8, 0.5, 3)", &k).unwrap(),
2978 vec![Value::F64(8.0), Value::F64(4.0), Value::F64(2.0)]
2979 );
2980 assert!(
2981 evaluate_spec("geometric_until(0, 2, 100)", &k)
2982 .unwrap()
2983 .is_empty()
2984 );
2985 }
2986
2987 #[test]
2991 fn refused_generator_call_looks_through_enclosing_calls() {
2992 let k = empty_kernel();
2993 let refused = refused_generator_call("concat(1..3, take(fib(94), 2))", &k).unwrap();
2994 assert!(refused.starts_with("fib(94): term 94"), "{refused}");
2995 assert_eq!(refused_generator_call("concat(fib(8), pow2(64))", &k), None);
2996 assert_eq!(refused_generator_call("hash(3)", &k), None);
2997 assert_eq!(refused_generator_call("1, 2, 3", &k), None);
2998 let refused = refused_generator_call("fib(-1)", &k).unwrap();
2999 assert!(
3000 refused.contains("fib.n: expected non-negative integer, got '-1'"),
3001 "{refused}"
3002 );
3003 }
3004
3005 #[test]
3008 fn concat_two_ranges() {
3009 let v = evaluate_spec("concat(1..4, 10..13)", &empty_kernel()).unwrap();
3010 assert_eq!(
3011 v,
3012 vec![
3013 Value::U64(1),
3014 Value::U64(2),
3015 Value::U64(3),
3016 Value::U64(10),
3017 Value::U64(11),
3018 Value::U64(12),
3019 ]
3020 );
3021 }
3022
3023 #[test]
3024 fn unique_dedupes_first_occurrence() {
3025 let v = evaluate_spec("unique(1..4, 3..6)", &empty_kernel()).unwrap();
3026 assert_eq!(
3028 v,
3029 vec![
3030 Value::U64(1),
3031 Value::U64(2),
3032 Value::U64(3),
3033 Value::U64(4),
3034 Value::U64(5),
3035 ]
3036 );
3037 }
3038
3039 #[test]
3040 fn intersect_keeps_only_common_values() {
3041 let v = evaluate_spec("intersect(1..10, 5..15)", &empty_kernel()).unwrap();
3042 assert_eq!(
3043 v,
3044 vec![
3045 Value::U64(5),
3046 Value::U64(6),
3047 Value::U64(7),
3048 Value::U64(8),
3049 Value::U64(9),
3050 ]
3051 );
3052 }
3053
3054 #[test]
3055 fn subtract_drops_values_in_b() {
3056 let v = evaluate_spec("subtract(1..6, 3..5)", &empty_kernel()).unwrap();
3057 assert_eq!(v, vec![Value::U64(1), Value::U64(2), Value::U64(5)]);
3059 }
3060
3061 #[test]
3062 fn interleave_round_robin_two_lists() {
3063 let v = evaluate_spec("interleave(1..4, 10..13)", &empty_kernel()).unwrap();
3064 assert_eq!(
3065 v,
3066 vec![
3067 Value::U64(1),
3068 Value::U64(10),
3069 Value::U64(2),
3070 Value::U64(11),
3071 Value::U64(3),
3072 Value::U64(12),
3073 ]
3074 );
3075 }
3076
3077 #[test]
3078 fn cycle_repeats_n_times() {
3079 let v = evaluate_spec("cycle(1..3, 3)", &empty_kernel()).unwrap();
3080 assert_eq!(
3081 v,
3082 vec![
3083 Value::U64(1),
3084 Value::U64(2),
3085 Value::U64(1),
3086 Value::U64(2),
3087 Value::U64(1),
3088 Value::U64(2),
3089 ]
3090 );
3091 }
3092
3093 #[test]
3094 fn reverse_inverts_list() {
3095 let v = evaluate_spec("reverse(1..5)", &empty_kernel()).unwrap();
3096 assert_eq!(
3097 v,
3098 vec![Value::U64(4), Value::U64(3), Value::U64(2), Value::U64(1),]
3099 );
3100 }
3101
3102 #[test]
3103 fn take_n_takes_prefix() {
3104 let v = evaluate_spec("take(1..10, 3)", &empty_kernel()).unwrap();
3105 assert_eq!(v, vec![Value::U64(1), Value::U64(2), Value::U64(3)]);
3106 }
3107
3108 #[test]
3109 fn skip_n_drops_prefix() {
3110 let v = evaluate_spec("skip(1..6, 2)", &empty_kernel()).unwrap();
3111 assert_eq!(v, vec![Value::U64(3), Value::U64(4), Value::U64(5)]);
3112 }
3113
3114 #[test]
3115 fn unique_composes_with_pow2_and_range() {
3116 let v = evaluate_spec("unique(pow2(8), 1..1000..100)", &empty_kernel()).unwrap();
3117 assert_eq!(v.len(), 17);
3121 assert_eq!(v[0], Value::U64(1));
3122 assert_eq!(v[7], Value::U64(128));
3123 assert_eq!(v[8], Value::U64(101));
3124 }
3125
3126 #[test]
3129 fn bucket_round_robin_3_1_2() {
3130 let v = evaluate_spec(
3132 "bucket(concat('ann', 'scan', 'fetch'), concat(3, 1, 2))",
3133 &empty_kernel(),
3134 )
3135 .unwrap();
3136 let _ = v;
3139 }
3140
3141 #[test]
3142 fn bucket_ratio_prefix_shorthand_round_robin() {
3143 let v = evaluate_spec("bucket(\"3:ann, 1:scan, 2:fetch\")", &empty_kernel()).unwrap();
3144 assert_eq!(v.len(), 6);
3147 let strs: Vec<&str> = v
3148 .iter()
3149 .filter_map(|v| match v {
3150 Value::Str(s) => Some(&**s),
3151 _ => None,
3152 })
3153 .collect();
3154 let counts = strs.iter().fold(
3158 std::collections::HashMap::<&str, usize>::new(),
3159 |mut m, s| {
3160 *m.entry(s).or_insert(0) += 1;
3161 m
3162 },
3163 );
3164 assert_eq!(counts.get("ann"), Some(&3));
3165 assert_eq!(counts.get("scan"), Some(&1));
3166 assert_eq!(counts.get("fetch"), Some(&2));
3167 }
3168
3169 #[test]
3170 fn concat_seq_emits_contiguous_runs() {
3171 let v = evaluate_spec(
3172 "concat_seq(\"2:warmup, 3:bench, 1:cooldown\")",
3173 &empty_kernel(),
3174 )
3175 .unwrap();
3176 let strs: Vec<String> = v
3177 .iter()
3178 .filter_map(|v| match v {
3179 Value::Str(s) => Some(s.to_string()),
3180 _ => None,
3181 })
3182 .collect();
3183 assert_eq!(
3184 strs,
3185 vec!["warmup", "warmup", "bench", "bench", "bench", "cooldown",]
3186 );
3187 }
3188
3189 #[test]
3190 fn interval_seq_evenly_spreads_higher_ratio() {
3191 let v = evaluate_spec("interval_seq(\"3:read, 1:write\")", &empty_kernel()).unwrap();
3192 let strs: Vec<String> = v
3195 .iter()
3196 .filter_map(|v| match v {
3197 Value::Str(s) => Some(s.to_string()),
3198 _ => None,
3199 })
3200 .collect();
3201 assert_eq!(strs.len(), 4);
3202 let writes: Vec<usize> = strs
3203 .iter()
3204 .enumerate()
3205 .filter(|(_, s)| *s == "write")
3206 .map(|(i, _)| i)
3207 .collect();
3208 assert_eq!(writes.len(), 1, "expected exactly one write: {strs:?}");
3209 }
3210
3211 #[test]
3212 fn parse_func_call_recognises_simple_call() {
3213 let (n, a) = parse_func_call("fib(8)").unwrap();
3214 assert_eq!(n, "fib");
3215 assert_eq!(a, "8");
3216 }
3217
3218 #[test]
3219 fn parse_func_call_rejects_non_calls() {
3220 assert!(parse_func_call("1..10").is_none());
3221 assert!(parse_func_call("foo + bar").is_none());
3222 assert!(parse_func_call("f(a) + g(b)").is_none()); }
3224
3225 #[test]
3226 fn split_args_top_level_skips_inner_commas() {
3227 let args = split_args_top_level("a, f(b, c), \"x, y\", 3");
3228 assert_eq!(args, vec!["a", "f(b, c)", "\"x, y\"", "3"]);
3229 }
3230}