Skip to main content

polydat_core/iteration/comprehension/
eval.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Comprehension spec evaluation — text → typed value list.
5//!
6//! ## What this module does
7//!
8//! A comprehension clause `var in expr` ships its `expr` as
9//! free-form workload-author text. At runtime, the executor
10//! needs to turn that text into a list of typed values to
11//! enumerate over. That's what [`evaluate_spec`] does, given a
12//! Polydat Kernel that holds the in-scope name space (own outputs +
13//! inherited externs from `materialize_wiring_from_outer`).
14//!
15//! ## Pipeline
16//!
17//! ```text
18//!   spec_text
19//!       │
20//!       ▼
21//!   interpolate_via_kernel  ← {name} → kernel.lookup(name)
22//!       │
23//!       ▼
24//!   eval_const_expr_for     ← optional: Polydat expression eval,
25//!                              charged to scope.ledger()
26//!       │
27//!       ▼
28//!   parse_list_with_types   ← comma-split, per-element type
29//!       │
30//!       ▼
31//!   Vec<Value>              ← what the executor enumerates
32//! ```
33//!
34//! ## Ownership
35//!
36//! Polydat owns what a comprehension *means*, including how its
37//! source strings resolve (comprehension_forms.md §3.1). A host
38//! consumes this API rather than implementing it.
39
40use 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
47/// Evaluate a comprehension clause's spec text against a `Lookup` scope.
48///
49/// Steps:
50///  1. [`interpolate_via_kernel`](crate::kernel::interp::interpolate_via_kernel) resolves `{name}` placeholders
51///     against the kernel's in-scope name space (own outputs +
52///     inherited extern values).
53///  2. Try `dsl::compile::eval_const_expr_for(…, kernel.ledger())`
54///     on the result. On
55///     success with a `Str` value, re-parse as a comma-separated
56///     list with per-element type detection. Other typed
57///     variants become a single-element typed list.
58///  3. On eval failure (most common case for literal lists like
59///     `"1, 10"` which aren't valid Polydat const expressions), fall
60///     back to [`parse_list_with_types`] on the interpolated
61///     text — `1` → `U64`, `1.5` → `F64`, `true` → `Bool`,
62///     anything else → `Str`.
63///
64/// Errors propagate from interpolation (unresolved placeholder,
65/// runaway round count, etc.) — those are the user-facing
66/// actionable diagnostics. A name the source reads that nothing binds
67/// is such an error here; a name bound to None reads None, and the
68/// source yields nothing (none_semantics.md Rule 1). A clause source
69/// evaluated as part of a comprehension reads an unbound name as None
70/// too (comprehension_forms.md §5 V3).
71pub 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
86/// The error [`evaluate_spec`] reports for `spec_text` failing with
87/// `message`.
88pub(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/// Why a source's spec text yields no values.
105#[derive(Debug, Clone)]
106pub(crate) enum SpecError {
107    /// The source read names that nothing binds or that are bound to
108    /// None, after composition, so it reads None and yields nothing
109    /// (none_semantics.md Rule 1, comprehension_forms.md §5 V3).
110    /// `message` is the diagnostic an API that refuses such a read
111    /// reports ([`evaluate_spec`]).
112    ReadsNone {
113        /// Each such name, once, with how it read None.
114        reads: Vec<NoneRead>,
115        /// The diagnostic for the first read.
116        message: String,
117    },
118    /// Evaluating the text failed.
119    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
136/// What `kernel` binds `name` to, for a source that reads it: a name
137/// nothing binds, or one bound to None, is [`SpecError::ReadsNone`],
138/// with `unbound` the diagnostic for the first.
139fn 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
157/// Interpolate `text`'s `{name}` placeholders against `kernel`, inside
158/// out, so a composed name (`{k_{k}_limits}`) reads the name its leaves
159/// compose to. A placeholder that names what nothing binds, or what is
160/// bound to None, is a read of None ([`SpecError::ReadsNone`]); any
161/// other failure is [`SpecError::Failed`].
162fn 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
190/// Whether a placeholder body is a name — an identifier, or a dotted
191/// path (`q.cursor.idx`, or `t_0.5_vals` composed from a float) — rather
192/// than an expression or a format specifier.
193fn 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
201/// The names `text` reads as free identifiers of the language that
202/// `kernel` does not bind or binds to None, as [`SpecError::ReadsNone`]
203/// with `message`; [`SpecError::Failed`] with `message` when there are
204/// none. An evaluation of `text` that failed reading such a name failed
205/// because the name reads None.
206fn 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    // A *bare identifier* source is a direct wire/param/const
230    // reference (comprehension_forms.md §3.1.4). It resolves
231    // against the kernel chain — the same `kernel.lookup` the
232    // `{name}` interpolation path uses — and its value is peeled or
233    // wrapped by `iteration_interior`, so `mnc in mnc_values` works
234    // identically to `mnc in {mnc_values}`. A bare name that does
235    // not resolve reads None, and the diagnostic an API that refuses
236    // it reports carries a quoting hint; it is never bound as its own
237    // name-string.
238    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    // What the interpolated text evaluates to. A failure that reads a
257    // free name the scope does not bind, or binds to None, is that read
258    // of None.
259    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
265/// [`evaluate_spec_internal`] past interpolation: the source forms over
266/// the interpolated text.
267fn evaluate_interpolated(interpolated: &str, kernel: &dyn Lookup) -> Result<Vec<Value>, SpecError> {
268    // List comprehension sugar `[e1, e2…, e3]`
269    // (comprehension_forms.md §3.1.3). Resolved after interpolation so `{name}` placeholders inside
270    // elements expand first; before the const-eval fallthrough so
271    // bracket structure isn't misparsed as an array-literal expr.
272    if let Some(values) = try_eval_bracket_list(interpolated, kernel)? {
273        return Ok(values);
274    }
275    // Range operator (`a..b`, `a..=b`, `a..b..s`, `a..=b..s`;
276    // comprehension_forms.md §3.1, polydat_grammar.md §16.2). Bounds
277    // and step are Polydat const expressions evaluated at this
278    // (post-interpolation) point.
279    if let Some(values) = try_eval_range(interpolated, kernel.ledger())? {
280        return Ok(values);
281    }
282    // Named generators (comprehension_forms.md §3.1).
283    if let Some(values) = try_eval_generator(interpolated)? {
284        return Ok(values);
285    }
286    // Set operators on lists (comprehension_forms.md §3.1).
287    if let Some(values) = try_eval_setop(interpolated, kernel)? {
288        return Ok(values);
289    }
290    // Sequencer expansions (bucket / concat_seq / interval_seq;
291    // comprehension_forms.md §3.1).
292    if let Some(values) = try_eval_sequencer(interpolated, kernel)? {
293        return Ok(values);
294    }
295    // Kernel-aware partition sources — `subdivide(outer, n)` where
296    // `outer` is a partition iter-var bound by an enclosing clause
297    // (cursor_partitions.md §7.1).
298    if let Some(values) = try_eval_partition_call(interpolated, kernel, false)? {
299        return Ok(values);
300    }
301    // `<param>.partitions` in comprehension position resolves the
302    // param's spec string and expands it into its PartitionList
303    // (cursor_partitions.md §7.1).
304    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        // Relaxed source resolution (comprehension_forms.md §3.1.2):
309        // a resolved value is peeled one level if it has an iteration
310        // interior (native vector, JSON array, PartitionList, or a
311        // string → its comprehension tokens), else wrapped as a
312        // singleton. `iteration_interior` is the single place that
313        // decision is made.
314        Ok(v) => Ok(
315            match crate::iteration::comprehension::source_values::iteration_interior(&v) {
316                Some(interior) => interior,
317                None => vec![v],
318            },
319        ),
320        // Fall back to the literal-list parse only when the text
321        // is unambiguously a comma-separated list of literals
322        // (e.g. `1, 10, 100` — `eval_const_expr` doesn't accept
323        // that shape because it isn't a single Polydat expression).
324        // Anything that looks like an expression (parens, GK
325        // operators, identifiers other than `true`/`false`) was
326        // *meant* to evaluate; if it failed, we MUST surface the
327        // failure rather than silently splitting and
328        // handing the workload an iter-var like
329        // `matching_profiles('x'` (truncated), which would produce
330        // malformed downstream output far removed from the fault.
331        Err(eval_err) => {
332            // A single bare identifier never reaches here: it is a
333            // reference, resolved or refused above. An unbracketed
334            // bare label list (`a, b, c`) keeps string-token striping
335            // (comprehension_forms.md §3.1.4).
336            if looks_like_literal_list(interpolated) {
337                // A bare unquoted token list strips on the same
338                // separator rule as a string comprehension.
339                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
359/// List comprehension sugar (comprehension_forms.md §3.1.3). Evaluate a
360/// bracketed source `[e1, e2…, e3]` to its bound sequence,
361/// peeling exactly one level:
362///   - a plain element contributes its value, whole (no peel);
363///   - a spread element `S…` / `S...` contributes `S`'s
364///     iteration interior (peel one level); a non-iterable `S`
365///     under spread is a hard error.
366///
367/// Elements are parsed by the core expression grammar: a bare
368/// identifier is a wire/param reference (resolved against the
369/// kernel), a quoted token is a string, numbers/bools are
370/// literals. Returns `Ok(None)` when `text` is not a bracketed
371/// list (so the caller falls through to the other source forms).
372fn 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        // Spread suffix: `…` (U+2026) or `...`.
385        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
419/// Resolve one list-comprehension element to a single value (no
420/// peeling). A bare identifier is a wire/param/const reference
421/// resolved against the kernel; anything else (quoted string,
422/// number, bool, expression) goes through the const evaluator.
423/// An unresolved bare reference reads None, never a silent literal-name
424/// binding, and the diagnostic an API that refuses it reports carries a
425/// quoting hint (comprehension_forms.md §3.1.4).
426fn 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
446/// True when `text` is exactly one bare identifier
447/// (`[A-Za-z_][A-Za-z0-9_]*`), excluding `true`/`false`. A bare
448/// identifier source is a direct reference resolved against the
449/// kernel (comprehension_forms.md §3.1.4); the keyword literals are
450/// values.
451fn 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
464/// Heuristic: does this interpolated spec text look like a
465/// "literal list" (comma-separated literals like `1, 10, 100` or
466/// `foo, bar, baz`) rather than an expression?
467///
468/// True only when no character suggests an expression: no
469/// parentheses, no operators, no string-quote characters that
470/// would imply a function-call shape. Whitespace, digits,
471/// alphanumerics, dots (for floats), minus (for negatives), and
472/// commas (the separator) are all OK.
473///
474/// The gate keeps list sources (`k in 1, 10, 100`) working through
475/// the literal-list fallback while still surfacing real evaluation
476/// failures for expression sources like `matching_profiles('x',
477/// 'y')`. A wrong call on a borderline case is cheap: it produces a
478/// clearer error from the eval layer instead of swallowed garbage.
479fn 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
511/// Pre-evaluate a clause's spec text at synthesis time, using
512/// `probes` for prior clauses' first values and `workload_params`
513/// as a fallback source for names not yet promoted to workload-
514/// kernel `const` bindings.
515///
516/// The runtime dispatcher uses [`evaluate_spec`] directly because
517/// (a) the runtime kernel has prior-clause values as real input
518/// slots, not text probes, and (b) by then workload params are
519/// already injected as final bindings on the for_each scope's
520/// kernel via the synthesis path.
521pub 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    // The `all(<cursor>)` form resolves cursor extents from the
528    // parent kernel's auxiliary outputs; it doesn't fit the
529    // const-eval pipeline (which returns a single Value), so it's
530    // intercepted here too — same as `evaluate_spec`.
531    if let Some(values) = try_eval_all_cursor(spec_text, parent_kernel)? {
532        return Ok(values);
533    }
534    // A bare identifier source is a direct reference
535    // (comprehension_forms.md §3.1.4). At synthesis it resolves
536    // against a prior iter-var probe, then the parent kernel, then
537    // the workload params, so a dependent clause (e.g. `limit in
538    // {k_{k}_limits}`) sees the *typed* prior value and infers the
539    // right extern type, rather than the literal name typed as a
540    // string.
541    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    // The range operator, on the pre-evaluation path too.
576    if let Some(values) = try_eval_range(&interpolated, parent_kernel.ledger())? {
577        return Ok(values);
578    }
579    // The same generator, set-operator, and sequencer forms the
580    // runtime path recognizes.
581    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    // Kernel-aware partition sources, as on the runtime path
591    // (cursor_partitions.md §7.1). At pre-evaluation the outer
592    // iter-var may not be installed yet; `try_eval_partition_call`
593    // then returns a single placeholder partition so iter-var type
594    // detection yields `ext`.
595    if let Some(values) = try_eval_partition_call(&interpolated, parent_kernel, true)? {
596        return Ok(values);
597    }
598    // `<param>.partitions` in comprehension position, by the same rule
599    // as the runtime path; the param may already be installed here.
600    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            // `<param>.partitions` and `partitions(spec, ...)`
607            // (cursor_partitions.md §7.1) both evaluate to a `PartitionList` Ext value. Unpack
608            // its entries into a vec of individual `Partition`
609            // values so the for-clause iterates partition-by-
610            // partition.
611            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            // Mirrors `evaluate_spec`'s gating: only fall back to
621            // parse_list_with_types when the text is unambiguously a
622            // literal list. See `looks_like_literal_list` for the
623            // rationale.
624            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
642/// Parse a comma-separated text list, detecting each element's
643/// native type, as element types are inferred
644/// (polydat_grammar.md §16.3): `"1, 10"` → `[U64(1), U64(10)]`, `"1.5, 2.5"` → `[F64(...)]`,
645/// mixed → each element gets its own native type.
646pub 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
666/// Recognize the comprehension-level `all(<cursor>)` clause form
667/// and resolve it against the parent kernel's cursor extent
668/// auxiliary outputs.
669///
670/// Cursors declared via the Polydat `cursor name = Cursor(start, end)`
671/// shape compile to two well-known auxiliary outputs on the
672/// kernel: `__cursor_extent_<name>_start` and
673/// `__cursor_extent_<name>_end`. Reading those gives the cursor's
674/// resolved extent at scope-init time. `all(<cursor>)` lowers to
675/// the half-open ordinal range `[start, end)` as a `Vec<Value::U64>`.
676///
677/// The source reads the cursor's extent, not a value named after the
678/// cursor ([`polydat_grammar::comprehension::source::Source::names_read`]).
679///
680/// Returns:
681/// - `Ok(Some(values))` if `spec_text` matches the `all(<ident>)`
682///   shape and the cursor's extent resolved successfully.
683/// - `Ok(None)` if `spec_text` doesn't match — caller continues
684///   with the normal interpolation + const-eval pipeline.
685/// - [`SpecError::ReadsNone`] if an extent output is not bound in
686///   scope (no cursor of that name is declared at or above it) or is
687///   bound to None.
688/// - [`SpecError::Failed`] if the extent is not an ordinal range.
689fn 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
736/// Recognise the range operator (comprehension_forms.md §3.1,
737/// polydat_grammar.md §16.2) and expand it into a `Vec<Value>`.
738///
739/// Four shapes:
740/// - `a..b`         half-open with step 1
741/// - `a..=b`        closed with step 1
742/// - `a..b..s`      half-open with step `s`
743/// - `a..=b..s`     closed with step `s`
744///
745/// Bounds and step are Polydat const expressions; this function
746/// evaluates each segment via `eval_const_expr_for(segment, ledger)`. Numeric
747/// type follows the bounds: if both are integers, the
748/// emitted list is `Value::U64`; otherwise `Value::F64`.
749///
750/// Returns:
751/// - `Ok(Some(values))` on a successful range expansion.
752/// - `Ok(None)` when `text` doesn't have a top-paren-depth
753///   `..` at all — caller falls through to the standard
754///   const-eval / list-parse path.
755/// - `Err(...)` when the form matches but evaluation fails
756///   (bound non-numeric, step is zero, bounds diverge from
757///   step direction, etc.).
758fn 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    // Find every top-paren-depth `..` (with optional `=`).
766    // Returns positions of the `..` start and whether the
767    // following `=` was present.
768    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                // Skip the rest of the quoted run.
778                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    // Slice out the segments.
813    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; // `..` only, not `..=`
825            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
858/// Materialise the value list once start/end/step have been
859/// const-folded. If any of the three is `F64`, the whole list
860/// is `F64`; otherwise everything is `U64`.
861fn 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        // Direction must match (start < end ⇒ step > 0; start > end ⇒ step < 0).
913        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    // Integer range.
942    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            // saturated; would loop forever on overflow.
980            break;
981        }
982    }
983    Ok(out)
984}
985
986// ============================================================
987// Function-call dispatch (Pushes 7, 8, 9)
988// ============================================================
989
990/// Recognise `name(args)` at the top paren depth. Returns
991/// `Some((name, args))` when the entire `text` is exactly
992/// one function call (with balanced parens, possibly empty
993/// args). Quoted strings within args are walked as opaque
994/// runs so internal commas / parens don't trip the split.
995fn 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    // Make sure the closing `)` matches the opening — i.e.
1006    // the entire text is a single call, not `f(a) + g(b)`.
1007    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; // close mid-text
1020                    }
1021                    let args: String = chars[open + 1..i].iter().collect();
1022                    // SAFETY: trimmed lives for fn duration; we
1023                    // index into the original string via slices
1024                    // with care. Instead of returning a borrowed
1025                    // slice from the local `args` String, return
1026                    // the slices directly from `trimmed`.
1027                    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
1039/// Split a function-argument list on top-level commas. Skips
1040/// commas inside parens, brackets, braces, or quoted strings.
1041fn 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
1071/// Parse a single argument text as a `u64`. Errors carry the
1072/// expected-form context for the user.
1073fn 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
1080/// Parse a single argument as either u64 or f64. Returns the
1081/// f64 representation regardless (callers that need an int
1082/// check `.fract() == 0.0`).
1083fn 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// ============================================================
1091// Named generators (comprehension_forms.md §3.1.3)
1092// ============================================================
1093
1094/// A named generator: a call in source position that expands its
1095/// literal arguments into a finite list of values
1096/// (comprehension_forms.md §3.1.3, "Named generators").
1097#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1098pub enum NamedGenerator {
1099    /// `fib(n)`: the first `n` Fibonacci numbers.
1100    Fib,
1101    /// `fib_until(max)`: the Fibonacci numbers up to `max`.
1102    FibUntil,
1103    /// `pow2(n)`: the first `n` powers of two.
1104    Pow2,
1105    /// `pow2_until(max)`: the powers of two up to `max`.
1106    Pow2Until,
1107    /// `binomial(n)`: row `n` of Pascal's triangle.
1108    Binomial,
1109    /// `geometric(start, factor, n)`: `n` terms of a geometric series.
1110    Geometric,
1111    /// `geometric_until(start, factor, max)`: a geometric series up to `max`.
1112    GeometricUntil,
1113    /// `linear_starts(start, end, n)`: the starts of `n` equal steps.
1114    LinearStarts,
1115    /// `linear_steps(start, end, n)`: `n` evenly spaced points, both ends included.
1116    LinearSteps,
1117    /// `log_steps(start, end, n)`: `n` log-spaced points, both ends included.
1118    LogSteps,
1119}
1120
1121impl NamedGenerator {
1122    /// Every named generator, in declaration order.
1123    pub fn all() -> impl Iterator<Item = NamedGenerator> {
1124        std::iter::successors(Some(Self::Fib), |g| g.after())
1125    }
1126
1127    /// The generator after `self` in [`Self::all`], `None` after the
1128    /// last. The match is exhaustive, so a new variant does not
1129    /// compile until it has a place in the walk.
1130    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    /// The call signature, as written in source position.
1147    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    /// The name a call is written with: the signature up to `(`.
1164    pub fn name(self) -> &'static str {
1165        let sig = self.signature();
1166        &sig[..sig.find('(').unwrap_or(sig.len())]
1167    }
1168
1169    /// The parameter names, from the signature.
1170    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    /// The generator a call name names, if any.
1177    pub fn from_name(name: &str) -> Option<Self> {
1178        Self::all().find(|g| g.name() == name)
1179    }
1180
1181    /// The generator `text` calls, when it is a call of one
1182    /// (`fib({n})`, `log_steps(1, 1000, 4)`).
1183    pub fn of_call(text: &str) -> Option<Self> {
1184        parse_func_call(text).and_then(|(name, _)| Self::from_name(name))
1185    }
1186
1187    /// Whether the generator's values are integers (`u64`); the others
1188    /// yield floats (`f64`).
1189    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    /// Expand the call's argument texts into the generator's values.
1200    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    /// The largest argument a call yields every term of, for a
1260    /// generator whose terms outgrow `u64` as its argument grows:
1261    /// `fib` 93, `pow2` 64, `binomial` 67. `None` for the others.
1262    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
1273/// The largest `n` whose first `n` Fibonacci numbers fit `u64`: term
1274/// 94 is `19740274219868223167`, past `u64::MAX`.
1275const FIB_MAX_N: u64 = 93;
1276/// The largest `n` whose first `n` powers of two fit `u64`: term 65 is
1277/// `2^64`.
1278const POW2_MAX_N: u64 = 64;
1279/// The largest row of Pascal's triangle whose coefficients all fit
1280/// `u64`: `C(68, 31)` is the first coefficient of row 68 past
1281/// `u64::MAX`.
1282const BINOMIAL_MAX_N: u64 = 67;
1283
1284/// Expand a named generator call (`fib(8)`, `linear_steps(0, 1, 4)`)
1285/// into its values. Returns `Ok(None)` when the text is not a call of
1286/// a [`NamedGenerator`] (the caller falls through to the set-op,
1287/// sequencer, and const-eval paths).
1288fn 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
1298/// The refusal of the first named generator call in `text` that fails,
1299/// looking through the arguments of the calls that enclose it
1300/// (`concat(fib(94), 1..3)`), after `{name}` interpolation against
1301/// `kernel`. A named generator's values depend on nothing but its
1302/// arguments, so once they are resolved its failure is the call's
1303/// error wherever it is evaluated (comprehension_forms.md §3.1.3).
1304/// `None` when every named generator call in `text` expands, when an
1305/// interpolation does not resolve, or when `text` calls none.
1306pub 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
1323/// First `n` Fibonacci numbers: 1, 1, 2, 3, 5, 8, ...
1324///
1325/// Term 94 is past `u64::MAX`, so `n` above 93 is refused before any
1326/// term is computed.
1327fn 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    // The pair runs two terms ahead of the last one pushed, past
1336    // `u64::MAX` at the end of `fib(93)`, so it is held in `u128`.
1337    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
1345/// Fibonacci values up to and including the largest ≤ `max`.
1346///
1347/// The pair is held in `u128`, so the walk reaches the 93rd term,
1348/// the largest in `u64`, even though the term after it does not fit.
1349fn 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
1359/// `1, 2, 4, ..., 2^(n-1)`. Term 65 is `2^64`, past `u64::MAX`, so `n`
1360/// above 64 is refused.
1361fn 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
1371/// Powers of two ≤ max.
1372fn 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
1388/// `start, start*factor, start*factor², …` (n terms).
1389fn 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
1399/// `start, start*factor, …` ≤ max. The caller refuses a `factor` that
1400/// is not a finite number above 1, so a positive `start` grows past
1401/// `max`; a `start` of zero or less never does and yields nothing.
1402fn 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
1415/// Binomial coefficients `C(n, 0), C(n, 1), …, C(n, n)`. Every
1416/// coefficient of rows up to 67 fits `u64`, and every later row has one
1417/// past `u64::MAX`, so a row past 67 is refused, naming its first
1418/// coefficient that does not fit. The walk reaches that coefficient
1419/// within a few terms for any large `n`: `binomial(10^12)` fails at
1420/// `C(n, 2)`, not after a trillion terms.
1421fn 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    // `c ≤ u64::MAX` before each step, so `c · (n − k + 1)` fits u128.
1424    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
1439/// Kernel-aware partition comprehension sources
1440/// (cursor_partitions.md §7.1).
1441///
1442/// `subdivide(<ident>, n)` — resolve `<ident>` through the
1443/// kernel's scope chain to a `Partition` (typically an iter-var
1444/// bound by an enclosing `for:` clause) and split it into `n`
1445/// sub-partitions, same boundary math as the `subdivide(p, n)`
1446/// node in polydat-nodes and the `*/N` spec token:
1447///
1448/// ```yaml
1449/// - for: "outer in partitions(\"50%,*\", 1000)"
1450///   phases:
1451///     - for: "inner in subdivide(outer, 5)"
1452///       phases: [walk]
1453/// ```
1454///
1455/// When the ident does not resolve and `probe` is set (synthesis-time
1456/// pre-evaluation probes the clause before the outer iteration
1457/// installs its value), a single placeholder partition is
1458/// returned so iter-var type detection still classifies the
1459/// variable as `ext`. At evaluation (`probe` clear) the source reads
1460/// the ident as any source reads a name: unbound or bound to None, it
1461/// reads None ([`SpecError::ReadsNone`]).
1462fn 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                // Pre-evaluation probe: the outer iter-var isn't
1484                // installed yet. Return one placeholder so the clause's
1485                // iter-var type-detects as `ext`; real values arrive at
1486                // runtime dispatch.
1487                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        // Desugaring of an explicit `partitions(spec, [extent])` source
1505        // in comprehension position (cursor_partitions.md §3, §7.1). The spec string is in a
1506        // comprehension position, so it is parsed + resolved HERE, on the
1507        // Result path — a bad spec (over-sum list, bad recipe/order/window,
1508        // malformed tail) surfaces a clean comprehension error rather than the
1509        // `partitions()` node's eval-time `panic!` (which const-fold swallows
1510        // into a misleading downstream type mismatch). The node is unchanged;
1511        // a spec in comprehension position simply never reaches its eval.
1512        // Default extent 100 (pct space) matches the node; the cursor's
1513        // `over p` re-scales each partition to its declared range.
1514        "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-driven partition source: `profile_partitions(dataset,
1534        // pattern)` cuts the dataset's vector space at the cumulative
1535        // sizes of the profiles matching `pattern`, one partition per
1536        // masked tier (see `library::vectors::build_profile_partitions`).
1537        // Resolved here (like `partitions`/`subdivide`) so the iter-var
1538        // type-detects as a partition even when the dataset can't be
1539        // const-folded at compile time: a resolvable group yields the
1540        // real tiers; an unresolvable one (a compile-time probe, or a
1541        // catalog miss surfaced later by the prebuffer) yields a single
1542        // placeholder so the iter-var still types as `ext`.
1543        "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                // Both args are literal strings after `{...}` interpolation;
1561                // strip matching outer quotes.
1562                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                        // Probe / dataset unavailable: one placeholder so the
1581                        // clause's iter-var type-detects as `ext`. Real tiers
1582                        // arrive once the catalog resolves the group.
1583                        Ok(Some(vec![placeholder_partition()]))
1584                    }
1585                }
1586            }
1587        }
1588        _ => Ok(None),
1589    }
1590}
1591
1592/// The one partition a pre-evaluation probe stands in for a partition
1593/// source whose values are not installed yet, so the clause's iter-var
1594/// type-detects as `ext`.
1595fn 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
1607/// Comprehension-position desugaring (cursor_partitions.md §7.1): a `<ident>.partitions`
1608/// source (the primary operator sweep flow, `for: "p in cursor.partitions"`).
1609///
1610/// In comprehension position a *string* spec desugars per the partition
1611/// grammar. `<ident>.partitions` resolves `<ident>` against the kernel chain
1612/// to its spec string — a workload param such as `cursor=linear:4` — and the
1613/// `.partitions` projection selects the partition-spec desugaring (as opposed
1614/// to the string→token-list desugaring a bare string source would get),
1615/// expanding it into the same `PartitionList` that `partitions(spec)` yields.
1616/// Resolution uses the `partitions(spec)` node's (polydat-nodes) default extent (100, pct
1617/// space); the cursor's `over p` clause re-scales each partition's percentages
1618/// to its actual declared range.
1619///
1620/// This MUST live here (not in `eval_const_expr`, which is kernel-less and
1621/// resolves the `cursor.partitions` field-access to `None`): only the
1622/// comprehension eval has the kernel needed to look the param up.
1623///
1624/// Returns `Ok(None)` when `text` is not a `<ident>.partitions` form. When the
1625/// ident does not resolve and `probe` is set (a pre-evaluation probe before
1626/// the value is installed), a single placeholder partition is returned so
1627/// iter-var type detection yields `ext`; at evaluation the ident reads None
1628/// — the same contract as [`try_eval_partition_call`].
1629fn 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        // Pre-eval probe: the param value isn't installed yet. Return one
1643        // placeholder so the clause's iter-var type-detects as `ext`.
1644        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    // Already a resolved PartitionList → unpack directly.
1650    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    // Otherwise it must be a spec string — desugar it per the partition
1659    // spec language (cursor_partitions.md §3).
1660    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
1676/// Parse + resolve a partition spec string into its unpacked partition
1677/// values, on the Result path. Shared by the comprehension-position
1678/// desugaring forms (`<ident>.partitions` and `partitions("...")`): a bad
1679/// spec surfaces a clean error labelled by `ctx` HERE — it never reaches the
1680/// `partitions()` node's eval-time `panic!`. This is a grammar-position
1681/// concern (cursor_partitions.md §7.1), so spec validation lives where the
1682/// spec is recognized.
1683fn 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
1691/// Resolve a `partitions(...)` spec argument to its string form: a quoted
1692/// string literal yields its inner text; a bare identifier resolves against
1693/// the kernel chain to its string value; anything else is taken verbatim (an
1694/// unquoted spec such as a raw percentage list).
1695fn 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
1717/// Evenly spaced numeric points over `[start, end]`.
1718///
1719/// Half-open form (`linear_starts`): the start of each of `n`
1720/// equal subdivisions of `[start, end)` — `end` is never
1721/// emitted. Inclusive form (`linear_steps`): `n` fence-post
1722/// points covering `[start, end]`, both ends emitted.
1723///
1724/// These yield *values*, not partitions; splitting a
1725/// `Partition` into sub-partitions is `subdivide(p, n)` in the
1726/// partition stdlib (cursor_partitions.md §7.3).
1727fn 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    // Not `(0..n).collect()`: a `u64` range reports its exact length,
1740    // so collecting reserves all `n` up front and a count from the
1741    // spec text no machine can hold aborts the process.
1742    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    // The inclusive form's last point is `end` itself, not the sum
1745    // that rounds near it.
1746    if inclusive && n >= 2 {
1747        out[n as usize - 1] = Value::F64(end);
1748    }
1749    Ok(out)
1750}
1751
1752/// `n` log-spaced points from `start` to `end`, both emitted exactly
1753/// as given; the points between are `exp` of evenly spaced logarithms.
1754/// The caller refuses a bound that is not positive.
1755fn 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
1772// ============================================================
1773// Set operators (comprehension_forms.md §3.1)
1774// ============================================================
1775
1776/// Recognise `concat(...)`, `unique(...)`, etc. Each set op
1777/// recursively evaluates its arguments as sources
1778/// (so `concat(1..10, fib(8))` works), then combines the
1779/// resulting lists. An argument that reads None makes the whole
1780/// source read None.
1781fn 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
1900// ============================================================
1901// Sequencer expansions (comprehension_forms.md §3.1): bucket /
1902// concat_seq / interval_seq, a lookup-table facility reusing the
1903// op-sequencing algorithms.
1904// ============================================================
1905
1906/// Recognise `bucket(items, ratios)` / `bucket("3:a, 1:b")`,
1907/// `concat_seq(...)`, `interval_seq(...)`. Reuses the
1908/// algorithms from the host's op-sequencing.
1909///
1910/// The algorithms aren't exposed cross-crate as raw functions
1911/// today, so we re-implement the small set we need here. The
1912/// outputs match `build_bucket_lut` / `build_concat_lut` /
1913/// `build_interval_lut` byte-for-byte (covered by the
1914/// the host's op-sequencing tests).
1915fn 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    // Two acceptable shapes:
1925    //   1. Single string arg: ratio-prefix shorthand
1926    //      `"3:ann, 1:scan, 2:fetch"`.
1927    //   2. Two list args: items + ratios in lockstep.
1928    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    // The output length is the sum of the ratios, which come from the
1962    // spec text: summed checked, and reserved fallibly, so an absurd
1963    // ratio is this error rather than a wrapped sum or an abort.
1964    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
1977/// Parse `"r1:item1, r2:item2, …"`. Each element is a
1978/// ratio (positive integer) and an item value separated
1979/// by `:`. The string itself comes through `evaluate_spec`
1980/// — typically as a quoted string literal.
1981fn parse_ratio_prefix_shorthand(text: &str) -> Result<(Vec<Value>, Vec<usize>), String> {
1982    // The arg might be a literal `"3:a, 1:b"` (with quotes
1983    // in the source) or already-stripped `3:a, 1:b`.
1984    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
2023/// Bucket sequencer: round-robin from per-item buckets sized
2024/// by ratio. Output length = sum(ratios), which is `total`; `out` is
2025/// reserved for it.
2026fn 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
2044/// Concat sequencer: contiguous runs (all of item 1, then
2045/// all of item 2, …).
2046fn 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
2055/// Interval sequencer: evenly spaced occurrences of each
2056/// item across the output. Picks each output position from
2057/// the item with the largest "weight × position - already
2058/// emitted" — same algorithm as op-sequencing's
2059/// build_interval_lut.
2060fn 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        // Pick the item whose target ratio is most under-met
2072        // at this slot. Target at slot k = (ratio_i * (k+1)) / total.
2073        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
2089/// Map a `Value` to the canonical polydat extern type keyword.
2090///
2091/// Delegates to [`Value::port_type`] + [`PortType::to_keyword`](crate::ast::PortType::to_keyword) —
2092/// the single source of truth for the str↔PortType table. The
2093/// returned keyword round-trips byte-cleanly through
2094/// [`PortType::from_keyword`](crate::ast::PortType::from_keyword) in the DSL extern parser, so every
2095/// typed `Value` variant (including `VecF32`, `Bytes`, `Json`,
2096/// `Handle`) becomes a precisely-typed input on the synthesized
2097/// inner kernel.
2098pub fn value_to_polydat_type_name(v: &Value) -> &'static str {
2099    v.port_type().to_keyword()
2100}
2101
2102// Expand `{name}` placeholders in `text`, resolving each leaf
2103// placeholder against `kernel`'s in-scope name space.
2104//
2105// `interpolate_via_kernel`, `interpolate_with_lookup`, and the
2106// internal `one_pass` / `first_unresolved` / `unescape` helpers
2107// live in `crate::kernel::interp`.
2108// `interpolate_via_kernel` and `interpolate_with_lookup` are
2109// imported above for internal use; external callers use the
2110// `polydat::kernel::interp` module directly.
2111
2112#[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(&[]), &params).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, &params).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, &params).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, &params).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(&[]), &params).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(&[]), &params).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(&[]), &params).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        // Simulate a cursor declaration at the parent scope by
2288        // exposing the auxiliary extent outputs as folded
2289        // constants. The real cursor compiler emits these via
2290        // `__cursor_extent_<name>_{start,end}` outputs; for this
2291        // test we synthesize them directly.
2292        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        // `all(<ident>)` is the only matched shape — anything
2338        // more complex falls through to the normal eval path.
2339        // `all(row, 5)` doesn't match the strict shape (the
2340        // comma breaks the bare-ident requirement), so the
2341        // pipeline tries to evaluate it as a regular GK
2342        // expression. There's no registered function named
2343        // `all`, so eval fails and the failure is propagated as
2344        // a clean clause-level error rather than split into a
2345        // literal list.
2346        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        // A workload runs on a system whose vectordata catalog
2367        // doesn't have the requested dataset. The source
2368        //   `profile in matching_profiles('nonexistent_dataset_xyz', 'label_')`
2369        // produces a clean clause-level error naming the
2370        // resolution failure, never a garbage iter-var like
2371        // `matching_profiles('nonexistent_dataset_xyz'`
2372        // (truncated at the first comma) that would flow
2373        // downstream into malformed output. Every layer on the
2374        // way (the dataset open, the handle read, and
2375        // `evaluate_spec`) propagates an actionable diagnostic.
2376        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        // Doesn't matter which exact error string we get from
2386        // the catalog layer — the test guards the *contract*:
2387        // the spec text appears in the error, the failure is
2388        // attributed to the dataset / resolver / open path, and
2389        // it is a Result::Err (not garbage data).
2390        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        // Defensive: any text containing `(` is an
2401        // expression — never a literal list. If eval fails, we
2402        // must propagate the failure rather than splitting on
2403        // commas. This guards the broader contract that
2404        // protected the dataset-resolution case above.
2405        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        // Counter-case: a plain comma-separated list of
2419        // literals (no parens, no operators) MUST still work
2420        // through the literal-list fallback after eval fails
2421        // (which it should — `1, 10, 100` isn't a single GK
2422        // expression). This is the legitimate use case that the
2423        // fallback exists for.
2424        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        // Real cursor declaration with literal extent — verifies
2443        // the compiler-side change that emits
2444        // __cursor_extent_<name>_{start,end} as final bindings
2445        // even in the literal-args case.
2446        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        // A bare identifier source is a direct wire/param reference
2499        // (comprehension_forms.md §3.1.4) and resolves identically to
2500        // the braced `{name}` interpolation form.
2501        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        // A bare identifier source that doesn't resolve is a hard
2513        // error (comprehension_forms.md §3.1.4) (not silently bound as its own
2514        // name-string), and the message points at the fix.
2515        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    /// A source reads None for the names it reads after composition: the
2524    /// composed target of `{k_{k}_limits}`, and a name bound to None, are
2525    /// reads of None, and a failure over a name the scope has is not.
2526    #[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        // The public evaluator reads a name bound to None as None and
2563        // still refuses one nothing binds.
2564        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        // `[xs…]` destructures (peels one level); `[xs]` binds the
2578        // whole value once.
2579        let kernel =
2580            crate::dsl::compile::compile_polydat_interpreter("const xs := \"1, 2, 3\"\n").unwrap();
2581        // spread → peel the string's tokens
2582        let spread = evaluate_spec("[xs…]", &kernel).unwrap();
2583        assert_eq!(spread, vec![Value::U64(1), Value::U64(2), Value::U64(3)]);
2584        // no-peel → the whole value once (the string, un-striped)
2585        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    // ── Partition-list unpacking (cursor_partitions.md §7.1) ──
2606
2607    #[test]
2608    fn evaluate_spec_unpacks_partition_list_into_partition_values() {
2609        // `partitions("linear:3")` evaluates to a PartitionList
2610        // Ext value. evaluate_spec must unpack the list into a
2611        // Vec of individual Partition values so the for-clause
2612        // iterates partition-by-partition (one iteration per
2613        // partition).
2614        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        // Partition indices increment from 0.
2631        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        // Same as the evaluate_spec test above but via the
2641        // synthesis-side pre_evaluate_clause entry point.
2642        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        // The for_each scope synthesizer uses this to emit
2662        // `extern <var>: <keyword>` for each iter-var. Ext-typed
2663        // values (Partition, PartitionSpec, PartitionList) must
2664        // declare as `ext` so the resulting input port is
2665        // PortType::Ext and downstream `over <iter-var>` clauses
2666        // see the right shape.
2667        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    // ── Range operator ──
2681
2682    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        // SI suffixes (polydat_grammar.md §2.3) compose with range
2782        // bounds.
2783        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        // `range(1, 10)` — the dots inside the function
2818        // call shouldn't trigger range-splitting at top
2819        // depth (there are no `..` here anyway, but verify
2820        // paren-balanced text passes through cleanly).
2821        // Use a literal with internal parens to exercise
2822        // the depth tracking.
2823        let v = evaluate_spec("(1)..(5)", &empty_kernel()).unwrap();
2824        assert_eq!(v.len(), 4); // 1, 2, 3, 4
2825    }
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    // ── Named generators ──
2856
2857    #[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        // C(5,0..5) = 1, 5, 10, 10, 5, 1
2930        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        // Floats because factor is float-cast at eval.
2948        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        // (100-0)/5 = 20 step. 0, 20, 40, 60, 80.
2962        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        // 0, 25, 50, 75, 100
2984        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        // 1, 10, 100, 1000
3006        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    /// The last point of `linear_steps` and both ends of `log_steps` are
3035    /// the given bounds exactly, not sums or `exp`s that round near them.
3036    #[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    /// The largest valid argument of each generator whose terms outgrow
3066    /// `u64`, found by computing the terms in `u128`: the first term
3067    /// past `u64::MAX` is the one the refusal names.
3068    #[test]
3069    fn overflow_limits_are_where_the_terms_leave_u64() {
3070        let max = u128::from(u64::MAX);
3071        // Fibonacci: the first term past u64::MAX, 1-based.
3072        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        // Powers of two: 2^64 is term 65.
3079        assert_eq!(1u128 << 64, max + 1);
3080        assert_eq!(NamedGenerator::Pow2.largest_valid_argument(), Some(64));
3081        // Pascal's triangle: the first row with a coefficient past
3082        // u64::MAX, and that coefficient.
3083        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    /// Each limit is the last argument that yields every term, and the
3101    /// next is refused, naming the call, the first term past
3102    /// `u64::MAX`, and the limit.
3103    #[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    /// `geometric` takes a positive, finite factor, and
3146    /// `geometric_until` a finite factor above 1; the refusal names the
3147    /// argument.
3148    #[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    /// The refusal of a named generator is found through the calls that
3188    /// enclose it; a call that expands, and a call of anything else, is
3189    /// not a refusal.
3190    #[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    // ── Set operators ──
3206
3207    #[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        // 1,2,3 (from first) + 4,5 (from second; 3 already present)
3227        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        // 1..6 = [1,2,3,4,5], minus [3,4] = [1, 2, 5]
3258        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        // pow2(8) = 1, 2, 4, 8, 16, 32, 64, 128
3318        // 1..1000..100 = 1, 101, 201, 301, 401, 501, 601, 701, 801, 901
3319        // dedupe: 1, 2, 4, 8, 16, 32, 64, 128, 101, 201, 301, 401, 501, 601, 701, 801, 901
3320        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    // ── Sequencer expansions ──
3327
3328    #[test]
3329    fn bucket_round_robin_3_1_2() {
3330        // Two-arg form: items list + ratios list.
3331        let v = evaluate_spec(
3332            "bucket(concat('ann', 'scan', 'fetch'), concat(3, 1, 2))",
3333            &empty_kernel(),
3334        )
3335        .unwrap();
3336        // Wait — concat doesn't make sense with these args (mixed types).
3337        // Use the literal form via the Polydat list parser.
3338        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        // Bucket sequencer round-robins; each "tick" pulls
3345        // one from each remaining bucket. Total = 6.
3346        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        // First tick: ann, scan, fetch (one from each).
3355        // Then ann (3 left), fetch (2 left). Next: ann, fetch.
3356        // Then ann. Total: ann*3, scan*1, fetch*2.
3357        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        // Total length 4. write should appear once,
3393        // somewhere in the middle (not bunched at edges).
3394        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()); // mid-text close
3423    }
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}