1use std::collections::HashMap;
41
42use crate::ast::Value;
43use crate::iteration::comprehension::eval_source::NoneRead;
44use crate::kernel::interp::Lookup;
45use crate::kernel::interp::interpolate_with_lookup;
46
47pub fn evaluate_spec(
72 spec_text: &str,
73 kernel: &dyn Lookup,
74) -> Result<Vec<Value>, crate::dsl::compile::EmbeddingError> {
75 match evaluate_spec_internal(spec_text, kernel) {
76 Ok(values) => Ok(values),
77 Err(SpecError::ReadsNone { reads, .. })
78 if reads.iter().all(|r| matches!(r, NoneRead::BoundNone(_))) =>
79 {
80 Ok(Vec::new())
81 }
82 Err(e) => Err(spec_error(spec_text, String::from(e))),
83 }
84}
85
86pub(crate) fn spec_error(spec_text: &str, message: String) -> crate::dsl::compile::EmbeddingError {
89 if let Some(rest) = message.strip_prefix("interpolation: unresolved placeholder '{")
90 && let Some(end) = rest.find('}')
91 {
92 return crate::dsl::compile::EmbeddingError::UnresolvedPlaceholder {
93 name: rest[..end].to_string(),
94 source: spec_text.to_string(),
95 };
96 }
97 crate::dsl::compile::EmbeddingError::Parse {
98 source: spec_text.to_string(),
99 message,
100 position: None,
101 }
102}
103
104#[derive(Debug, Clone)]
106pub(crate) enum SpecError {
107 ReadsNone {
113 reads: Vec<NoneRead>,
115 message: String,
117 },
118 Failed(String),
120}
121
122impl From<String> for SpecError {
123 fn from(message: String) -> Self {
124 SpecError::Failed(message)
125 }
126}
127
128impl From<SpecError> for String {
129 fn from(e: SpecError) -> Self {
130 match e {
131 SpecError::ReadsNone { message, .. } | SpecError::Failed(message) => message,
132 }
133 }
134}
135
136fn read_name(
140 kernel: &dyn Lookup,
141 name: &str,
142 unbound: impl FnOnce() -> String,
143) -> Result<Value, SpecError> {
144 match kernel.lookup(name) {
145 Some(Value::None) => Err(SpecError::ReadsNone {
146 reads: vec![NoneRead::BoundNone(name.to_string())],
147 message: format!("`{name}` is None"),
148 }),
149 Some(value) => Ok(value),
150 None => Err(SpecError::ReadsNone {
151 reads: vec![NoneRead::Unbound(name.to_string())],
152 message: unbound(),
153 }),
154 }
155}
156
157fn interpolate_reading(text: &str, kernel: &dyn Lookup) -> Result<String, SpecError> {
163 let reads = std::cell::RefCell::new(Vec::new());
164 let interpolated = interpolate_with_lookup(text, |name| match kernel.lookup(name) {
165 Some(Value::None) => {
166 reads
167 .borrow_mut()
168 .push(NoneRead::BoundNone(name.to_string()));
169 None
170 }
171 Some(value) => Some(value.to_display_string()),
172 None => {
173 if is_name_path(name) {
174 reads.borrow_mut().push(NoneRead::Unbound(name.to_string()));
175 }
176 None
177 }
178 });
179 interpolated.map_err(|message| {
180 let mut reads = reads.into_inner();
181 if reads.is_empty() {
182 return SpecError::Failed(message);
183 }
184 reads.sort();
185 reads.dedup();
186 SpecError::ReadsNone { reads, message }
187 })
188}
189
190fn is_name_path(body: &str) -> bool {
194 let mut chars = body.chars();
195 chars
196 .next()
197 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
198 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
199}
200
201fn failure_reading(text: &str, kernel: &dyn Lookup, message: String) -> SpecError {
207 let reads: Vec<NoneRead> = polydat_grammar::refs::referenced_names(text)
208 .into_iter()
209 .filter_map(|name| match kernel.lookup(&name) {
210 None => Some(NoneRead::Unbound(name)),
211 Some(Value::None) => Some(NoneRead::BoundNone(name)),
212 Some(_) => None,
213 })
214 .collect();
215 if reads.is_empty() {
216 SpecError::Failed(message)
217 } else {
218 SpecError::ReadsNone { reads, message }
219 }
220}
221
222pub(crate) fn evaluate_spec_internal(
223 spec_text: &str,
224 kernel: &dyn Lookup,
225) -> Result<Vec<Value>, SpecError> {
226 if let Some(values) = try_eval_all_cursor(spec_text, kernel)? {
227 return Ok(values);
228 }
229 if is_single_bare_ident(spec_text) {
239 let src = spec_text.trim();
240 let v = read_name(kernel, src, || {
241 format!(
242 "comprehension source `{src}` did not resolve to a value — no \
243 wire, const, param, or outer iter-var by that name is in scope \
244 here. If you meant the literal string \"{src}\", quote it: \
245 `\"{src}\"`."
246 )
247 })?;
248 return Ok(
249 match crate::iteration::comprehension::source_values::iteration_interior(&v) {
250 Some(interior) => interior,
251 None => vec![v],
252 },
253 );
254 }
255 let interpolated = interpolate_reading(spec_text, kernel)?;
256 evaluate_interpolated(&interpolated, kernel).map_err(|e| match e {
260 SpecError::Failed(message) => failure_reading(&interpolated, kernel, message),
261 reads_none => reads_none,
262 })
263}
264
265fn evaluate_interpolated(interpolated: &str, kernel: &dyn Lookup) -> Result<Vec<Value>, SpecError> {
268 if let Some(values) = try_eval_bracket_list(interpolated, kernel)? {
273 return Ok(values);
274 }
275 if let Some(values) = try_eval_range(interpolated, kernel.ledger())? {
280 return Ok(values);
281 }
282 if let Some(values) = try_eval_generator(interpolated)? {
284 return Ok(values);
285 }
286 if let Some(values) = try_eval_setop(interpolated, kernel)? {
288 return Ok(values);
289 }
290 if let Some(values) = try_eval_sequencer(interpolated, kernel)? {
293 return Ok(values);
294 }
295 if let Some(values) = try_eval_partition_call(interpolated, kernel, false)? {
299 return Ok(values);
300 }
301 if let Some(values) = try_eval_param_partitions(interpolated, kernel, false)? {
305 return Ok(values);
306 }
307 match crate::dsl::compile::eval_const_expr_for(interpolated, kernel.ledger()) {
308 Ok(v) => Ok(
315 match crate::iteration::comprehension::source_values::iteration_interior(&v) {
316 Some(interior) => interior,
317 None => vec![v],
318 },
319 ),
320 Err(eval_err) => {
332 if looks_like_literal_list(interpolated) {
337 Ok(
340 crate::iteration::comprehension::source_values::strip_string_tokens(
341 interpolated,
342 ),
343 )
344 } else {
345 Err(format!(
346 "for_each clause expression failed to evaluate: {eval_err}\n\
347 spec: {interpolated}\n\
348 If this was meant as a literal list (e.g. `1, 10, 100`), \
349 it should contain only literal values separated by commas. \
350 If it was meant as an expression, fix the underlying \
351 evaluation error."
352 )
353 .into())
354 }
355 }
356 }
357}
358
359fn try_eval_bracket_list(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, SpecError> {
373 let t = text.trim();
374 if !(t.starts_with('[') && t.ends_with(']') && t.len() >= 2) {
375 return Ok(None);
376 }
377 let inner = &t[1..t.len() - 1];
378 if inner.trim().is_empty() {
379 return Ok(Some(Vec::new()));
380 }
381 let mut out = Vec::new();
382 for elem in split_args_top_level(inner) {
383 let elem = elem.trim();
384 let (expr, spread) = if let Some(stripped) = elem.strip_suffix('…') {
386 (stripped.trim(), true)
387 } else if let Some(stripped) = elem.strip_suffix("...") {
388 (stripped.trim(), true)
389 } else {
390 (elem, false)
391 };
392 if expr.is_empty() {
393 return Err("empty element in list comprehension `[...]`"
394 .to_string()
395 .into());
396 }
397 let value = eval_element_value(expr, kernel)?;
398 if spread {
399 match crate::iteration::comprehension::source_values::iteration_interior(&value) {
400 Some(interior) => out.extend(interior),
401 None => {
402 return Err(format!(
403 "list comprehension spread `{expr}…` requires an iterable \
404 source, but `{expr}` resolved to a scalar \
405 {ty:?}. Use `[{expr}]` to pass it as a single element, \
406 or supply a list.",
407 ty = value.port_type(),
408 )
409 .into());
410 }
411 }
412 } else {
413 out.push(value);
414 }
415 }
416 Ok(Some(out))
417}
418
419fn eval_element_value(expr: &str, kernel: &dyn Lookup) -> Result<Value, SpecError> {
427 let e = expr.trim();
428 if is_single_bare_ident(e) {
429 return read_name(kernel, e, || {
430 format!(
431 "list element `{e}` did not resolve to a value — no wire, const, \
432 param, or outer iter-var by that name is in scope here. \
433 If you meant the literal string \"{e}\", quote it: `\"{e}\"`."
434 )
435 });
436 }
437 crate::dsl::compile::eval_const_expr_for(e, kernel.ledger()).map_err(|err| {
438 failure_reading(
439 e,
440 kernel,
441 format!("list element `{e}` failed to evaluate: {err}"),
442 )
443 })
444}
445
446fn is_single_bare_ident(text: &str) -> bool {
452 let t = text.trim();
453 if t == "true" || t == "false" {
454 return false;
455 }
456 let mut chars = t.chars();
457 match chars.next() {
458 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
459 _ => return false,
460 }
461 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
462}
463
464fn looks_like_literal_list(text: &str) -> bool {
480 let trimmed = text.trim();
481 if trimmed.is_empty() {
482 return false;
483 }
484 !trimmed.chars().any(|c| {
485 matches!(
486 c,
487 '(' | ')'
488 | '['
489 | ']'
490 | '{'
491 | '}'
492 | '\''
493 | '"'
494 | '+'
495 | '*'
496 | '/'
497 | '%'
498 | '='
499 | '<'
500 | '>'
501 | '!'
502 | '&'
503 | '|'
504 | '~'
505 | '^'
506 | '?'
507 )
508 })
509}
510
511pub fn pre_evaluate_clause(
522 spec_text: &str,
523 parent_kernel: &dyn Lookup,
524 workload_params: &HashMap<String, String>,
525 probes: &HashMap<String, String>,
526) -> Result<Vec<Value>, String> {
527 if let Some(values) = try_eval_all_cursor(spec_text, parent_kernel)? {
532 return Ok(values);
533 }
534 if is_single_bare_ident(spec_text) {
542 let name = spec_text.trim();
543 if let Some(pv) = probes.get(name) {
544 return Ok(crate::iteration::comprehension::source_values::strip_string_tokens(pv));
545 }
546 if let Some(v) = parent_kernel.lookup(name) {
547 return Ok(
548 match crate::iteration::comprehension::source_values::iteration_interior(&v) {
549 Some(interior) => interior,
550 None => vec![v],
551 },
552 );
553 }
554 if let Some(s) = workload_params.get(name) {
555 return Ok(crate::iteration::comprehension::source_values::strip_string_tokens(s));
556 }
557 return Err(format!(
558 "comprehension source `{name}` did not resolve to a value — no wire, \
559 const, param, or outer iter-var by that name is in scope here. \
560 If you meant the literal string \"{name}\", quote it: `\"{name}\"`."
561 ));
562 }
563 let mut text = spec_text.to_string();
564 for (var, probe_value) in probes {
565 text = text.replace(&format!("{{{var}}}"), probe_value);
566 }
567
568 let interpolated = interpolate_with_lookup(&text, |name| {
569 parent_kernel
570 .lookup(name)
571 .map(|v| v.to_display_string())
572 .or_else(|| workload_params.get(name).cloned())
573 })?;
574
575 if let Some(values) = try_eval_range(&interpolated, parent_kernel.ledger())? {
577 return Ok(values);
578 }
579 if let Some(values) = try_eval_generator(&interpolated)? {
582 return Ok(values);
583 }
584 if let Some(values) = try_eval_setop(&interpolated, parent_kernel)? {
585 return Ok(values);
586 }
587 if let Some(values) = try_eval_sequencer(&interpolated, parent_kernel)? {
588 return Ok(values);
589 }
590 if let Some(values) = try_eval_partition_call(&interpolated, parent_kernel, true)? {
596 return Ok(values);
597 }
598 if let Some(values) = try_eval_param_partitions(&interpolated, parent_kernel, true)? {
601 return Ok(values);
602 }
603 let value_str =
604 match crate::dsl::compile::eval_const_expr_for(&interpolated, parent_kernel.ledger()) {
605 Ok(Value::Str(s)) => s.to_string(),
606 Ok(ref v) if v.as_partition_list().is_some() => {
612 let list = v.as_partition_list().unwrap();
613 return Ok(list
614 .as_slice()
615 .iter()
616 .map(|p| Value::from_partition(*p))
617 .collect());
618 }
619 Ok(other) => return Ok(vec![other]),
620 Err(eval_err) => {
625 if looks_like_literal_list(&interpolated) {
626 interpolated
627 } else {
628 return Err(format!(
629 "for_each clause expression failed to evaluate: {eval_err}\n\
630 spec: {interpolated}\n\
631 If this was meant as a literal list (e.g. `1, 10, 100`), \
632 it should contain only literal values separated by commas. \
633 If it was meant as an expression, fix the underlying \
634 evaluation error."
635 ));
636 }
637 }
638 };
639 Ok(parse_list_with_types(&value_str))
640}
641
642pub fn parse_list_with_types(text: &str) -> Vec<Value> {
647 text.split(',')
648 .map(str::trim)
649 .filter(|s| !s.is_empty())
650 .map(|s| {
651 if let Ok(n) = s.parse::<u64>() {
652 Value::U64(n)
653 } else if let Ok(n) = s.parse::<f64>() {
654 Value::F64(n)
655 } else if s == "true" {
656 Value::Bool(true)
657 } else if s == "false" {
658 Value::Bool(false)
659 } else {
660 Value::Str(s.to_string().into())
661 }
662 })
663 .collect()
664}
665
666fn try_eval_all_cursor(
690 spec_text: &str,
691 kernel: &dyn Lookup,
692) -> Result<Option<Vec<Value>>, SpecError> {
693 use polydat_grammar::comprehension::source::{all_cursor_argument, cursor_extent_names};
694 let Some(cursor_name) = all_cursor_argument(spec_text) else {
695 return Ok(None);
696 };
697 let [start_key, end_key] = cursor_extent_names(cursor_name);
698 let extent = |key: &str| -> Result<u64, SpecError> {
699 match read_name(kernel, key, || {
700 format!(
701 "all({cursor_name}): cursor '{cursor_name}' has no resolvable extent — \
702 check that the cursor is declared at or above this scope and that \
703 its range arguments are init-resolvable. Looked for output '{key}'."
704 )
705 })? {
706 Value::U64(n) => Ok(n),
707 other => Err(format!(
708 "all({cursor_name}): extent output '{key}' is {}, not an ordinal",
709 other.to_display_string()
710 )
711 .into()),
712 }
713 };
714 let start = extent(&start_key)?;
715 let end = extent(&end_key)?;
716
717 if end < start {
718 return Err(format!(
719 "all({cursor_name}): cursor extent end={end} is less than start={start} — \
720 cannot enumerate a negative-extent range."
721 )
722 .into());
723 }
724 Ok(Some((start..end).map(Value::U64).collect()))
725}
726
727fn is_valid_ident(s: &str) -> bool {
728 let mut chars = s.chars();
729 match chars.next() {
730 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
731 _ => return false,
732 }
733 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
734}
735
736fn try_eval_range(
759 text: &str,
760 ledger: &std::sync::Arc<crate::kernel::CompileLedger>,
761) -> Result<Option<Vec<Value>>, String> {
762 let trimmed = text.trim();
763 let chars: Vec<char> = trimmed.chars().collect();
764
765 let mut splits: Vec<(usize, bool)> = Vec::new();
769 let mut depth: i32 = 0;
770 let mut i = 0;
771 while i < chars.len() {
772 let c = chars[i];
773 match c {
774 '(' | '[' | '{' => depth += 1,
775 ')' | ']' | '}' => depth -= 1,
776 '"' | '\'' => {
777 let q = c;
779 i += 1;
780 while i < chars.len() && chars[i] != q {
781 i += 1;
782 }
783 }
784 '.' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '.' => {
785 let inclusive = i + 2 < chars.len() && chars[i + 2] == '=';
786 splits.push((i, inclusive));
787 i += if inclusive { 3 } else { 2 };
788 continue;
789 }
790 _ => {}
791 }
792 i += 1;
793 }
794
795 if splits.is_empty() {
796 return Ok(None);
797 }
798 if splits.len() > 2 {
799 return Err(format!(
800 "range expression '{trimmed}': more than two `..` operators \
801 at top level — expected one of `a..b`, `a..=b`, `a..b..s`, \
802 or `a..=b..s`"
803 ));
804 }
805 if splits.len() == 2 && splits[1].1 {
806 return Err(format!(
807 "range expression '{trimmed}': step delimiter cannot be \
808 `..=` — only the bound separator may be inclusive"
809 ));
810 }
811
812 let inclusive = splits[0].1;
814 let first_end = splits[0].0;
815 let after_first = first_end + if inclusive { 3 } else { 2 };
816 let (start_text, mid_text, step_text) = match splits.len() {
817 1 => {
818 let start_s: String = chars[..first_end].iter().collect();
819 let end_s: String = chars[after_first..].iter().collect();
820 (start_s, end_s, None)
821 }
822 2 => {
823 let mid_end = splits[1].0;
824 let after_mid = mid_end + 2; let start_s: String = chars[..first_end].iter().collect();
826 let mid_s: String = chars[after_first..mid_end].iter().collect();
827 let step_s: String = chars[after_mid..].iter().collect();
828 (start_s, mid_s, Some(step_s))
829 }
830 _ => unreachable!(),
831 };
832
833 let start_val = eval_range_segment(&start_text, "range start", ledger)?;
834 let end_val = eval_range_segment(&mid_text, "range end", ledger)?;
835 let step_val = match step_text {
836 Some(s) => Some(eval_range_segment(&s, "range step", ledger)?),
837 None => None,
838 };
839
840 Ok(Some(expand_range(
841 start_val, end_val, step_val, inclusive, trimmed,
842 )?))
843}
844
845fn eval_range_segment(
846 text: &str,
847 what: &str,
848 ledger: &std::sync::Arc<crate::kernel::CompileLedger>,
849) -> Result<Value, String> {
850 let trimmed = text.trim();
851 if trimmed.is_empty() {
852 return Err(format!("range expression: {what} is empty"));
853 }
854 crate::dsl::compile::eval_const_expr_for(trimmed, ledger)
855 .map_err(|e| format!("range expression: {what} '{trimmed}' did not const-fold — {e}"))
856}
857
858fn expand_range(
862 start: Value,
863 end: Value,
864 step: Option<Value>,
865 inclusive: bool,
866 src: &str,
867) -> Result<Vec<Value>, String> {
868 let any_float = matches!(start, Value::F64(_))
869 || matches!(end, Value::F64(_))
870 || matches!(step, Some(Value::F64(_)));
871
872 let to_f64 = |v: &Value| -> Result<f64, String> {
873 match v {
874 Value::U64(n) => Ok(*n as f64),
875 Value::F64(f) => Ok(*f),
876 other => Err(format!(
877 "range expression '{src}': bound has non-numeric value {other:?}"
878 )),
879 }
880 };
881 let to_i64 = |v: &Value| -> Result<i64, String> {
882 match v {
883 Value::U64(n) => i64::try_from(*n).map_err(|_| {
884 format!("range expression '{src}': bound {n} exceeds signed 64-bit range")
885 }),
886 Value::F64(f) => {
887 if f.fract() == 0.0 && *f >= i64::MIN as f64 && *f <= i64::MAX as f64 {
888 Ok(*f as i64)
889 } else {
890 Err(format!(
891 "range expression '{src}': float bound {f} is not integral; \
892 mix with an explicit float step (e.g. `1.0..10..0.5`) for a float range"
893 ))
894 }
895 }
896 other => Err(format!(
897 "range expression '{src}': bound has non-numeric value {other:?}"
898 )),
899 }
900 };
901
902 if any_float {
903 let s = to_f64(&start)?;
904 let e = to_f64(&end)?;
905 let st = match step.as_ref() {
906 Some(v) => to_f64(v)?,
907 None => 1.0,
908 };
909 if st == 0.0 {
910 return Err(format!("range expression '{src}': step is zero"));
911 }
912 if (e - s).is_sign_positive() && st < 0.0 {
914 return Ok(Vec::new());
915 }
916 if (e - s).is_sign_negative() && st > 0.0 {
917 return Ok(Vec::new());
918 }
919 let mut out = Vec::new();
920 let mut cur = s;
921 let cmp = |x: f64| -> bool {
922 if st > 0.0 {
923 if inclusive {
924 x <= e + 1e-12
925 } else {
926 x < e - 1e-12
927 }
928 } else if inclusive {
929 x >= e - 1e-12
930 } else {
931 x > e + 1e-12
932 }
933 };
934 while cmp(cur) {
935 out.push(Value::F64(cur));
936 cur += st;
937 }
938 return Ok(out);
939 }
940
941 let s = to_i64(&start)?;
943 let e = to_i64(&end)?;
944 let st = match step.as_ref() {
945 Some(v) => to_i64(v)?,
946 None => 1,
947 };
948 if st == 0 {
949 return Err(format!("range expression '{src}': step is zero"));
950 }
951 if st > 0 && s > e {
952 return Ok(Vec::new());
953 }
954 if st < 0 && s < e {
955 return Ok(Vec::new());
956 }
957 let mut out = Vec::new();
958 let mut cur = s;
959 let cmp = |x: i64| -> bool {
960 if st > 0 {
961 if inclusive { x <= e } else { x < e }
962 } else if inclusive {
963 x >= e
964 } else {
965 x > e
966 }
967 };
968 while cmp(cur) {
969 if cur < 0 {
970 return Err(format!(
971 "range expression '{src}': negative value {cur} can't be \
972 represented as Value::U64; use a float range \
973 (mix any bound or step with `.0`) for signed walks"
974 ));
975 }
976 out.push(Value::U64(cur as u64));
977 cur = cur.saturating_add(st);
978 if (st > 0 && cur < s) || (st < 0 && cur > s) {
979 break;
981 }
982 }
983 Ok(out)
984}
985
986fn parse_func_call(text: &str) -> Option<(&str, &str)> {
996 let trimmed = text.trim();
997 if !trimmed.ends_with(')') {
998 return None;
999 }
1000 let open = trimmed.find('(')?;
1001 let name = trimmed[..open].trim();
1002 if name.is_empty() || !is_valid_ident(name) {
1003 return None;
1004 }
1005 let chars: Vec<char> = trimmed.chars().collect();
1008 let mut depth = 0i32;
1009 let mut in_quote: Option<char> = None;
1010 for (i, &c) in chars.iter().enumerate().skip(open) {
1011 match (c, in_quote) {
1012 ('"' | '\'', None) => in_quote = Some(c),
1013 (q, Some(open_q)) if q == open_q => in_quote = None,
1014 ('(', None) => depth += 1,
1015 (')', None) => {
1016 depth -= 1;
1017 if depth == 0 {
1018 if i != chars.len() - 1 {
1019 return None; }
1021 let args: String = chars[open + 1..i].iter().collect();
1022 let _ = args;
1028 let name_slice = &trimmed[..open];
1029 let args_slice = &trimmed[open + 1..trimmed.len() - 1];
1030 return Some((name_slice.trim(), args_slice));
1031 }
1032 }
1033 _ => {}
1034 }
1035 }
1036 None
1037}
1038
1039fn split_args_top_level(args: &str) -> Vec<&str> {
1042 let mut out: Vec<&str> = Vec::new();
1043 let chars: Vec<char> = args.chars().collect();
1044 let bytes_per_char: Vec<usize> = chars.iter().map(|c| c.len_utf8()).collect();
1045 let mut start_byte = 0usize;
1046 let mut byte = 0usize;
1047 let mut depth = 0i32;
1048 let mut in_quote: Option<char> = None;
1049 for (i, &c) in chars.iter().enumerate() {
1050 match (c, in_quote) {
1051 ('"' | '\'', None) => in_quote = Some(c),
1052 (q, Some(open_q)) if q == open_q => in_quote = None,
1053 ('(' | '[' | '{', None) => depth += 1,
1054 (')' | ']' | '}', None) => depth -= 1,
1055 (',', None) if depth == 0 => {
1056 let seg = &args[start_byte..byte];
1057 out.push(seg.trim());
1058 start_byte = byte + bytes_per_char[i];
1059 }
1060 _ => {}
1061 }
1062 byte += bytes_per_char[i];
1063 }
1064 let last = &args[start_byte..];
1065 if !last.trim().is_empty() || !out.is_empty() {
1066 out.push(last.trim());
1067 }
1068 out
1069}
1070
1071fn parse_u64_arg(text: &str, what: &str) -> Result<u64, String> {
1074 let trimmed = text.trim();
1075 trimmed
1076 .parse::<u64>()
1077 .map_err(|_| format!("{what}: expected non-negative integer, got '{trimmed}'"))
1078}
1079
1080fn parse_num_arg(text: &str, what: &str) -> Result<f64, String> {
1084 let trimmed = text.trim();
1085 trimmed
1086 .parse::<f64>()
1087 .map_err(|_| format!("{what}: expected numeric, got '{trimmed}'"))
1088}
1089
1090#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1098pub enum NamedGenerator {
1099 Fib,
1101 FibUntil,
1103 Pow2,
1105 Pow2Until,
1107 Binomial,
1109 Geometric,
1111 GeometricUntil,
1113 LinearStarts,
1115 LinearSteps,
1117 LogSteps,
1119}
1120
1121impl NamedGenerator {
1122 pub fn all() -> impl Iterator<Item = NamedGenerator> {
1124 std::iter::successors(Some(Self::Fib), |g| g.after())
1125 }
1126
1127 fn after(self) -> Option<Self> {
1131 use NamedGenerator as G;
1132 match self {
1133 G::Fib => Some(G::FibUntil),
1134 G::FibUntil => Some(G::Pow2),
1135 G::Pow2 => Some(G::Pow2Until),
1136 G::Pow2Until => Some(G::Binomial),
1137 G::Binomial => Some(G::Geometric),
1138 G::Geometric => Some(G::GeometricUntil),
1139 G::GeometricUntil => Some(G::LinearStarts),
1140 G::LinearStarts => Some(G::LinearSteps),
1141 G::LinearSteps => Some(G::LogSteps),
1142 G::LogSteps => None,
1143 }
1144 }
1145
1146 pub fn signature(self) -> &'static str {
1148 use NamedGenerator as G;
1149 match self {
1150 G::Fib => "fib(n)",
1151 G::FibUntil => "fib_until(max)",
1152 G::Pow2 => "pow2(n)",
1153 G::Pow2Until => "pow2_until(max)",
1154 G::Binomial => "binomial(n)",
1155 G::Geometric => "geometric(start, factor, n)",
1156 G::GeometricUntil => "geometric_until(start, factor, max)",
1157 G::LinearStarts => "linear_starts(start, end, n)",
1158 G::LinearSteps => "linear_steps(start, end, n)",
1159 G::LogSteps => "log_steps(start, end, n)",
1160 }
1161 }
1162
1163 pub fn name(self) -> &'static str {
1165 let sig = self.signature();
1166 &sig[..sig.find('(').unwrap_or(sig.len())]
1167 }
1168
1169 fn params(self) -> Vec<&'static str> {
1171 let sig = self.signature();
1172 let inner = &sig[self.name().len() + 1..sig.len() - 1];
1173 inner.split(',').map(str::trim).collect()
1174 }
1175
1176 pub fn from_name(name: &str) -> Option<Self> {
1178 Self::all().find(|g| g.name() == name)
1179 }
1180
1181 pub fn of_call(text: &str) -> Option<Self> {
1184 parse_func_call(text).and_then(|(name, _)| Self::from_name(name))
1185 }
1186
1187 pub fn yields_integers(self) -> bool {
1190 use NamedGenerator as G;
1191 match self {
1192 G::Fib | G::FibUntil | G::Pow2 | G::Pow2Until | G::Binomial => true,
1193 G::Geometric | G::GeometricUntil | G::LinearStarts | G::LinearSteps | G::LogSteps => {
1194 false
1195 }
1196 }
1197 }
1198
1199 fn expand(self, args: &[&str]) -> Result<Vec<Value>, String> {
1201 use NamedGenerator as G;
1202 let params = self.params();
1203 if args.len() != params.len() {
1204 return Err(format!(
1205 "{}: expected {} argument{}, got {}",
1206 self.signature(),
1207 params.len(),
1208 if params.len() == 1 { "" } else { "s" },
1209 args.len()
1210 ));
1211 }
1212 let what = |i: usize| format!("{}.{}", self.name(), params[i]);
1213 let int = |i: usize| parse_u64_arg(args[i], &what(i));
1214 let num = |i: usize| parse_num_arg(args[i], &what(i));
1215 let call = format!("{}({})", self.name(), args.join(", "));
1216 match self {
1217 G::Fib => generate_fib_n(int(0)?, &call),
1218 G::FibUntil => Ok(generate_fib_until(int(0)?)),
1219 G::Pow2 => generate_pow2_n(int(0)?, &call),
1220 G::Pow2Until => Ok(generate_pow2_until(int(0)?)),
1221 G::Binomial => generate_binomial(int(0)?, &call),
1222 G::Geometric => {
1223 let (start, factor) = (num(0)?, num(1)?);
1224 if !(factor.is_finite() && factor > 0.0) {
1225 return Err(format!(
1226 "{}: expected a positive, finite number, got {factor}",
1227 what(1)
1228 ));
1229 }
1230 generate_geometric(start, factor, int(2)?)
1231 }
1232 G::GeometricUntil => {
1233 let (start, factor) = (num(0)?, num(1)?);
1234 if !(factor.is_finite() && factor > 1.0) {
1235 return Err(format!(
1236 "{}: expected a finite number greater than 1, got {factor}",
1237 what(1)
1238 ));
1239 }
1240 Ok(generate_geometric_until(start, factor, num(2)?))
1241 }
1242 G::LinearStarts => generate_linear_points(num(0)?, num(1)?, int(2)?, false),
1243 G::LinearSteps => generate_linear_points(num(0)?, num(1)?, int(2)?, true),
1244 G::LogSteps => {
1245 let (start, end) = (num(0)?, num(1)?);
1246 for (i, bound) in [(0, start), (1, end)] {
1247 if bound.is_nan() || bound <= 0.0 {
1248 return Err(format!(
1249 "{}: expected a positive number, got {bound}",
1250 what(i)
1251 ));
1252 }
1253 }
1254 generate_log_steps(start, end, int(2)?)
1255 }
1256 }
1257 }
1258
1259 pub fn largest_valid_argument(self) -> Option<u64> {
1263 use NamedGenerator as G;
1264 match self {
1265 G::Fib => Some(FIB_MAX_N),
1266 G::Pow2 => Some(POW2_MAX_N),
1267 G::Binomial => Some(BINOMIAL_MAX_N),
1268 _ => None,
1269 }
1270 }
1271}
1272
1273const FIB_MAX_N: u64 = 93;
1276const POW2_MAX_N: u64 = 64;
1279const BINOMIAL_MAX_N: u64 = 67;
1283
1284fn try_eval_generator(text: &str) -> Result<Option<Vec<Value>>, String> {
1289 let Some((name, args)) = parse_func_call(text) else {
1290 return Ok(None);
1291 };
1292 let Some(generator) = NamedGenerator::from_name(name) else {
1293 return Ok(None);
1294 };
1295 generator.expand(&split_args_top_level(args)).map(Some)
1296}
1297
1298pub fn refused_generator_call(text: &str, kernel: &dyn Lookup) -> Option<String> {
1307 let interpolated = crate::kernel::interp::interpolate_with_lookup(text, |name| {
1308 kernel.lookup(name).map(|v| v.to_display_string())
1309 })
1310 .ok()?;
1311 first_refused_call(&interpolated)
1312}
1313
1314fn first_refused_call(text: &str) -> Option<String> {
1315 let (name, args) = parse_func_call(text)?;
1316 let args = split_args_top_level(args);
1317 match NamedGenerator::from_name(name) {
1318 Some(generator) => generator.expand(&args).err(),
1319 None => args.iter().find_map(|a| first_refused_call(a)),
1320 }
1321}
1322
1323fn generate_fib_n(n: u64, call: &str) -> Result<Vec<Value>, String> {
1328 if n > FIB_MAX_N {
1329 return Err(format!(
1330 "{call}: term {} is past u64::MAX; fib.n is at most {FIB_MAX_N}",
1331 FIB_MAX_N + 1
1332 ));
1333 }
1334 let mut out = Vec::with_capacity(n as usize);
1335 let (mut a, mut b): (u128, u128) = (1, 1);
1338 for _ in 0..n {
1339 out.push(Value::U64(a as u64));
1340 (a, b) = (b, a + b);
1341 }
1342 Ok(out)
1343}
1344
1345fn generate_fib_until(max: u64) -> Vec<Value> {
1350 let mut out = Vec::new();
1351 let (mut a, mut b): (u128, u128) = (1, 1);
1352 while a <= u128::from(max) {
1353 out.push(Value::U64(a as u64));
1354 (a, b) = (b, a + b);
1355 }
1356 out
1357}
1358
1359fn generate_pow2_n(n: u64, call: &str) -> Result<Vec<Value>, String> {
1362 if n > POW2_MAX_N {
1363 return Err(format!(
1364 "{call}: term {}, 2^64, is past u64::MAX; pow2.n is at most {POW2_MAX_N}",
1365 POW2_MAX_N + 1
1366 ));
1367 }
1368 Ok((0..n).map(|i| Value::U64(1u64 << i)).collect())
1369}
1370
1371fn generate_pow2_until(max: u64) -> Vec<Value> {
1373 let mut out = Vec::new();
1374 let mut v: u64 = 1;
1375 loop {
1376 if v > max {
1377 break;
1378 }
1379 out.push(Value::U64(v));
1380 v = match v.checked_mul(2) {
1381 Some(x) => x,
1382 None => break,
1383 };
1384 }
1385 out
1386}
1387
1388fn generate_geometric(start: f64, factor: f64, n: u64) -> Result<Vec<Value>, String> {
1390 let mut out = crate::derive_support::try_buffer_for(n, "geometric(start, factor, n)")?;
1391 let mut v = start;
1392 for _ in 0..n {
1393 out.push(Value::F64(v));
1394 v *= factor;
1395 }
1396 Ok(out)
1397}
1398
1399fn generate_geometric_until(start: f64, factor: f64, max: f64) -> Vec<Value> {
1403 let mut out = Vec::new();
1404 let mut v = start;
1405 if start.is_nan() || start <= 0.0 {
1406 return out;
1407 }
1408 while v <= max {
1409 out.push(Value::F64(v));
1410 v *= factor;
1411 }
1412 out
1413}
1414
1415fn generate_binomial(n: u64, call: &str) -> Result<Vec<Value>, String> {
1422 let mut out = Vec::with_capacity(n.min(BINOMIAL_MAX_N) as usize + 1);
1423 let mut c: u128 = 1;
1425 out.push(Value::U64(1));
1426 for k in 1..=n {
1427 c = c * u128::from(n - k + 1) / u128::from(k);
1428 if c > u128::from(u64::MAX) {
1429 return Err(format!(
1430 "{call}: term C({n}, {k}) is past u64::MAX; binomial.n is at most \
1431 {BINOMIAL_MAX_N}"
1432 ));
1433 }
1434 out.push(Value::U64(c as u64));
1435 }
1436 Ok(out)
1437}
1438
1439fn try_eval_partition_call(
1463 text: &str,
1464 kernel: &dyn Lookup,
1465 probe: bool,
1466) -> Result<Option<Vec<Value>>, SpecError> {
1467 let Some((name, args)) = parse_func_call(text) else {
1468 return Ok(None);
1469 };
1470 let arg_list = split_args_top_level(args);
1471 match name {
1472 "subdivide" => {
1473 if arg_list.len() != 2 {
1474 return Err(format!(
1475 "subdivide(p, n): expected 2 arguments (a partition and a count), got {}",
1476 arg_list.len()
1477 )
1478 .into());
1479 }
1480 let src = arg_list[0].trim();
1481 let n = parse_u64_arg(arg_list[1], "subdivide.n")?;
1482 if probe && kernel.lookup(src).is_none() {
1483 return Ok(Some(vec![placeholder_partition()]));
1488 }
1489 let value = read_name(kernel, src, || {
1490 format!("subdivide({src}, {n}): `{src}` is not bound in scope")
1491 })?;
1492 let Some(p) = value.as_partition().copied() else {
1493 return Err(format!(
1494 "subdivide({src}, {n}): `{src}` resolved to {} — expected a \
1495 Partition value (an iter-var from `for: \"p in partitions(...)\"` \
1496 or a cursor's `.cursor` projection)",
1497 value.to_display_string(),
1498 )
1499 .into());
1500 };
1501 let subs = crate::iteration::cursor_partition::subdivide_partition(&p, n)?;
1502 Ok(Some(subs.into_iter().map(Value::from_partition).collect()))
1503 }
1504 "partitions" => {
1515 if arg_list.is_empty() || arg_list.len() > 2 {
1516 return Err(format!(
1517 "partitions(spec, [extent]): expected 1 or 2 arguments, got {}",
1518 arg_list.len(),
1519 )
1520 .into());
1521 }
1522 let spec = resolve_partition_spec_arg(arg_list[0], kernel)?;
1523 let extent = match arg_list.get(1) {
1524 Some(a) => parse_u64_arg(a, "partitions.extent")?,
1525 None => 100,
1526 };
1527 Ok(Some(desugar_partition_spec(
1528 &spec,
1529 extent,
1530 "comprehension source `partitions(...)`",
1531 )?))
1532 }
1533 "profile_partitions" => {
1544 #[cfg(not(feature = "vectordata"))]
1545 {
1546 Err("profile_partitions requires the `vectordata` Cargo feature"
1547 .to_string()
1548 .into())
1549 }
1550
1551 #[cfg(feature = "vectordata")]
1552 {
1553 if arg_list.len() != 2 {
1554 return Err(format!(
1555 "profile_partitions(dataset, pattern): expected 2 arguments, got {}",
1556 arg_list.len()
1557 )
1558 .into());
1559 }
1560 let strip = |s: &str| -> String {
1563 let s = s.trim();
1564 let b = s.as_bytes();
1565 if b.len() >= 2 && (b[0] == b'\'' || b[0] == b'"') && b[b.len() - 1] == b[0] {
1566 s[1..s.len() - 1].to_string()
1567 } else {
1568 s.to_string()
1569 }
1570 };
1571 let dataset = strip(arg_list[0]);
1572 let pattern = strip(arg_list[1]);
1573 match crate::library::vectors::load_dataset_group(&dataset) {
1574 Ok(group) => {
1575 let parts =
1576 crate::library::vectors::build_profile_partitions(&group, &pattern);
1577 Ok(Some(parts.into_iter().map(Value::from_partition).collect()))
1578 }
1579 Err(_) => {
1580 Ok(Some(vec![placeholder_partition()]))
1584 }
1585 }
1586 }
1587 }
1588 _ => Ok(None),
1589 }
1590}
1591
1592fn placeholder_partition() -> Value {
1596 Value::from_partition(crate::iteration::cursor_partition::Partition {
1597 idx: 0,
1598 count: 1,
1599 start_ord: 0,
1600 end_ord: 1,
1601 start_pct: 0.0,
1602 end_pct: 100.0,
1603 base_extent: 1,
1604 })
1605}
1606
1607fn try_eval_param_partitions(
1630 text: &str,
1631 kernel: &dyn Lookup,
1632 probe: bool,
1633) -> Result<Option<Vec<Value>>, SpecError> {
1634 let Some(ident) = text.trim().strip_suffix(".partitions") else {
1635 return Ok(None);
1636 };
1637 let ident = ident.trim();
1638 if !is_single_bare_ident(ident) {
1639 return Ok(None);
1640 }
1641 if probe && kernel.lookup(ident).is_none() {
1642 return Ok(Some(vec![placeholder_partition()]));
1645 }
1646 let value = read_name(kernel, ident, || {
1647 format!("comprehension source `{ident}.partitions`: `{ident}` is not bound in scope")
1648 })?;
1649 if let Some(list) = value.as_partition_list() {
1651 return Ok(Some(
1652 list.as_slice()
1653 .iter()
1654 .map(|p| Value::from_partition(*p))
1655 .collect(),
1656 ));
1657 }
1658 let Value::Str(spec) = &value else {
1661 return Err(format!(
1662 "comprehension source `{ident}.partitions`: `{ident}` resolved to \
1663 {} — expected a partition-spec string (a workload param such as \
1664 `cursor=linear:4`) or a PartitionList.",
1665 value.to_display_string(),
1666 )
1667 .into());
1668 };
1669 Ok(Some(desugar_partition_spec(
1670 spec,
1671 100,
1672 &format!("comprehension source `{ident}.partitions`"),
1673 )?))
1674}
1675
1676fn desugar_partition_spec(spec: &str, extent: u64, ctx: &str) -> Result<Vec<Value>, String> {
1684 let parsed = crate::iteration::cursor_partition::parse(spec)
1685 .map_err(|e| format!("{ctx}: bad spec `{spec}`: {e}"))?;
1686 let parts = crate::iteration::cursor_partition::resolve(&parsed, 0, extent)
1687 .map_err(|e| format!("{ctx}: resolve failed for `{spec}`: {e}"))?;
1688 Ok(parts.into_iter().map(Value::from_partition).collect())
1689}
1690
1691fn resolve_partition_spec_arg(arg: &str, kernel: &dyn Lookup) -> Result<String, SpecError> {
1696 let a = arg.trim();
1697 if a.len() >= 2
1698 && ((a.starts_with('"') && a.ends_with('"')) || (a.starts_with('\'') && a.ends_with('\'')))
1699 {
1700 return Ok(a[1..a.len() - 1].to_string());
1701 }
1702 if is_single_bare_ident(a) {
1703 return match read_name(kernel, a, || {
1704 format!("partitions(...): `{a}` did not resolve to a spec string in scope")
1705 })? {
1706 Value::Str(s) => Ok(s.to_string()),
1707 other => Err(format!(
1708 "partitions(...): `{a}` resolved to {} — expected a spec string",
1709 other.to_display_string(),
1710 )
1711 .into()),
1712 };
1713 }
1714 Ok(a.to_string())
1715}
1716
1717fn generate_linear_points(
1728 start: f64,
1729 end: f64,
1730 n: u64,
1731 inclusive: bool,
1732) -> Result<Vec<Value>, String> {
1733 let denom = if inclusive {
1734 (n.saturating_sub(1)).max(1) as f64
1735 } else {
1736 n as f64
1737 };
1738 let step = (end - start) / denom;
1739 let mut out = crate::derive_support::try_buffer_for(n, "linear points")?;
1743 out.extend((0..n).map(|i| Value::F64(start + step * i as f64)));
1744 if inclusive && n >= 2 {
1747 out[n as usize - 1] = Value::F64(end);
1748 }
1749 Ok(out)
1750}
1751
1752fn generate_log_steps(start: f64, end: f64, n: u64) -> Result<Vec<Value>, String> {
1756 if n == 0 {
1757 return Ok(Vec::new());
1758 }
1759 if n == 1 {
1760 return Ok(vec![Value::F64(start)]);
1761 }
1762 let log_s = start.ln();
1763 let log_e = end.ln();
1764 let step = (log_e - log_s) / (n - 1) as f64;
1765 let mut out = crate::derive_support::try_buffer_for(n, "log_steps(start, end, n)")?;
1766 out.push(Value::F64(start));
1767 out.extend((1..n - 1).map(|i| Value::F64((log_s + step * i as f64).exp())));
1768 out.push(Value::F64(end));
1769 Ok(out)
1770}
1771
1772fn try_eval_setop(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, SpecError> {
1782 let Some((name, args)) = parse_func_call(text) else {
1783 return Ok(None);
1784 };
1785 let arg_texts = split_args_top_level(args);
1786 let recursively_evaluate = |t: &str| evaluate_spec_internal(t, kernel);
1787 match name {
1788 "concat" => {
1789 let mut out = Vec::new();
1790 for a in &arg_texts {
1791 out.extend(recursively_evaluate(a)?);
1792 }
1793 Ok(Some(out))
1794 }
1795 "unique" => {
1796 let mut out: Vec<Value> = Vec::new();
1797 for a in &arg_texts {
1798 for v in recursively_evaluate(a)? {
1799 if !out.contains(&v) {
1800 out.push(v);
1801 }
1802 }
1803 }
1804 Ok(Some(out))
1805 }
1806 "intersect" => {
1807 if arg_texts.is_empty() {
1808 return Ok(Some(Vec::new()));
1809 }
1810 let first = recursively_evaluate(arg_texts[0])?;
1811 let mut out: Vec<Value> = Vec::new();
1812 for v in first {
1813 let mut in_all = true;
1814 for a in &arg_texts[1..] {
1815 let other = recursively_evaluate(a)?;
1816 if !other.contains(&v) {
1817 in_all = false;
1818 break;
1819 }
1820 }
1821 if in_all && !out.contains(&v) {
1822 out.push(v);
1823 }
1824 }
1825 Ok(Some(out))
1826 }
1827 "subtract" => {
1828 if arg_texts.len() != 2 {
1829 return Err(
1830 format!("subtract(a, b): expected 2 args, got {}", arg_texts.len()).into(),
1831 );
1832 }
1833 let a = recursively_evaluate(arg_texts[0])?;
1834 let b = recursively_evaluate(arg_texts[1])?;
1835 Ok(Some(a.into_iter().filter(|v| !b.contains(v)).collect()))
1836 }
1837 "interleave" => {
1838 let lists: Result<Vec<Vec<Value>>, SpecError> =
1839 arg_texts.iter().map(|a| recursively_evaluate(a)).collect();
1840 let lists = lists?;
1841 let mut out = Vec::new();
1842 let max_len = lists.iter().map(|l| l.len()).max().unwrap_or(0);
1843 for i in 0..max_len {
1844 for l in &lists {
1845 if let Some(v) = l.get(i) {
1846 out.push(v.clone());
1847 }
1848 }
1849 }
1850 Ok(Some(out))
1851 }
1852 "cycle" => {
1853 if arg_texts.len() != 2 {
1854 return Err(
1855 format!("cycle(a, n): expected 2 args, got {}", arg_texts.len()).into(),
1856 );
1857 }
1858 let a = recursively_evaluate(arg_texts[0])?;
1859 let n = parse_u64_arg(arg_texts[1], "cycle.n")?;
1860 let total = (a.len() as u64).checked_mul(n).ok_or_else(|| {
1861 format!(
1862 "cycle(a, n): {} values repeated {n} times is more than can be counted",
1863 a.len()
1864 )
1865 })?;
1866 let mut out = crate::derive_support::try_buffer_for(total, "cycle(a, n)")?;
1867 for _ in 0..n {
1868 out.extend(a.iter().cloned());
1869 }
1870 Ok(Some(out))
1871 }
1872 "reverse" => {
1873 if arg_texts.len() != 1 {
1874 return Err(format!("reverse(a): expected 1 arg, got {}", arg_texts.len()).into());
1875 }
1876 let mut a = recursively_evaluate(arg_texts[0])?;
1877 a.reverse();
1878 Ok(Some(a))
1879 }
1880 "take" => {
1881 if arg_texts.len() != 2 {
1882 return Err(format!("take(a, n): expected 2 args, got {}", arg_texts.len()).into());
1883 }
1884 let a = recursively_evaluate(arg_texts[0])?;
1885 let n = parse_u64_arg(arg_texts[1], "take.n")?;
1886 Ok(Some(a.into_iter().take(n as usize).collect()))
1887 }
1888 "skip" => {
1889 if arg_texts.len() != 2 {
1890 return Err(format!("skip(a, n): expected 2 args, got {}", arg_texts.len()).into());
1891 }
1892 let a = recursively_evaluate(arg_texts[0])?;
1893 let n = parse_u64_arg(arg_texts[1], "skip.n")?;
1894 Ok(Some(a.into_iter().skip(n as usize).collect()))
1895 }
1896 _ => Ok(None),
1897 }
1898}
1899
1900fn try_eval_sequencer(text: &str, kernel: &dyn Lookup) -> Result<Option<Vec<Value>>, SpecError> {
1916 let Some((name, args)) = parse_func_call(text) else {
1917 return Ok(None);
1918 };
1919 if !matches!(name, "bucket" | "concat_seq" | "interval_seq") {
1920 return Ok(None);
1921 }
1922 let arg_texts = split_args_top_level(args);
1923
1924 let (items, ratios): (Vec<Value>, Vec<usize>) = match arg_texts.len() {
1929 1 => parse_ratio_prefix_shorthand(arg_texts[0])?,
1930 2 => {
1931 let items = evaluate_spec_internal(arg_texts[0], kernel)?;
1932 let raw_ratios = evaluate_spec_internal(arg_texts[1], kernel)?;
1933 let ratios: Result<Vec<usize>, String> = raw_ratios
1934 .iter()
1935 .map(|v| match v {
1936 Value::U64(n) => Ok(*n as usize),
1937 other => Err(format!(
1938 "{name}: ratio must be non-negative integer, got {other:?}"
1939 )),
1940 })
1941 .collect();
1942 (items, ratios?)
1943 }
1944 _ => {
1945 return Err(format!(
1946 "{name}: expected `(items, ratios)` or `(\"r1:item1, r2:item2, ...\")`; got {} args",
1947 arg_texts.len()
1948 )
1949 .into());
1950 }
1951 };
1952
1953 if items.len() != ratios.len() {
1954 return Err(format!(
1955 "{name}: items.len() ({}) != ratios.len() ({})",
1956 items.len(),
1957 ratios.len(),
1958 )
1959 .into());
1960 }
1961 let total = ratios
1965 .iter()
1966 .try_fold(0usize, |acc, &r| acc.checked_add(r))
1967 .ok_or_else(|| format!("{name}: the ratios sum past what can be counted"))?;
1968 let out = crate::derive_support::try_buffer_for(total as u64, name)?;
1969 Ok(Some(match name {
1970 "bucket" => seq_bucket(&items, &ratios, total, out),
1971 "concat_seq" => seq_concat(&items, &ratios, out),
1972 "interval_seq" => seq_interval(&items, &ratios, total, out),
1973 _ => unreachable!(),
1974 }))
1975}
1976
1977fn parse_ratio_prefix_shorthand(text: &str) -> Result<(Vec<Value>, Vec<usize>), String> {
1982 let stripped = text
1985 .trim()
1986 .trim_start_matches(['"', '\''])
1987 .trim_end_matches(['"', '\'']);
1988 let mut items = Vec::new();
1989 let mut ratios = Vec::new();
1990 for part in stripped.split(',') {
1991 let part = part.trim();
1992 if part.is_empty() {
1993 continue;
1994 }
1995 let (r, i) = part
1996 .split_once(':')
1997 .ok_or_else(|| format!("ratio-prefix shorthand: missing ':' in '{part}'"))?;
1998 let ratio: usize = r.trim().parse().map_err(|_| {
1999 format!("ratio-prefix shorthand: ratio '{r}' is not a non-negative integer")
2000 })?;
2001 ratios.push(ratio);
2002 items.push(parse_one_value(i.trim()));
2003 }
2004 Ok((items, ratios))
2005}
2006
2007fn parse_one_value(s: &str) -> Value {
2008 if let Ok(n) = s.parse::<u64>() {
2009 return Value::U64(n);
2010 }
2011 if let Ok(f) = s.parse::<f64>() {
2012 return Value::F64(f);
2013 }
2014 if s == "true" {
2015 return Value::Bool(true);
2016 }
2017 if s == "false" {
2018 return Value::Bool(false);
2019 }
2020 Value::Str(s.to_string().into())
2021}
2022
2023fn seq_bucket(items: &[Value], ratios: &[usize], total: usize, mut out: Vec<Value>) -> Vec<Value> {
2027 let mut remaining: Vec<usize> = ratios.to_vec();
2028 while out.len() < total {
2029 let mut emitted_any = false;
2030 for (i, item) in items.iter().enumerate() {
2031 if remaining[i] > 0 {
2032 out.push(item.clone());
2033 remaining[i] -= 1;
2034 emitted_any = true;
2035 }
2036 }
2037 if !emitted_any {
2038 break;
2039 }
2040 }
2041 out
2042}
2043
2044fn seq_concat(items: &[Value], ratios: &[usize], mut out: Vec<Value>) -> Vec<Value> {
2047 for (item, &r) in items.iter().zip(ratios.iter()) {
2048 for _ in 0..r {
2049 out.push(item.clone());
2050 }
2051 }
2052 out
2053}
2054
2055fn seq_interval(
2061 items: &[Value],
2062 ratios: &[usize],
2063 total: usize,
2064 mut out: Vec<Value>,
2065) -> Vec<Value> {
2066 if total == 0 {
2067 return out;
2068 }
2069 let mut emitted: Vec<usize> = vec![0; items.len()];
2070 for slot in 0..total {
2071 let mut best = 0usize;
2074 let mut best_deficit: f64 = f64::NEG_INFINITY;
2075 for i in 0..items.len() {
2076 let target = ratios[i] as f64 * (slot + 1) as f64 / total as f64;
2077 let deficit = target - emitted[i] as f64;
2078 if deficit > best_deficit {
2079 best_deficit = deficit;
2080 best = i;
2081 }
2082 }
2083 out.push(items[best].clone());
2084 emitted[best] += 1;
2085 }
2086 out
2087}
2088
2089pub fn value_to_polydat_type_name(v: &Value) -> &'static str {
2099 v.port_type().to_keyword()
2100}
2101
2102#[cfg(test)]
2113mod tests {
2114 use super::*;
2115 use crate::kernel::PolydatKernel;
2116 use crate::kernel::interp::interpolate_via_kernel;
2117
2118 fn h(pairs: &[(&str, &str)]) -> HashMap<String, String> {
2119 pairs
2120 .iter()
2121 .map(|(k, v)| (k.to_string(), v.to_string()))
2122 .collect()
2123 }
2124
2125 fn interpolate(
2126 text: &str,
2127 bindings: &HashMap<String, String>,
2128 workload_params: &HashMap<String, String>,
2129 ) -> Result<String, String> {
2130 interpolate_with_lookup(text, |name| {
2131 bindings
2132 .get(name)
2133 .or_else(|| workload_params.get(name))
2134 .cloned()
2135 })
2136 }
2137
2138 #[test]
2139 fn flat_substitution() {
2140 let params = h(&[("dataset", "example"), ("prefix", "label")]);
2141 let out = interpolate("matching('{dataset}', '{prefix}')", &h(&[]), ¶ms).unwrap();
2142 assert_eq!(out, "matching('example', 'label')");
2143 }
2144
2145 #[test]
2146 fn bindings_shadow_params() {
2147 let params = h(&[("profile", "default")]);
2148 let bindings = h(&[("profile", "label_07")]);
2149 let out = interpolate("vec_{profile}", &bindings, ¶ms).unwrap();
2150 assert_eq!(out, "vec_label_07");
2151 }
2152
2153 #[test]
2154 fn nested_placeholder_resolves_inside_out() {
2155 let params = h(&[("k_1_limits", "1,2,4,8"), ("k_10_limits", "10,20,30")]);
2156 let bindings = h(&[("k", "1")]);
2157 let out = interpolate("{k_{k}_limits}", &bindings, ¶ms).unwrap();
2158 assert_eq!(out, "1,2,4,8");
2159 }
2160
2161 #[test]
2162 fn deeply_nested() {
2163 let params = h(&[("a_b_c", "WIN")]);
2164 let bindings = h(&[("x", "a"), ("y", "b"), ("z", "c")]);
2165 let out = interpolate("{{x}_{y}_{z}}", &bindings, ¶ms).unwrap();
2166 assert_eq!(out, "WIN");
2167 }
2168
2169 #[test]
2170 fn escape_emits_literal_brace() {
2171 let out = interpolate("\\{not_a_var\\}", &h(&[]), &h(&[])).unwrap();
2172 assert_eq!(out, "{not_a_var}");
2173 }
2174
2175 #[test]
2176 fn escape_inside_otherwise_resolved_text() {
2177 let params = h(&[("x", "1")]);
2178 let out = interpolate("a={x} literal=\\{x\\}", &h(&[]), ¶ms).unwrap();
2179 assert_eq!(out, "a=1 literal={x}");
2180 }
2181
2182 #[test]
2183 fn unresolved_is_hard_error() {
2184 let err = interpolate("hello {nope}", &h(&[]), &h(&[])).unwrap_err();
2185 assert!(err.contains("unresolved"));
2186 assert!(err.contains("nope"));
2187 }
2188
2189 #[test]
2190 fn empty_placeholder_rejected() {
2191 let err = interpolate("a{}b", &h(&[]), &h(&[])).unwrap_err();
2192 assert!(err.contains("empty"));
2193 }
2194
2195 #[test]
2196 fn unmatched_brace_rejected() {
2197 let err = interpolate("a {x", &h(&[]), &h(&[])).unwrap_err();
2198 assert!(err.contains("unmatched"));
2199 }
2200
2201 #[test]
2202 fn idempotent_when_no_placeholders() {
2203 let out = interpolate("plain text", &h(&[]), &h(&[])).unwrap();
2204 assert_eq!(out, "plain text");
2205 }
2206
2207 #[test]
2208 fn resolved_value_with_braces_does_not_re_expand() {
2209 let params = h(&[("greeting", "hello {planet}")]);
2210 let err = interpolate("{greeting}", &h(&[]), ¶ms).unwrap_err();
2211 assert!(err.contains("planet"));
2212 }
2213
2214 #[test]
2215 fn cyclic_placeholders_hit_round_cap() {
2216 let params = h(&[("a", "{b}"), ("b", "{a}")]);
2217 let err = interpolate("{a}", &h(&[]), ¶ms).unwrap_err();
2218 assert!(err.contains("did not stabilize") || err.contains("rounds"));
2219 }
2220
2221 #[test]
2222 fn kernel_resolves_via_get_constant() {
2223 let kernel =
2224 crate::dsl::compile::compile_polydat_interpreter("const dataset := \"example\"\n")
2225 .unwrap();
2226 let out = interpolate_via_kernel("path/{dataset}/data", &kernel).unwrap();
2227 assert_eq!(out, "path/example/data");
2228 }
2229
2230 #[test]
2231 fn kernel_resolves_via_get_input() {
2232 let parent =
2233 crate::dsl::compile::compile_polydat_interpreter("const k_values := \"1, 10\"\n")
2234 .unwrap();
2235 let child_program =
2236 crate::dsl::compile::compile_polydat_interpreter("extern k_values: String\n")
2237 .unwrap()
2238 .program()
2239 .clone();
2240 let child = parent.materialize_subscope(child_program, &[]);
2241 let out = interpolate_via_kernel("values={k_values}", &child).unwrap();
2242 assert_eq!(out, "values=1, 10");
2243 }
2244
2245 #[test]
2246 fn kernel_unresolved_name_errors() {
2247 let kernel = crate::dsl::compile::compile_polydat_interpreter("const x := 1\n").unwrap();
2248 let err = interpolate_via_kernel("hello {nope}", &kernel)
2249 .unwrap_err()
2250 .to_string();
2251 assert!(err.contains("unresolved"));
2252 assert!(err.contains("nope"));
2253 }
2254
2255 #[test]
2256 fn kernel_nested_template_iterates_to_fixed_point() {
2257 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2258 "const k := \"1\"\nconst k_1_limits := \"1, 2, 4, 8\"\n",
2259 )
2260 .unwrap();
2261 let out = interpolate_via_kernel("{k_{k}_limits}", &kernel).unwrap();
2262 assert_eq!(out, "1, 2, 4, 8");
2263 }
2264
2265 #[test]
2266 fn parse_list_native_types() {
2267 let v = parse_list_with_types("1, 10, 100");
2268 assert_eq!(v, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2269 }
2270
2271 #[test]
2272 fn parse_list_mixed_types() {
2273 let v = parse_list_with_types("1, 1.5, true, hello");
2274 assert_eq!(
2275 v,
2276 vec![
2277 Value::U64(1),
2278 Value::F64(1.5),
2279 Value::Bool(true),
2280 Value::Str("hello".to_string().into()),
2281 ]
2282 );
2283 }
2284
2285 #[test]
2286 fn all_cursor_returns_extent_range() {
2287 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2293 "const __cursor_extent_row_start := 0\n\
2294 const __cursor_extent_row_end := 5\n",
2295 )
2296 .unwrap();
2297 let values = evaluate_spec("all(row)", &kernel).unwrap();
2298 assert_eq!(
2299 values,
2300 vec![
2301 Value::U64(0),
2302 Value::U64(1),
2303 Value::U64(2),
2304 Value::U64(3),
2305 Value::U64(4),
2306 ]
2307 );
2308 }
2309
2310 #[test]
2311 fn all_cursor_non_zero_start() {
2312 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2313 "const __cursor_extent_data_start := 100\n\
2314 const __cursor_extent_data_end := 103\n",
2315 )
2316 .unwrap();
2317 let values = evaluate_spec("all(data)", &kernel).unwrap();
2318 assert_eq!(
2319 values,
2320 vec![Value::U64(100), Value::U64(101), Value::U64(102)]
2321 );
2322 }
2323
2324 #[test]
2325 fn all_cursor_missing_extent_errors() {
2326 let kernel =
2327 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2328 let err = evaluate_spec("all(no_such_cursor)", &kernel)
2329 .unwrap_err()
2330 .to_string();
2331 assert!(err.contains("all(no_such_cursor)"));
2332 assert!(err.contains("no resolvable extent"));
2333 }
2334
2335 #[test]
2336 fn all_cursor_only_matches_exact_shape() {
2337 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2347 "const __cursor_extent_row_start := 0\n\
2348 const __cursor_extent_row_end := 5\n",
2349 )
2350 .unwrap();
2351 let err = evaluate_spec("all(row, 5)", &kernel)
2352 .unwrap_err()
2353 .to_string();
2354 assert!(
2355 err.contains("all(row, 5)"),
2356 "error must mention the failing spec, got: {err}"
2357 );
2358 assert!(
2359 err.contains("failed to evaluate") || err.contains("unknown function"),
2360 "error must explain the eval failure, got: {err}"
2361 );
2362 }
2363
2364 #[test]
2365 fn missing_dataset_surface_as_clean_error_not_garbage() {
2366 let kernel =
2377 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2378 let result = evaluate_spec(
2379 "matching_profiles('nonexistent_dataset_xyz_qqq', 'label_')",
2380 &kernel,
2381 );
2382 let err = result
2383 .expect_err("missing dataset must surface as Err, not silent literal-list fallback")
2384 .to_string();
2385 assert!(
2391 err.contains("nonexistent_dataset_xyz_qqq")
2392 || err.contains("matching_profiles")
2393 || err.contains("dataset"),
2394 "error must point at the actual fault, got: {err}"
2395 );
2396 }
2397
2398 #[test]
2399 fn function_call_eval_failure_is_not_silently_split() {
2400 let kernel =
2406 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2407 let err = evaluate_spec("nonexistent_func('a', 'b', 'c')", &kernel)
2408 .unwrap_err()
2409 .to_string();
2410 assert!(
2411 err.contains("failed to evaluate") || err.contains("unknown"),
2412 "expected a clean eval-failure error, got: {err}"
2413 );
2414 }
2415
2416 #[test]
2417 fn literal_list_path_still_works() {
2418 let kernel =
2425 crate::dsl::compile::compile_polydat_interpreter("const unrelated := 1\n").unwrap();
2426 let values = evaluate_spec("1, 10, 100", &kernel).unwrap();
2427 assert_eq!(values, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2428
2429 let names = evaluate_spec("foo, bar, baz", &kernel).unwrap();
2430 assert_eq!(
2431 names,
2432 vec![
2433 Value::Str("foo".into()),
2434 Value::Str("bar".into()),
2435 Value::Str("baz".into()),
2436 ]
2437 );
2438 }
2439
2440 #[test]
2441 fn literal_cursor_exposes_extent_auxiliaries() {
2442 let kernel =
2447 crate::dsl::compile::compile_polydat_interpreter("cursor row = range(0, 50)\n")
2448 .unwrap();
2449 let start = kernel.lookup("__cursor_extent_row_start");
2450 let end = kernel.lookup("__cursor_extent_row_end");
2451 assert_eq!(
2452 start,
2453 Some(Value::U64(0)),
2454 "expected start=0, got {start:?}"
2455 );
2456 assert_eq!(end, Some(Value::U64(50)), "expected end=50, got {end:?}");
2457 }
2458
2459 #[test]
2460 fn all_cursor_with_real_cursor_decl_works() {
2461 let kernel =
2462 crate::dsl::compile::compile_polydat_interpreter("cursor row = range(0, 5)\n").unwrap();
2463 let values = evaluate_spec("all(row)", &kernel).unwrap();
2464 assert_eq!(
2465 values,
2466 vec![
2467 Value::U64(0),
2468 Value::U64(1),
2469 Value::U64(2),
2470 Value::U64(3),
2471 Value::U64(4),
2472 ]
2473 );
2474 }
2475
2476 #[test]
2477 fn all_cursor_ignores_whitespace() {
2478 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2479 "const __cursor_extent_row_start := 0\n\
2480 const __cursor_extent_row_end := 3\n",
2481 )
2482 .unwrap();
2483 let values = evaluate_spec(" all( row ) ", &kernel).unwrap();
2484 assert_eq!(values.len(), 3);
2485 }
2486
2487 #[test]
2488 fn evaluate_spec_resolves_against_kernel() {
2489 let kernel =
2490 crate::dsl::compile::compile_polydat_interpreter("const k_values := \"1, 10, 100\"\n")
2491 .unwrap();
2492 let v = evaluate_spec("{k_values}", &kernel).unwrap();
2493 assert_eq!(v, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2494 }
2495
2496 #[test]
2497 fn evaluate_spec_bare_ident_resolves_like_braced() {
2498 let kernel =
2502 crate::dsl::compile::compile_polydat_interpreter("const k_values := \"1, 10, 100\"\n")
2503 .unwrap();
2504 let bare = evaluate_spec("k_values", &kernel).unwrap();
2505 let braced = evaluate_spec("{k_values}", &kernel).unwrap();
2506 assert_eq!(bare, braced);
2507 assert_eq!(bare, vec![Value::U64(1), Value::U64(10), Value::U64(100)]);
2508 }
2509
2510 #[test]
2511 fn evaluate_spec_unresolved_bare_is_error_with_quoting_hint() {
2512 let kernel = crate::dsl::compile::compile_polydat_interpreter("\n").unwrap();
2516 let err = evaluate_spec("nonexistent", &kernel)
2517 .unwrap_err()
2518 .to_string();
2519 assert!(err.contains("did not resolve"), "got: {err}");
2520 assert!(err.contains("quote it"), "should hint quoting: {err}");
2521 }
2522
2523 #[test]
2527 fn a_read_of_none_is_decided_on_the_composed_name() {
2528 let kernel = crate::dsl::compile::compile_polydat_interpreter(
2529 "const k_1_limits := \"1, 2\"\nconst n := 3\n",
2530 )
2531 .unwrap();
2532 let at = |k: u64| vec![("k".to_string(), Value::U64(k))];
2533 let reads = |spec: &str, prefix: &[(String, Value)]| {
2534 let scope = crate::kernel::interp::Layered {
2535 prefix,
2536 inner: &kernel,
2537 };
2538 match evaluate_spec_internal(spec, &scope) {
2539 Ok(values) => Ok(values.len()),
2540 Err(SpecError::ReadsNone { reads, .. }) => Err(Some(reads)),
2541 Err(SpecError::Failed(_)) => Err(None),
2542 }
2543 };
2544 assert_eq!(reads("{k_{k}_limits}", &at(1)), Ok(2));
2545 assert_eq!(
2546 reads("{k_{k}_limits}", &at(7)),
2547 Err(Some(vec![NoneRead::Unbound("k_7_limits".into())]))
2548 );
2549 let none = [("z".to_string(), Value::None)];
2550 for spec in ["{z}", "z", "[z]", "pow2({z})", "concat(z, 1..3)"] {
2551 assert_eq!(
2552 reads(spec, &none),
2553 Err(Some(vec![NoneRead::BoundNone("z".into())])),
2554 "{spec}"
2555 );
2556 }
2557 assert_eq!(
2558 reads("u64_add(zz, 1)", &[]),
2559 Err(Some(vec![NoneRead::Unbound("zz".into())]))
2560 );
2561 assert_eq!(reads("pow2(99999)", &[]), Err(None));
2562 let scope = crate::kernel::interp::Layered {
2565 prefix: &none,
2566 inner: &kernel,
2567 };
2568 assert_eq!(evaluate_spec("{z}", &scope).unwrap(), Vec::<Value>::new());
2569 assert!(matches!(
2570 evaluate_spec("{zz}", &scope),
2571 Err(crate::dsl::compile::EmbeddingError::UnresolvedPlaceholder { .. })
2572 ));
2573 }
2574
2575 #[test]
2576 fn bracket_list_spread_and_no_peel() {
2577 let kernel =
2580 crate::dsl::compile::compile_polydat_interpreter("const xs := \"1, 2, 3\"\n").unwrap();
2581 let spread = evaluate_spec("[xs…]", &kernel).unwrap();
2583 assert_eq!(spread, vec![Value::U64(1), Value::U64(2), Value::U64(3)]);
2584 let whole = evaluate_spec("[xs]", &kernel).unwrap();
2586 assert_eq!(whole, vec![Value::Str("1, 2, 3".into())]);
2587 }
2588
2589 #[test]
2590 fn bracket_list_mixes_refs_literals_and_spread() {
2591 let kernel =
2592 crate::dsl::compile::compile_polydat_interpreter("const mid := \"7, 8\"\n").unwrap();
2593 let v = evaluate_spec("[1, mid…, \"x\"]", &kernel).unwrap();
2594 assert_eq!(
2595 v,
2596 vec![
2597 Value::U64(1),
2598 Value::U64(7),
2599 Value::U64(8),
2600 Value::Str("x".into()),
2601 ]
2602 );
2603 }
2604
2605 #[test]
2608 fn evaluate_spec_unpacks_partition_list_into_partition_values() {
2609 let kernel = empty_kernel();
2615 let v = evaluate_spec("partitions(\"linear:3\")", &kernel).unwrap();
2616 assert_eq!(v.len(), 3, "expected 3 partitions, got {}", v.len());
2617 for value in &v {
2618 assert!(
2619 value.as_partition().is_some(),
2620 "every iter value should be a Partition, got {value:?}"
2621 );
2622 }
2623 }
2624
2625 #[test]
2626 fn evaluate_spec_unpacks_partition_list_with_explicit_extent() {
2627 let kernel = empty_kernel();
2628 let v = evaluate_spec("partitions(\"fib:5\", 1000)", &kernel).unwrap();
2629 assert_eq!(v.len(), 5);
2630 for (i, value) in v.iter().enumerate() {
2632 let p = value.as_partition().unwrap();
2633 assert_eq!(p.idx, i as u64);
2634 assert_eq!(p.base_extent, 1000);
2635 }
2636 }
2637
2638 #[test]
2639 fn pre_evaluate_clause_returns_partition_values_for_partitions_call() {
2640 let kernel = empty_kernel();
2643 let v = pre_evaluate_clause(
2644 "partitions(\"linear:4\")",
2645 &kernel,
2646 &HashMap::new(),
2647 &HashMap::new(),
2648 )
2649 .unwrap();
2650 assert_eq!(v.len(), 4);
2651 for value in &v {
2652 assert!(
2653 value.as_partition().is_some(),
2654 "pre_evaluate_clause must unpack PartitionList, got {value:?}"
2655 );
2656 }
2657 }
2658
2659 #[test]
2660 fn value_to_polydat_type_name_returns_ext_for_partition_value() {
2661 let p = crate::iteration::cursor_partition::Partition {
2668 idx: 0,
2669 count: 1,
2670 start_ord: 0,
2671 end_ord: 10,
2672 start_pct: 0.0,
2673 end_pct: 100.0,
2674 base_extent: 10,
2675 };
2676 let v = Value::from_partition(p);
2677 assert_eq!(value_to_polydat_type_name(&v), "ext");
2678 }
2679
2680 fn empty_kernel() -> PolydatKernel {
2683 crate::dsl::compile::compile_polydat_interpreter("\n").unwrap()
2684 }
2685
2686 #[test]
2687 fn range_half_open_integer() {
2688 let v = evaluate_spec("1..5", &empty_kernel()).unwrap();
2689 assert_eq!(
2690 v,
2691 vec![Value::U64(1), Value::U64(2), Value::U64(3), Value::U64(4),]
2692 );
2693 }
2694
2695 #[test]
2696 fn range_inclusive_integer() {
2697 let v = evaluate_spec("1..=5", &empty_kernel()).unwrap();
2698 assert_eq!(
2699 v,
2700 vec![
2701 Value::U64(1),
2702 Value::U64(2),
2703 Value::U64(3),
2704 Value::U64(4),
2705 Value::U64(5),
2706 ]
2707 );
2708 }
2709
2710 #[test]
2711 fn range_with_step() {
2712 let v = evaluate_spec("0..100..10", &empty_kernel()).unwrap();
2713 assert_eq!(
2714 v,
2715 vec![
2716 Value::U64(0),
2717 Value::U64(10),
2718 Value::U64(20),
2719 Value::U64(30),
2720 Value::U64(40),
2721 Value::U64(50),
2722 Value::U64(60),
2723 Value::U64(70),
2724 Value::U64(80),
2725 Value::U64(90),
2726 ]
2727 );
2728 }
2729
2730 #[test]
2731 fn range_inclusive_with_step() {
2732 let v = evaluate_spec("0..=100..25", &empty_kernel()).unwrap();
2733 assert_eq!(
2734 v,
2735 vec![
2736 Value::U64(0),
2737 Value::U64(25),
2738 Value::U64(50),
2739 Value::U64(75),
2740 Value::U64(100),
2741 ]
2742 );
2743 }
2744
2745 #[test]
2746 fn range_float_step() {
2747 let v = evaluate_spec("0.0..=1.0..0.25", &empty_kernel()).unwrap();
2748 assert_eq!(v.len(), 5, "got {v:?}");
2749 if let [
2750 Value::F64(a),
2751 Value::F64(b),
2752 Value::F64(c),
2753 Value::F64(d),
2754 Value::F64(e),
2755 ] = v.as_slice()
2756 {
2757 assert!((a - 0.0).abs() < 1e-12);
2758 assert!((b - 0.25).abs() < 1e-12);
2759 assert!((c - 0.5).abs() < 1e-12);
2760 assert!((d - 0.75).abs() < 1e-12);
2761 assert!((e - 1.0).abs() < 1e-12);
2762 } else {
2763 panic!("expected 5 floats, got {v:?}");
2764 }
2765 }
2766
2767 #[test]
2768 fn range_empty_when_start_equals_end_half_open() {
2769 let v = evaluate_spec("5..5", &empty_kernel()).unwrap();
2770 assert!(v.is_empty(), "got {v:?}");
2771 }
2772
2773 #[test]
2774 fn range_inclusive_with_equal_bounds_emits_one() {
2775 let v = evaluate_spec("5..=5", &empty_kernel()).unwrap();
2776 assert_eq!(v, vec![Value::U64(5)]);
2777 }
2778
2779 #[test]
2780 fn range_with_si_suffix_bounds() {
2781 let v = evaluate_spec("1K..1K..200", &empty_kernel()).unwrap();
2784 assert!(v.is_empty(), "1K..1K with positive step → empty");
2785
2786 let v = evaluate_spec("0..1K..200", &empty_kernel()).unwrap();
2787 assert_eq!(
2788 v,
2789 vec![
2790 Value::U64(0),
2791 Value::U64(200),
2792 Value::U64(400),
2793 Value::U64(600),
2794 Value::U64(800),
2795 ]
2796 );
2797 }
2798
2799 #[test]
2800 fn range_zero_step_errors() {
2801 let err = evaluate_spec("1..10..0", &empty_kernel())
2802 .unwrap_err()
2803 .to_string();
2804 assert!(err.contains("step is zero"), "{err}");
2805 }
2806
2807 #[test]
2808 fn range_too_many_dotdot_errors() {
2809 let err = evaluate_spec("1..2..3..4", &empty_kernel())
2810 .unwrap_err()
2811 .to_string();
2812 assert!(err.contains("more than two `..`"), "{err}");
2813 }
2814
2815 #[test]
2816 fn range_inside_parens_doesnt_split() {
2817 let v = evaluate_spec("(1)..(5)", &empty_kernel()).unwrap();
2824 assert_eq!(v.len(), 4); }
2826
2827 #[test]
2828 fn range_step_with_inclusive_separator_errors() {
2829 let err = evaluate_spec("1..10..=2", &empty_kernel())
2830 .unwrap_err()
2831 .to_string();
2832 assert!(err.contains("step delimiter cannot be `..=`"), "{err}");
2833 }
2834
2835 #[test]
2836 fn range_with_kernel_referenced_bounds() {
2837 let kernel =
2838 crate::dsl::compile::compile_polydat_interpreter("const lo := 5\nconst hi := 12\n")
2839 .unwrap();
2840 let v = evaluate_spec("{lo}..{hi}", &kernel).unwrap();
2841 assert_eq!(
2842 v,
2843 vec![
2844 Value::U64(5),
2845 Value::U64(6),
2846 Value::U64(7),
2847 Value::U64(8),
2848 Value::U64(9),
2849 Value::U64(10),
2850 Value::U64(11),
2851 ]
2852 );
2853 }
2854
2855 #[test]
2858 fn fib_n_first_eight() {
2859 let v = evaluate_spec("fib(8)", &empty_kernel()).unwrap();
2860 assert_eq!(
2861 v,
2862 vec![
2863 Value::U64(1),
2864 Value::U64(1),
2865 Value::U64(2),
2866 Value::U64(3),
2867 Value::U64(5),
2868 Value::U64(8),
2869 Value::U64(13),
2870 Value::U64(21),
2871 ]
2872 );
2873 }
2874
2875 #[test]
2876 fn fib_until_50() {
2877 let v = evaluate_spec("fib_until(50)", &empty_kernel()).unwrap();
2878 assert_eq!(
2879 v,
2880 vec![
2881 Value::U64(1),
2882 Value::U64(1),
2883 Value::U64(2),
2884 Value::U64(3),
2885 Value::U64(5),
2886 Value::U64(8),
2887 Value::U64(13),
2888 Value::U64(21),
2889 Value::U64(34),
2890 ]
2891 );
2892 }
2893
2894 #[test]
2895 fn pow2_n_six() {
2896 let v = evaluate_spec("pow2(6)", &empty_kernel()).unwrap();
2897 assert_eq!(
2898 v,
2899 vec![
2900 Value::U64(1),
2901 Value::U64(2),
2902 Value::U64(4),
2903 Value::U64(8),
2904 Value::U64(16),
2905 Value::U64(32),
2906 ]
2907 );
2908 }
2909
2910 #[test]
2911 fn pow2_until_100() {
2912 let v = evaluate_spec("pow2_until(100)", &empty_kernel()).unwrap();
2913 assert_eq!(
2914 v,
2915 vec![
2916 Value::U64(1),
2917 Value::U64(2),
2918 Value::U64(4),
2919 Value::U64(8),
2920 Value::U64(16),
2921 Value::U64(32),
2922 Value::U64(64),
2923 ]
2924 );
2925 }
2926
2927 #[test]
2928 fn binomial_n_5() {
2929 let v = evaluate_spec("binomial(5)", &empty_kernel()).unwrap();
2931 assert_eq!(
2932 v,
2933 vec![
2934 Value::U64(1),
2935 Value::U64(5),
2936 Value::U64(10),
2937 Value::U64(10),
2938 Value::U64(5),
2939 Value::U64(1),
2940 ]
2941 );
2942 }
2943
2944 #[test]
2945 fn geometric_2_doubles_4_terms() {
2946 let v = evaluate_spec("geometric(1, 2, 4)", &empty_kernel()).unwrap();
2947 if let [Value::F64(a), Value::F64(b), Value::F64(c), Value::F64(d)] = v.as_slice() {
2949 assert!((a - 1.0).abs() < 1e-12);
2950 assert!((b - 2.0).abs() < 1e-12);
2951 assert!((c - 4.0).abs() < 1e-12);
2952 assert!((d - 8.0).abs() < 1e-12);
2953 } else {
2954 panic!("expected 4 f64 values, got {v:?}");
2955 }
2956 }
2957
2958 #[test]
2959 fn linear_starts_half_open_5_points() {
2960 let v = evaluate_spec("linear_starts(0, 100, 5)", &empty_kernel()).unwrap();
2961 if let [
2963 Value::F64(a),
2964 Value::F64(b),
2965 Value::F64(c),
2966 Value::F64(d),
2967 Value::F64(e),
2968 ] = v.as_slice()
2969 {
2970 assert!((a - 0.0).abs() < 1e-12);
2971 assert!((b - 20.0).abs() < 1e-12);
2972 assert!((c - 40.0).abs() < 1e-12);
2973 assert!((d - 60.0).abs() < 1e-12);
2974 assert!((e - 80.0).abs() < 1e-12);
2975 } else {
2976 panic!("got {v:?}");
2977 }
2978 }
2979
2980 #[test]
2981 fn linear_steps_inclusive_5_points() {
2982 let v = evaluate_spec("linear_steps(0, 100, 5)", &empty_kernel()).unwrap();
2983 if let [
2985 Value::F64(a),
2986 Value::F64(b),
2987 Value::F64(c),
2988 Value::F64(d),
2989 Value::F64(e),
2990 ] = v.as_slice()
2991 {
2992 assert!((a - 0.0).abs() < 1e-12);
2993 assert!((b - 25.0).abs() < 1e-12);
2994 assert!((c - 50.0).abs() < 1e-12);
2995 assert!((d - 75.0).abs() < 1e-12);
2996 assert!((e - 100.0).abs() < 1e-12);
2997 } else {
2998 panic!("got {v:?}");
2999 }
3000 }
3001
3002 #[test]
3003 fn log_steps_3_decades() {
3004 let v = evaluate_spec("log_steps(1, 1000, 4)", &empty_kernel()).unwrap();
3005 if let [Value::F64(a), Value::F64(b), Value::F64(c), Value::F64(d)] = v.as_slice() {
3007 assert!((a - 1.0).abs() < 1e-9);
3008 assert!((b - 10.0).abs() < 1e-9);
3009 assert!((c - 100.0).abs() < 1e-9);
3010 assert_eq!(*d, 1000.0);
3011 } else {
3012 panic!("got {v:?}");
3013 }
3014 }
3015
3016 #[test]
3017 fn log_steps_rejects_non_positive_bounds() {
3018 let err = evaluate_spec("log_steps(0, 100, 5)", &empty_kernel())
3019 .unwrap_err()
3020 .to_string();
3021 assert!(
3022 err.contains("log_steps.start: expected a positive number, got 0"),
3023 "{err}"
3024 );
3025 let err = evaluate_spec("log_steps(1, -2, 5)", &empty_kernel())
3026 .unwrap_err()
3027 .to_string();
3028 assert!(
3029 err.contains("log_steps.end: expected a positive number, got -2"),
3030 "{err}"
3031 );
3032 }
3033
3034 #[test]
3037 fn inclusive_steps_end_exactly_at_their_bounds() {
3038 let floats = |spec: &str| -> Vec<f64> {
3039 evaluate_spec(spec, &empty_kernel())
3040 .unwrap()
3041 .iter()
3042 .map(|v| match v {
3043 Value::F64(f) => *f,
3044 other => panic!("{spec}: {other:?}"),
3045 })
3046 .collect()
3047 };
3048 for (spec, start, end) in [
3049 ("log_steps(1, 1000, 4)", 1.0, 1000.0),
3050 ("log_steps(3, 7, 9)", 3.0, 7.0),
3051 ("log_steps(0.1, 0.7, 13)", 0.1, 0.7),
3052 ("log_steps(1000, 1, 4)", 1000.0, 1.0),
3053 ("linear_steps(0, 1, 4)", 0.0, 1.0),
3054 ("linear_steps(0.1, 0.7, 13)", 0.1, 0.7),
3055 ("linear_steps(-3, 1e9, 7)", -3.0, 1e9),
3056 ] {
3057 let v = floats(spec);
3058 assert_eq!(v.first(), Some(&start), "{spec}: {v:?}");
3059 assert_eq!(v.last(), Some(&end), "{spec}: {v:?}");
3060 }
3061 assert_eq!(floats("linear_steps(2, 5, 1)"), vec![2.0]);
3062 assert_eq!(floats("log_steps(2, 5, 1)"), vec![2.0]);
3063 }
3064
3065 #[test]
3069 fn overflow_limits_are_where_the_terms_leave_u64() {
3070 let max = u128::from(u64::MAX);
3071 let (mut a, mut b, mut term) = (1u128, 1u128, 1u64);
3073 while a <= max {
3074 (a, b, term) = (b, a + b, term + 1);
3075 }
3076 assert_eq!(term, 94);
3077 assert_eq!(NamedGenerator::Fib.largest_valid_argument(), Some(term - 1));
3078 assert_eq!(1u128 << 64, max + 1);
3080 assert_eq!(NamedGenerator::Pow2.largest_valid_argument(), Some(64));
3081 let row_overflow = |n: u64| -> Option<u64> {
3084 let mut c = 1u128;
3085 (1..=n).find(|&k| {
3086 c = c * u128::from(n - k + 1) / u128::from(k);
3087 c > max
3088 })
3089 };
3090 let first_row = (0..).find(|&n| row_overflow(n).is_some()).unwrap();
3091 assert_eq!(first_row, 68);
3092 assert_eq!(row_overflow(68), Some(31));
3093 assert_eq!(
3094 NamedGenerator::Binomial.largest_valid_argument(),
3095 Some(first_row - 1)
3096 );
3097 assert_eq!(NamedGenerator::Geometric.largest_valid_argument(), None);
3098 }
3099
3100 #[test]
3104 fn a_call_past_its_limit_is_refused_by_its_first_overflowing_term() {
3105 let k = empty_kernel();
3106 let fib = evaluate_spec("fib(93)", &k).unwrap();
3107 assert_eq!(fib.len(), 93);
3108 assert_eq!(fib[92], Value::U64(12_200_160_415_121_876_738));
3109 let pow2 = evaluate_spec("pow2(64)", &k).unwrap();
3110 assert_eq!(pow2.last(), Some(&Value::U64(1 << 63)));
3111 let row = evaluate_spec("binomial(67)", &k).unwrap();
3112 assert_eq!(row.len(), 68);
3113 assert_eq!(row[33], Value::U64(14_226_520_737_620_288_370));
3114 for (spec, message) in [
3115 (
3116 "fib(94)",
3117 "fib(94): term 94 is past u64::MAX; fib.n is at most 93",
3118 ),
3119 (
3120 "fib(18446744073709551615)",
3121 "fib(18446744073709551615): term 94 is past u64::MAX",
3122 ),
3123 (
3124 "pow2(65)",
3125 "pow2(65): term 65, 2^64, is past u64::MAX; pow2.n is at most 64",
3126 ),
3127 (
3128 "binomial(68)",
3129 "binomial(68): term C(68, 31) is past u64::MAX; binomial.n is at most 67",
3130 ),
3131 (
3132 "binomial(70)",
3133 "binomial(70): term C(70, 28) is past u64::MAX",
3134 ),
3135 (
3136 "binomial(1000000000000)",
3137 "binomial(1000000000000): term C(1000000000000, 2) is past u64::MAX",
3138 ),
3139 ] {
3140 let err = evaluate_spec(spec, &k).unwrap_err().to_string();
3141 assert!(err.contains(message), "{spec}: {err}");
3142 }
3143 }
3144
3145 #[test]
3149 fn a_geometric_factor_out_of_range_is_refused_by_name() {
3150 let k = empty_kernel();
3151 for (spec, message) in [
3152 (
3153 "geometric(1, 0, 4)",
3154 "geometric.factor: expected a positive, finite number, got 0",
3155 ),
3156 (
3157 "geometric(1, -2, 4)",
3158 "geometric.factor: expected a positive, finite number, got -2",
3159 ),
3160 (
3161 "geometric(1, inf, 4)",
3162 "geometric.factor: expected a positive, finite number, got inf",
3163 ),
3164 (
3165 "geometric_until(1, 1, 100)",
3166 "geometric_until.factor: expected a finite number greater than 1, got 1",
3167 ),
3168 (
3169 "geometric_until(1, 0.5, 100)",
3170 "geometric_until.factor: expected a finite number greater than 1, got 0.5",
3171 ),
3172 ] {
3173 let err = evaluate_spec(spec, &k).unwrap_err().to_string();
3174 assert!(err.contains(message), "{spec}: {err}");
3175 }
3176 assert_eq!(
3177 evaluate_spec("geometric(8, 0.5, 3)", &k).unwrap(),
3178 vec![Value::F64(8.0), Value::F64(4.0), Value::F64(2.0)]
3179 );
3180 assert!(
3181 evaluate_spec("geometric_until(0, 2, 100)", &k)
3182 .unwrap()
3183 .is_empty()
3184 );
3185 }
3186
3187 #[test]
3191 fn refused_generator_call_looks_through_enclosing_calls() {
3192 let k = empty_kernel();
3193 let refused = refused_generator_call("concat(1..3, take(fib(94), 2))", &k).unwrap();
3194 assert!(refused.starts_with("fib(94): term 94"), "{refused}");
3195 assert_eq!(refused_generator_call("concat(fib(8), pow2(64))", &k), None);
3196 assert_eq!(refused_generator_call("hash(3)", &k), None);
3197 assert_eq!(refused_generator_call("1, 2, 3", &k), None);
3198 let refused = refused_generator_call("fib(-1)", &k).unwrap();
3199 assert!(
3200 refused.contains("fib.n: expected non-negative integer, got '-1'"),
3201 "{refused}"
3202 );
3203 }
3204
3205 #[test]
3208 fn concat_two_ranges() {
3209 let v = evaluate_spec("concat(1..4, 10..13)", &empty_kernel()).unwrap();
3210 assert_eq!(
3211 v,
3212 vec![
3213 Value::U64(1),
3214 Value::U64(2),
3215 Value::U64(3),
3216 Value::U64(10),
3217 Value::U64(11),
3218 Value::U64(12),
3219 ]
3220 );
3221 }
3222
3223 #[test]
3224 fn unique_dedupes_first_occurrence() {
3225 let v = evaluate_spec("unique(1..4, 3..6)", &empty_kernel()).unwrap();
3226 assert_eq!(
3228 v,
3229 vec![
3230 Value::U64(1),
3231 Value::U64(2),
3232 Value::U64(3),
3233 Value::U64(4),
3234 Value::U64(5),
3235 ]
3236 );
3237 }
3238
3239 #[test]
3240 fn intersect_keeps_only_common_values() {
3241 let v = evaluate_spec("intersect(1..10, 5..15)", &empty_kernel()).unwrap();
3242 assert_eq!(
3243 v,
3244 vec![
3245 Value::U64(5),
3246 Value::U64(6),
3247 Value::U64(7),
3248 Value::U64(8),
3249 Value::U64(9),
3250 ]
3251 );
3252 }
3253
3254 #[test]
3255 fn subtract_drops_values_in_b() {
3256 let v = evaluate_spec("subtract(1..6, 3..5)", &empty_kernel()).unwrap();
3257 assert_eq!(v, vec![Value::U64(1), Value::U64(2), Value::U64(5)]);
3259 }
3260
3261 #[test]
3262 fn interleave_round_robin_two_lists() {
3263 let v = evaluate_spec("interleave(1..4, 10..13)", &empty_kernel()).unwrap();
3264 assert_eq!(
3265 v,
3266 vec![
3267 Value::U64(1),
3268 Value::U64(10),
3269 Value::U64(2),
3270 Value::U64(11),
3271 Value::U64(3),
3272 Value::U64(12),
3273 ]
3274 );
3275 }
3276
3277 #[test]
3278 fn cycle_repeats_n_times() {
3279 let v = evaluate_spec("cycle(1..3, 3)", &empty_kernel()).unwrap();
3280 assert_eq!(
3281 v,
3282 vec![
3283 Value::U64(1),
3284 Value::U64(2),
3285 Value::U64(1),
3286 Value::U64(2),
3287 Value::U64(1),
3288 Value::U64(2),
3289 ]
3290 );
3291 }
3292
3293 #[test]
3294 fn reverse_inverts_list() {
3295 let v = evaluate_spec("reverse(1..5)", &empty_kernel()).unwrap();
3296 assert_eq!(
3297 v,
3298 vec![Value::U64(4), Value::U64(3), Value::U64(2), Value::U64(1),]
3299 );
3300 }
3301
3302 #[test]
3303 fn take_n_takes_prefix() {
3304 let v = evaluate_spec("take(1..10, 3)", &empty_kernel()).unwrap();
3305 assert_eq!(v, vec![Value::U64(1), Value::U64(2), Value::U64(3)]);
3306 }
3307
3308 #[test]
3309 fn skip_n_drops_prefix() {
3310 let v = evaluate_spec("skip(1..6, 2)", &empty_kernel()).unwrap();
3311 assert_eq!(v, vec![Value::U64(3), Value::U64(4), Value::U64(5)]);
3312 }
3313
3314 #[test]
3315 fn unique_composes_with_pow2_and_range() {
3316 let v = evaluate_spec("unique(pow2(8), 1..1000..100)", &empty_kernel()).unwrap();
3317 assert_eq!(v.len(), 17);
3321 assert_eq!(v[0], Value::U64(1));
3322 assert_eq!(v[7], Value::U64(128));
3323 assert_eq!(v[8], Value::U64(101));
3324 }
3325
3326 #[test]
3329 fn bucket_round_robin_3_1_2() {
3330 let v = evaluate_spec(
3332 "bucket(concat('ann', 'scan', 'fetch'), concat(3, 1, 2))",
3333 &empty_kernel(),
3334 )
3335 .unwrap();
3336 let _ = v;
3339 }
3340
3341 #[test]
3342 fn bucket_ratio_prefix_shorthand_round_robin() {
3343 let v = evaluate_spec("bucket(\"3:ann, 1:scan, 2:fetch\")", &empty_kernel()).unwrap();
3344 assert_eq!(v.len(), 6);
3347 let strs: Vec<&str> = v
3348 .iter()
3349 .filter_map(|v| match v {
3350 Value::Str(s) => Some(&**s),
3351 _ => None,
3352 })
3353 .collect();
3354 let counts = strs.iter().fold(
3358 std::collections::HashMap::<&str, usize>::new(),
3359 |mut m, s| {
3360 *m.entry(s).or_insert(0) += 1;
3361 m
3362 },
3363 );
3364 assert_eq!(counts.get("ann"), Some(&3));
3365 assert_eq!(counts.get("scan"), Some(&1));
3366 assert_eq!(counts.get("fetch"), Some(&2));
3367 }
3368
3369 #[test]
3370 fn concat_seq_emits_contiguous_runs() {
3371 let v = evaluate_spec(
3372 "concat_seq(\"2:warmup, 3:bench, 1:cooldown\")",
3373 &empty_kernel(),
3374 )
3375 .unwrap();
3376 let strs: Vec<String> = v
3377 .iter()
3378 .filter_map(|v| match v {
3379 Value::Str(s) => Some(s.to_string()),
3380 _ => None,
3381 })
3382 .collect();
3383 assert_eq!(
3384 strs,
3385 vec!["warmup", "warmup", "bench", "bench", "bench", "cooldown",]
3386 );
3387 }
3388
3389 #[test]
3390 fn interval_seq_evenly_spreads_higher_ratio() {
3391 let v = evaluate_spec("interval_seq(\"3:read, 1:write\")", &empty_kernel()).unwrap();
3392 let strs: Vec<String> = v
3395 .iter()
3396 .filter_map(|v| match v {
3397 Value::Str(s) => Some(s.to_string()),
3398 _ => None,
3399 })
3400 .collect();
3401 assert_eq!(strs.len(), 4);
3402 let writes: Vec<usize> = strs
3403 .iter()
3404 .enumerate()
3405 .filter(|(_, s)| *s == "write")
3406 .map(|(i, _)| i)
3407 .collect();
3408 assert_eq!(writes.len(), 1, "expected exactly one write: {strs:?}");
3409 }
3410
3411 #[test]
3412 fn parse_func_call_recognises_simple_call() {
3413 let (n, a) = parse_func_call("fib(8)").unwrap();
3414 assert_eq!(n, "fib");
3415 assert_eq!(a, "8");
3416 }
3417
3418 #[test]
3419 fn parse_func_call_rejects_non_calls() {
3420 assert!(parse_func_call("1..10").is_none());
3421 assert!(parse_func_call("foo + bar").is_none());
3422 assert!(parse_func_call("f(a) + g(b)").is_none()); }
3424
3425 #[test]
3426 fn split_args_top_level_skips_inner_commas() {
3427 let args = split_args_top_level("a, f(b, c), \"x, y\", 3");
3428 assert_eq!(args, vec!["a", "f(b, c)", "\"x, y\"", "3"]);
3429 }
3430}