Skip to main content

eui_core/
cel.rs

1//! VAPP-191 (eApps Q2): expressions, the Rust twin of
2//! `packages/exponential-ui/src/cel.ts` (line by line; read its header for
3//! the language). A SUBSET of CEL with ONE number type (a double, like
4//! JSON), hand-written so both twins replay `fixtures/expressions.json`
5//! exactly: numbers, dates and error texts. No loops, no user functions, a
6//! length cap, no nested comprehensions and a step limit, so it always
7//! terminates. The functions are ONE table shared with the A2UI function
8//! form (`{"call": "mul", …}`, [`call_expression_function`]); dates run in
9//! the team zone (an offset per instant, [`ExpressionEnv::offset`]).
10
11use crate::dates::{day_from_number, day_number, days_in_month, format_day, iso_week, parse_day, weekday_of};
12use crate::json;
13use serde_json::{Map, Value};
14
15/// The cost caps (spec §4.2): characters (code points), nesting, steps.
16pub const MAX_LENGTH: usize = 1000;
17pub const MAX_DEPTH: usize = 32;
18pub const MAX_STEPS: usize = 10_000;
19
20/// Every unit `dateAdd` / `dateDiff` take; `startOf` / `endOf` take the calendar ones.
21pub const DATE_UNITS: &[&str] = &["second", "minute", "hour", "day", "week", "month", "quarter", "year"];
22pub const CALENDAR_UNITS: &[&str] = &["day", "week", "month", "quarter", "year"];
23/// The formats `formatDuration` writes.
24pub const DURATION_FORMATS: &[&str] = &["h:mm", "hh:mm"];
25
26/// The clock, the team zone and the step limit an evaluation runs with.
27#[derive(Clone, Copy, Default)]
28pub struct ExpressionEnv<'a> {
29    /// Epoch ms `today()` / `now()` read; `None` = the wall clock.
30    pub now: Option<f64>,
31    /// Minutes east of UTC at an instant; `None` = UTC.
32    pub offset: Option<&'a (dyn Fn(f64) -> i32 + Send + Sync)>,
33    /// The step limit (default [`MAX_STEPS`]).
34    pub max_steps: Option<usize>,
35}
36
37impl std::fmt::Debug for ExpressionEnv<'_> {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("ExpressionEnv").field("now", &self.now).field("max_steps", &self.max_steps).finish_non_exhaustive()
40    }
41}
42
43struct Env<'a> {
44    now: f64,
45    offset: Option<&'a (dyn Fn(f64) -> i32 + Send + Sync)>,
46}
47
48impl<'a> Env<'a> {
49    fn of(env: &ExpressionEnv<'a>) -> Env<'a> {
50        Env { now: env.now.unwrap_or_else(crate::format::now_ms).floor(), offset: env.offset }
51    }
52    fn off(&self, ms: f64) -> f64 {
53        self.offset.map_or(0.0, |o| f64::from(o(ms)))
54    }
55}
56
57type R<T> = Result<T, String>;
58
59// ---------------------------------------------------------------------------
60// Values
61// ---------------------------------------------------------------------------
62
63/// The runtime type name errors print.
64pub fn type_name(v: &Value) -> &'static str {
65    match v {
66        Value::Null => "null",
67        Value::Number(_) => "number",
68        Value::String(_) => "string",
69        Value::Bool(_) => "bool",
70        Value::Array(_) => "list",
71        Value::Object(_) => "map",
72    }
73}
74
75/// A finite result with `-0` as 0; anything else is out of range.
76fn num(v: f64) -> R<f64> {
77    if !v.is_finite() {
78        return Err("number out of range".into());
79    }
80    Ok(if v == 0.0 { 0.0 } else { v })
81}
82
83fn num_text(n: f64) -> String {
84    json::number_to_string(n)
85}
86
87fn deep_equal(a: &Value, b: &Value) -> bool {
88    match (a, b) {
89        (Value::Number(x), Value::Number(y)) => x.as_f64() == y.as_f64(),
90        (Value::Array(x), Value::Array(y)) => x.len() == y.len() && x.iter().zip(y).all(|(a, b)| deep_equal(a, b)),
91        (Value::Object(x), Value::Object(y)) => x.len() == y.len() && x.iter().all(|(k, v)| y.get(k).is_some_and(|w| deep_equal(v, w))),
92        _ => a == b,
93    }
94}
95
96/// Round half away from zero without `x + 0.5`.
97fn round_half_away(x: f64) -> f64 {
98    let a = x.abs();
99    let f = a.floor();
100    let r = if a - f >= 0.5 { f + 1.0 } else { f };
101    if x < 0.0 {
102        -r
103    } else {
104        r
105    }
106}
107
108fn utf16_cmp(a: &str, b: &str) -> std::cmp::Ordering {
109    a.encode_utf16().cmp(b.encode_utf16())
110}
111
112// ---------------------------------------------------------------------------
113// Dates (the team zone)
114// ---------------------------------------------------------------------------
115
116const DAY_MS: f64 = 86_400_000.0;
117
118fn unit_ms(unit: &str) -> Option<f64> {
119    match unit {
120        "second" => Some(1000.0),
121        "minute" => Some(60_000.0),
122        "hour" => Some(3_600_000.0),
123        _ => None,
124    }
125}
126
127#[derive(Clone, Copy)]
128enum Temporal {
129    Date(i64),
130    Ts(f64),
131}
132
133/// An ISO instant (`YYYY-MM-DDTHH:MM[:SS[.fff]][Z|±HH:MM]`, no offset = UTC)
134/// as epoch ms; `None` when it is not one.
135pub fn parse_timestamp(text: &str) -> Option<f64> {
136    let b = text.as_bytes();
137    let digit = |i: usize| b.get(i).is_some_and(u8::is_ascii_digit);
138    let int = |from: usize, n: usize| b[from..from + n].iter().fold(0i64, |acc, c| acc * 10 + i64::from(c - b'0'));
139    if b.len() < 16 || b[10] != b'T' || !(digit(11) && digit(12) && b[13] == b':' && digit(14) && digit(15)) {
140        return None;
141    }
142    let day = parse_day(&text[..10]).filter(|_| text.is_char_boundary(10))?;
143    if !(b[..10].iter().enumerate().all(|(i, c)| if i == 4 || i == 7 { *c == b'-' } else { c.is_ascii_digit() })) {
144        return None;
145    }
146    let (h, mi) = (int(11, 2), int(14, 2));
147    let mut i = 16;
148    let mut s = 0;
149    let mut ms = 0;
150    if b.get(i) == Some(&b':') {
151        if !(digit(i + 1) && digit(i + 2)) {
152            return None;
153        }
154        s = int(i + 1, 2);
155        i += 3;
156        if b.get(i) == Some(&b'.') {
157            let start = i + 1;
158            let mut end = start;
159            while digit(end) {
160                end += 1;
161            }
162            if end == start {
163                return None;
164            }
165            let frac: String = text[start..end.min(start + 3)].chars().collect();
166            ms = format!("{frac:0<3}").parse::<i64>().ok()?;
167            i = end;
168        }
169    }
170    let mut offset = 0;
171    match b.get(i) {
172        None => {}
173        Some(b'Z') if i + 1 == b.len() => {}
174        Some(c @ (b'+' | b'-')) if b.len() == i + 6 && digit(i + 1) && digit(i + 2) && b[i + 3] == b':' && digit(i + 4) && digit(i + 5) => {
175            let (oh, om) = (int(i + 1, 2), int(i + 4, 2));
176            if oh > 23 || om > 59 {
177                return None;
178            }
179            offset = (if *c == b'-' { -1 } else { 1 }) * (oh * 60 + om);
180        }
181        _ => return None,
182    }
183    if h > 23 || mi > 59 || s > 59 {
184        return None;
185    }
186    Some((day_number(day.y, day.m, day.d) as f64) * DAY_MS + (h * 3_600_000 + mi * 60_000 + s * 1000 + ms - offset * 60_000) as f64)
187}
188
189/// An instant as `YYYY-MM-DDTHH:MM:SS.sssZ` (JS `toISOString`), years 0..9999.
190pub fn format_timestamp(ms: f64) -> R<String> {
191    let day = (ms / DAY_MS).floor();
192    let tod = (ms - day * DAY_MS) as i64;
193    let d = day_from_number(day as i64);
194    if d.y < 0 || d.y > 9999 {
195        return Err("date out of range".into());
196    }
197    Ok(format!("{}T{:02}:{:02}:{:02}.{:03}Z", format_day(d), tod / 3_600_000, (tod / 60_000) % 60, (tod / 1000) % 60, tod % 1000))
198}
199
200fn day_text(day: i64) -> R<String> {
201    let d = day_from_number(day);
202    if d.y < 0 || d.y > 9999 {
203        return Err("date out of range".into());
204    }
205    Ok(format_day(d))
206}
207
208/// The wall clock (as if UTC) of an instant in the zone.
209fn to_local(env: &Env, ms: f64) -> f64 {
210    ms + env.off(ms) * 60_000.0
211}
212
213/// The instant of a wall-clock time in the zone: a time in a DST gap moves
214/// forward by the gap; a repeated time takes its later (standard) instant.
215fn from_local(env: &Env, wall: f64) -> f64 {
216    let first = env.off(wall);
217    let utc = wall - first * 60_000.0;
218    let second = env.off(utc);
219    if second == first {
220        utc
221    } else {
222        wall - second * 60_000.0
223    }
224}
225
226fn temporal(f: &str, v: &Value) -> R<Temporal> {
227    let Value::String(s) = v else { return Err(format!("{f} needs a date, got {}", type_name(v))) };
228    if s.chars().count() == 10 {
229        if let Some(d) = parse_day(s) {
230            return Ok(Temporal::Date(day_number(d.y, d.m, d.d)));
231        }
232    }
233    parse_timestamp(s).map(Temporal::Ts).ok_or_else(|| format!("not a date: {s}"))
234}
235
236fn floor_day(wall: f64) -> i64 {
237    (wall / DAY_MS).floor() as i64
238}
239
240fn wall_of(env: &Env, t: Temporal) -> f64 {
241    match t {
242        Temporal::Date(day) => day as f64 * DAY_MS,
243        Temporal::Ts(ms) => to_local(env, ms),
244    }
245}
246
247fn unit_of<'a>(f: &str, v: &'a Value, units: &[&str]) -> R<&'a str> {
248    let Value::String(s) = v else { return Err(format!("{f} needs a string, got {}", type_name(v))) };
249    if !units.contains(&s.as_str()) {
250        return Err(format!("unknown unit {s}"));
251    }
252    Ok(s)
253}
254
255fn add_months(day: i64, months: i64) -> i64 {
256    let c = day_from_number(day);
257    let total = c.y * 12 + (c.m - 1) + months;
258    let y = total.div_euclid(12);
259    let m = total - y * 12 + 1;
260    day_number(y, m, c.d.min(days_in_month(y, m)))
261}
262
263fn add_calendar(day: i64, n: i64, unit: &str) -> i64 {
264    match unit {
265        "day" => day + n,
266        "week" => day + 7 * n,
267        "month" => add_months(day, n),
268        "quarter" => add_months(day, n * 3),
269        _ => add_months(day, n * 12),
270    }
271}
272
273fn start_day(day: i64, unit: &str) -> i64 {
274    let c = day_from_number(day);
275    match unit {
276        "day" => day,
277        "week" => {
278            let wd = match weekday_of(c.y, c.m, c.d) {
279                0 => 7,
280                n => n as i64,
281            };
282            day - (wd - 1)
283        }
284        "month" => day_number(c.y, c.m, 1),
285        "quarter" => day_number(c.y, (c.m - 1) / 3 * 3 + 1, 1),
286        _ => day_number(c.y, 1, 1),
287    }
288}
289
290fn number(f: &str, v: &Value) -> R<f64> {
291    match v {
292        Value::Number(n) => Ok(n.as_f64().unwrap_or(0.0)),
293        _ => Err(format!("{f} needs a number, got {}", type_name(v))),
294    }
295}
296
297fn whole_number(f: &str, v: &Value) -> R<f64> {
298    let n = number(f, v)?;
299    if n.fract() != 0.0 {
300        return Err(format!("{f} needs a whole number, got {}", num_text(n)));
301    }
302    Ok(n)
303}
304
305fn text<'a>(f: &str, v: &'a Value) -> R<&'a str> {
306    match v {
307        Value::String(s) => Ok(s),
308        _ => Err(format!("{f} needs a string, got {}", type_name(v))),
309    }
310}
311
312fn date_add(args: &[Value], env: &Env) -> R<Value> {
313    let t = temporal("dateAdd", &args[0])?;
314    let n = whole_number("dateAdd", &args[1])?;
315    let unit = unit_of("dateAdd", &args[2], DATE_UNITS)?;
316    let out = match t {
317        Temporal::Date(day) => {
318            if unit_ms(unit).is_some() {
319                return Err(format!("unit {unit} needs a timestamp"));
320            }
321            day_text(add_calendar(day, n as i64, unit))?
322        }
323        Temporal::Ts(ms) => match unit_ms(unit) {
324            Some(u) => format_timestamp(num(ms + n * u)?)?,
325            None => {
326                let wall = to_local(env, ms);
327                let day = floor_day(wall);
328                let tod = wall - day as f64 * DAY_MS;
329                format_timestamp(from_local(env, add_calendar(day, n as i64, unit) as f64 * DAY_MS + tod))?
330            }
331        },
332    };
333    Ok(Value::String(out))
334}
335
336fn date_diff(args: &[Value], env: &Env) -> R<Value> {
337    let a = temporal("dateDiff", &args[0])?;
338    let b = temporal("dateDiff", &args[1])?;
339    let unit = unit_of("dateDiff", &args[2], DATE_UNITS)?;
340    if let Some(u) = unit_ms(unit) {
341        let instant = |t: Temporal| match t {
342            Temporal::Date(day) => from_local(env, day as f64 * DAY_MS),
343            Temporal::Ts(ms) => ms,
344        };
345        return Ok(json::number(num(((instant(b) - instant(a)) / u).trunc())?));
346    }
347    let (wa, wb) = (wall_of(env, a), wall_of(env, b));
348    if unit == "day" || unit == "week" {
349        let span = if unit == "day" { DAY_MS } else { 7.0 * DAY_MS };
350        return Ok(json::number(num(((wb - wa) / span).trunc())?));
351    }
352    let (da, db) = (floor_day(wa), floor_day(wb));
353    let (ca, cb) = (day_from_number(da), day_from_number(db));
354    let mut months = (cb.y - ca.y) * 12 + (cb.m - ca.m);
355    let rest_a = ca.d as f64 * DAY_MS + (wa - da as f64 * DAY_MS);
356    let rest_b = cb.d as f64 * DAY_MS + (wb - db as f64 * DAY_MS);
357    if months > 0 && rest_b < rest_a {
358        months -= 1;
359    } else if months < 0 && rest_b > rest_a {
360        months += 1;
361    }
362    let per = match unit {
363        "month" => 1.0,
364        "quarter" => 3.0,
365        _ => 12.0,
366    };
367    Ok(json::number(num((months as f64 / per).trunc())?))
368}
369
370fn start_end(f: &str, args: &[Value], env: &Env) -> R<Value> {
371    let t = temporal(f, &args[0])?;
372    let unit = unit_of(f, &args[1], CALENDAR_UNITS)?;
373    let day = match t {
374        Temporal::Date(day) => day,
375        Temporal::Ts(ms) => floor_day(to_local(env, ms)),
376    };
377    let is_date = matches!(t, Temporal::Date(_));
378    let out = if f == "startOf" {
379        let start = start_day(day, unit);
380        if is_date {
381            day_text(start)?
382        } else {
383            format_timestamp(from_local(env, start as f64 * DAY_MS))?
384        }
385    } else {
386        let next = add_calendar(start_day(day, unit), 1, unit);
387        if is_date {
388            day_text(next - 1)?
389        } else {
390            format_timestamp(from_local(env, next as f64 * DAY_MS) - 1.0)?
391        }
392    };
393    Ok(Value::String(out))
394}
395
396fn local_day(f: &str, v: &Value, env: &Env) -> R<crate::dates::CalendarDay> {
397    Ok(day_from_number(match temporal(f, v)? {
398        Temporal::Date(day) => day,
399        Temporal::Ts(ms) => floor_day(to_local(env, ms)),
400    }))
401}
402
403fn format_duration(args: &[Value]) -> R<Value> {
404    let h = number("formatDuration", &args[0])?;
405    let format = text("formatDuration", &args[1])?;
406    if !DURATION_FORMATS.contains(&format) {
407        return Err(format!("unknown duration format {format}"));
408    }
409    let total = round_half_away(h.abs() * 60.0);
410    if total > 1e15 {
411        return Err("number out of range".into());
412    }
413    let hours = (total / 60.0).floor() as i64;
414    let minutes = total as i64 - hours * 60;
415    let sign = if h < 0.0 && total > 0.0 { "-" } else { "" };
416    let hours = if format == "hh:mm" { format!("{hours:02}") } else { hours.to_string() };
417    Ok(Value::String(format!("{sign}{hours}:{minutes:02}")))
418}
419
420fn round(args: &[Value]) -> R<Value> {
421    let x = number("round", &args[0])?;
422    let digits = match args.get(1) {
423        None => 0.0,
424        Some(Value::Number(n)) => {
425            let d = n.as_f64().unwrap_or(f64::NAN);
426            if d.fract() != 0.0 || !(0.0..=10.0).contains(&d) {
427                return Err("round needs 0 to 10 digits".into());
428            }
429            d
430        }
431        Some(_) => return Err("round needs 0 to 10 digits".into()),
432    };
433    let mut p = 1.0;
434    for _ in 0..digits as usize {
435        p *= 10.0;
436    }
437    Ok(json::number(num(round_half_away(x * p) / p)?))
438}
439
440fn size(v: &Value) -> R<Value> {
441    Ok(json::number(match v {
442        Value::String(s) => s.chars().count() as f64,
443        Value::Array(a) => a.len() as f64,
444        Value::Object(m) => m.len() as f64,
445        _ => return Err(format!("size needs a list, string or map, got {}", type_name(v))),
446    }))
447}
448
449#[derive(Clone, Copy, PartialEq, Eq)]
450enum Param {
451    Number,
452    Text,
453    Temporal,
454    Sized,
455}
456
457struct Def {
458    params: &'static [Param],
459    min: usize,
460    variadic: bool,
461    args: &'static [&'static str],
462}
463
464use Param::{Number as N, Sized as Z, Temporal as T, Text as S};
465
466/// THE function table (spec §4.2), positional for CEL, named (`args`) for
467/// the A2UI form. Generic only: no working days, no holidays.
468pub const FUNCTION_NAMES: &[&str] = &["mul", "div", "mod", "round", "floor", "ceil", "abs", "min", "max", "today", "now", "dateAdd", "dateDiff", "startOf", "endOf", "weekday", "isoWeek", "hours", "formatDuration", "size"];
469
470/// The A2UI form's names in the bind table (`size` = the existing `len`;
471/// `expr` = the string form, `{expr, vars}`).
472pub const CALL_NAMES: &[&str] = &["mul", "div", "mod", "round", "floor", "ceil", "abs", "min", "max", "today", "now", "dateAdd", "dateDiff", "startOf", "endOf", "weekday", "isoWeek", "hours", "formatDuration", "expr"];
473
474fn def(name: &str) -> Option<Def> {
475    let d = |params: &'static [Param], args: &'static [&'static str]| Def { params, min: params.len(), variadic: false, args };
476    Some(match name {
477        "mul" | "div" | "mod" => d(&[N, N], &["a", "b"]),
478        "round" => Def { params: &[N, N], min: 1, variadic: false, args: &["value", "digits"] },
479        "floor" | "ceil" | "abs" => d(&[N], &["value"]),
480        "min" | "max" => Def { params: &[N], min: 1, variadic: true, args: &["values"] },
481        "today" | "now" => d(&[], &[]),
482        "dateAdd" => d(&[T, N, S], &["date", "n", "unit"]),
483        "dateDiff" => d(&[T, T, S], &["a", "b", "unit"]),
484        "startOf" | "endOf" => d(&[T, S], &["date", "unit"]),
485        "weekday" | "isoWeek" => d(&[T], &["date"]),
486        "hours" => d(&[N], &["minutes"]),
487        "formatDuration" => d(&[N, S], &["hours", "format"]),
488        "size" => d(&[Z], &["value"]),
489        _ => return None,
490    })
491}
492
493fn arity_error(name: &str, def: &Def, n: usize) -> Option<String> {
494    if def.variadic {
495        return (n < 1).then(|| format!("{name} takes at least 1 argument"));
496    }
497    let (min, max) = (def.min, def.params.len());
498    if n >= min && n <= max {
499        return None;
500    }
501    Some(if min == max { format!("{name} takes {min} argument{}", if min == 1 { "" } else { "s" }) } else { format!("{name} takes {min} or {max} arguments") })
502}
503
504fn run(name: &str, args: &[Value], env: &Env) -> R<Value> {
505    let n = |f: fn(f64) -> f64| -> R<Value> { Ok(json::number(num(f(number(name, &args[0])?))?)) };
506    match name {
507        "mul" => Ok(json::number(num(number("mul", &args[0])? * number("mul", &args[1])?)?)),
508        "div" => {
509            let (x, y) = (number("div", &args[0])?, number("div", &args[1])?);
510            if y == 0.0 {
511                return Err("division by zero".into());
512            }
513            Ok(json::number(num(x / y)?))
514        }
515        "mod" => {
516            let (x, y) = (number("mod", &args[0])?, number("mod", &args[1])?);
517            if y == 0.0 {
518                return Err("modulo by zero".into());
519            }
520            Ok(json::number(num(x % y)?))
521        }
522        "round" => round(args),
523        "floor" => n(f64::floor),
524        "ceil" => n(f64::ceil),
525        "abs" => n(f64::abs),
526        "min" | "max" => {
527            let mut out = if name == "min" { f64::INFINITY } else { f64::NEG_INFINITY };
528            for v in args {
529                let x = number(name, v)?;
530                out = if name == "min" { out.min(x) } else { out.max(x) };
531            }
532            Ok(json::number(num(out)?))
533        }
534        "today" => Ok(Value::String(day_text(floor_day(to_local(env, env.now)))?)),
535        "now" => Ok(Value::String(format_timestamp(env.now)?)),
536        "dateAdd" => date_add(args, env),
537        "dateDiff" => date_diff(args, env),
538        "startOf" | "endOf" => start_end(name, args, env),
539        "weekday" => {
540            let d = local_day("weekday", &args[0], env)?;
541            Ok(json::number(match weekday_of(d.y, d.m, d.d) {
542                0 => 7.0,
543                w => w as f64,
544            }))
545        }
546        "isoWeek" => {
547            let d = local_day("isoWeek", &args[0], env)?;
548            Ok(json::number(iso_week(d.y, d.m, d.d).1 as f64))
549        }
550        "hours" => Ok(json::number(num(number("hours", &args[0])? / 60.0)?)),
551        "formatDuration" => format_duration(args),
552        "size" => size(&args[0]),
553        _ => Err(format!("unknown function {name}")),
554    }
555}
556
557/// The A2UI function form `{call: name, args}` with its arguments resolved.
558/// Outer `None` = not an expression function; inner `None` = no value (an
559/// error, like every bind-time function).
560pub fn call_expression_function(name: &str, args: &Map<String, Value>, env: &ExpressionEnv) -> Option<Option<Value>> {
561    if name == "expr" {
562        let Some(Value::String(expr)) = args.get("expr") else { return Some(None) };
563        let empty = Map::new();
564        let vars = args.get("vars").and_then(Value::as_object).unwrap_or(&empty);
565        return Some(evaluate_expression(expr, vars, env).ok());
566    }
567    if name == "size" {
568        return None;
569    }
570    let def = def(name)?;
571    let positional: Vec<Value> = if def.variadic {
572        args.get("values").and_then(Value::as_array).cloned().unwrap_or_default()
573    } else {
574        let mut list: Vec<Option<&Value>> = def.args.iter().map(|k| args.get(*k)).collect();
575        while list.len() > def.min && list.last().is_some_and(Option::is_none) {
576            list.pop();
577        }
578        list.into_iter().map(|v| v.cloned().unwrap_or(Value::Null)).collect()
579    };
580    if arity_error(name, &def, positional.len()).is_some() {
581        return Some(None);
582    }
583    Some(run(name, &positional, &Env::of(env)).ok())
584}
585
586// ---------------------------------------------------------------------------
587// Parsing
588// ---------------------------------------------------------------------------
589
590/// A parsed expression (`at` = the code point it starts at).
591#[derive(Debug, Clone, PartialEq)]
592pub enum Node {
593    Lit(Value, usize),
594    Id(String, usize),
595    List(Vec<Node>, usize),
596    Field(Box<Node>, String, usize),
597    Index(Box<Node>, Box<Node>, usize),
598    Unary(char, Box<Node>, usize),
599    Bin(&'static str, Box<Node>, Box<Node>, usize),
600    Cond(Box<Node>, Box<Node>, Box<Node>, usize),
601    Call(String, Vec<Node>, usize),
602    Macro(String, Box<Node>, String, Box<Node>, usize),
603}
604
605#[derive(Debug, Clone, PartialEq)]
606enum Tok {
607    Num(f64),
608    Str(String),
609    Id(String),
610    Op(&'static str),
611    End,
612}
613
614#[derive(Debug, Clone)]
615struct Token {
616    t: Tok,
617    at: usize,
618    text: String,
619}
620
621const OPS2: &[&str] = &["==", "!=", "<=", ">=", "&&", "||"];
622const OPS1: &[&str] = &["+", "-", "*", "/", "%", "<", ">", "!", "(", ")", "[", "]", ",", ".", "?", ":"];
623
624fn is_digit(c: Option<&char>) -> bool {
625    c.is_some_and(char::is_ascii_digit)
626}
627fn is_id_start(c: Option<&char>) -> bool {
628    c.is_some_and(|c| c.is_ascii_alphabetic() || *c == '_')
629}
630fn is_id_part(c: Option<&char>) -> bool {
631    is_id_start(c) || is_digit(c)
632}
633
634fn tokenize(cs: &[char]) -> R<Vec<Token>> {
635    let mut out = Vec::new();
636    let mut i = 0;
637    while i < cs.len() {
638        let c = cs[i];
639        if matches!(c, ' ' | '\t' | '\n' | '\r') {
640            i += 1;
641            continue;
642        }
643        let at = i;
644        if c.is_ascii_digit() {
645            while is_digit(cs.get(i)) {
646                i += 1;
647            }
648            if cs.get(i) == Some(&'.') && is_digit(cs.get(i + 1)) {
649                i += 1;
650                while is_digit(cs.get(i)) {
651                    i += 1;
652                }
653            }
654            if matches!(cs.get(i), Some('e' | 'E')) && (is_digit(cs.get(i + 1)) || (matches!(cs.get(i + 1), Some('+' | '-')) && is_digit(cs.get(i + 2)))) {
655                i += 2;
656                while is_digit(cs.get(i)) {
657                    i += 1;
658                }
659            }
660            let text: String = cs[at..i].iter().collect();
661            let v = text.parse::<f64>().unwrap_or(f64::INFINITY);
662            out.push(Token { t: Tok::Num(v), at, text });
663            continue;
664        }
665        if c == '\'' || c == '"' {
666            i += 1;
667            let mut s = String::new();
668            loop {
669                let d = cs.get(i).copied();
670                match d {
671                    None | Some('\n') => return Err(format!("at {at}: unterminated string")),
672                    Some(d) if d == c => break,
673                    Some('\\') => {
674                        let e = cs.get(i + 1).copied();
675                        let mapped = match e {
676                            Some('\\') => '\\',
677                            Some('\'') => '\'',
678                            Some('"') => '"',
679                            Some('n') => '\n',
680                            Some('t') => '\t',
681                            Some('r') => '\r',
682                            _ => return Err(format!("at {i}: unknown escape \\{}", e.map(String::from).unwrap_or_default())),
683                        };
684                        s.push(mapped);
685                        i += 2;
686                    }
687                    Some(d) => {
688                        s.push(d);
689                        i += 1;
690                    }
691                }
692            }
693            i += 1;
694            out.push(Token { t: Tok::Str(s), at, text: cs[at..i].iter().collect() });
695            continue;
696        }
697        if is_id_start(Some(&c)) {
698            while is_id_part(cs.get(i)) {
699                i += 1;
700            }
701            let text: String = cs[at..i].iter().collect();
702            out.push(Token { t: Tok::Id(text.clone()), at, text });
703            continue;
704        }
705        let two: String = cs[i..(i + 2).min(cs.len())].iter().collect();
706        if let Some(op) = OPS2.iter().find(|o| **o == two) {
707            out.push(Token { t: Tok::Op(op), at, text: two });
708            i += 2;
709            continue;
710        }
711        if let Some(op) = OPS1.iter().find(|o| o.starts_with(c)) {
712            out.push(Token { t: Tok::Op(op), at, text: c.to_string() });
713            i += 1;
714            continue;
715        }
716        return Err(format!("at {at}: unexpected {c}"));
717    }
718    out.push(Token { t: Tok::End, at: cs.len(), text: "end".into() });
719    Ok(out)
720}
721
722const MACROS: &[&str] = &["map", "filter", "all", "exists"];
723
724struct Parser {
725    tokens: Vec<Token>,
726    i: usize,
727    depth: usize,
728    in_body: usize,
729}
730
731impl Parser {
732    fn peek(&self) -> &Token {
733        &self.tokens[self.i]
734    }
735    fn is_op(&self, v: &str) -> bool {
736        matches!(&self.peek().t, Tok::Op(o) if *o == v)
737    }
738    fn next(&mut self) -> Token {
739        let t = self.tokens[self.i].clone();
740        self.i += 1;
741        t
742    }
743    fn unexpected<T>(&self) -> R<T> {
744        let t = self.peek();
745        Err(format!("at {}: unexpected {}", t.at, t.text))
746    }
747    fn expect(&mut self, v: &str) -> R<Token> {
748        if !self.is_op(v) {
749            return self.unexpected();
750        }
751        Ok(self.next())
752    }
753    fn enter(&mut self, at: usize) -> R<()> {
754        self.depth += 1;
755        if self.depth > MAX_DEPTH {
756            return Err(format!("at {at}: expression is nested too deeply"));
757        }
758        Ok(())
759    }
760
761    fn parse_all(&mut self) -> R<Node> {
762        let node = self.expr()?;
763        if self.peek().t != Tok::End {
764            return self.unexpected();
765        }
766        Ok(node)
767    }
768
769    fn expr(&mut self) -> R<Node> {
770        crate::deep(|| self.expr_inner())
771    }
772
773    fn expr_inner(&mut self) -> R<Node> {
774        let at = self.peek().at;
775        self.enter(at)?;
776        let test = self.or()?;
777        let mut out = test;
778        if self.is_op("?") {
779            self.next();
780            let then = self.expr()?;
781            self.expect(":")?;
782            let otherwise = self.expr()?;
783            out = Node::Cond(Box::new(out), Box::new(then), Box::new(otherwise), at);
784        }
785        self.depth -= 1;
786        Ok(out)
787    }
788
789    fn binary(&mut self, ops: &[&'static str], operand: fn(&mut Parser) -> R<Node>) -> R<Node> {
790        let mut a = operand(self)?;
791        loop {
792            let t = self.peek().clone();
793            let Tok::Op(op) = t.t else { return Ok(a) };
794            let Some(op) = ops.iter().find(|o| **o == op) else { return Ok(a) };
795            self.next();
796            let b = operand(self)?;
797            a = Node::Bin(op, Box::new(a), Box::new(b), t.at);
798        }
799    }
800    fn or(&mut self) -> R<Node> {
801        self.binary(&["||"], Parser::and)
802    }
803    fn and(&mut self) -> R<Node> {
804        self.binary(&["&&"], Parser::rel)
805    }
806    fn rel(&mut self) -> R<Node> {
807        self.binary(&["==", "!=", "<", "<=", ">", ">="], Parser::add)
808    }
809    fn add(&mut self) -> R<Node> {
810        self.binary(&["+", "-"], Parser::mul)
811    }
812    fn mul(&mut self) -> R<Node> {
813        self.binary(&["*", "/", "%"], Parser::unary)
814    }
815
816    fn unary(&mut self) -> R<Node> {
817        let t = self.peek().clone();
818        if let Tok::Op(op @ ("!" | "-")) = t.t {
819            self.next();
820            self.enter(t.at)?;
821            let arg = crate::deep(|| self.unary())?;
822            self.depth -= 1;
823            return Ok(Node::Unary(op.chars().next().unwrap_or('!'), Box::new(arg), t.at));
824        }
825        self.postfix()
826    }
827
828    fn postfix(&mut self) -> R<Node> {
829        let mut node = self.primary()?;
830        loop {
831            if self.is_op(".") {
832                self.next();
833                let name = self.peek().clone();
834                let Tok::Id(field) = name.t.clone() else { return self.unexpected() };
835                self.next();
836                if self.is_op("(") {
837                    if !MACROS.contains(&field.as_str()) {
838                        return Err(format!("at {}: unknown method {field}", name.at));
839                    }
840                    if self.in_body > 0 {
841                        return Err(format!("at {}: nested comprehensions are not allowed", name.at));
842                    }
843                    self.next();
844                    let v = self.peek().clone();
845                    let comma = matches!(self.tokens.get(self.i + 1).map(|t| &t.t), Some(Tok::Op(",")));
846                    let Tok::Id(var) = v.t.clone() else { return Err(format!("at {}: {field} needs a name first", v.at)) };
847                    if !comma {
848                        return Err(format!("at {}: {field} needs a name first", v.at));
849                    }
850                    self.next();
851                    self.next();
852                    self.in_body += 1;
853                    let body = self.expr()?;
854                    self.in_body -= 1;
855                    self.expect(")")?;
856                    node = Node::Macro(field, Box::new(node), var, Box::new(body), name.at);
857                } else {
858                    node = Node::Field(Box::new(node), field, name.at);
859                }
860                continue;
861            }
862            if self.is_op("[") {
863                let at = self.next().at;
864                let index = self.expr()?;
865                self.expect("]")?;
866                node = Node::Index(Box::new(node), Box::new(index), at);
867                continue;
868            }
869            return Ok(node);
870        }
871    }
872
873    fn list(&mut self, close: &str) -> R<Vec<Node>> {
874        let mut items = Vec::new();
875        if self.is_op(close) {
876            self.next();
877            return Ok(items);
878        }
879        loop {
880            items.push(self.expr()?);
881            if self.is_op(",") {
882                self.next();
883                if self.is_op(close) && close == "]" {
884                    self.next();
885                    return Ok(items);
886                }
887                continue;
888            }
889            self.expect(close)?;
890            return Ok(items);
891        }
892    }
893
894    fn primary(&mut self) -> R<Node> {
895        let t = self.peek().clone();
896        match t.t {
897            Tok::Num(v) => {
898                self.next();
899                if !v.is_finite() {
900                    return Err(format!("at {}: number out of range", t.at));
901                }
902                Ok(Node::Lit(json::number(v), t.at))
903            }
904            Tok::Str(s) => {
905                self.next();
906                Ok(Node::Lit(Value::String(s), t.at))
907            }
908            Tok::Id(name) => {
909                self.next();
910                match name.as_str() {
911                    "true" | "false" => return Ok(Node::Lit(Value::Bool(name == "true"), t.at)),
912                    "null" => return Ok(Node::Lit(Value::Null, t.at)),
913                    _ => {}
914                }
915                if self.is_op("(") {
916                    if def(&name).is_none() {
917                        return Err(format!("at {}: unknown function {name}", t.at));
918                    }
919                    self.next();
920                    let args = self.list(")")?;
921                    return Ok(Node::Call(name, args, t.at));
922                }
923                Ok(Node::Id(name, t.at))
924            }
925            Tok::Op("(") => {
926                self.next();
927                let inner = self.expr()?;
928                self.expect(")")?;
929                Ok(inner)
930            }
931            Tok::Op("[") => {
932                self.next();
933                Ok(Node::List(self.list("]")?, t.at))
934            }
935            _ => self.unexpected(),
936        }
937    }
938}
939
940/// Parse once (at package validation, or before evaluating): the tree, or
941/// the first syntax / cost error (`at <code point>: <reason>`).
942pub fn parse_expression(source: &str) -> Result<Node, String> {
943    let cs: Vec<char> = source.chars().collect();
944    if cs.len() > MAX_LENGTH {
945        return Err(format!("expression is longer than {MAX_LENGTH} characters"));
946    }
947    Parser { tokens: tokenize(&cs)?, i: 0, depth: 0, in_body: 0 }.parse_all()
948}
949
950// ---------------------------------------------------------------------------
951// Evaluation
952// ---------------------------------------------------------------------------
953
954struct Scope<'a> {
955    name: &'a str,
956    value: &'a Value,
957    parent: Option<&'a Scope<'a>>,
958}
959
960struct Evaluator<'v> {
961    vars: &'v Map<String, Value>,
962    env: Env<'v>,
963    steps: usize,
964    max_steps: usize,
965}
966
967impl Evaluator<'_> {
968    fn step(&mut self) -> R<()> {
969        self.steps += 1;
970        if self.steps > self.max_steps {
971            return Err("step limit exceeded".into());
972        }
973        Ok(())
974    }
975
976    fn ev(&mut self, node: &Node, scope: Option<&Scope>) -> R<Value> {
977        crate::deep(|| self.ev_inner(node, scope))
978    }
979
980    fn ev_inner(&mut self, node: &Node, scope: Option<&Scope>) -> R<Value> {
981        self.step()?;
982        match node {
983            Node::Lit(v, _) => Ok(v.clone()),
984            Node::List(items, _) => items.iter().map(|i| self.ev(i, scope)).collect::<R<Vec<_>>>().map(Value::Array),
985            Node::Id(name, _) => {
986                let mut s = scope;
987                while let Some(sc) = s {
988                    if sc.name == name {
989                        return Ok(sc.value.clone());
990                    }
991                    s = sc.parent;
992                }
993                self.vars.get(name).cloned().ok_or_else(|| format!("unknown name {name}"))
994            }
995            Node::Field(of, name, _) => match self.ev(of, scope)? {
996                Value::Null => Ok(Value::Null),
997                Value::Object(m) => Ok(m.get(name).cloned().unwrap_or(Value::Null)),
998                other => Err(format!("no field {name} on {}", type_name(&other))),
999            },
1000            Node::Index(of, index, _) => {
1001                let of = self.ev(of, scope)?;
1002                let index = self.ev(index, scope)?;
1003                match of {
1004                    Value::Array(list) => {
1005                        let Value::Number(n) = &index else { return Err(format!("a list index is a number, got {}", type_name(&index))) };
1006                        let n = n.as_f64().unwrap_or(0.0);
1007                        if n.fract() != 0.0 {
1008                            return Err(format!("a list index is a whole number, got {}", num_text(n)));
1009                        }
1010                        if n < 0.0 || n >= list.len() as f64 {
1011                            return Err(format!("index {} out of range", num_text(n)));
1012                        }
1013                        Ok(list[n as usize].clone())
1014                    }
1015                    Value::Object(m) => {
1016                        let Value::String(k) = &index else { return Err(format!("a map key is a string, got {}", type_name(&index))) };
1017                        Ok(m.get(k).cloned().unwrap_or(Value::Null))
1018                    }
1019                    Value::Null => Ok(Value::Null),
1020                    other => Err(format!("cannot index {}", type_name(&other))),
1021                }
1022            }
1023            Node::Unary(op, arg, _) => {
1024                let v = self.ev(arg, scope)?;
1025                if *op == '-' {
1026                    let Value::Number(n) = &v else { return Err(format!("cannot apply - to {}", type_name(&v))) };
1027                    return Ok(json::number(num(-n.as_f64().unwrap_or(0.0))?));
1028                }
1029                let Value::Bool(b) = v else { return Err(format!("cannot apply ! to {}", type_name(&v))) };
1030                Ok(Value::Bool(!b))
1031            }
1032            Node::Cond(test, then, otherwise, _) => {
1033                let t = self.ev(test, scope)?;
1034                let Value::Bool(t) = t else { return Err(format!("cannot use {} as a condition", type_name(&t))) };
1035                self.ev(if t { then } else { otherwise }, scope)
1036            }
1037            Node::Bin(op, a, b, _) => self.binary(op, a, b, scope),
1038            Node::Call(name, args, _) => {
1039                let args = args.iter().map(|a| self.ev(a, scope)).collect::<R<Vec<_>>>()?;
1040                let d = def(name).ok_or_else(|| format!("unknown function {name}"))?;
1041                if let Some(e) = arity_error(name, &d, args.len()) {
1042                    return Err(e);
1043                }
1044                run(name, &args, &self.env)
1045            }
1046            Node::Macro(f, of, var, body, _) => {
1047                let of = self.ev(of, scope)?;
1048                let Value::Array(list) = of else { return Err(format!("{f} needs a list, got {}", type_name(&of))) };
1049                let mut out = Vec::new();
1050                for item in &list {
1051                    self.step()?;
1052                    let inner = Scope { name: var, value: item, parent: scope };
1053                    let v = self.ev(body, Some(&inner))?;
1054                    if f == "map" {
1055                        out.push(v);
1056                        continue;
1057                    }
1058                    let Value::Bool(b) = v else { return Err(format!("{f} needs a bool, got {}", type_name(&v))) };
1059                    match f.as_str() {
1060                        "filter" if b => out.push(item.clone()),
1061                        "all" if !b => return Ok(Value::Bool(false)),
1062                        "exists" if b => return Ok(Value::Bool(true)),
1063                        _ => {}
1064                    }
1065                }
1066                Ok(match f.as_str() {
1067                    "all" => Value::Bool(true),
1068                    "exists" => Value::Bool(false),
1069                    _ => Value::Array(out),
1070                })
1071            }
1072        }
1073    }
1074
1075    fn binary(&mut self, op: &str, a: &Node, b: &Node, scope: Option<&Scope>) -> R<Value> {
1076        if op == "&&" || op == "||" {
1077            let x = self.ev(a, scope)?;
1078            let Value::Bool(x) = x else { return Err(format!("cannot apply {op} to {}", type_name(&x))) };
1079            if (op == "&&" && !x) || (op == "||" && x) {
1080                return Ok(Value::Bool(x));
1081            }
1082            let y = self.ev(b, scope)?;
1083            let Value::Bool(_) = y else { return Err(format!("cannot apply {op} to {}", type_name(&y))) };
1084            return Ok(y);
1085        }
1086        let x = self.ev(a, scope)?;
1087        let y = self.ev(b, scope)?;
1088        match op {
1089            "==" => Ok(Value::Bool(deep_equal(&x, &y))),
1090            "!=" => Ok(Value::Bool(!deep_equal(&x, &y))),
1091            "<" | "<=" | ">" | ">=" => {
1092                let c = match (&x, &y) {
1093                    (Value::Number(p), Value::Number(q)) => p.as_f64().partial_cmp(&q.as_f64()).unwrap_or(std::cmp::Ordering::Equal),
1094                    (Value::String(p), Value::String(q)) => utf16_cmp(p, q),
1095                    _ => return Err(format!("cannot compare {} and {}", type_name(&x), type_name(&y))),
1096                };
1097                use std::cmp::Ordering::*;
1098                Ok(Value::Bool(match op {
1099                    "<" => c == Less,
1100                    "<=" => c != Greater,
1101                    ">" => c == Greater,
1102                    _ => c != Less,
1103                }))
1104            }
1105            "+" => match (x, y) {
1106                (Value::Number(p), Value::Number(q)) => Ok(json::number(num(p.as_f64().unwrap_or(0.0) + q.as_f64().unwrap_or(0.0))?)),
1107                (Value::String(p), Value::String(q)) => Ok(Value::String(p + &q)),
1108                (Value::Array(mut p), Value::Array(q)) => {
1109                    p.extend(q);
1110                    Ok(Value::Array(p))
1111                }
1112                (x, y) => Err(format!("cannot apply + to {} and {}", type_name(&x), type_name(&y))),
1113            },
1114            _ => {
1115                let (Value::Number(p), Value::Number(q)) = (&x, &y) else { return Err(format!("cannot apply {op} to {} and {}", type_name(&x), type_name(&y))) };
1116                let (p, q) = (p.as_f64().unwrap_or(0.0), q.as_f64().unwrap_or(0.0));
1117                match op {
1118                    "-" => Ok(json::number(num(p - q)?)),
1119                    "*" => Ok(json::number(num(p * q)?)),
1120                    _ => {
1121                        if q == 0.0 {
1122                            return Err(if op == "/" { "division by zero" } else { "modulo by zero" }.into());
1123                        }
1124                        Ok(json::number(num(if op == "/" { p / q } else { p % q })?))
1125                    }
1126                }
1127            }
1128        }
1129    }
1130}
1131
1132/// Evaluate an expression over `vars`: the value, or the error (a syntax,
1133/// type, range or cost error).
1134pub fn evaluate_expression(source: &str, vars: &Map<String, Value>, env: &ExpressionEnv) -> Result<Value, String> {
1135    let ast = parse_expression(source)?;
1136    evaluate_node(&ast, vars, env)
1137}
1138
1139/// Evaluate a parsed tree (see [`parse_expression`]).
1140pub fn evaluate_node<'a>(ast: &Node, vars: &'a Map<String, Value>, env: &ExpressionEnv<'a>) -> Result<Value, String> {
1141    let mut e = Evaluator { vars, env: Env::of(env), steps: 0, max_steps: env.max_steps.unwrap_or(MAX_STEPS) };
1142    e.ev(ast, None)
1143}
1144
1145// ---------------------------------------------------------------------------
1146// Type checking (package validation)
1147// ---------------------------------------------------------------------------
1148
1149/// The name a type prints as in an error: a list `list`, a row `map`.
1150pub fn type_display(t: &Value) -> &str {
1151    match t {
1152        Value::String(s) => s,
1153        Value::Array(_) => "list",
1154        _ => "map",
1155    }
1156}
1157
1158fn is_row(t: &Value) -> bool {
1159    t.is_object()
1160}
1161fn is_textual(t: &Value) -> bool {
1162    matches!(t.as_str(), Some("string" | "date" | "timestamp"))
1163}
1164fn loose(t: &Value) -> bool {
1165    matches!(t.as_str(), Some("dyn" | "null"))
1166}
1167fn is(t: &Value, name: &str) -> bool {
1168    t.as_str() == Some(name)
1169}
1170fn ty(name: &str) -> Value {
1171    Value::String(name.into())
1172}
1173
1174fn accepts(param: Param, t: &Value) -> bool {
1175    if loose(t) {
1176        return true;
1177    }
1178    match param {
1179        Param::Number => is(t, "number"),
1180        Param::Text | Param::Temporal => is_textual(t),
1181        Param::Sized => is(t, "string") || is(t, "map") || t.is_array() || is_row(t),
1182    }
1183}
1184
1185fn param_name(p: Param) -> &'static str {
1186    match p {
1187        Param::Number => "a number",
1188        Param::Text => "a string",
1189        Param::Temporal => "a date",
1190        Param::Sized => "a list, string or map",
1191    }
1192}
1193
1194struct TypeScope<'a> {
1195    name: &'a str,
1196    ty: Value,
1197    parent: Option<&'a TypeScope<'a>>,
1198}
1199
1200fn literal(node: &Node) -> Option<&Value> {
1201    match node {
1202        Node::Lit(v, _) => Some(v),
1203        _ => None,
1204    }
1205}
1206
1207struct Checker<'t> {
1208    types: &'t Map<String, Value>,
1209}
1210
1211impl Checker<'_> {
1212    fn tc(&self, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1213        crate::deep(|| self.tc_inner(node, scope))
1214    }
1215
1216    fn tc_inner(&self, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1217        match node {
1218            Node::Lit(v, _) => Ok(ty(type_name(v))),
1219            Node::List(items, _) => {
1220                let items = items.iter().map(|i| self.tc(i, scope)).collect::<R<Vec<_>>>()?;
1221                Ok(Value::Array(vec![match items.first() {
1222                    None => ty("dyn"),
1223                    Some(first) if items.iter().all(|t| t == first) => first.clone(),
1224                    Some(_) => ty("dyn"),
1225                }]))
1226            }
1227            Node::Id(name, _) => {
1228                let mut s = scope;
1229                while let Some(sc) = s {
1230                    if sc.name == name {
1231                        return Ok(sc.ty.clone());
1232                    }
1233                    s = sc.parent;
1234                }
1235                self.types.get(name).cloned().ok_or_else(|| format!("unknown name {name}"))
1236            }
1237            Node::Field(of, name, _) => {
1238                let of = self.tc(of, scope)?;
1239                if matches!(of.as_str(), Some("dyn" | "map" | "null")) {
1240                    return Ok(ty("dyn"));
1241                }
1242                if let Value::Object(fields) = &of {
1243                    return fields.get(name).cloned().ok_or_else(|| format!("unknown field {name}"));
1244                }
1245                Err(format!("no field {name} on {}", type_display(&of)))
1246            }
1247            Node::Index(of, index, _) => {
1248                let of_t = self.tc(of, scope)?;
1249                let index_t = self.tc(index, scope)?;
1250                if let Value::Array(item) = &of_t {
1251                    if !loose(&index_t) && !is(&index_t, "number") {
1252                        return Err(format!("a list index is a number, got {}", type_display(&index_t)));
1253                    }
1254                    return Ok(item.first().cloned().unwrap_or_else(|| ty("dyn")));
1255                }
1256                if is_row(&of_t) || is(&of_t, "map") {
1257                    if !loose(&index_t) && !is_textual(&index_t) {
1258                        return Err(format!("a map key is a string, got {}", type_display(&index_t)));
1259                    }
1260                    if let (Value::Object(fields), Some(Value::String(key))) = (&of_t, literal(index)) {
1261                        return fields.get(key).cloned().ok_or_else(|| format!("unknown field {key}"));
1262                    }
1263                    return Ok(ty("dyn"));
1264                }
1265                if loose(&of_t) {
1266                    return Ok(ty("dyn"));
1267                }
1268                Err(format!("cannot index {}", type_display(&of_t)))
1269            }
1270            Node::Unary(op, arg, _) => {
1271                let t = self.tc(arg, scope)?;
1272                let want = if *op == '-' { "number" } else { "bool" };
1273                if !loose(&t) && !is(&t, want) {
1274                    return Err(format!("cannot apply {op} to {}", type_display(&t)));
1275                }
1276                Ok(ty(want))
1277            }
1278            Node::Cond(test, then, otherwise, _) => {
1279                let t = self.tc(test, scope)?;
1280                if !loose(&t) && !is(&t, "bool") {
1281                    return Err(format!("cannot use {} as a condition", type_display(&t)));
1282                }
1283                let a = self.tc(then, scope)?;
1284                let b = self.tc(otherwise, scope)?;
1285                Ok(if a == b {
1286                    a
1287                } else if is(&a, "null") {
1288                    b
1289                } else if is(&b, "null") {
1290                    a
1291                } else {
1292                    ty("dyn")
1293                })
1294            }
1295            Node::Bin(op, a, b, _) => self.binary(op, a, b, scope),
1296            Node::Call(name, args, _) => self.call(name, args, scope),
1297            Node::Macro(f, of, var, body, _) => {
1298                let of = self.tc(of, scope)?;
1299                if !of.is_array() && !loose(&of) {
1300                    return Err(format!("{f} needs a list, got {}", type_display(&of)));
1301                }
1302                let item = of.as_array().and_then(|a| a.first().cloned()).unwrap_or_else(|| ty("dyn"));
1303                let inner = TypeScope { name: var, ty: item, parent: scope };
1304                let body = self.tc(body, Some(&inner))?;
1305                if f == "map" {
1306                    return Ok(Value::Array(vec![body]));
1307                }
1308                if !loose(&body) && !is(&body, "bool") {
1309                    return Err(format!("{f} needs a bool, got {}", type_display(&body)));
1310                }
1311                Ok(if f == "filter" {
1312                    if of.is_array() {
1313                        of
1314                    } else {
1315                        Value::Array(vec![ty("dyn")])
1316                    }
1317                } else {
1318                    ty("bool")
1319                })
1320            }
1321        }
1322    }
1323
1324    fn binary(&self, op: &str, a: &Node, b: &Node, scope: Option<&TypeScope>) -> R<Value> {
1325        let x = self.tc(a, scope)?;
1326        let y = self.tc(b, scope)?;
1327        let (dx, dy) = (type_display(&x).to_string(), type_display(&y).to_string());
1328        let numeric = |t: &Value| is(t, "number") || loose(t);
1329        match op {
1330            "&&" | "||" => {
1331                for t in [&x, &y] {
1332                    if !loose(t) && !is(t, "bool") {
1333                        return Err(format!("cannot apply {op} to {}", type_display(t)));
1334                    }
1335                }
1336                Ok(ty("bool"))
1337            }
1338            "==" | "!=" => {
1339                let comparable = loose(&x) || loose(&y) || (is_textual(&x) && is_textual(&y)) || dx == dy;
1340                if !comparable {
1341                    return Err(format!("cannot compare {dx} and {dy}"));
1342                }
1343                Ok(ty("bool"))
1344            }
1345            "<" | "<=" | ">" | ">=" => {
1346                let ok = |t: &Value| loose(t) || is(t, "number") || is_textual(t);
1347                if !ok(&x) || !ok(&y) || (is(&x, "number") && is_textual(&y)) || (is(&y, "number") && is_textual(&x)) {
1348                    return Err(format!("cannot compare {dx} and {dy}"));
1349                }
1350                Ok(ty("bool"))
1351            }
1352            "+" => {
1353                if loose(&x) && loose(&y) {
1354                    return Ok(ty("dyn"));
1355                }
1356                if numeric(&x) && numeric(&y) {
1357                    return Ok(ty("number"));
1358                }
1359                if (is_textual(&x) || loose(&x)) && (is_textual(&y) || loose(&y)) {
1360                    return Ok(ty("string"));
1361                }
1362                if (x.is_array() || loose(&x)) && (y.is_array() || loose(&y)) {
1363                    return Ok(if x.is_array() && y.is_array() && x == y { x } else { Value::Array(vec![ty("dyn")]) });
1364                }
1365                Err(format!("cannot apply + to {dx} and {dy}"))
1366            }
1367            _ => {
1368                if !(numeric(&x) && numeric(&y)) {
1369                    return Err(format!("cannot apply {op} to {dx} and {dy}"));
1370                }
1371                if (op == "/" || op == "%") && literal(b).is_some_and(|v| v.as_f64() == Some(0.0)) {
1372                    return Err(if op == "/" { "division by zero" } else { "modulo by zero" }.into());
1373                }
1374                Ok(ty("number"))
1375            }
1376        }
1377    }
1378
1379    fn call(&self, name: &str, args: &[Node], scope: Option<&TypeScope>) -> R<Value> {
1380        let d = def(name).ok_or_else(|| format!("unknown function {name}"))?;
1381        if let Some(e) = arity_error(name, &d, args.len()) {
1382            return Err(e);
1383        }
1384        let types = args.iter().map(|a| self.tc(a, scope)).collect::<R<Vec<_>>>()?;
1385        for (i, t) in types.iter().enumerate() {
1386            let param = d.params[i.min(d.params.len() - 1)];
1387            if !accepts(param, t) {
1388                return Err(format!("{name} needs {}, got {}", param_name(param), type_display(t)));
1389            }
1390        }
1391        let lit = |i: usize| args.get(i).and_then(literal);
1392        let unit_at = match name {
1393            "dateAdd" | "dateDiff" => Some(2),
1394            "startOf" | "endOf" => Some(1),
1395            _ => None,
1396        };
1397        if let Some(Value::String(unit)) = unit_at.and_then(lit) {
1398            let units = if unit_at == Some(2) { DATE_UNITS } else { CALENDAR_UNITS };
1399            if !units.contains(&unit.as_str()) {
1400                return Err(format!("unknown unit {unit}"));
1401            }
1402            if name == "dateAdd" && is(&types[0], "date") && unit_ms(unit).is_some() {
1403                return Err(format!("unit {unit} needs a timestamp"));
1404            }
1405        }
1406        if name == "dateAdd" {
1407            if let Some(n) = lit(1).and_then(Value::as_f64) {
1408                if n.fract() != 0.0 {
1409                    return Err(format!("dateAdd needs a whole number, got {}", num_text(n)));
1410                }
1411            }
1412        }
1413        if name == "round" && args.len() > 1 {
1414            if let Some(d) = lit(1).and_then(Value::as_f64) {
1415                if d.fract() != 0.0 || !(0.0..=10.0).contains(&d) {
1416                    return Err("round needs 0 to 10 digits".into());
1417                }
1418            }
1419        }
1420        if name == "div" && lit(1).and_then(Value::as_f64) == Some(0.0) {
1421            return Err("division by zero".into());
1422        }
1423        if name == "mod" && lit(1).and_then(Value::as_f64) == Some(0.0) {
1424            return Err("modulo by zero".into());
1425        }
1426        if name == "formatDuration" {
1427            if let Some(Value::String(f)) = lit(1) {
1428                if !DURATION_FORMATS.contains(&f.as_str()) {
1429                    return Err(format!("unknown duration format {f}"));
1430                }
1431            }
1432        }
1433        Ok(match name {
1434            "dateAdd" | "startOf" | "endOf" => {
1435                if is(&types[0], "date") || is(&types[0], "timestamp") {
1436                    types[0].clone()
1437                } else {
1438                    ty("dyn")
1439                }
1440            }
1441            "today" => ty("date"),
1442            "now" => ty("timestamp"),
1443            "formatDuration" => ty("string"),
1444            _ => ty("number"),
1445        })
1446    }
1447}
1448
1449/// Type-check an expression against the names it may read (a kind's
1450/// fields, a binding's value): the type, or the error (the parse error, or
1451/// the first type error). Types are spelled as JSON (`"number"`, `[T]`, a
1452/// row object).
1453pub fn check_expression(source: &str, types: &Map<String, Value>) -> Result<Value, String> {
1454    let ast = parse_expression(source)?;
1455    Checker { types }.tc(&ast, None)
1456}