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/// VAPP-194 (E1): what calling a function does. Expressions are PURE: a
27/// binding, `visible`, a computed field or a query param never writes. A
28/// `write` changes records or the surface data, a `host` function reaches
29/// outside the surface (a toast, an MCP call, a link, another action); both
30/// run only as steps of a package action ([`crate::host::actions`]).
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum FunctionEffect {
33    Pure,
34    Write,
35    Host,
36}
37
38impl FunctionEffect {
39    /// `pure` | `write` | `host`.
40    pub fn as_str(self) -> &'static str {
41        match self {
42            FunctionEffect::Pure => "pure",
43            FunctionEffect::Write => "write",
44            FunctionEffect::Host => "host",
45        }
46    }
47}
48
49/// The functions that write (records, the surface data model).
50pub const WRITE_FUNCTIONS: &[&str] = &["set", "records.upsert", "records.delete", "records.batch"];
51/// The built-in host functions; every name under a host prefix is one too.
52pub const HOST_FUNCTIONS: &[&str] = &["openUrl", "bind", "app.navigate"];
53pub const HOST_FUNCTION_PREFIXES: &[&str] = &["actions.", "harness.", "exponential.", "tools.", "upstream."];
54
55/// The effect of a function by name: the signature table's (every
56/// expression function, every bind-time function) is `pure`.
57pub fn function_effect(name: &str) -> FunctionEffect {
58    if WRITE_FUNCTIONS.contains(&name) {
59        return FunctionEffect::Write;
60    }
61    if HOST_FUNCTIONS.contains(&name) || HOST_FUNCTION_PREFIXES.iter().any(|p| name.starts_with(p)) {
62        return FunctionEffect::Host;
63    }
64    FunctionEffect::Pure
65}
66
67/// Why an effectful function is refused in a pure place.
68pub fn effect_words(name: &str, effect: FunctionEffect) -> String {
69    format!("{name} {}", if effect == FunctionEffect::Write { "writes" } else { "is a host function" })
70}
71
72/// The clock, the team zone and the step limit an evaluation runs with.
73#[derive(Clone, Copy, Default)]
74pub struct ExpressionEnv<'a> {
75    /// Epoch ms `today()` / `now()` read; `None` = the wall clock.
76    pub now: Option<f64>,
77    /// Minutes east of UTC at an instant; `None` = UTC.
78    pub offset: Option<&'a (dyn Fn(f64) -> i32 + Send + Sync)>,
79    /// The step limit (default [`MAX_STEPS`]).
80    pub max_steps: Option<usize>,
81}
82
83impl std::fmt::Debug for ExpressionEnv<'_> {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.debug_struct("ExpressionEnv").field("now", &self.now).field("max_steps", &self.max_steps).finish_non_exhaustive()
86    }
87}
88
89struct Env<'a> {
90    now: f64,
91    offset: Option<&'a (dyn Fn(f64) -> i32 + Send + Sync)>,
92}
93
94impl<'a> Env<'a> {
95    fn of(env: &ExpressionEnv<'a>) -> Env<'a> {
96        Env { now: env.now.unwrap_or_else(crate::format::now_ms).floor(), offset: env.offset }
97    }
98    fn off(&self, ms: f64) -> f64 {
99        self.offset.map_or(0.0, |o| f64::from(o(ms)))
100    }
101}
102
103type R<T> = Result<T, String>;
104
105// ---------------------------------------------------------------------------
106// Values
107// ---------------------------------------------------------------------------
108
109/// The runtime type name errors print.
110pub fn type_name(v: &Value) -> &'static str {
111    match v {
112        Value::Null => "null",
113        Value::Number(_) => "number",
114        Value::String(_) => "string",
115        Value::Bool(_) => "bool",
116        Value::Array(_) => "list",
117        Value::Object(_) => "map",
118    }
119}
120
121/// A finite result with `-0` as 0; anything else is out of range.
122fn num(v: f64) -> R<f64> {
123    if !v.is_finite() {
124        return Err("number out of range".into());
125    }
126    Ok(if v == 0.0 { 0.0 } else { v })
127}
128
129fn num_text(n: f64) -> String {
130    json::number_to_string(n)
131}
132
133fn deep_equal(a: &Value, b: &Value) -> bool {
134    match (a, b) {
135        (Value::Number(x), Value::Number(y)) => x.as_f64() == y.as_f64(),
136        (Value::Array(x), Value::Array(y)) => x.len() == y.len() && x.iter().zip(y).all(|(a, b)| deep_equal(a, b)),
137        (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))),
138        _ => a == b,
139    }
140}
141
142/// Round half away from zero without `x + 0.5`.
143fn round_half_away(x: f64) -> f64 {
144    let a = x.abs();
145    let f = a.floor();
146    let r = if a - f >= 0.5 { f + 1.0 } else { f };
147    if x < 0.0 {
148        -r
149    } else {
150        r
151    }
152}
153
154fn utf16_cmp(a: &str, b: &str) -> std::cmp::Ordering {
155    a.encode_utf16().cmp(b.encode_utf16())
156}
157
158// ---------------------------------------------------------------------------
159// Dates (the team zone)
160// ---------------------------------------------------------------------------
161
162const DAY_MS: f64 = 86_400_000.0;
163
164fn unit_ms(unit: &str) -> Option<f64> {
165    match unit {
166        "second" => Some(1000.0),
167        "minute" => Some(60_000.0),
168        "hour" => Some(3_600_000.0),
169        _ => None,
170    }
171}
172
173#[derive(Clone, Copy)]
174enum Temporal {
175    Date(i64),
176    Ts(f64),
177}
178
179/// An ISO instant (`YYYY-MM-DDTHH:MM[:SS[.fff]][Z|±HH:MM]`, no offset = UTC)
180/// as epoch ms; `None` when it is not one.
181pub fn parse_timestamp(text: &str) -> Option<f64> {
182    let b = text.as_bytes();
183    let digit = |i: usize| b.get(i).is_some_and(u8::is_ascii_digit);
184    let int = |from: usize, n: usize| b[from..from + n].iter().fold(0i64, |acc, c| acc * 10 + i64::from(c - b'0'));
185    if b.len() < 16 || b[10] != b'T' || !(digit(11) && digit(12) && b[13] == b':' && digit(14) && digit(15)) {
186        return None;
187    }
188    let day = parse_day(&text[..10]).filter(|_| text.is_char_boundary(10))?;
189    if !(b[..10].iter().enumerate().all(|(i, c)| if i == 4 || i == 7 { *c == b'-' } else { c.is_ascii_digit() })) {
190        return None;
191    }
192    let (h, mi) = (int(11, 2), int(14, 2));
193    let mut i = 16;
194    let mut s = 0;
195    let mut ms = 0;
196    if b.get(i) == Some(&b':') {
197        if !(digit(i + 1) && digit(i + 2)) {
198            return None;
199        }
200        s = int(i + 1, 2);
201        i += 3;
202        if b.get(i) == Some(&b'.') {
203            let start = i + 1;
204            let mut end = start;
205            while digit(end) {
206                end += 1;
207            }
208            if end == start {
209                return None;
210            }
211            let frac: String = text[start..end.min(start + 3)].chars().collect();
212            ms = format!("{frac:0<3}").parse::<i64>().ok()?;
213            i = end;
214        }
215    }
216    let mut offset = 0;
217    match b.get(i) {
218        None => {}
219        Some(b'Z') if i + 1 == b.len() => {}
220        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) => {
221            let (oh, om) = (int(i + 1, 2), int(i + 4, 2));
222            if oh > 23 || om > 59 {
223                return None;
224            }
225            offset = (if *c == b'-' { -1 } else { 1 }) * (oh * 60 + om);
226        }
227        _ => return None,
228    }
229    if h > 23 || mi > 59 || s > 59 {
230        return None;
231    }
232    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)
233}
234
235/// An instant as `YYYY-MM-DDTHH:MM:SS.sssZ` (JS `toISOString`), years 0..9999.
236pub fn format_timestamp(ms: f64) -> R<String> {
237    if !(ms >= MIN_DAY as f64 * DAY_MS && ms < (MAX_DAY + 1) as f64 * DAY_MS) {
238        return Err("date out of range".into());
239    }
240    let day = (ms / DAY_MS).floor();
241    let tod = (ms - day * DAY_MS) as i64;
242    let d = day_from_number(day as i64);
243    if d.y < 0 || d.y > 9999 {
244        return Err("date out of range".into());
245    }
246    Ok(format!("{}T{:02}:{:02}:{:02}.{:03}Z", format_day(d), tod / 3_600_000, (tod / 60_000) % 60, (tod / 1000) % 60, tod % 1000))
247}
248
249/// The days a date may be: 0000-01-01 ..= 9999-12-31 (checked BEFORE the
250/// calendar maths, so a huge day never overflows it).
251const MIN_DAY: i64 = -719_528;
252const MAX_DAY: i64 = 2_932_896;
253/// The most calendar steps (days) any `dateAdd` can take and stay in range.
254const MAX_CALENDAR_STEPS: f64 = 4_000_000.0;
255
256fn day_text(day: i64) -> R<String> {
257    if !(MIN_DAY..=MAX_DAY).contains(&day) {
258        return Err("date out of range".into());
259    }
260    let d = day_from_number(day);
261    if d.y < 0 || d.y > 9999 {
262        return Err("date out of range".into());
263    }
264    Ok(format_day(d))
265}
266
267/// The wall clock (as if UTC) of an instant in the zone.
268fn to_local(env: &Env, ms: f64) -> f64 {
269    ms + env.off(ms) * 60_000.0
270}
271
272/// The instant of a wall-clock time in the zone: a time in a DST gap moves
273/// forward by the gap; a repeated time takes its later (standard) instant.
274fn from_local(env: &Env, wall: f64) -> f64 {
275    let first = env.off(wall);
276    let utc = wall - first * 60_000.0;
277    let second = env.off(utc);
278    if second == first {
279        utc
280    } else {
281        wall - second * 60_000.0
282    }
283}
284
285fn temporal(f: &str, v: &Value) -> R<Temporal> {
286    let Value::String(s) = v else { return Err(format!("{f} needs a date, got {}", type_name(v))) };
287    if s.chars().count() == 10 {
288        if let Some(d) = parse_day(s) {
289            return Ok(Temporal::Date(day_number(d.y, d.m, d.d)));
290        }
291    }
292    parse_timestamp(s).map(Temporal::Ts).ok_or_else(|| format!("not a date: {s}"))
293}
294
295fn floor_day(wall: f64) -> i64 {
296    (wall / DAY_MS).floor() as i64
297}
298
299fn wall_of(env: &Env, t: Temporal) -> f64 {
300    match t {
301        Temporal::Date(day) => day as f64 * DAY_MS,
302        Temporal::Ts(ms) => to_local(env, ms),
303    }
304}
305
306fn unit_of<'a>(f: &str, v: &'a Value, units: &[&str]) -> R<&'a str> {
307    let Value::String(s) = v else { return Err(format!("{f} needs a string, got {}", type_name(v))) };
308    if !units.contains(&s.as_str()) {
309        return Err(format!("unknown unit {s}"));
310    }
311    Ok(s)
312}
313
314fn add_months(day: i64, months: i64) -> i64 {
315    let c = day_from_number(day);
316    let total = c.y * 12 + (c.m - 1) + months;
317    let y = total.div_euclid(12);
318    let m = total - y * 12 + 1;
319    day_number(y, m, c.d.min(days_in_month(y, m)))
320}
321
322fn add_calendar(day: i64, n: i64, unit: &str) -> i64 {
323    match unit {
324        "day" => day + n,
325        "week" => day + 7 * n,
326        "month" => add_months(day, n),
327        "quarter" => add_months(day, n * 3),
328        _ => add_months(day, n * 12),
329    }
330}
331
332fn start_day(day: i64, unit: &str) -> i64 {
333    let c = day_from_number(day);
334    match unit {
335        "day" => day,
336        "week" => {
337            let wd = match weekday_of(c.y, c.m, c.d) {
338                0 => 7,
339                n => n as i64,
340            };
341            day - (wd - 1)
342        }
343        "month" => day_number(c.y, c.m, 1),
344        "quarter" => day_number(c.y, (c.m - 1) / 3 * 3 + 1, 1),
345        _ => day_number(c.y, 1, 1),
346    }
347}
348
349fn number(f: &str, v: &Value) -> R<f64> {
350    match v {
351        Value::Number(n) => Ok(n.as_f64().unwrap_or(0.0)),
352        _ => Err(format!("{f} needs a number, got {}", type_name(v))),
353    }
354}
355
356fn whole_number(f: &str, v: &Value) -> R<f64> {
357    let n = number(f, v)?;
358    if n.fract() != 0.0 {
359        return Err(format!("{f} needs a whole number, got {}", num_text(n)));
360    }
361    Ok(n)
362}
363
364fn text<'a>(f: &str, v: &'a Value) -> R<&'a str> {
365    match v {
366        Value::String(s) => Ok(s),
367        _ => Err(format!("{f} needs a string, got {}", type_name(v))),
368    }
369}
370
371/// VAPP-245: a day and a wall-clock time (`HH:MM`, none = midnight) in the
372/// team zone as an instant: the Record form's date-time pair.
373fn date_time(args: &[Value], env: &Env) -> R<Value> {
374    let Temporal::Date(day) = temporal("dateTime", &args[0])? else {
375        return Err("dateTime needs a day".into());
376    };
377    let clock = match args.get(1) {
378        None | Some(Value::Null) => "00:00",
379        Some(v) => text("dateTime", v)?,
380    };
381    let bytes = clock.as_bytes();
382    let digits = |i: usize| bytes[i].is_ascii_digit();
383    if bytes.len() != 5 || bytes[2] != b':' || !digits(0) || !digits(1) || !digits(3) || !digits(4) {
384        return Err(format!("not a time: {clock}"));
385    }
386    let (h, m) = ((bytes[0] - b'0') as i64 * 10 + (bytes[1] - b'0') as i64, (bytes[3] - b'0') as i64 * 10 + (bytes[4] - b'0') as i64);
387    if h > 23 || m > 59 {
388        return Err(format!("not a time: {clock}"));
389    }
390    Ok(Value::String(format_timestamp(from_local(env, day as f64 * DAY_MS + ((h * 60 + m) * 60_000) as f64))?))
391}
392
393fn date_add(args: &[Value], env: &Env) -> R<Value> {
394    let t = temporal("dateAdd", &args[0])?;
395    let n = whole_number("dateAdd", &args[1])?;
396    let unit = unit_of("dateAdd", &args[2], DATE_UNITS)?;
397    let out = match t {
398        Temporal::Date(day) => {
399            if unit_ms(unit).is_some() {
400                return Err(format!("unit {unit} needs a timestamp"));
401            }
402            if !(n.abs() <= MAX_CALENDAR_STEPS) {
403                return Err("date out of range".into());
404            }
405            day_text(add_calendar(day, n as i64, unit))?
406        }
407        Temporal::Ts(ms) => match unit_ms(unit) {
408            Some(u) => format_timestamp(num(ms + n * u)?)?,
409            None => {
410                if !(n.abs() <= MAX_CALENDAR_STEPS) {
411                    return Err("date out of range".into());
412                }
413                let wall = to_local(env, ms);
414                let day = floor_day(wall);
415                let tod = wall - day as f64 * DAY_MS;
416                format_timestamp(from_local(env, add_calendar(day, n as i64, unit) as f64 * DAY_MS + tod))?
417            }
418        },
419    };
420    Ok(Value::String(out))
421}
422
423fn date_diff(args: &[Value], env: &Env) -> R<Value> {
424    let a = temporal("dateDiff", &args[0])?;
425    let b = temporal("dateDiff", &args[1])?;
426    let unit = unit_of("dateDiff", &args[2], DATE_UNITS)?;
427    if let Some(u) = unit_ms(unit) {
428        let instant = |t: Temporal| match t {
429            Temporal::Date(day) => from_local(env, day as f64 * DAY_MS),
430            Temporal::Ts(ms) => ms,
431        };
432        return Ok(json::number(num(((instant(b) - instant(a)) / u).trunc())?));
433    }
434    let (wa, wb) = (wall_of(env, a), wall_of(env, b));
435    if unit == "day" || unit == "week" {
436        let span = if unit == "day" { DAY_MS } else { 7.0 * DAY_MS };
437        return Ok(json::number(num(((wb - wa) / span).trunc())?));
438    }
439    let (da, db) = (floor_day(wa), floor_day(wb));
440    let (ca, cb) = (day_from_number(da), day_from_number(db));
441    let mut months = (cb.y - ca.y) * 12 + (cb.m - ca.m);
442    let rest_a = ca.d as f64 * DAY_MS + (wa - da as f64 * DAY_MS);
443    let rest_b = cb.d as f64 * DAY_MS + (wb - db as f64 * DAY_MS);
444    if months > 0 && rest_b < rest_a {
445        months -= 1;
446    } else if months < 0 && rest_b > rest_a {
447        months += 1;
448    }
449    let per = match unit {
450        "month" => 1.0,
451        "quarter" => 3.0,
452        _ => 12.0,
453    };
454    Ok(json::number(num((months as f64 / per).trunc())?))
455}
456
457fn start_end(f: &str, args: &[Value], env: &Env) -> R<Value> {
458    let t = temporal(f, &args[0])?;
459    let unit = unit_of(f, &args[1], CALENDAR_UNITS)?;
460    let day = match t {
461        Temporal::Date(day) => day,
462        Temporal::Ts(ms) => floor_day(to_local(env, ms)),
463    };
464    let is_date = matches!(t, Temporal::Date(_));
465    let out = if f == "startOf" {
466        let start = start_day(day, unit);
467        if is_date {
468            day_text(start)?
469        } else {
470            format_timestamp(from_local(env, start as f64 * DAY_MS))?
471        }
472    } else {
473        let next = add_calendar(start_day(day, unit), 1, unit);
474        if is_date {
475            day_text(next - 1)?
476        } else {
477            format_timestamp(from_local(env, next as f64 * DAY_MS) - 1.0)?
478        }
479    };
480    Ok(Value::String(out))
481}
482
483fn local_day(f: &str, v: &Value, env: &Env) -> R<crate::dates::CalendarDay> {
484    Ok(day_from_number(match temporal(f, v)? {
485        Temporal::Date(day) => day,
486        Temporal::Ts(ms) => floor_day(to_local(env, ms)),
487    }))
488}
489
490fn format_duration(args: &[Value]) -> R<Value> {
491    let h = number("formatDuration", &args[0])?;
492    let format = text("formatDuration", &args[1])?;
493    if !DURATION_FORMATS.contains(&format) {
494        return Err(format!("unknown duration format {format}"));
495    }
496    let total = round_half_away(h.abs() * 60.0);
497    if total > 1e15 {
498        return Err("number out of range".into());
499    }
500    let hours = (total / 60.0).floor() as i64;
501    let minutes = total as i64 - hours * 60;
502    let sign = if h < 0.0 && total > 0.0 { "-" } else { "" };
503    let hours = if format == "hh:mm" { format!("{hours:02}") } else { hours.to_string() };
504    Ok(Value::String(format!("{sign}{hours}:{minutes:02}")))
505}
506
507fn round(args: &[Value]) -> R<Value> {
508    let x = number("round", &args[0])?;
509    let digits = match args.get(1) {
510        None => 0.0,
511        Some(Value::Number(n)) => {
512            let d = n.as_f64().unwrap_or(f64::NAN);
513            if d.fract() != 0.0 || !(0.0..=10.0).contains(&d) {
514                return Err("round needs 0 to 10 digits".into());
515            }
516            d
517        }
518        Some(_) => return Err("round needs 0 to 10 digits".into()),
519    };
520    let mut p = 1.0;
521    for _ in 0..digits as usize {
522        p *= 10.0;
523    }
524    Ok(json::number(num(round_half_away(x * p) / p)?))
525}
526
527fn size(v: &Value) -> R<Value> {
528    Ok(json::number(match v {
529        Value::String(s) => s.chars().count() as f64,
530        Value::Array(a) => a.len() as f64,
531        Value::Object(m) => m.len() as f64,
532        _ => return Err(format!("size needs a list, string or map, got {}", type_name(v))),
533    }))
534}
535
536/// VAPP-206: `string(v)`: a number's or a bool's text, a string as it is.
537fn string_of(v: &Value) -> R<Value> {
538    Ok(Value::String(match v {
539        Value::String(s) => s.clone(),
540        Value::Number(n) => num_text(n.as_f64().unwrap_or(0.0)),
541        Value::Bool(b) => b.to_string(),
542        _ => return Err(format!("string needs a number, string or bool, got {}", type_name(v))),
543    }))
544}
545
546fn list_of<'a>(f: &str, v: &'a Value) -> R<&'a Vec<Value>> {
547    v.as_array().ok_or_else(|| format!("{f} needs a list, got {}", type_name(v)))
548}
549
550/// VAPP-206: a list's numbers; a null entry (a row's missing optional field)
551/// is skipped.
552fn numbers_of(f: &str, v: &Value) -> R<Vec<f64>> {
553    let mut out = Vec::new();
554    for x in list_of(f, v)? {
555        match x {
556            Value::Null => {}
557            Value::Number(n) => out.push(n.as_f64().unwrap_or(0.0)),
558            _ => return Err(format!("{f} needs a list of numbers, got {}", type_name(x))),
559        }
560    }
561    Ok(out)
562}
563
564fn field_of<'a>(row: &'a Value, field: &str) -> &'a Value {
565    row.as_object().and_then(|m| m.get(field)).unwrap_or(&Value::Null)
566}
567
568/// VAPP-206: `sortBy`'s order: numbers by value, strings by UTF-16 code unit,
569/// anything else equal (the sort is stable). A missing value sorts last in
570/// either order.
571fn sort_by(args: &[Value]) -> R<Value> {
572    use std::cmp::Ordering;
573    let list = list_of("sortBy", &args[0])?;
574    let field = text("sortBy", &args[1])?;
575    let desc = match args.get(2) {
576        None => false,
577        Some(Value::String(o)) if o == "asc" => false,
578        Some(Value::String(o)) if o == "desc" => true,
579        Some(_) => return Err("sortBy order is asc or desc".into()),
580    };
581    let mut out = list.clone();
582    out.sort_by(|x, y| {
583        let (a, b) = (field_of(x, field), field_of(y, field));
584        if a.is_null() || b.is_null() {
585            return match (a.is_null(), b.is_null()) {
586                (true, true) => Ordering::Equal,
587                (true, false) => Ordering::Greater,
588                _ => Ordering::Less,
589            };
590        }
591        let c = match (a, b) {
592            (Value::Number(p), Value::Number(q)) => p.as_f64().unwrap_or(0.0).partial_cmp(&q.as_f64().unwrap_or(0.0)).unwrap_or(Ordering::Equal),
593            (Value::String(p), Value::String(q)) => utf16_cmp(p, q),
594            _ => Ordering::Equal,
595        };
596        if desc {
597            c.reverse()
598        } else {
599            c
600        }
601    });
602    Ok(Value::Array(out))
603}
604
605fn group_of(args: &[Value]) -> R<Value> {
606    let field = text("groupOf", &args[1])?;
607    let mut out: Vec<(Value, Vec<Value>)> = Vec::new();
608    for row in list_of("groupOf", &args[0])? {
609        let key = field_of(row, field);
610        match out.iter_mut().find(|(k, _)| deep_equal(k, key)) {
611            Some((_, rows)) => rows.push(row.clone()),
612            None => out.push((key.clone(), vec![row.clone()])),
613        }
614    }
615    Ok(Value::Array(out.into_iter().map(|(key, rows)| serde_json::json!({"key": key, "rows": rows})).collect()))
616}
617
618/// The TS twin's `String.prototype.trim` set: White_Space without U+0085, plus U+FEFF.
619fn trim_text(v: &str) -> &str {
620    v.trim_matches(|c: char| (c.is_whitespace() && c != '\u{85}') || c == '\u{FEFF}')
621}
622
623#[derive(Clone, Copy, PartialEq, Eq)]
624enum Param {
625    Number,
626    Text,
627    Temporal,
628    Sized,
629    List,
630    Any,
631}
632
633struct Def {
634    params: &'static [Param],
635    min: usize,
636    variadic: bool,
637    args: &'static [&'static str],
638}
639
640use Param::{Any as A, List as L, Number as N, Sized as Z, Temporal as T, Text as S};
641
642/// THE function table (spec §4.2), positional for CEL, named (`args`) for
643/// the A2UI form. Generic only: no working days, no holidays.
644pub const FUNCTION_NAMES: &[&str] = &["mul", "div", "mod", "round", "floor", "ceil", "abs", "min", "max", "today", "now", "dateTime", "dateAdd", "dateDiff", "startOf", "endOf", "weekday", "isoWeek", "hours", "formatDuration", "size", "orElse", "string", "trim", "lower", "upper", "split", "join", "sumOf", "avgOf", "minOf", "maxOf", "countOf", "sortBy", "groupOf"];
645
646/// The A2UI form's names in the bind table (`size` = the existing `len`;
647/// `expr` = the string form, `{expr, vars}`).
648pub const CALL_NAMES: &[&str] = &["mul", "div", "mod", "round", "floor", "ceil", "abs", "min", "max", "today", "now", "dateTime", "dateAdd", "dateDiff", "startOf", "endOf", "weekday", "isoWeek", "hours", "formatDuration", "orElse", "expr"];
649
650/// VAPP-206: reachable from CEL only (`{call: expr}`): the bind table and the
651/// catalog keep their names.
652pub const CEL_ONLY: &[&str] = &["size", "string", "trim", "lower", "upper", "split", "join", "sumOf", "avgOf", "minOf", "maxOf", "countOf", "sortBy", "groupOf"];
653
654fn def(name: &str) -> Option<Def> {
655    let d = |params: &'static [Param], args: &'static [&'static str]| Def { params, min: params.len(), variadic: false, args };
656    Some(match name {
657        "mul" | "div" | "mod" => d(&[N, N], &["a", "b"]),
658        "round" => Def { params: &[N, N], min: 1, variadic: false, args: &["value", "digits"] },
659        "floor" | "ceil" | "abs" => d(&[N], &["value"]),
660        "min" | "max" => Def { params: &[N], min: 1, variadic: true, args: &["values"] },
661        "today" | "now" => d(&[], &[]),
662        "dateTime" => Def { params: &[T, A], min: 1, variadic: false, args: &["date", "time"] },
663        "dateAdd" => d(&[T, N, S], &["date", "n", "unit"]),
664        "dateDiff" => d(&[T, T, S], &["a", "b", "unit"]),
665        "startOf" | "endOf" => d(&[T, S], &["date", "unit"]),
666        "weekday" | "isoWeek" => d(&[T], &["date"]),
667        "hours" => d(&[N], &["minutes"]),
668        "formatDuration" => d(&[N, S], &["hours", "format"]),
669        "size" => d(&[Z], &["value"]),
670        // VAPP-206: the Text and Arrays rows (CEL only, like size: `CEL_ONLY`).
671        "string" => d(&[A], &["value"]),
672        "trim" | "lower" | "upper" => d(&[S], &["value"]),
673        "split" => d(&[S, S], &["value", "separator"]),
674        "join" => Def { params: &[L, S], min: 1, variadic: false, args: &["values", "separator"] },
675        "sumOf" | "avgOf" | "minOf" | "maxOf" | "countOf" => d(&[L], &["values"]),
676        "sortBy" => Def { params: &[L, S, S], min: 2, variadic: false, args: &["values", "field", "order"] },
677        "groupOf" => d(&[L, S], &["values", "field"]),
678        // VAPP-195: the value, or the default when it is missing (null).
679        "orElse" => d(&[A, A], &["value", "default"]),
680        _ => return None,
681    })
682}
683
684fn arity_error(name: &str, def: &Def, n: usize) -> Option<String> {
685    if def.variadic {
686        return (n < 1).then(|| format!("{name} takes at least 1 argument"));
687    }
688    let (min, max) = (def.min, def.params.len());
689    if n >= min && n <= max {
690        return None;
691    }
692    Some(if min == max { format!("{name} takes {min} argument{}", if min == 1 { "" } else { "s" }) } else { format!("{name} takes {min} or {max} arguments") })
693}
694
695fn run(name: &str, args: &[Value], env: &Env) -> R<Value> {
696    let n = |f: fn(f64) -> f64| -> R<Value> { Ok(json::number(num(f(number(name, &args[0])?))?)) };
697    match name {
698        "mul" => Ok(json::number(num(number("mul", &args[0])? * number("mul", &args[1])?)?)),
699        "div" => {
700            let (x, y) = (number("div", &args[0])?, number("div", &args[1])?);
701            if y == 0.0 {
702                return Err("division by zero".into());
703            }
704            Ok(json::number(num(x / y)?))
705        }
706        "mod" => {
707            let (x, y) = (number("mod", &args[0])?, number("mod", &args[1])?);
708            if y == 0.0 {
709                return Err("modulo by zero".into());
710            }
711            Ok(json::number(num(x % y)?))
712        }
713        "round" => round(args),
714        "floor" => n(f64::floor),
715        "ceil" => n(f64::ceil),
716        "abs" => n(f64::abs),
717        "min" | "max" => {
718            let mut out = if name == "min" { f64::INFINITY } else { f64::NEG_INFINITY };
719            for v in args {
720                let x = number(name, v)?;
721                out = if name == "min" { out.min(x) } else { out.max(x) };
722            }
723            Ok(json::number(num(out)?))
724        }
725        "today" => Ok(Value::String(day_text(floor_day(to_local(env, env.now)))?)),
726        "now" => Ok(Value::String(format_timestamp(env.now)?)),
727        "dateTime" => date_time(args, env),
728        "dateAdd" => date_add(args, env),
729        "dateDiff" => date_diff(args, env),
730        "startOf" | "endOf" => start_end(name, args, env),
731        "weekday" => {
732            let d = local_day("weekday", &args[0], env)?;
733            Ok(json::number(match weekday_of(d.y, d.m, d.d) {
734                0 => 7.0,
735                w => w as f64,
736            }))
737        }
738        "isoWeek" => {
739            let d = local_day("isoWeek", &args[0], env)?;
740            Ok(json::number(iso_week(d.y, d.m, d.d).1 as f64))
741        }
742        "hours" => Ok(json::number(num(number("hours", &args[0])? / 60.0)?)),
743        "formatDuration" => format_duration(args),
744        "size" => size(&args[0]),
745        "string" => string_of(&args[0]),
746        "trim" => Ok(Value::String(trim_text(text("trim", &args[0])?).to_string())),
747        "lower" => Ok(Value::String(text("lower", &args[0])?.to_lowercase())),
748        "upper" => Ok(Value::String(text("upper", &args[0])?.to_uppercase())),
749        "split" => {
750            let value = text("split", &args[0])?;
751            let separator = text("split", &args[1])?;
752            if separator.is_empty() {
753                return Err("split needs a separator".into());
754            }
755            Ok(Value::Array(value.split(separator).map(|s| Value::String(s.to_string())).collect()))
756        }
757        "join" => {
758            let separator = match args.get(1) {
759                Some(v) => text("join", v)?,
760                None => "",
761            };
762            let mut parts: Vec<&str> = Vec::new();
763            for x in list_of("join", &args[0])? {
764                match x {
765                    Value::String(s) => parts.push(s),
766                    _ => return Err(format!("join needs a list of strings, got {}", type_name(x))),
767                }
768            }
769            Ok(Value::String(parts.join(separator)))
770        }
771        "sumOf" => Ok(json::number(num(numbers_of("sumOf", &args[0])?.iter().sum::<f64>())?)),
772        "avgOf" => {
773            let ns = numbers_of("avgOf", &args[0])?;
774            if ns.is_empty() {
775                return Ok(Value::Null);
776            }
777            Ok(json::number(num(ns.iter().sum::<f64>() / ns.len() as f64)?))
778        }
779        "minOf" | "maxOf" => {
780            let ns = numbers_of(name, &args[0])?;
781            let picked = ns.into_iter().reduce(|a, b| if (name == "minOf" && b < a) || (name == "maxOf" && b > a) { b } else { a });
782            Ok(picked.map_or(Value::Null, json::number))
783        }
784        "countOf" => Ok(json::number(list_of("countOf", &args[0])?.iter().filter(|x| !x.is_null()).count() as f64)),
785        "sortBy" => sort_by(args),
786        "groupOf" => group_of(args),
787        "orElse" => Ok(if args[0].is_null() { args.get(1).cloned().unwrap_or(Value::Null) } else { args[0].clone() }),
788        _ => Err(format!("unknown function {name}")),
789    }
790}
791
792/// The A2UI function form `{call: name, args}` with its arguments resolved.
793/// Outer `None` = not an expression function; inner `None` = no value (an
794/// error, like every bind-time function). VAPP-195: a failed call shows its
795/// `default` (absent = no value).
796pub fn call_expression_function(name: &str, args: &Map<String, Value>, env: &ExpressionEnv) -> Option<Option<Value>> {
797    let fallback = || Some(args.get("default").cloned());
798    if name == "expr" {
799        let Some(Value::String(expr)) = args.get("expr") else { return fallback() };
800        let empty = Map::new();
801        let vars = args.get("vars").and_then(Value::as_object).unwrap_or(&empty);
802        return evaluate_expression(expr, vars, env).map_or_else(|_| fallback(), |v| Some(Some(v)));
803    }
804    if CEL_ONLY.contains(&name) {
805        return None;
806    }
807    let def = def(name)?;
808    let positional: Vec<Value> = if def.variadic {
809        args.get("values").and_then(Value::as_array).cloned().unwrap_or_default()
810    } else {
811        let mut list: Vec<Option<&Value>> = def.args.iter().map(|k| args.get(*k)).collect();
812        while list.len() > def.min && list.last().is_some_and(Option::is_none) {
813            list.pop();
814        }
815        list.into_iter().map(|v| v.cloned().unwrap_or(Value::Null)).collect()
816    };
817    if arity_error(name, &def, positional.len()).is_some() {
818        return fallback();
819    }
820    run(name, &positional, &Env::of(env)).map_or_else(|_| fallback(), |v| Some(Some(v)))
821}
822
823// ---------------------------------------------------------------------------
824// Parsing
825// ---------------------------------------------------------------------------
826
827/// A parsed expression (`at` = the code point it starts at).
828#[derive(Debug, Clone, PartialEq)]
829pub enum Node {
830    Lit(Value, usize),
831    Id(String, usize),
832    List(Vec<Node>, usize),
833    /// VAPP-206: `{'k': e}`, string keys in source order.
834    Map(Vec<(String, Node)>, usize),
835    Field(Box<Node>, String, usize),
836    Index(Box<Node>, Box<Node>, usize),
837    Unary(char, Box<Node>, usize),
838    Bin(&'static str, Box<Node>, Box<Node>, usize),
839    Cond(Box<Node>, Box<Node>, Box<Node>, usize),
840    Call(String, Vec<Node>, usize),
841    Macro(String, Box<Node>, String, Box<Node>, usize),
842    /// VAPP-195: `has(a.b)`, CEL's presence test (the node is a `Field`).
843    Has(Box<Node>, usize),
844}
845
846#[derive(Debug, Clone, PartialEq)]
847enum Tok {
848    Num(f64),
849    Str(String),
850    Id(String),
851    Op(&'static str),
852    End,
853}
854
855#[derive(Debug, Clone)]
856struct Token {
857    t: Tok,
858    at: usize,
859    text: String,
860}
861
862const OPS2: &[&str] = &["==", "!=", "<=", ">=", "&&", "||"];
863const OPS1: &[&str] = &["+", "-", "*", "/", "%", "<", ">", "!", "(", ")", "[", "]", "{", "}", ",", ".", "?", ":"];
864
865fn is_digit(c: Option<&char>) -> bool {
866    c.is_some_and(char::is_ascii_digit)
867}
868fn is_id_start(c: Option<&char>) -> bool {
869    c.is_some_and(|c| c.is_ascii_alphabetic() || *c == '_')
870}
871fn is_id_part(c: Option<&char>) -> bool {
872    is_id_start(c) || is_digit(c)
873}
874
875fn tokenize(cs: &[char]) -> R<Vec<Token>> {
876    let mut out = Vec::new();
877    let mut i = 0;
878    while i < cs.len() {
879        let c = cs[i];
880        if matches!(c, ' ' | '\t' | '\n' | '\r') {
881            i += 1;
882            continue;
883        }
884        let at = i;
885        if c.is_ascii_digit() {
886            while is_digit(cs.get(i)) {
887                i += 1;
888            }
889            if cs.get(i) == Some(&'.') && is_digit(cs.get(i + 1)) {
890                i += 1;
891                while is_digit(cs.get(i)) {
892                    i += 1;
893                }
894            }
895            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)))) {
896                i += 2;
897                while is_digit(cs.get(i)) {
898                    i += 1;
899                }
900            }
901            let text: String = cs[at..i].iter().collect();
902            let v = text.parse::<f64>().unwrap_or(f64::INFINITY);
903            out.push(Token { t: Tok::Num(v), at, text });
904            continue;
905        }
906        if c == '\'' || c == '"' {
907            i += 1;
908            let mut s = String::new();
909            loop {
910                let d = cs.get(i).copied();
911                match d {
912                    None | Some('\n') => return Err(format!("at {at}: unterminated string")),
913                    Some(d) if d == c => break,
914                    Some('\\') => {
915                        let e = cs.get(i + 1).copied();
916                        let mapped = match e {
917                            Some('\\') => '\\',
918                            Some('\'') => '\'',
919                            Some('"') => '"',
920                            Some('n') => '\n',
921                            Some('t') => '\t',
922                            Some('r') => '\r',
923                            _ => return Err(format!("at {i}: unknown escape \\{}", e.map(String::from).unwrap_or_default())),
924                        };
925                        s.push(mapped);
926                        i += 2;
927                    }
928                    Some(d) => {
929                        s.push(d);
930                        i += 1;
931                    }
932                }
933            }
934            i += 1;
935            out.push(Token { t: Tok::Str(s), at, text: cs[at..i].iter().collect() });
936            continue;
937        }
938        if is_id_start(Some(&c)) {
939            while is_id_part(cs.get(i)) {
940                i += 1;
941            }
942            let text: String = cs[at..i].iter().collect();
943            out.push(Token { t: Tok::Id(text.clone()), at, text });
944            continue;
945        }
946        let two: String = cs[i..(i + 2).min(cs.len())].iter().collect();
947        if let Some(op) = OPS2.iter().find(|o| **o == two) {
948            out.push(Token { t: Tok::Op(op), at, text: two });
949            i += 2;
950            continue;
951        }
952        if let Some(op) = OPS1.iter().find(|o| o.starts_with(c)) {
953            out.push(Token { t: Tok::Op(op), at, text: c.to_string() });
954            i += 1;
955            continue;
956        }
957        return Err(format!("at {at}: unexpected {c}"));
958    }
959    out.push(Token { t: Tok::End, at: cs.len(), text: "end".into() });
960    Ok(out)
961}
962
963const MACROS: &[&str] = &["map", "filter", "all", "exists"];
964
965struct Parser {
966    tokens: Vec<Token>,
967    i: usize,
968    depth: usize,
969    in_body: usize,
970}
971
972impl Parser {
973    fn peek(&self) -> &Token {
974        &self.tokens[self.i]
975    }
976    fn is_op(&self, v: &str) -> bool {
977        matches!(&self.peek().t, Tok::Op(o) if *o == v)
978    }
979    fn next(&mut self) -> Token {
980        let t = self.tokens[self.i].clone();
981        self.i += 1;
982        t
983    }
984    fn unexpected<T>(&self) -> R<T> {
985        let t = self.peek();
986        Err(format!("at {}: unexpected {}", t.at, t.text))
987    }
988    fn expect(&mut self, v: &str) -> R<Token> {
989        if !self.is_op(v) {
990            return self.unexpected();
991        }
992        Ok(self.next())
993    }
994    fn enter(&mut self, at: usize) -> R<()> {
995        self.depth += 1;
996        if self.depth > MAX_DEPTH {
997            return Err(format!("at {at}: expression is nested too deeply"));
998        }
999        Ok(())
1000    }
1001
1002    fn parse_all(&mut self) -> R<Node> {
1003        let node = self.expr()?;
1004        if self.peek().t != Tok::End {
1005            return self.unexpected();
1006        }
1007        Ok(node)
1008    }
1009
1010    fn expr(&mut self) -> R<Node> {
1011        crate::deep(|| self.expr_inner())
1012    }
1013
1014    fn expr_inner(&mut self) -> R<Node> {
1015        let at = self.peek().at;
1016        self.enter(at)?;
1017        let test = self.or()?;
1018        let mut out = test;
1019        if self.is_op("?") {
1020            self.next();
1021            let then = self.expr()?;
1022            self.expect(":")?;
1023            let otherwise = self.expr()?;
1024            out = Node::Cond(Box::new(out), Box::new(then), Box::new(otherwise), at);
1025        }
1026        self.depth -= 1;
1027        Ok(out)
1028    }
1029
1030    fn binary(&mut self, ops: &[&'static str], operand: fn(&mut Parser) -> R<Node>) -> R<Node> {
1031        let mut a = operand(self)?;
1032        loop {
1033            let t = self.peek().clone();
1034            let Tok::Op(op) = t.t else { return Ok(a) };
1035            let Some(op) = ops.iter().find(|o| **o == op) else { return Ok(a) };
1036            self.next();
1037            let b = operand(self)?;
1038            a = Node::Bin(op, Box::new(a), Box::new(b), t.at);
1039        }
1040    }
1041    fn or(&mut self) -> R<Node> {
1042        self.binary(&["||"], Parser::and)
1043    }
1044    fn and(&mut self) -> R<Node> {
1045        self.binary(&["&&"], Parser::rel)
1046    }
1047    fn rel(&mut self) -> R<Node> {
1048        self.binary(&["==", "!=", "<", "<=", ">", ">="], Parser::add)
1049    }
1050    fn add(&mut self) -> R<Node> {
1051        self.binary(&["+", "-"], Parser::mul)
1052    }
1053    fn mul(&mut self) -> R<Node> {
1054        self.binary(&["*", "/", "%"], Parser::unary)
1055    }
1056
1057    fn unary(&mut self) -> R<Node> {
1058        let t = self.peek().clone();
1059        if let Tok::Op(op @ ("!" | "-")) = t.t {
1060            self.next();
1061            self.enter(t.at)?;
1062            let arg = crate::deep(|| self.unary())?;
1063            self.depth -= 1;
1064            return Ok(Node::Unary(op.chars().next().unwrap_or('!'), Box::new(arg), t.at));
1065        }
1066        self.postfix()
1067    }
1068
1069    fn postfix(&mut self) -> R<Node> {
1070        let mut node = self.primary()?;
1071        loop {
1072            if self.is_op(".") {
1073                self.next();
1074                let name = self.peek().clone();
1075                let Tok::Id(field) = name.t.clone() else { return self.unexpected() };
1076                self.next();
1077                if self.is_op("(") {
1078                    if !MACROS.contains(&field.as_str()) {
1079                        if let Some(dotted) = dotted_name(&node) {
1080                            let full = format!("{dotted}.{field}");
1081                            let effect = function_effect(&full);
1082                            if effect != FunctionEffect::Pure {
1083                                return Err(format!("at {}: {}: an expression is pure, use an action step", root_at(&node), effect_words(&full, effect)));
1084                            }
1085                        }
1086                        return Err(format!("at {}: unknown method {field}", name.at));
1087                    }
1088                    if self.in_body > 0 {
1089                        return Err(format!("at {}: nested comprehensions are not allowed", name.at));
1090                    }
1091                    self.next();
1092                    let v = self.peek().clone();
1093                    let comma = matches!(self.tokens.get(self.i + 1).map(|t| &t.t), Some(Tok::Op(",")));
1094                    let Tok::Id(var) = v.t.clone() else { return Err(format!("at {}: {field} needs a name first", v.at)) };
1095                    if !comma {
1096                        return Err(format!("at {}: {field} needs a name first", v.at));
1097                    }
1098                    self.next();
1099                    self.next();
1100                    self.in_body += 1;
1101                    let body = self.expr()?;
1102                    self.in_body -= 1;
1103                    self.expect(")")?;
1104                    node = Node::Macro(field, Box::new(node), var, Box::new(body), name.at);
1105                } else {
1106                    node = Node::Field(Box::new(node), field, name.at);
1107                }
1108                continue;
1109            }
1110            if self.is_op("[") {
1111                let at = self.next().at;
1112                let index = self.expr()?;
1113                self.expect("]")?;
1114                node = Node::Index(Box::new(node), Box::new(index), at);
1115                continue;
1116            }
1117            return Ok(node);
1118        }
1119    }
1120
1121    fn list(&mut self, close: &str) -> R<Vec<Node>> {
1122        let mut items = Vec::new();
1123        if self.is_op(close) {
1124            self.next();
1125            return Ok(items);
1126        }
1127        loop {
1128            items.push(self.expr()?);
1129            if self.is_op(",") {
1130                self.next();
1131                if self.is_op(close) && close == "]" {
1132                    self.next();
1133                    return Ok(items);
1134                }
1135                continue;
1136            }
1137            self.expect(close)?;
1138            return Ok(items);
1139        }
1140    }
1141
1142    /// VAPP-206: `{'k': e, …}`: string keys, no duplicates, `?` never (it
1143    /// spells an optional type).
1144    fn map(&mut self) -> R<Vec<(String, Node)>> {
1145        let mut entries: Vec<(String, Node)> = Vec::new();
1146        while !self.is_op("}") {
1147            let key = self.peek().clone();
1148            let Tok::Str(name) = key.t else {
1149                return Err(format!("at {}: a map key is a string", key.at));
1150            };
1151            if name == "?" {
1152                return Err(format!("at {}: ? is not a map key", key.at));
1153            }
1154            if entries.iter().any(|(k, _)| *k == name) {
1155                return Err(format!("at {}: duplicate key {name}", key.at));
1156            }
1157            self.next();
1158            self.expect(":")?;
1159            let value = self.expr()?;
1160            entries.push((name, value));
1161            if !self.is_op(",") {
1162                break;
1163            }
1164            self.next();
1165        }
1166        self.expect("}")?;
1167        Ok(entries)
1168    }
1169
1170    fn primary(&mut self) -> R<Node> {
1171        let t = self.peek().clone();
1172        match t.t {
1173            Tok::Num(v) => {
1174                self.next();
1175                if !v.is_finite() {
1176                    return Err(format!("at {}: number out of range", t.at));
1177                }
1178                Ok(Node::Lit(json::number(v), t.at))
1179            }
1180            Tok::Str(s) => {
1181                self.next();
1182                Ok(Node::Lit(Value::String(s), t.at))
1183            }
1184            Tok::Id(name) => {
1185                self.next();
1186                match name.as_str() {
1187                    "true" | "false" => return Ok(Node::Lit(Value::Bool(name == "true"), t.at)),
1188                    "null" => return Ok(Node::Lit(Value::Null, t.at)),
1189                    _ => {}
1190                }
1191                // VAPP-195: CEL's presence test, `has(a.b)`.
1192                if name == "has" && self.is_op("(") {
1193                    self.next();
1194                    let arg = self.expr()?;
1195                    self.expect(")")?;
1196                    if !matches!(arg, Node::Field(..)) {
1197                        let hint = match &arg {
1198                            Node::Id(name, _) => format!("; for a name write {name} != null"),
1199                            _ => String::new(),
1200                        };
1201                        return Err(format!("at {}: has needs a field: has(a.b){hint}", t.at));
1202                    }
1203                    return Ok(Node::Has(Box::new(arg), t.at));
1204                }
1205                if self.is_op("(") {
1206                    if def(&name).is_none() {
1207                        let effect = function_effect(&name);
1208                        if effect != FunctionEffect::Pure {
1209                            return Err(format!("at {}: {}: an expression is pure, use an action step", t.at, effect_words(&name, effect)));
1210                        }
1211                        return Err(format!("at {}: unknown function {name}", t.at));
1212                    }
1213                    self.next();
1214                    let args = self.list(")")?;
1215                    return Ok(Node::Call(name, args, t.at));
1216                }
1217                Ok(Node::Id(name, t.at))
1218            }
1219            Tok::Op("(") => {
1220                self.next();
1221                let inner = self.expr()?;
1222                self.expect(")")?;
1223                Ok(inner)
1224            }
1225            Tok::Op("[") => {
1226                self.next();
1227                Ok(Node::List(self.list("]")?, t.at))
1228            }
1229            Tok::Op("{") => {
1230                self.next();
1231                Ok(Node::Map(self.map()?, t.at))
1232            }
1233            _ => self.unexpected(),
1234        }
1235    }
1236}
1237
1238/// `a.b.c` as a name (a chain of plain names), else `None`.
1239fn dotted_name(node: &Node) -> Option<String> {
1240    match node {
1241        Node::Id(name, _) => Some(name.clone()),
1242        Node::Field(of, name, _) => dotted_name(of).map(|of| format!("{of}.{name}")),
1243        _ => None,
1244    }
1245}
1246
1247/// VAPP-214: the free names an expression reads, sorted (a macro's own
1248/// variable is not free inside its body).
1249pub fn expression_names(node: &Node) -> Vec<String> {
1250    fn walk(n: &Node, bound: &mut Vec<String>, out: &mut std::collections::BTreeSet<String>) {
1251        match n {
1252            Node::Lit(..) => {}
1253            Node::Id(name, _) => {
1254                if !bound.iter().any(|b| b == name) {
1255                    out.insert(name.clone());
1256                }
1257            }
1258            Node::List(items, _) | Node::Call(_, items, _) => items.iter().for_each(|x| walk(x, bound, out)),
1259            Node::Map(entries, _) => entries.iter().for_each(|(_, x)| walk(x, bound, out)),
1260            Node::Field(of, _, _) | Node::Has(of, _) | Node::Unary(_, of, _) => walk(of, bound, out),
1261            Node::Index(of, index, _) => {
1262                walk(of, bound, out);
1263                walk(index, bound, out);
1264            }
1265            Node::Bin(_, a, b, _) => {
1266                walk(a, bound, out);
1267                walk(b, bound, out);
1268            }
1269            Node::Cond(test, then, other, _) => {
1270                walk(test, bound, out);
1271                walk(then, bound, out);
1272                walk(other, bound, out);
1273            }
1274            Node::Macro(_, of, v, body, _) => {
1275                walk(of, bound, out);
1276                bound.push(v.clone());
1277                walk(body, bound, out);
1278                bound.pop();
1279            }
1280        }
1281    }
1282    let mut out = std::collections::BTreeSet::new();
1283    walk(node, &mut Vec::new(), &mut out);
1284    out.into_iter().collect()
1285}
1286
1287/// VAPP-195: a value's path as written (`a.b[0].c`, `m['k']`): names,
1288/// fields and literal indexes; `None` for anything else.
1289pub fn path_text(node: &Node) -> Option<String> {
1290    match node {
1291        Node::Id(name, _) => Some(name.clone()),
1292        Node::Field(of, name, _) => path_text(of).map(|of| format!("{of}.{name}")),
1293        Node::Index(of, index, _) => {
1294            let of = path_text(of)?;
1295            match literal(index) {
1296                Some(Value::Number(n)) => Some(format!("{of}[{}]", num_text(n.as_f64().unwrap_or(0.0)))),
1297                Some(Value::String(k)) => Some(format!("{of}['{k}']")),
1298                _ => None,
1299            }
1300        }
1301        _ => None,
1302    }
1303}
1304
1305/// The code point a node starts at (its own `at`).
1306fn node_at(node: &Node) -> usize {
1307    match node {
1308        Node::Lit(_, at) | Node::Id(_, at) | Node::List(_, at) | Node::Map(_, at) | Node::Field(_, _, at) | Node::Index(_, _, at) | Node::Unary(_, _, at) | Node::Bin(_, _, _, at) | Node::Cond(_, _, _, at) | Node::Call(_, _, at) | Node::Macro(_, _, _, _, at) | Node::Has(_, at) => *at,
1309    }
1310}
1311
1312fn root_at(node: &Node) -> usize {
1313    match node {
1314        Node::Field(of, _, _) => root_at(of),
1315        _ => node_at(node),
1316    }
1317}
1318
1319/// Parse once (at package validation, or before evaluating): the tree, or
1320/// the first syntax / cost error (`at <code point>: <reason>`).
1321pub fn parse_expression(source: &str) -> Result<Node, String> {
1322    let cs: Vec<char> = source.chars().collect();
1323    if cs.len() > MAX_LENGTH {
1324        return Err(format!("expression is longer than {MAX_LENGTH} characters"));
1325    }
1326    Parser { tokens: tokenize(&cs)?, i: 0, depth: 0, in_body: 0 }.parse_all()
1327}
1328
1329// ---------------------------------------------------------------------------
1330// Evaluation
1331// ---------------------------------------------------------------------------
1332
1333struct Scope<'a> {
1334    name: &'a str,
1335    value: &'a Value,
1336    parent: Option<&'a Scope<'a>>,
1337}
1338
1339struct Evaluator<'v> {
1340    vars: &'v Map<String, Value>,
1341    env: Env<'v>,
1342    steps: usize,
1343    max_steps: usize,
1344}
1345
1346impl Evaluator<'_> {
1347    fn step(&mut self) -> R<()> {
1348        self.steps += 1;
1349        if self.steps > self.max_steps {
1350            return Err("step limit exceeded".into());
1351        }
1352        Ok(())
1353    }
1354
1355    fn ev(&mut self, node: &Node, scope: Option<&Scope>) -> R<Value> {
1356        crate::deep(|| self.ev_inner(node, scope))
1357    }
1358
1359    fn ev_inner(&mut self, node: &Node, scope: Option<&Scope>) -> R<Value> {
1360        self.step()?;
1361        match node {
1362            Node::Lit(v, _) => Ok(v.clone()),
1363            Node::List(items, _) => items.iter().map(|i| self.ev(i, scope)).collect::<R<Vec<_>>>().map(Value::Array),
1364            Node::Map(entries, _) => {
1365                let mut out = Map::new();
1366                for (k, v) in entries {
1367                    out.insert(k.clone(), self.ev(v, scope)?);
1368                }
1369                Ok(Value::Object(out))
1370            }
1371            Node::Id(name, _) => {
1372                let mut s = scope;
1373                while let Some(sc) = s {
1374                    if sc.name == name {
1375                        return Ok(sc.value.clone());
1376                    }
1377                    s = sc.parent;
1378                }
1379                self.vars.get(name).cloned().ok_or_else(|| format!("unknown name {name}"))
1380            }
1381            Node::Field(of, name, _) => match self.ev(of, scope)? {
1382                Value::Null => Ok(Value::Null),
1383                Value::Object(m) => Ok(m.get(name).cloned().unwrap_or(Value::Null)),
1384                other => Err(format!("no field {name} on {}", type_name(&other))),
1385            },
1386            Node::Has(field, _) => {
1387                let Node::Field(receiver, name, _) = field.as_ref() else { return Err("has needs a field: has(a.b)".into()) };
1388                match self.ev(receiver, scope)? {
1389                    Value::Null => Ok(Value::Bool(false)),
1390                    Value::Object(m) => Ok(Value::Bool(m.get(name).is_some_and(|v| !v.is_null()))),
1391                    other => Err(format!("no field {name} on {}", type_name(&other))),
1392                }
1393            }
1394            Node::Index(of, index, _) => {
1395                let of = self.ev(of, scope)?;
1396                let index = self.ev(index, scope)?;
1397                match of {
1398                    Value::Array(list) => {
1399                        let Value::Number(n) = &index else { return Err(format!("a list index is a number, got {}", type_name(&index))) };
1400                        let n = n.as_f64().unwrap_or(0.0);
1401                        if n.fract() != 0.0 {
1402                            return Err(format!("a list index is a whole number, got {}", num_text(n)));
1403                        }
1404                        // VAPP-204: an index past either end is null, like a missing field.
1405                        if n < 0.0 || n >= list.len() as f64 {
1406                            return Ok(Value::Null);
1407                        }
1408                        Ok(list[n as usize].clone())
1409                    }
1410                    Value::Object(m) => {
1411                        let Value::String(k) = &index else { return Err(format!("a map key is a string, got {}", type_name(&index))) };
1412                        Ok(m.get(k).cloned().unwrap_or(Value::Null))
1413                    }
1414                    Value::Null => Ok(Value::Null),
1415                    other => Err(format!("cannot index {}", type_name(&other))),
1416                }
1417            }
1418            Node::Unary(op, arg, _) => {
1419                let v = self.ev(arg, scope)?;
1420                if *op == '-' {
1421                    let Value::Number(n) = &v else { return Err(format!("cannot apply - to {}", type_name(&v))) };
1422                    return Ok(json::number(num(-n.as_f64().unwrap_or(0.0))?));
1423                }
1424                let Value::Bool(b) = v else { return Err(format!("cannot apply ! to {}", type_name(&v))) };
1425                Ok(Value::Bool(!b))
1426            }
1427            Node::Cond(test, then, otherwise, _) => {
1428                let t = self.ev(test, scope)?;
1429                let Value::Bool(t) = t else { return Err(format!("cannot use {} as a condition", type_name(&t))) };
1430                self.ev(if t { then } else { otherwise }, scope)
1431            }
1432            Node::Bin(op, a, b, _) => self.binary(op, a, b, scope),
1433            Node::Call(name, args, _) => {
1434                let args = args.iter().map(|a| self.ev(a, scope)).collect::<R<Vec<_>>>()?;
1435                let d = def(name).ok_or_else(|| format!("unknown function {name}"))?;
1436                if let Some(e) = arity_error(name, &d, args.len()) {
1437                    return Err(e);
1438                }
1439                run(name, &args, &self.env)
1440            }
1441            Node::Macro(f, of, var, body, _) => {
1442                let of = self.ev(of, scope)?;
1443                let Value::Array(list) = of else { return Err(format!("{f} needs a list, got {}", type_name(&of))) };
1444                let mut out = Vec::new();
1445                for item in &list {
1446                    self.step()?;
1447                    let inner = Scope { name: var, value: item, parent: scope };
1448                    let v = self.ev(body, Some(&inner))?;
1449                    if f == "map" {
1450                        out.push(v);
1451                        continue;
1452                    }
1453                    let Value::Bool(b) = v else { return Err(format!("{f} needs a bool, got {}", type_name(&v))) };
1454                    match f.as_str() {
1455                        "filter" if b => out.push(item.clone()),
1456                        "all" if !b => return Ok(Value::Bool(false)),
1457                        "exists" if b => return Ok(Value::Bool(true)),
1458                        _ => {}
1459                    }
1460                }
1461                Ok(match f.as_str() {
1462                    "all" => Value::Bool(true),
1463                    "exists" => Value::Bool(false),
1464                    _ => Value::Array(out),
1465                })
1466            }
1467        }
1468    }
1469
1470    fn binary(&mut self, op: &str, a: &Node, b: &Node, scope: Option<&Scope>) -> R<Value> {
1471        if op == "&&" || op == "||" {
1472            let x = self.ev(a, scope)?;
1473            let Value::Bool(x) = x else { return Err(format!("cannot apply {op} to {}", type_name(&x))) };
1474            if (op == "&&" && !x) || (op == "||" && x) {
1475                return Ok(Value::Bool(x));
1476            }
1477            let y = self.ev(b, scope)?;
1478            let Value::Bool(_) = y else { return Err(format!("cannot apply {op} to {}", type_name(&y))) };
1479            return Ok(y);
1480        }
1481        let x = self.ev(a, scope)?;
1482        let y = self.ev(b, scope)?;
1483        match op {
1484            "==" => Ok(Value::Bool(deep_equal(&x, &y))),
1485            "!=" => Ok(Value::Bool(!deep_equal(&x, &y))),
1486            "<" | "<=" | ">" | ">=" => {
1487                let c = match (&x, &y) {
1488                    (Value::Number(p), Value::Number(q)) => p.as_f64().partial_cmp(&q.as_f64()).unwrap_or(std::cmp::Ordering::Equal),
1489                    (Value::String(p), Value::String(q)) => utf16_cmp(p, q),
1490                    _ => return Err(format!("cannot compare {} and {}", type_name(&x), type_name(&y))),
1491                };
1492                use std::cmp::Ordering::*;
1493                Ok(Value::Bool(match op {
1494                    "<" => c == Less,
1495                    "<=" => c != Greater,
1496                    ">" => c == Greater,
1497                    _ => c != Less,
1498                }))
1499            }
1500            "+" => match (x, y) {
1501                (Value::Number(p), Value::Number(q)) => Ok(json::number(num(p.as_f64().unwrap_or(0.0) + q.as_f64().unwrap_or(0.0))?)),
1502                (Value::String(p), Value::String(q)) => Ok(Value::String(p + &q)),
1503                (Value::Array(mut p), Value::Array(q)) => {
1504                    p.extend(q);
1505                    Ok(Value::Array(p))
1506                }
1507                (x, y) => Err(format!("cannot apply + to {} and {}", type_name(&x), type_name(&y))),
1508            },
1509            _ => {
1510                let (Value::Number(p), Value::Number(q)) = (&x, &y) else { return Err(format!("cannot apply {op} to {} and {}", type_name(&x), type_name(&y))) };
1511                let (p, q) = (p.as_f64().unwrap_or(0.0), q.as_f64().unwrap_or(0.0));
1512                match op {
1513                    "-" => Ok(json::number(num(p - q)?)),
1514                    "*" => Ok(json::number(num(p * q)?)),
1515                    _ => {
1516                        if q == 0.0 {
1517                            return Err(if op == "/" { "division by zero" } else { "modulo by zero" }.into());
1518                        }
1519                        Ok(json::number(num(if op == "/" { p / q } else { p % q })?))
1520                    }
1521                }
1522            }
1523        }
1524    }
1525}
1526
1527/// Evaluate an expression over `vars`: the value, or the error (a syntax,
1528/// type, range or cost error).
1529pub fn evaluate_expression(source: &str, vars: &Map<String, Value>, env: &ExpressionEnv) -> Result<Value, String> {
1530    let ast = parse_expression(source)?;
1531    evaluate_node(&ast, vars, env)
1532}
1533
1534/// Evaluate a parsed tree (see [`parse_expression`]).
1535pub fn evaluate_node<'a>(ast: &Node, vars: &'a Map<String, Value>, env: &ExpressionEnv<'a>) -> Result<Value, String> {
1536    let mut e = Evaluator { vars, env: Env::of(env), steps: 0, max_steps: env.max_steps.unwrap_or(MAX_STEPS) };
1537    e.ev(ast, None)
1538}
1539
1540/// VAPP-194: evaluate a parsed tree and count its steps (an action spends
1541/// one budget over all of its expressions): the result and the steps used.
1542pub fn evaluate_counted<'a>(ast: &Node, vars: &'a Map<String, Value>, env: &ExpressionEnv<'a>) -> (Result<Value, String>, usize) {
1543    let mut e = Evaluator { vars, env: Env::of(env), steps: 0, max_steps: env.max_steps.unwrap_or(MAX_STEPS) };
1544    let result = e.ev(ast, None);
1545    (result, e.steps)
1546}
1547
1548// ---------------------------------------------------------------------------
1549// Type checking (package validation)
1550// ---------------------------------------------------------------------------
1551
1552/// VAPP-195 (E2): a value that may be missing is `T?`, spelled `{"?": T}`
1553/// (a row never has a field named `?`).
1554pub fn is_optional(t: &Value) -> bool {
1555    matches!(t, Value::Object(m) if m.len() == 1 && m.contains_key("?"))
1556}
1557
1558/// `T?` (null and an optional stay as they are).
1559pub fn optional_type(t: Value) -> Value {
1560    if is_optional(&t) || is(&t, "null") {
1561        return t;
1562    }
1563    let mut m = Map::new();
1564    m.insert("?".into(), t);
1565    Value::Object(m)
1566}
1567
1568/// `T` of a `T?`.
1569pub fn required_type(t: &Value) -> Value {
1570    match t {
1571        Value::Object(m) if is_optional(t) => m["?"].clone(),
1572        _ => t.clone(),
1573    }
1574}
1575
1576/// The name a type prints as in an error: a list `list`, a row `map`, an
1577/// optional `T?`.
1578pub fn type_display(t: &Value) -> String {
1579    match t {
1580        Value::Object(m) if is_optional(t) => format!("{}?", type_display(&m["?"])),
1581        Value::String(s) => s.clone(),
1582        Value::Array(_) => "list".into(),
1583        _ => "map".into(),
1584    }
1585}
1586
1587fn is_row(t: &Value) -> bool {
1588    t.is_object() && !is_optional(t)
1589}
1590fn is_textual(t: &Value) -> bool {
1591    matches!(t.as_str(), Some("string" | "date" | "timestamp"))
1592}
1593fn loose(t: &Value) -> bool {
1594    matches!(t.as_str(), Some("dyn" | "null"))
1595}
1596fn is(t: &Value, name: &str) -> bool {
1597    t.as_str() == Some(name)
1598}
1599fn ty(name: &str) -> Value {
1600    Value::String(name.into())
1601}
1602
1603fn accepts(param: Param, t: &Value) -> bool {
1604    if param == Param::Any || loose(t) {
1605        return true;
1606    }
1607    match param {
1608        Param::Number => is(t, "number"),
1609        Param::Text | Param::Temporal => is_textual(t),
1610        Param::Sized => is(t, "string") || is(t, "map") || t.is_array() || is_row(t),
1611        Param::List => t.is_array(),
1612        Param::Any => true,
1613    }
1614}
1615
1616fn param_name(p: Param) -> &'static str {
1617    match p {
1618        Param::Number => "a number",
1619        Param::Text => "a string",
1620        Param::Temporal => "a date",
1621        Param::Sized => "a list, string or map",
1622        Param::List => "a list",
1623        Param::Any => "a value",
1624    }
1625}
1626
1627struct TypeScope<'a> {
1628    name: &'a str,
1629    ty: Value,
1630    parent: Option<&'a TypeScope<'a>>,
1631}
1632
1633fn literal(node: &Node) -> Option<&Value> {
1634    match node {
1635        Node::Lit(v, _) => Some(v),
1636        _ => None,
1637    }
1638}
1639
1640/// The paths a test proves present when it is true (`yes`) / when it is
1641/// false (`no`): `has(p)`, `p != null`, `p == null`, through `&&`, `||`
1642/// and `!`.
1643fn guards(node: &Node) -> (Vec<String>, Vec<String>) {
1644    match node {
1645        Node::Has(field, _) => (path_text(field).into_iter().collect(), Vec::new()),
1646        Node::Bin(op @ ("==" | "!="), a, b, _) => {
1647            let side = if matches!(a.as_ref(), Node::Lit(Value::Null, _)) {
1648                Some(b)
1649            } else if matches!(b.as_ref(), Node::Lit(Value::Null, _)) {
1650                Some(a)
1651            } else {
1652                None
1653            };
1654            match side.and_then(|s| path_text(s)) {
1655                None => (Vec::new(), Vec::new()),
1656                Some(p) if *op == "!=" => (vec![p], Vec::new()),
1657                Some(p) => (Vec::new(), vec![p]),
1658            }
1659        }
1660        Node::Bin("&&", a, b, _) => {
1661            let (mut yes, _) = guards(a);
1662            yes.extend(guards(b).0);
1663            (yes, Vec::new())
1664        }
1665        Node::Bin("||", a, b, _) => {
1666            let (_, mut no) = guards(a);
1667            no.extend(guards(b).1);
1668            (Vec::new(), no)
1669        }
1670        Node::Unary('!', arg, _) => {
1671            let (yes, no) = guards(arg);
1672            (no, yes)
1673        }
1674        _ => (Vec::new(), Vec::new()),
1675    }
1676}
1677
1678/// An unguarded use of a value that may be missing.
1679fn missing<T>(node: &Node) -> R<T> {
1680    let p = path_text(node).unwrap_or_else(|| format!("the value at {}", node_at(node)));
1681    Err(format!("{p} may be missing: guard it with has(), a null check or orElse()"))
1682}
1683
1684struct Checker<'t> {
1685    types: &'t Map<String, Value>,
1686    /// The paths a guard proved present here (`has(r.f) ? r.f + 1 : 0`).
1687    present: std::cell::RefCell<Vec<String>>,
1688}
1689
1690impl Checker<'_> {
1691    /// Type `node` with `paths` proved present.
1692    fn within(&self, paths: Vec<String>, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1693        let before = self.present.borrow().clone();
1694        self.present.borrow_mut().extend(paths);
1695        let out = self.tc(node, scope);
1696        *self.present.borrow_mut() = before;
1697        out
1698    }
1699
1700    /// The type of a use that must not be missing.
1701    fn need(&self, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1702        let t = self.tc(node, scope)?;
1703        if is_optional(&t) {
1704            return missing(node);
1705        }
1706        Ok(t)
1707    }
1708
1709    fn tc(&self, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1710        let t = crate::deep(|| self.tc_inner(node, scope))?;
1711        let p = if matches!(node, Node::Id(..) | Node::Field(..) | Node::Index(..)) { path_text(node) } else { None };
1712        Ok(match p {
1713            Some(p) if self.present.borrow().contains(&p) => required_type(&t),
1714            _ => t,
1715        })
1716    }
1717
1718    fn tc_inner(&self, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1719        match node {
1720            Node::Lit(v, _) => Ok(ty(type_name(v))),
1721            Node::List(items, _) => {
1722                let items = items.iter().map(|i| self.tc(i, scope)).collect::<R<Vec<_>>>()?;
1723                Ok(Value::Array(vec![match items.first() {
1724                    None => ty("dyn"),
1725                    Some(first) if items.iter().all(|t| t == first) => first.clone(),
1726                    Some(_) => ty("dyn"),
1727                }]))
1728            }
1729            Node::Map(entries, _) => {
1730                let mut out = Map::new();
1731                for (k, v) in entries {
1732                    out.insert(k.clone(), self.tc(v, scope)?);
1733                }
1734                Ok(Value::Object(out))
1735            }
1736            Node::Id(name, _) => {
1737                let mut s = scope;
1738                while let Some(sc) = s {
1739                    if sc.name == name {
1740                        return Ok(sc.ty.clone());
1741                    }
1742                    s = sc.parent;
1743                }
1744                self.types.get(name).cloned().ok_or_else(|| format!("unknown name {name}"))
1745            }
1746            Node::Field(..) | Node::Has(..) => {
1747                let has = matches!(node, Node::Has(..));
1748                let field = match node {
1749                    Node::Has(field, _) => field.as_ref(),
1750                    _ => node,
1751                };
1752                let Node::Field(of, name, _) = field else { return Err("has needs a field: has(a.b)".into()) };
1753                let raw = self.tc(of, scope)?;
1754                let of = required_type(&raw);
1755                // VAPP-204: a field of a map may be missing (`dyn?`).
1756                let t = if is(&of, "map") {
1757                    optional_type(ty("dyn"))
1758                } else if matches!(of.as_str(), Some("dyn" | "null")) {
1759                    ty("dyn")
1760                } else if let Value::Object(fields) = &of {
1761                    fields.get(name).cloned().ok_or_else(|| format!("unknown field {name}"))?
1762                } else {
1763                    return Err(format!("no field {name} on {}", type_display(&of)));
1764                };
1765                if has {
1766                    return Ok(ty("bool"));
1767                }
1768                Ok(if is_optional(&raw) { optional_type(t) } else { t })
1769            }
1770            Node::Index(of, index, _) => {
1771                let raw = self.tc(of, scope)?;
1772                let of_t = required_type(&raw);
1773                let index_t = self.need(index, scope)?;
1774                let wrap = |t: Value| if is_optional(&raw) { optional_type(t) } else { t };
1775                if let Value::Array(item) = &of_t {
1776                    if !loose(&index_t) && !is(&index_t, "number") {
1777                        return Err(format!("a list index is a number, got {}", type_display(&index_t)));
1778                    }
1779                    // VAPP-204: an index may be past the end (null).
1780                    return Ok(optional_type(item.first().cloned().unwrap_or_else(|| ty("dyn"))));
1781                }
1782                if is_row(&of_t) || is(&of_t, "map") {
1783                    if !loose(&index_t) && !is_textual(&index_t) {
1784                        return Err(format!("a map key is a string, got {}", type_display(&index_t)));
1785                    }
1786                    if let (Value::Object(fields), Some(Value::String(key))) = (&of_t, literal(index)) {
1787                        return fields.get(key).cloned().map(wrap).ok_or_else(|| format!("unknown field {key}"));
1788                    }
1789                    return Ok(optional_type(ty("dyn")));
1790                }
1791                if loose(&of_t) {
1792                    return Ok(wrap(ty("dyn")));
1793                }
1794                Err(format!("cannot index {}", type_display(&of_t)))
1795            }
1796            Node::Unary(op, arg, _) => {
1797                let t = self.need(arg, scope)?;
1798                let want = if *op == '-' { "number" } else { "bool" };
1799                if !loose(&t) && !is(&t, want) {
1800                    return Err(format!("cannot apply {op} to {}", type_display(&t)));
1801                }
1802                Ok(ty(want))
1803            }
1804            Node::Cond(test, then, otherwise, _) => {
1805                let t = self.need(test, scope)?;
1806                if !loose(&t) && !is(&t, "bool") {
1807                    return Err(format!("cannot use {} as a condition", type_display(&t)));
1808                }
1809                let (yes, no) = guards(test);
1810                let a = self.within(yes, then, scope)?;
1811                let b = self.within(no, otherwise, scope)?;
1812                Ok(if a == b {
1813                    a
1814                } else if is(&a, "null") {
1815                    optional_type(b)
1816                } else if is(&b, "null") {
1817                    optional_type(a)
1818                } else if required_type(&a) == required_type(&b) {
1819                    optional_type(required_type(&a))
1820                } else if is_optional(&a) || is_optional(&b) {
1821                    optional_type(ty("dyn"))
1822                } else {
1823                    ty("dyn")
1824                })
1825            }
1826            Node::Bin(op, a, b, _) => self.binary(op, a, b, scope),
1827            Node::Call(name, args, _) => self.call(name, args, scope),
1828            Node::Macro(f, of, var, body, _) => {
1829                let of = self.need(of, scope)?;
1830                if !of.is_array() && !loose(&of) {
1831                    return Err(format!("{f} needs a list, got {}", type_display(&of)));
1832                }
1833                let item = of.as_array().and_then(|a| a.first().cloned()).unwrap_or_else(|| ty("dyn"));
1834                let inner = TypeScope { name: var, ty: item, parent: scope };
1835                let body_t = self.tc(body, Some(&inner))?;
1836                if f == "map" {
1837                    return Ok(Value::Array(vec![body_t]));
1838                }
1839                if is_optional(&body_t) {
1840                    return missing(body);
1841                }
1842                if !loose(&body_t) && !is(&body_t, "bool") {
1843                    return Err(format!("{f} needs a bool, got {}", type_display(&body_t)));
1844                }
1845                Ok(if f == "filter" {
1846                    if of.is_array() {
1847                        of
1848                    } else {
1849                        Value::Array(vec![ty("dyn")])
1850                    }
1851                } else {
1852                    ty("bool")
1853                })
1854            }
1855        }
1856    }
1857
1858    fn binary(&self, op: &str, a: &Node, b: &Node, scope: Option<&TypeScope>) -> R<Value> {
1859        if op == "&&" || op == "||" {
1860            let x = self.need(a, scope)?;
1861            let (yes, no) = guards(a);
1862            let y = self.within(if op == "&&" { yes } else { no }, b, scope)?;
1863            if is_optional(&y) {
1864                return missing(b);
1865            }
1866            for t in [&x, &y] {
1867                if !loose(t) && !is(t, "bool") {
1868                    return Err(format!("cannot apply {op} to {}", type_display(t)));
1869                }
1870            }
1871            return Ok(ty("bool"));
1872        }
1873        if op == "==" || op == "!=" {
1874            // The null check: either side may be missing.
1875            let x = required_type(&self.tc(a, scope)?);
1876            let y = required_type(&self.tc(b, scope)?);
1877            let (dx, dy) = (type_display(&x), type_display(&y));
1878            let comparable = loose(&x) || loose(&y) || (is_textual(&x) && is_textual(&y)) || dx == dy;
1879            if !comparable {
1880                return Err(format!("cannot compare {dx} and {dy}"));
1881            }
1882            return Ok(ty("bool"));
1883        }
1884        let x = self.need(a, scope)?;
1885        let y = self.need(b, scope)?;
1886        let (dx, dy) = (type_display(&x), type_display(&y));
1887        let numeric = |t: &Value| is(t, "number") || loose(t);
1888        match op {
1889            "<" | "<=" | ">" | ">=" => {
1890                let ok = |t: &Value| loose(t) || is(t, "number") || is_textual(t);
1891                if !ok(&x) || !ok(&y) || (is(&x, "number") && is_textual(&y)) || (is(&y, "number") && is_textual(&x)) {
1892                    return Err(format!("cannot compare {dx} and {dy}"));
1893                }
1894                Ok(ty("bool"))
1895            }
1896            "+" => {
1897                if loose(&x) && loose(&y) {
1898                    return Ok(ty("dyn"));
1899                }
1900                if numeric(&x) && numeric(&y) {
1901                    return Ok(ty("number"));
1902                }
1903                if (is_textual(&x) || loose(&x)) && (is_textual(&y) || loose(&y)) {
1904                    return Ok(ty("string"));
1905                }
1906                if (x.is_array() || loose(&x)) && (y.is_array() || loose(&y)) {
1907                    return Ok(if x.is_array() && y.is_array() && x == y { x } else { Value::Array(vec![ty("dyn")]) });
1908                }
1909                Err(format!("cannot apply + to {dx} and {dy}"))
1910            }
1911            _ => {
1912                if !(numeric(&x) && numeric(&y)) {
1913                    return Err(format!("cannot apply {op} to {dx} and {dy}"));
1914                }
1915                if (op == "/" || op == "%") && literal(b).is_some_and(|v| v.as_f64() == Some(0.0)) {
1916                    return Err(if op == "/" { "division by zero" } else { "modulo by zero" }.into());
1917                }
1918                Ok(ty("number"))
1919            }
1920        }
1921    }
1922
1923    fn call(&self, name: &str, args: &[Node], scope: Option<&TypeScope>) -> R<Value> {
1924        let d = def(name).ok_or_else(|| format!("unknown function {name}"))?;
1925        if let Some(e) = arity_error(name, &d, args.len()) {
1926            return Err(e);
1927        }
1928        // orElse takes a value that may be missing; every other argument is used.
1929        let types = args.iter().enumerate().map(|(i, a)| if name == "orElse" && i == 0 { self.tc(a, scope) } else { self.need(a, scope) }).collect::<R<Vec<_>>>()?;
1930        for (i, t) in types.iter().enumerate() {
1931            let param = d.params[i.min(d.params.len() - 1)];
1932            if !accepts(param, &required_type(t)) {
1933                return Err(format!("{name} needs {}, got {}", param_name(param), type_display(t)));
1934            }
1935        }
1936        if name == "orElse" {
1937            let (x, y) = (required_type(&types[0]), &types[1]);
1938            if !loose(&x) && !loose(y) && !(is_textual(&x) && is_textual(y)) && type_display(&x) != type_display(y) {
1939                return Err(format!("orElse needs a {} default, got {}", type_display(&x), type_display(y)));
1940            }
1941        }
1942        let lit = |i: usize| args.get(i).and_then(literal);
1943        let unit_at = match name {
1944            "dateAdd" | "dateDiff" => Some(2),
1945            "startOf" | "endOf" => Some(1),
1946            _ => None,
1947        };
1948        if let Some(Value::String(unit)) = unit_at.and_then(lit) {
1949            let units = if unit_at == Some(2) { DATE_UNITS } else { CALENDAR_UNITS };
1950            if !units.contains(&unit.as_str()) {
1951                return Err(format!("unknown unit {unit}"));
1952            }
1953            if name == "dateAdd" && is(&types[0], "date") && unit_ms(unit).is_some() {
1954                return Err(format!("unit {unit} needs a timestamp"));
1955            }
1956        }
1957        if name == "dateAdd" {
1958            if let Some(n) = lit(1).and_then(Value::as_f64) {
1959                if n.fract() != 0.0 {
1960                    return Err(format!("dateAdd needs a whole number, got {}", num_text(n)));
1961                }
1962            }
1963        }
1964        if name == "round" && args.len() > 1 {
1965            if let Some(d) = lit(1).and_then(Value::as_f64) {
1966                if d.fract() != 0.0 || !(0.0..=10.0).contains(&d) {
1967                    return Err("round needs 0 to 10 digits".into());
1968                }
1969            }
1970        }
1971        if name == "sortBy" {
1972            if let Some(Value::String(o)) = lit(2) {
1973                if o != "asc" && o != "desc" {
1974                    return Err("sortBy order is asc or desc".into());
1975                }
1976            }
1977        }
1978        if name == "split" && matches!(lit(1), Some(Value::String(s)) if s.is_empty()) {
1979            return Err("split needs a separator".into());
1980        }
1981        if name == "div" && lit(1).and_then(Value::as_f64) == Some(0.0) {
1982            return Err("division by zero".into());
1983        }
1984        if name == "mod" && lit(1).and_then(Value::as_f64) == Some(0.0) {
1985            return Err("modulo by zero".into());
1986        }
1987        if name == "formatDuration" {
1988            if let Some(Value::String(f)) = lit(1) {
1989                if !DURATION_FORMATS.contains(&f.as_str()) {
1990                    return Err(format!("unknown duration format {f}"));
1991                }
1992            }
1993        }
1994        Ok(match name {
1995            "dateAdd" | "startOf" | "endOf" => {
1996                if is(&types[0], "date") || is(&types[0], "timestamp") {
1997                    types[0].clone()
1998                } else {
1999                    ty("dyn")
2000                }
2001            }
2002            "today" => ty("date"),
2003            "now" | "dateTime" => ty("timestamp"),
2004            "formatDuration" | "string" | "trim" | "lower" | "upper" | "join" => ty("string"),
2005            "split" => Value::Array(vec![ty("string")]),
2006            "avgOf" | "minOf" | "maxOf" => optional_type(ty("number")),
2007            "sortBy" => {
2008                if types[0].is_array() {
2009                    types[0].clone()
2010                } else {
2011                    Value::Array(vec![ty("dyn")])
2012                }
2013            }
2014            "groupOf" => {
2015                let rows = if types[0].is_array() { types[0].clone() } else { Value::Array(vec![ty("dyn")]) };
2016                Value::Array(vec![serde_json::json!({"key": optional_type(ty("dyn")), "rows": rows})])
2017            }
2018            "orElse" => {
2019                let x = required_type(&types[0]);
2020                if is(&x, "null") {
2021                    required_type(&types[1])
2022                } else {
2023                    x
2024                }
2025            }
2026            _ => ty("number"),
2027        })
2028    }
2029}
2030
2031/// Type-check an expression against the names it may read (a kind's
2032/// fields, a binding's value): the type, or the error (the parse error, or
2033/// the first type error). Types are spelled as JSON (`"number"`, `[T]`, a
2034/// row object, `{"?": T}` for a value that may be missing).
2035pub fn check_expression(source: &str, types: &Map<String, Value>) -> Result<Value, String> {
2036    let ast = parse_expression(source)?;
2037    Checker { types, present: std::cell::RefCell::new(Vec::new()) }.tc(&ast, None)
2038}