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"];
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    let day = (ms / DAY_MS).floor();
238    let tod = (ms - day * DAY_MS) as i64;
239    let d = day_from_number(day as i64);
240    if d.y < 0 || d.y > 9999 {
241        return Err("date out of range".into());
242    }
243    Ok(format!("{}T{:02}:{:02}:{:02}.{:03}Z", format_day(d), tod / 3_600_000, (tod / 60_000) % 60, (tod / 1000) % 60, tod % 1000))
244}
245
246fn day_text(day: i64) -> R<String> {
247    let d = day_from_number(day);
248    if d.y < 0 || d.y > 9999 {
249        return Err("date out of range".into());
250    }
251    Ok(format_day(d))
252}
253
254/// The wall clock (as if UTC) of an instant in the zone.
255fn to_local(env: &Env, ms: f64) -> f64 {
256    ms + env.off(ms) * 60_000.0
257}
258
259/// The instant of a wall-clock time in the zone: a time in a DST gap moves
260/// forward by the gap; a repeated time takes its later (standard) instant.
261fn from_local(env: &Env, wall: f64) -> f64 {
262    let first = env.off(wall);
263    let utc = wall - first * 60_000.0;
264    let second = env.off(utc);
265    if second == first {
266        utc
267    } else {
268        wall - second * 60_000.0
269    }
270}
271
272fn temporal(f: &str, v: &Value) -> R<Temporal> {
273    let Value::String(s) = v else { return Err(format!("{f} needs a date, got {}", type_name(v))) };
274    if s.chars().count() == 10 {
275        if let Some(d) = parse_day(s) {
276            return Ok(Temporal::Date(day_number(d.y, d.m, d.d)));
277        }
278    }
279    parse_timestamp(s).map(Temporal::Ts).ok_or_else(|| format!("not a date: {s}"))
280}
281
282fn floor_day(wall: f64) -> i64 {
283    (wall / DAY_MS).floor() as i64
284}
285
286fn wall_of(env: &Env, t: Temporal) -> f64 {
287    match t {
288        Temporal::Date(day) => day as f64 * DAY_MS,
289        Temporal::Ts(ms) => to_local(env, ms),
290    }
291}
292
293fn unit_of<'a>(f: &str, v: &'a Value, units: &[&str]) -> R<&'a str> {
294    let Value::String(s) = v else { return Err(format!("{f} needs a string, got {}", type_name(v))) };
295    if !units.contains(&s.as_str()) {
296        return Err(format!("unknown unit {s}"));
297    }
298    Ok(s)
299}
300
301fn add_months(day: i64, months: i64) -> i64 {
302    let c = day_from_number(day);
303    let total = c.y * 12 + (c.m - 1) + months;
304    let y = total.div_euclid(12);
305    let m = total - y * 12 + 1;
306    day_number(y, m, c.d.min(days_in_month(y, m)))
307}
308
309fn add_calendar(day: i64, n: i64, unit: &str) -> i64 {
310    match unit {
311        "day" => day + n,
312        "week" => day + 7 * n,
313        "month" => add_months(day, n),
314        "quarter" => add_months(day, n * 3),
315        _ => add_months(day, n * 12),
316    }
317}
318
319fn start_day(day: i64, unit: &str) -> i64 {
320    let c = day_from_number(day);
321    match unit {
322        "day" => day,
323        "week" => {
324            let wd = match weekday_of(c.y, c.m, c.d) {
325                0 => 7,
326                n => n as i64,
327            };
328            day - (wd - 1)
329        }
330        "month" => day_number(c.y, c.m, 1),
331        "quarter" => day_number(c.y, (c.m - 1) / 3 * 3 + 1, 1),
332        _ => day_number(c.y, 1, 1),
333    }
334}
335
336fn number(f: &str, v: &Value) -> R<f64> {
337    match v {
338        Value::Number(n) => Ok(n.as_f64().unwrap_or(0.0)),
339        _ => Err(format!("{f} needs a number, got {}", type_name(v))),
340    }
341}
342
343fn whole_number(f: &str, v: &Value) -> R<f64> {
344    let n = number(f, v)?;
345    if n.fract() != 0.0 {
346        return Err(format!("{f} needs a whole number, got {}", num_text(n)));
347    }
348    Ok(n)
349}
350
351fn text<'a>(f: &str, v: &'a Value) -> R<&'a str> {
352    match v {
353        Value::String(s) => Ok(s),
354        _ => Err(format!("{f} needs a string, got {}", type_name(v))),
355    }
356}
357
358fn date_add(args: &[Value], env: &Env) -> R<Value> {
359    let t = temporal("dateAdd", &args[0])?;
360    let n = whole_number("dateAdd", &args[1])?;
361    let unit = unit_of("dateAdd", &args[2], DATE_UNITS)?;
362    let out = match t {
363        Temporal::Date(day) => {
364            if unit_ms(unit).is_some() {
365                return Err(format!("unit {unit} needs a timestamp"));
366            }
367            day_text(add_calendar(day, n as i64, unit))?
368        }
369        Temporal::Ts(ms) => match unit_ms(unit) {
370            Some(u) => format_timestamp(num(ms + n * u)?)?,
371            None => {
372                let wall = to_local(env, ms);
373                let day = floor_day(wall);
374                let tod = wall - day as f64 * DAY_MS;
375                format_timestamp(from_local(env, add_calendar(day, n as i64, unit) as f64 * DAY_MS + tod))?
376            }
377        },
378    };
379    Ok(Value::String(out))
380}
381
382fn date_diff(args: &[Value], env: &Env) -> R<Value> {
383    let a = temporal("dateDiff", &args[0])?;
384    let b = temporal("dateDiff", &args[1])?;
385    let unit = unit_of("dateDiff", &args[2], DATE_UNITS)?;
386    if let Some(u) = unit_ms(unit) {
387        let instant = |t: Temporal| match t {
388            Temporal::Date(day) => from_local(env, day as f64 * DAY_MS),
389            Temporal::Ts(ms) => ms,
390        };
391        return Ok(json::number(num(((instant(b) - instant(a)) / u).trunc())?));
392    }
393    let (wa, wb) = (wall_of(env, a), wall_of(env, b));
394    if unit == "day" || unit == "week" {
395        let span = if unit == "day" { DAY_MS } else { 7.0 * DAY_MS };
396        return Ok(json::number(num(((wb - wa) / span).trunc())?));
397    }
398    let (da, db) = (floor_day(wa), floor_day(wb));
399    let (ca, cb) = (day_from_number(da), day_from_number(db));
400    let mut months = (cb.y - ca.y) * 12 + (cb.m - ca.m);
401    let rest_a = ca.d as f64 * DAY_MS + (wa - da as f64 * DAY_MS);
402    let rest_b = cb.d as f64 * DAY_MS + (wb - db as f64 * DAY_MS);
403    if months > 0 && rest_b < rest_a {
404        months -= 1;
405    } else if months < 0 && rest_b > rest_a {
406        months += 1;
407    }
408    let per = match unit {
409        "month" => 1.0,
410        "quarter" => 3.0,
411        _ => 12.0,
412    };
413    Ok(json::number(num((months as f64 / per).trunc())?))
414}
415
416fn start_end(f: &str, args: &[Value], env: &Env) -> R<Value> {
417    let t = temporal(f, &args[0])?;
418    let unit = unit_of(f, &args[1], CALENDAR_UNITS)?;
419    let day = match t {
420        Temporal::Date(day) => day,
421        Temporal::Ts(ms) => floor_day(to_local(env, ms)),
422    };
423    let is_date = matches!(t, Temporal::Date(_));
424    let out = if f == "startOf" {
425        let start = start_day(day, unit);
426        if is_date {
427            day_text(start)?
428        } else {
429            format_timestamp(from_local(env, start as f64 * DAY_MS))?
430        }
431    } else {
432        let next = add_calendar(start_day(day, unit), 1, unit);
433        if is_date {
434            day_text(next - 1)?
435        } else {
436            format_timestamp(from_local(env, next as f64 * DAY_MS) - 1.0)?
437        }
438    };
439    Ok(Value::String(out))
440}
441
442fn local_day(f: &str, v: &Value, env: &Env) -> R<crate::dates::CalendarDay> {
443    Ok(day_from_number(match temporal(f, v)? {
444        Temporal::Date(day) => day,
445        Temporal::Ts(ms) => floor_day(to_local(env, ms)),
446    }))
447}
448
449fn format_duration(args: &[Value]) -> R<Value> {
450    let h = number("formatDuration", &args[0])?;
451    let format = text("formatDuration", &args[1])?;
452    if !DURATION_FORMATS.contains(&format) {
453        return Err(format!("unknown duration format {format}"));
454    }
455    let total = round_half_away(h.abs() * 60.0);
456    if total > 1e15 {
457        return Err("number out of range".into());
458    }
459    let hours = (total / 60.0).floor() as i64;
460    let minutes = total as i64 - hours * 60;
461    let sign = if h < 0.0 && total > 0.0 { "-" } else { "" };
462    let hours = if format == "hh:mm" { format!("{hours:02}") } else { hours.to_string() };
463    Ok(Value::String(format!("{sign}{hours}:{minutes:02}")))
464}
465
466fn round(args: &[Value]) -> R<Value> {
467    let x = number("round", &args[0])?;
468    let digits = match args.get(1) {
469        None => 0.0,
470        Some(Value::Number(n)) => {
471            let d = n.as_f64().unwrap_or(f64::NAN);
472            if d.fract() != 0.0 || !(0.0..=10.0).contains(&d) {
473                return Err("round needs 0 to 10 digits".into());
474            }
475            d
476        }
477        Some(_) => return Err("round needs 0 to 10 digits".into()),
478    };
479    let mut p = 1.0;
480    for _ in 0..digits as usize {
481        p *= 10.0;
482    }
483    Ok(json::number(num(round_half_away(x * p) / p)?))
484}
485
486fn size(v: &Value) -> R<Value> {
487    Ok(json::number(match v {
488        Value::String(s) => s.chars().count() as f64,
489        Value::Array(a) => a.len() as f64,
490        Value::Object(m) => m.len() as f64,
491        _ => return Err(format!("size needs a list, string or map, got {}", type_name(v))),
492    }))
493}
494
495#[derive(Clone, Copy, PartialEq, Eq)]
496enum Param {
497    Number,
498    Text,
499    Temporal,
500    Sized,
501    Any,
502}
503
504struct Def {
505    params: &'static [Param],
506    min: usize,
507    variadic: bool,
508    args: &'static [&'static str],
509}
510
511use Param::{Any as A, Number as N, Sized as Z, Temporal as T, Text as S};
512
513/// THE function table (spec §4.2), positional for CEL, named (`args`) for
514/// the A2UI form. Generic only: no working days, no holidays.
515pub const FUNCTION_NAMES: &[&str] = &["mul", "div", "mod", "round", "floor", "ceil", "abs", "min", "max", "today", "now", "dateAdd", "dateDiff", "startOf", "endOf", "weekday", "isoWeek", "hours", "formatDuration", "size", "orElse"];
516
517/// The A2UI form's names in the bind table (`size` = the existing `len`;
518/// `expr` = the string form, `{expr, vars}`).
519pub const CALL_NAMES: &[&str] = &["mul", "div", "mod", "round", "floor", "ceil", "abs", "min", "max", "today", "now", "dateAdd", "dateDiff", "startOf", "endOf", "weekday", "isoWeek", "hours", "formatDuration", "orElse", "expr"];
520
521fn def(name: &str) -> Option<Def> {
522    let d = |params: &'static [Param], args: &'static [&'static str]| Def { params, min: params.len(), variadic: false, args };
523    Some(match name {
524        "mul" | "div" | "mod" => d(&[N, N], &["a", "b"]),
525        "round" => Def { params: &[N, N], min: 1, variadic: false, args: &["value", "digits"] },
526        "floor" | "ceil" | "abs" => d(&[N], &["value"]),
527        "min" | "max" => Def { params: &[N], min: 1, variadic: true, args: &["values"] },
528        "today" | "now" => d(&[], &[]),
529        "dateAdd" => d(&[T, N, S], &["date", "n", "unit"]),
530        "dateDiff" => d(&[T, T, S], &["a", "b", "unit"]),
531        "startOf" | "endOf" => d(&[T, S], &["date", "unit"]),
532        "weekday" | "isoWeek" => d(&[T], &["date"]),
533        "hours" => d(&[N], &["minutes"]),
534        "formatDuration" => d(&[N, S], &["hours", "format"]),
535        "size" => d(&[Z], &["value"]),
536        // VAPP-195: the value, or the default when it is missing (null).
537        "orElse" => d(&[A, A], &["value", "default"]),
538        _ => return None,
539    })
540}
541
542fn arity_error(name: &str, def: &Def, n: usize) -> Option<String> {
543    if def.variadic {
544        return (n < 1).then(|| format!("{name} takes at least 1 argument"));
545    }
546    let (min, max) = (def.min, def.params.len());
547    if n >= min && n <= max {
548        return None;
549    }
550    Some(if min == max { format!("{name} takes {min} argument{}", if min == 1 { "" } else { "s" }) } else { format!("{name} takes {min} or {max} arguments") })
551}
552
553fn run(name: &str, args: &[Value], env: &Env) -> R<Value> {
554    let n = |f: fn(f64) -> f64| -> R<Value> { Ok(json::number(num(f(number(name, &args[0])?))?)) };
555    match name {
556        "mul" => Ok(json::number(num(number("mul", &args[0])? * number("mul", &args[1])?)?)),
557        "div" => {
558            let (x, y) = (number("div", &args[0])?, number("div", &args[1])?);
559            if y == 0.0 {
560                return Err("division by zero".into());
561            }
562            Ok(json::number(num(x / y)?))
563        }
564        "mod" => {
565            let (x, y) = (number("mod", &args[0])?, number("mod", &args[1])?);
566            if y == 0.0 {
567                return Err("modulo by zero".into());
568            }
569            Ok(json::number(num(x % y)?))
570        }
571        "round" => round(args),
572        "floor" => n(f64::floor),
573        "ceil" => n(f64::ceil),
574        "abs" => n(f64::abs),
575        "min" | "max" => {
576            let mut out = if name == "min" { f64::INFINITY } else { f64::NEG_INFINITY };
577            for v in args {
578                let x = number(name, v)?;
579                out = if name == "min" { out.min(x) } else { out.max(x) };
580            }
581            Ok(json::number(num(out)?))
582        }
583        "today" => Ok(Value::String(day_text(floor_day(to_local(env, env.now)))?)),
584        "now" => Ok(Value::String(format_timestamp(env.now)?)),
585        "dateAdd" => date_add(args, env),
586        "dateDiff" => date_diff(args, env),
587        "startOf" | "endOf" => start_end(name, args, env),
588        "weekday" => {
589            let d = local_day("weekday", &args[0], env)?;
590            Ok(json::number(match weekday_of(d.y, d.m, d.d) {
591                0 => 7.0,
592                w => w as f64,
593            }))
594        }
595        "isoWeek" => {
596            let d = local_day("isoWeek", &args[0], env)?;
597            Ok(json::number(iso_week(d.y, d.m, d.d).1 as f64))
598        }
599        "hours" => Ok(json::number(num(number("hours", &args[0])? / 60.0)?)),
600        "formatDuration" => format_duration(args),
601        "size" => size(&args[0]),
602        "orElse" => Ok(if args[0].is_null() { args.get(1).cloned().unwrap_or(Value::Null) } else { args[0].clone() }),
603        _ => Err(format!("unknown function {name}")),
604    }
605}
606
607/// The A2UI function form `{call: name, args}` with its arguments resolved.
608/// Outer `None` = not an expression function; inner `None` = no value (an
609/// error, like every bind-time function). VAPP-195: a failed call shows its
610/// `default` (absent = no value).
611pub fn call_expression_function(name: &str, args: &Map<String, Value>, env: &ExpressionEnv) -> Option<Option<Value>> {
612    let fallback = || Some(args.get("default").cloned());
613    if name == "expr" {
614        let Some(Value::String(expr)) = args.get("expr") else { return fallback() };
615        let empty = Map::new();
616        let vars = args.get("vars").and_then(Value::as_object).unwrap_or(&empty);
617        return evaluate_expression(expr, vars, env).map_or_else(|_| fallback(), |v| Some(Some(v)));
618    }
619    if name == "size" {
620        return None;
621    }
622    let def = def(name)?;
623    let positional: Vec<Value> = if def.variadic {
624        args.get("values").and_then(Value::as_array).cloned().unwrap_or_default()
625    } else {
626        let mut list: Vec<Option<&Value>> = def.args.iter().map(|k| args.get(*k)).collect();
627        while list.len() > def.min && list.last().is_some_and(Option::is_none) {
628            list.pop();
629        }
630        list.into_iter().map(|v| v.cloned().unwrap_or(Value::Null)).collect()
631    };
632    if arity_error(name, &def, positional.len()).is_some() {
633        return fallback();
634    }
635    run(name, &positional, &Env::of(env)).map_or_else(|_| fallback(), |v| Some(Some(v)))
636}
637
638// ---------------------------------------------------------------------------
639// Parsing
640// ---------------------------------------------------------------------------
641
642/// A parsed expression (`at` = the code point it starts at).
643#[derive(Debug, Clone, PartialEq)]
644pub enum Node {
645    Lit(Value, usize),
646    Id(String, usize),
647    List(Vec<Node>, usize),
648    Field(Box<Node>, String, usize),
649    Index(Box<Node>, Box<Node>, usize),
650    Unary(char, Box<Node>, usize),
651    Bin(&'static str, Box<Node>, Box<Node>, usize),
652    Cond(Box<Node>, Box<Node>, Box<Node>, usize),
653    Call(String, Vec<Node>, usize),
654    Macro(String, Box<Node>, String, Box<Node>, usize),
655    /// VAPP-195: `has(a.b)`, CEL's presence test (the node is a `Field`).
656    Has(Box<Node>, usize),
657}
658
659#[derive(Debug, Clone, PartialEq)]
660enum Tok {
661    Num(f64),
662    Str(String),
663    Id(String),
664    Op(&'static str),
665    End,
666}
667
668#[derive(Debug, Clone)]
669struct Token {
670    t: Tok,
671    at: usize,
672    text: String,
673}
674
675const OPS2: &[&str] = &["==", "!=", "<=", ">=", "&&", "||"];
676const OPS1: &[&str] = &["+", "-", "*", "/", "%", "<", ">", "!", "(", ")", "[", "]", ",", ".", "?", ":"];
677
678fn is_digit(c: Option<&char>) -> bool {
679    c.is_some_and(char::is_ascii_digit)
680}
681fn is_id_start(c: Option<&char>) -> bool {
682    c.is_some_and(|c| c.is_ascii_alphabetic() || *c == '_')
683}
684fn is_id_part(c: Option<&char>) -> bool {
685    is_id_start(c) || is_digit(c)
686}
687
688fn tokenize(cs: &[char]) -> R<Vec<Token>> {
689    let mut out = Vec::new();
690    let mut i = 0;
691    while i < cs.len() {
692        let c = cs[i];
693        if matches!(c, ' ' | '\t' | '\n' | '\r') {
694            i += 1;
695            continue;
696        }
697        let at = i;
698        if c.is_ascii_digit() {
699            while is_digit(cs.get(i)) {
700                i += 1;
701            }
702            if cs.get(i) == Some(&'.') && is_digit(cs.get(i + 1)) {
703                i += 1;
704                while is_digit(cs.get(i)) {
705                    i += 1;
706                }
707            }
708            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)))) {
709                i += 2;
710                while is_digit(cs.get(i)) {
711                    i += 1;
712                }
713            }
714            let text: String = cs[at..i].iter().collect();
715            let v = text.parse::<f64>().unwrap_or(f64::INFINITY);
716            out.push(Token { t: Tok::Num(v), at, text });
717            continue;
718        }
719        if c == '\'' || c == '"' {
720            i += 1;
721            let mut s = String::new();
722            loop {
723                let d = cs.get(i).copied();
724                match d {
725                    None | Some('\n') => return Err(format!("at {at}: unterminated string")),
726                    Some(d) if d == c => break,
727                    Some('\\') => {
728                        let e = cs.get(i + 1).copied();
729                        let mapped = match e {
730                            Some('\\') => '\\',
731                            Some('\'') => '\'',
732                            Some('"') => '"',
733                            Some('n') => '\n',
734                            Some('t') => '\t',
735                            Some('r') => '\r',
736                            _ => return Err(format!("at {i}: unknown escape \\{}", e.map(String::from).unwrap_or_default())),
737                        };
738                        s.push(mapped);
739                        i += 2;
740                    }
741                    Some(d) => {
742                        s.push(d);
743                        i += 1;
744                    }
745                }
746            }
747            i += 1;
748            out.push(Token { t: Tok::Str(s), at, text: cs[at..i].iter().collect() });
749            continue;
750        }
751        if is_id_start(Some(&c)) {
752            while is_id_part(cs.get(i)) {
753                i += 1;
754            }
755            let text: String = cs[at..i].iter().collect();
756            out.push(Token { t: Tok::Id(text.clone()), at, text });
757            continue;
758        }
759        let two: String = cs[i..(i + 2).min(cs.len())].iter().collect();
760        if let Some(op) = OPS2.iter().find(|o| **o == two) {
761            out.push(Token { t: Tok::Op(op), at, text: two });
762            i += 2;
763            continue;
764        }
765        if let Some(op) = OPS1.iter().find(|o| o.starts_with(c)) {
766            out.push(Token { t: Tok::Op(op), at, text: c.to_string() });
767            i += 1;
768            continue;
769        }
770        return Err(format!("at {at}: unexpected {c}"));
771    }
772    out.push(Token { t: Tok::End, at: cs.len(), text: "end".into() });
773    Ok(out)
774}
775
776const MACROS: &[&str] = &["map", "filter", "all", "exists"];
777
778struct Parser {
779    tokens: Vec<Token>,
780    i: usize,
781    depth: usize,
782    in_body: usize,
783}
784
785impl Parser {
786    fn peek(&self) -> &Token {
787        &self.tokens[self.i]
788    }
789    fn is_op(&self, v: &str) -> bool {
790        matches!(&self.peek().t, Tok::Op(o) if *o == v)
791    }
792    fn next(&mut self) -> Token {
793        let t = self.tokens[self.i].clone();
794        self.i += 1;
795        t
796    }
797    fn unexpected<T>(&self) -> R<T> {
798        let t = self.peek();
799        Err(format!("at {}: unexpected {}", t.at, t.text))
800    }
801    fn expect(&mut self, v: &str) -> R<Token> {
802        if !self.is_op(v) {
803            return self.unexpected();
804        }
805        Ok(self.next())
806    }
807    fn enter(&mut self, at: usize) -> R<()> {
808        self.depth += 1;
809        if self.depth > MAX_DEPTH {
810            return Err(format!("at {at}: expression is nested too deeply"));
811        }
812        Ok(())
813    }
814
815    fn parse_all(&mut self) -> R<Node> {
816        let node = self.expr()?;
817        if self.peek().t != Tok::End {
818            return self.unexpected();
819        }
820        Ok(node)
821    }
822
823    fn expr(&mut self) -> R<Node> {
824        crate::deep(|| self.expr_inner())
825    }
826
827    fn expr_inner(&mut self) -> R<Node> {
828        let at = self.peek().at;
829        self.enter(at)?;
830        let test = self.or()?;
831        let mut out = test;
832        if self.is_op("?") {
833            self.next();
834            let then = self.expr()?;
835            self.expect(":")?;
836            let otherwise = self.expr()?;
837            out = Node::Cond(Box::new(out), Box::new(then), Box::new(otherwise), at);
838        }
839        self.depth -= 1;
840        Ok(out)
841    }
842
843    fn binary(&mut self, ops: &[&'static str], operand: fn(&mut Parser) -> R<Node>) -> R<Node> {
844        let mut a = operand(self)?;
845        loop {
846            let t = self.peek().clone();
847            let Tok::Op(op) = t.t else { return Ok(a) };
848            let Some(op) = ops.iter().find(|o| **o == op) else { return Ok(a) };
849            self.next();
850            let b = operand(self)?;
851            a = Node::Bin(op, Box::new(a), Box::new(b), t.at);
852        }
853    }
854    fn or(&mut self) -> R<Node> {
855        self.binary(&["||"], Parser::and)
856    }
857    fn and(&mut self) -> R<Node> {
858        self.binary(&["&&"], Parser::rel)
859    }
860    fn rel(&mut self) -> R<Node> {
861        self.binary(&["==", "!=", "<", "<=", ">", ">="], Parser::add)
862    }
863    fn add(&mut self) -> R<Node> {
864        self.binary(&["+", "-"], Parser::mul)
865    }
866    fn mul(&mut self) -> R<Node> {
867        self.binary(&["*", "/", "%"], Parser::unary)
868    }
869
870    fn unary(&mut self) -> R<Node> {
871        let t = self.peek().clone();
872        if let Tok::Op(op @ ("!" | "-")) = t.t {
873            self.next();
874            self.enter(t.at)?;
875            let arg = crate::deep(|| self.unary())?;
876            self.depth -= 1;
877            return Ok(Node::Unary(op.chars().next().unwrap_or('!'), Box::new(arg), t.at));
878        }
879        self.postfix()
880    }
881
882    fn postfix(&mut self) -> R<Node> {
883        let mut node = self.primary()?;
884        loop {
885            if self.is_op(".") {
886                self.next();
887                let name = self.peek().clone();
888                let Tok::Id(field) = name.t.clone() else { return self.unexpected() };
889                self.next();
890                if self.is_op("(") {
891                    if !MACROS.contains(&field.as_str()) {
892                        if let Some(dotted) = dotted_name(&node) {
893                            let full = format!("{dotted}.{field}");
894                            let effect = function_effect(&full);
895                            if effect != FunctionEffect::Pure {
896                                return Err(format!("at {}: {}: an expression is pure, use an action step", root_at(&node), effect_words(&full, effect)));
897                            }
898                        }
899                        return Err(format!("at {}: unknown method {field}", name.at));
900                    }
901                    if self.in_body > 0 {
902                        return Err(format!("at {}: nested comprehensions are not allowed", name.at));
903                    }
904                    self.next();
905                    let v = self.peek().clone();
906                    let comma = matches!(self.tokens.get(self.i + 1).map(|t| &t.t), Some(Tok::Op(",")));
907                    let Tok::Id(var) = v.t.clone() else { return Err(format!("at {}: {field} needs a name first", v.at)) };
908                    if !comma {
909                        return Err(format!("at {}: {field} needs a name first", v.at));
910                    }
911                    self.next();
912                    self.next();
913                    self.in_body += 1;
914                    let body = self.expr()?;
915                    self.in_body -= 1;
916                    self.expect(")")?;
917                    node = Node::Macro(field, Box::new(node), var, Box::new(body), name.at);
918                } else {
919                    node = Node::Field(Box::new(node), field, name.at);
920                }
921                continue;
922            }
923            if self.is_op("[") {
924                let at = self.next().at;
925                let index = self.expr()?;
926                self.expect("]")?;
927                node = Node::Index(Box::new(node), Box::new(index), at);
928                continue;
929            }
930            return Ok(node);
931        }
932    }
933
934    fn list(&mut self, close: &str) -> R<Vec<Node>> {
935        let mut items = Vec::new();
936        if self.is_op(close) {
937            self.next();
938            return Ok(items);
939        }
940        loop {
941            items.push(self.expr()?);
942            if self.is_op(",") {
943                self.next();
944                if self.is_op(close) && close == "]" {
945                    self.next();
946                    return Ok(items);
947                }
948                continue;
949            }
950            self.expect(close)?;
951            return Ok(items);
952        }
953    }
954
955    fn primary(&mut self) -> R<Node> {
956        let t = self.peek().clone();
957        match t.t {
958            Tok::Num(v) => {
959                self.next();
960                if !v.is_finite() {
961                    return Err(format!("at {}: number out of range", t.at));
962                }
963                Ok(Node::Lit(json::number(v), t.at))
964            }
965            Tok::Str(s) => {
966                self.next();
967                Ok(Node::Lit(Value::String(s), t.at))
968            }
969            Tok::Id(name) => {
970                self.next();
971                match name.as_str() {
972                    "true" | "false" => return Ok(Node::Lit(Value::Bool(name == "true"), t.at)),
973                    "null" => return Ok(Node::Lit(Value::Null, t.at)),
974                    _ => {}
975                }
976                // VAPP-195: CEL's presence test, `has(a.b)`.
977                if name == "has" && self.is_op("(") {
978                    self.next();
979                    let arg = self.expr()?;
980                    self.expect(")")?;
981                    if !matches!(arg, Node::Field(..)) {
982                        return Err(format!("at {}: has needs a field: has(a.b)", t.at));
983                    }
984                    return Ok(Node::Has(Box::new(arg), t.at));
985                }
986                if self.is_op("(") {
987                    if def(&name).is_none() {
988                        let effect = function_effect(&name);
989                        if effect != FunctionEffect::Pure {
990                            return Err(format!("at {}: {}: an expression is pure, use an action step", t.at, effect_words(&name, effect)));
991                        }
992                        return Err(format!("at {}: unknown function {name}", t.at));
993                    }
994                    self.next();
995                    let args = self.list(")")?;
996                    return Ok(Node::Call(name, args, t.at));
997                }
998                Ok(Node::Id(name, t.at))
999            }
1000            Tok::Op("(") => {
1001                self.next();
1002                let inner = self.expr()?;
1003                self.expect(")")?;
1004                Ok(inner)
1005            }
1006            Tok::Op("[") => {
1007                self.next();
1008                Ok(Node::List(self.list("]")?, t.at))
1009            }
1010            _ => self.unexpected(),
1011        }
1012    }
1013}
1014
1015/// `a.b.c` as a name (a chain of plain names), else `None`.
1016fn dotted_name(node: &Node) -> Option<String> {
1017    match node {
1018        Node::Id(name, _) => Some(name.clone()),
1019        Node::Field(of, name, _) => dotted_name(of).map(|of| format!("{of}.{name}")),
1020        _ => None,
1021    }
1022}
1023
1024/// VAPP-195: a value's path as written (`a.b[0].c`, `m['k']`): names,
1025/// fields and literal indexes; `None` for anything else.
1026pub fn path_text(node: &Node) -> Option<String> {
1027    match node {
1028        Node::Id(name, _) => Some(name.clone()),
1029        Node::Field(of, name, _) => path_text(of).map(|of| format!("{of}.{name}")),
1030        Node::Index(of, index, _) => {
1031            let of = path_text(of)?;
1032            match literal(index) {
1033                Some(Value::Number(n)) => Some(format!("{of}[{}]", num_text(n.as_f64().unwrap_or(0.0)))),
1034                Some(Value::String(k)) => Some(format!("{of}['{k}']")),
1035                _ => None,
1036            }
1037        }
1038        _ => None,
1039    }
1040}
1041
1042/// The code point a node starts at (its own `at`).
1043fn node_at(node: &Node) -> usize {
1044    match node {
1045        Node::Lit(_, at) | Node::Id(_, at) | Node::List(_, 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,
1046    }
1047}
1048
1049fn root_at(node: &Node) -> usize {
1050    match node {
1051        Node::Field(of, _, _) => root_at(of),
1052        _ => node_at(node),
1053    }
1054}
1055
1056/// Parse once (at package validation, or before evaluating): the tree, or
1057/// the first syntax / cost error (`at <code point>: <reason>`).
1058pub fn parse_expression(source: &str) -> Result<Node, String> {
1059    let cs: Vec<char> = source.chars().collect();
1060    if cs.len() > MAX_LENGTH {
1061        return Err(format!("expression is longer than {MAX_LENGTH} characters"));
1062    }
1063    Parser { tokens: tokenize(&cs)?, i: 0, depth: 0, in_body: 0 }.parse_all()
1064}
1065
1066// ---------------------------------------------------------------------------
1067// Evaluation
1068// ---------------------------------------------------------------------------
1069
1070struct Scope<'a> {
1071    name: &'a str,
1072    value: &'a Value,
1073    parent: Option<&'a Scope<'a>>,
1074}
1075
1076struct Evaluator<'v> {
1077    vars: &'v Map<String, Value>,
1078    env: Env<'v>,
1079    steps: usize,
1080    max_steps: usize,
1081}
1082
1083impl Evaluator<'_> {
1084    fn step(&mut self) -> R<()> {
1085        self.steps += 1;
1086        if self.steps > self.max_steps {
1087            return Err("step limit exceeded".into());
1088        }
1089        Ok(())
1090    }
1091
1092    fn ev(&mut self, node: &Node, scope: Option<&Scope>) -> R<Value> {
1093        crate::deep(|| self.ev_inner(node, scope))
1094    }
1095
1096    fn ev_inner(&mut self, node: &Node, scope: Option<&Scope>) -> R<Value> {
1097        self.step()?;
1098        match node {
1099            Node::Lit(v, _) => Ok(v.clone()),
1100            Node::List(items, _) => items.iter().map(|i| self.ev(i, scope)).collect::<R<Vec<_>>>().map(Value::Array),
1101            Node::Id(name, _) => {
1102                let mut s = scope;
1103                while let Some(sc) = s {
1104                    if sc.name == name {
1105                        return Ok(sc.value.clone());
1106                    }
1107                    s = sc.parent;
1108                }
1109                self.vars.get(name).cloned().ok_or_else(|| format!("unknown name {name}"))
1110            }
1111            Node::Field(of, name, _) => match self.ev(of, scope)? {
1112                Value::Null => Ok(Value::Null),
1113                Value::Object(m) => Ok(m.get(name).cloned().unwrap_or(Value::Null)),
1114                other => Err(format!("no field {name} on {}", type_name(&other))),
1115            },
1116            Node::Has(field, _) => {
1117                let Node::Field(receiver, name, _) = field.as_ref() else { return Err("has needs a field: has(a.b)".into()) };
1118                match self.ev(receiver, scope)? {
1119                    Value::Null => Ok(Value::Bool(false)),
1120                    Value::Object(m) => Ok(Value::Bool(m.get(name).is_some_and(|v| !v.is_null()))),
1121                    other => Err(format!("no field {name} on {}", type_name(&other))),
1122                }
1123            }
1124            Node::Index(of, index, _) => {
1125                let of = self.ev(of, scope)?;
1126                let index = self.ev(index, scope)?;
1127                match of {
1128                    Value::Array(list) => {
1129                        let Value::Number(n) = &index else { return Err(format!("a list index is a number, got {}", type_name(&index))) };
1130                        let n = n.as_f64().unwrap_or(0.0);
1131                        if n.fract() != 0.0 {
1132                            return Err(format!("a list index is a whole number, got {}", num_text(n)));
1133                        }
1134                        if n < 0.0 || n >= list.len() as f64 {
1135                            return Err(format!("index {} out of range", num_text(n)));
1136                        }
1137                        Ok(list[n as usize].clone())
1138                    }
1139                    Value::Object(m) => {
1140                        let Value::String(k) = &index else { return Err(format!("a map key is a string, got {}", type_name(&index))) };
1141                        Ok(m.get(k).cloned().unwrap_or(Value::Null))
1142                    }
1143                    Value::Null => Ok(Value::Null),
1144                    other => Err(format!("cannot index {}", type_name(&other))),
1145                }
1146            }
1147            Node::Unary(op, arg, _) => {
1148                let v = self.ev(arg, scope)?;
1149                if *op == '-' {
1150                    let Value::Number(n) = &v else { return Err(format!("cannot apply - to {}", type_name(&v))) };
1151                    return Ok(json::number(num(-n.as_f64().unwrap_or(0.0))?));
1152                }
1153                let Value::Bool(b) = v else { return Err(format!("cannot apply ! to {}", type_name(&v))) };
1154                Ok(Value::Bool(!b))
1155            }
1156            Node::Cond(test, then, otherwise, _) => {
1157                let t = self.ev(test, scope)?;
1158                let Value::Bool(t) = t else { return Err(format!("cannot use {} as a condition", type_name(&t))) };
1159                self.ev(if t { then } else { otherwise }, scope)
1160            }
1161            Node::Bin(op, a, b, _) => self.binary(op, a, b, scope),
1162            Node::Call(name, args, _) => {
1163                let args = args.iter().map(|a| self.ev(a, scope)).collect::<R<Vec<_>>>()?;
1164                let d = def(name).ok_or_else(|| format!("unknown function {name}"))?;
1165                if let Some(e) = arity_error(name, &d, args.len()) {
1166                    return Err(e);
1167                }
1168                run(name, &args, &self.env)
1169            }
1170            Node::Macro(f, of, var, body, _) => {
1171                let of = self.ev(of, scope)?;
1172                let Value::Array(list) = of else { return Err(format!("{f} needs a list, got {}", type_name(&of))) };
1173                let mut out = Vec::new();
1174                for item in &list {
1175                    self.step()?;
1176                    let inner = Scope { name: var, value: item, parent: scope };
1177                    let v = self.ev(body, Some(&inner))?;
1178                    if f == "map" {
1179                        out.push(v);
1180                        continue;
1181                    }
1182                    let Value::Bool(b) = v else { return Err(format!("{f} needs a bool, got {}", type_name(&v))) };
1183                    match f.as_str() {
1184                        "filter" if b => out.push(item.clone()),
1185                        "all" if !b => return Ok(Value::Bool(false)),
1186                        "exists" if b => return Ok(Value::Bool(true)),
1187                        _ => {}
1188                    }
1189                }
1190                Ok(match f.as_str() {
1191                    "all" => Value::Bool(true),
1192                    "exists" => Value::Bool(false),
1193                    _ => Value::Array(out),
1194                })
1195            }
1196        }
1197    }
1198
1199    fn binary(&mut self, op: &str, a: &Node, b: &Node, scope: Option<&Scope>) -> R<Value> {
1200        if op == "&&" || op == "||" {
1201            let x = self.ev(a, scope)?;
1202            let Value::Bool(x) = x else { return Err(format!("cannot apply {op} to {}", type_name(&x))) };
1203            if (op == "&&" && !x) || (op == "||" && x) {
1204                return Ok(Value::Bool(x));
1205            }
1206            let y = self.ev(b, scope)?;
1207            let Value::Bool(_) = y else { return Err(format!("cannot apply {op} to {}", type_name(&y))) };
1208            return Ok(y);
1209        }
1210        let x = self.ev(a, scope)?;
1211        let y = self.ev(b, scope)?;
1212        match op {
1213            "==" => Ok(Value::Bool(deep_equal(&x, &y))),
1214            "!=" => Ok(Value::Bool(!deep_equal(&x, &y))),
1215            "<" | "<=" | ">" | ">=" => {
1216                let c = match (&x, &y) {
1217                    (Value::Number(p), Value::Number(q)) => p.as_f64().partial_cmp(&q.as_f64()).unwrap_or(std::cmp::Ordering::Equal),
1218                    (Value::String(p), Value::String(q)) => utf16_cmp(p, q),
1219                    _ => return Err(format!("cannot compare {} and {}", type_name(&x), type_name(&y))),
1220                };
1221                use std::cmp::Ordering::*;
1222                Ok(Value::Bool(match op {
1223                    "<" => c == Less,
1224                    "<=" => c != Greater,
1225                    ">" => c == Greater,
1226                    _ => c != Less,
1227                }))
1228            }
1229            "+" => match (x, y) {
1230                (Value::Number(p), Value::Number(q)) => Ok(json::number(num(p.as_f64().unwrap_or(0.0) + q.as_f64().unwrap_or(0.0))?)),
1231                (Value::String(p), Value::String(q)) => Ok(Value::String(p + &q)),
1232                (Value::Array(mut p), Value::Array(q)) => {
1233                    p.extend(q);
1234                    Ok(Value::Array(p))
1235                }
1236                (x, y) => Err(format!("cannot apply + to {} and {}", type_name(&x), type_name(&y))),
1237            },
1238            _ => {
1239                let (Value::Number(p), Value::Number(q)) = (&x, &y) else { return Err(format!("cannot apply {op} to {} and {}", type_name(&x), type_name(&y))) };
1240                let (p, q) = (p.as_f64().unwrap_or(0.0), q.as_f64().unwrap_or(0.0));
1241                match op {
1242                    "-" => Ok(json::number(num(p - q)?)),
1243                    "*" => Ok(json::number(num(p * q)?)),
1244                    _ => {
1245                        if q == 0.0 {
1246                            return Err(if op == "/" { "division by zero" } else { "modulo by zero" }.into());
1247                        }
1248                        Ok(json::number(num(if op == "/" { p / q } else { p % q })?))
1249                    }
1250                }
1251            }
1252        }
1253    }
1254}
1255
1256/// Evaluate an expression over `vars`: the value, or the error (a syntax,
1257/// type, range or cost error).
1258pub fn evaluate_expression(source: &str, vars: &Map<String, Value>, env: &ExpressionEnv) -> Result<Value, String> {
1259    let ast = parse_expression(source)?;
1260    evaluate_node(&ast, vars, env)
1261}
1262
1263/// Evaluate a parsed tree (see [`parse_expression`]).
1264pub fn evaluate_node<'a>(ast: &Node, vars: &'a Map<String, Value>, env: &ExpressionEnv<'a>) -> Result<Value, String> {
1265    let mut e = Evaluator { vars, env: Env::of(env), steps: 0, max_steps: env.max_steps.unwrap_or(MAX_STEPS) };
1266    e.ev(ast, None)
1267}
1268
1269/// VAPP-194: evaluate a parsed tree and count its steps (an action spends
1270/// one budget over all of its expressions): the result and the steps used.
1271pub fn evaluate_counted<'a>(ast: &Node, vars: &'a Map<String, Value>, env: &ExpressionEnv<'a>) -> (Result<Value, String>, usize) {
1272    let mut e = Evaluator { vars, env: Env::of(env), steps: 0, max_steps: env.max_steps.unwrap_or(MAX_STEPS) };
1273    let result = e.ev(ast, None);
1274    (result, e.steps)
1275}
1276
1277// ---------------------------------------------------------------------------
1278// Type checking (package validation)
1279// ---------------------------------------------------------------------------
1280
1281/// VAPP-195 (E2): a value that may be missing is `T?`, spelled `{"?": T}`
1282/// (a row never has a field named `?`).
1283pub fn is_optional(t: &Value) -> bool {
1284    matches!(t, Value::Object(m) if m.len() == 1 && m.contains_key("?"))
1285}
1286
1287/// `T?` (null and an optional stay as they are).
1288pub fn optional_type(t: Value) -> Value {
1289    if is_optional(&t) || is(&t, "null") {
1290        return t;
1291    }
1292    let mut m = Map::new();
1293    m.insert("?".into(), t);
1294    Value::Object(m)
1295}
1296
1297/// `T` of a `T?`.
1298pub fn required_type(t: &Value) -> Value {
1299    match t {
1300        Value::Object(m) if is_optional(t) => m["?"].clone(),
1301        _ => t.clone(),
1302    }
1303}
1304
1305/// The name a type prints as in an error: a list `list`, a row `map`, an
1306/// optional `T?`.
1307pub fn type_display(t: &Value) -> String {
1308    match t {
1309        Value::Object(m) if is_optional(t) => format!("{}?", type_display(&m["?"])),
1310        Value::String(s) => s.clone(),
1311        Value::Array(_) => "list".into(),
1312        _ => "map".into(),
1313    }
1314}
1315
1316fn is_row(t: &Value) -> bool {
1317    t.is_object() && !is_optional(t)
1318}
1319fn is_textual(t: &Value) -> bool {
1320    matches!(t.as_str(), Some("string" | "date" | "timestamp"))
1321}
1322fn loose(t: &Value) -> bool {
1323    matches!(t.as_str(), Some("dyn" | "null"))
1324}
1325fn is(t: &Value, name: &str) -> bool {
1326    t.as_str() == Some(name)
1327}
1328fn ty(name: &str) -> Value {
1329    Value::String(name.into())
1330}
1331
1332fn accepts(param: Param, t: &Value) -> bool {
1333    if param == Param::Any || loose(t) {
1334        return true;
1335    }
1336    match param {
1337        Param::Number => is(t, "number"),
1338        Param::Text | Param::Temporal => is_textual(t),
1339        Param::Sized => is(t, "string") || is(t, "map") || t.is_array() || is_row(t),
1340        Param::Any => true,
1341    }
1342}
1343
1344fn param_name(p: Param) -> &'static str {
1345    match p {
1346        Param::Number => "a number",
1347        Param::Text => "a string",
1348        Param::Temporal => "a date",
1349        Param::Sized => "a list, string or map",
1350        Param::Any => "a value",
1351    }
1352}
1353
1354struct TypeScope<'a> {
1355    name: &'a str,
1356    ty: Value,
1357    parent: Option<&'a TypeScope<'a>>,
1358}
1359
1360fn literal(node: &Node) -> Option<&Value> {
1361    match node {
1362        Node::Lit(v, _) => Some(v),
1363        _ => None,
1364    }
1365}
1366
1367/// The paths a test proves present when it is true (`yes`) / when it is
1368/// false (`no`): `has(p)`, `p != null`, `p == null`, through `&&`, `||`
1369/// and `!`.
1370fn guards(node: &Node) -> (Vec<String>, Vec<String>) {
1371    match node {
1372        Node::Has(field, _) => (path_text(field).into_iter().collect(), Vec::new()),
1373        Node::Bin(op @ ("==" | "!="), a, b, _) => {
1374            let side = if matches!(a.as_ref(), Node::Lit(Value::Null, _)) {
1375                Some(b)
1376            } else if matches!(b.as_ref(), Node::Lit(Value::Null, _)) {
1377                Some(a)
1378            } else {
1379                None
1380            };
1381            match side.and_then(|s| path_text(s)) {
1382                None => (Vec::new(), Vec::new()),
1383                Some(p) if *op == "!=" => (vec![p], Vec::new()),
1384                Some(p) => (Vec::new(), vec![p]),
1385            }
1386        }
1387        Node::Bin("&&", a, b, _) => {
1388            let (mut yes, _) = guards(a);
1389            yes.extend(guards(b).0);
1390            (yes, Vec::new())
1391        }
1392        Node::Bin("||", a, b, _) => {
1393            let (_, mut no) = guards(a);
1394            no.extend(guards(b).1);
1395            (Vec::new(), no)
1396        }
1397        Node::Unary('!', arg, _) => {
1398            let (yes, no) = guards(arg);
1399            (no, yes)
1400        }
1401        _ => (Vec::new(), Vec::new()),
1402    }
1403}
1404
1405/// An unguarded use of a value that may be missing.
1406fn missing<T>(node: &Node) -> R<T> {
1407    let p = path_text(node).unwrap_or_else(|| format!("the value at {}", node_at(node)));
1408    Err(format!("{p} may be missing: guard it with has(), a null check or orElse()"))
1409}
1410
1411struct Checker<'t> {
1412    types: &'t Map<String, Value>,
1413    /// The paths a guard proved present here (`has(r.f) ? r.f + 1 : 0`).
1414    present: std::cell::RefCell<Vec<String>>,
1415}
1416
1417impl Checker<'_> {
1418    /// Type `node` with `paths` proved present.
1419    fn within(&self, paths: Vec<String>, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1420        let before = self.present.borrow().clone();
1421        self.present.borrow_mut().extend(paths);
1422        let out = self.tc(node, scope);
1423        *self.present.borrow_mut() = before;
1424        out
1425    }
1426
1427    /// The type of a use that must not be missing.
1428    fn need(&self, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1429        let t = self.tc(node, scope)?;
1430        if is_optional(&t) {
1431            return missing(node);
1432        }
1433        Ok(t)
1434    }
1435
1436    fn tc(&self, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1437        let t = crate::deep(|| self.tc_inner(node, scope))?;
1438        let p = if matches!(node, Node::Id(..) | Node::Field(..) | Node::Index(..)) { path_text(node) } else { None };
1439        Ok(match p {
1440            Some(p) if self.present.borrow().contains(&p) => required_type(&t),
1441            _ => t,
1442        })
1443    }
1444
1445    fn tc_inner(&self, node: &Node, scope: Option<&TypeScope>) -> R<Value> {
1446        match node {
1447            Node::Lit(v, _) => Ok(ty(type_name(v))),
1448            Node::List(items, _) => {
1449                let items = items.iter().map(|i| self.tc(i, scope)).collect::<R<Vec<_>>>()?;
1450                Ok(Value::Array(vec![match items.first() {
1451                    None => ty("dyn"),
1452                    Some(first) if items.iter().all(|t| t == first) => first.clone(),
1453                    Some(_) => ty("dyn"),
1454                }]))
1455            }
1456            Node::Id(name, _) => {
1457                let mut s = scope;
1458                while let Some(sc) = s {
1459                    if sc.name == name {
1460                        return Ok(sc.ty.clone());
1461                    }
1462                    s = sc.parent;
1463                }
1464                self.types.get(name).cloned().ok_or_else(|| format!("unknown name {name}"))
1465            }
1466            Node::Field(..) | Node::Has(..) => {
1467                let has = matches!(node, Node::Has(..));
1468                let field = match node {
1469                    Node::Has(field, _) => field.as_ref(),
1470                    _ => node,
1471                };
1472                let Node::Field(of, name, _) = field else { return Err("has needs a field: has(a.b)".into()) };
1473                let raw = self.tc(of, scope)?;
1474                let of = required_type(&raw);
1475                let t = if matches!(of.as_str(), Some("dyn" | "map" | "null")) {
1476                    ty("dyn")
1477                } else if let Value::Object(fields) = &of {
1478                    fields.get(name).cloned().ok_or_else(|| format!("unknown field {name}"))?
1479                } else {
1480                    return Err(format!("no field {name} on {}", type_display(&of)));
1481                };
1482                if has {
1483                    return Ok(ty("bool"));
1484                }
1485                Ok(if is_optional(&raw) { optional_type(t) } else { t })
1486            }
1487            Node::Index(of, index, _) => {
1488                let raw = self.tc(of, scope)?;
1489                let of_t = required_type(&raw);
1490                let index_t = self.need(index, scope)?;
1491                let wrap = |t: Value| if is_optional(&raw) { optional_type(t) } else { t };
1492                if let Value::Array(item) = &of_t {
1493                    if !loose(&index_t) && !is(&index_t, "number") {
1494                        return Err(format!("a list index is a number, got {}", type_display(&index_t)));
1495                    }
1496                    return Ok(wrap(item.first().cloned().unwrap_or_else(|| ty("dyn"))));
1497                }
1498                if is_row(&of_t) || is(&of_t, "map") {
1499                    if !loose(&index_t) && !is_textual(&index_t) {
1500                        return Err(format!("a map key is a string, got {}", type_display(&index_t)));
1501                    }
1502                    if let (Value::Object(fields), Some(Value::String(key))) = (&of_t, literal(index)) {
1503                        return fields.get(key).cloned().map(wrap).ok_or_else(|| format!("unknown field {key}"));
1504                    }
1505                    return Ok(wrap(ty("dyn")));
1506                }
1507                if loose(&of_t) {
1508                    return Ok(wrap(ty("dyn")));
1509                }
1510                Err(format!("cannot index {}", type_display(&of_t)))
1511            }
1512            Node::Unary(op, arg, _) => {
1513                let t = self.need(arg, scope)?;
1514                let want = if *op == '-' { "number" } else { "bool" };
1515                if !loose(&t) && !is(&t, want) {
1516                    return Err(format!("cannot apply {op} to {}", type_display(&t)));
1517                }
1518                Ok(ty(want))
1519            }
1520            Node::Cond(test, then, otherwise, _) => {
1521                let t = self.need(test, scope)?;
1522                if !loose(&t) && !is(&t, "bool") {
1523                    return Err(format!("cannot use {} as a condition", type_display(&t)));
1524                }
1525                let (yes, no) = guards(test);
1526                let a = self.within(yes, then, scope)?;
1527                let b = self.within(no, otherwise, scope)?;
1528                Ok(if a == b {
1529                    a
1530                } else if is(&a, "null") {
1531                    optional_type(b)
1532                } else if is(&b, "null") {
1533                    optional_type(a)
1534                } else if required_type(&a) == required_type(&b) {
1535                    optional_type(required_type(&a))
1536                } else if is_optional(&a) || is_optional(&b) {
1537                    optional_type(ty("dyn"))
1538                } else {
1539                    ty("dyn")
1540                })
1541            }
1542            Node::Bin(op, a, b, _) => self.binary(op, a, b, scope),
1543            Node::Call(name, args, _) => self.call(name, args, scope),
1544            Node::Macro(f, of, var, body, _) => {
1545                let of = self.need(of, scope)?;
1546                if !of.is_array() && !loose(&of) {
1547                    return Err(format!("{f} needs a list, got {}", type_display(&of)));
1548                }
1549                let item = of.as_array().and_then(|a| a.first().cloned()).unwrap_or_else(|| ty("dyn"));
1550                let inner = TypeScope { name: var, ty: item, parent: scope };
1551                let body_t = self.tc(body, Some(&inner))?;
1552                if f == "map" {
1553                    return Ok(Value::Array(vec![body_t]));
1554                }
1555                if is_optional(&body_t) {
1556                    return missing(body);
1557                }
1558                if !loose(&body_t) && !is(&body_t, "bool") {
1559                    return Err(format!("{f} needs a bool, got {}", type_display(&body_t)));
1560                }
1561                Ok(if f == "filter" {
1562                    if of.is_array() {
1563                        of
1564                    } else {
1565                        Value::Array(vec![ty("dyn")])
1566                    }
1567                } else {
1568                    ty("bool")
1569                })
1570            }
1571        }
1572    }
1573
1574    fn binary(&self, op: &str, a: &Node, b: &Node, scope: Option<&TypeScope>) -> R<Value> {
1575        if op == "&&" || op == "||" {
1576            let x = self.need(a, scope)?;
1577            let (yes, no) = guards(a);
1578            let y = self.within(if op == "&&" { yes } else { no }, b, scope)?;
1579            if is_optional(&y) {
1580                return missing(b);
1581            }
1582            for t in [&x, &y] {
1583                if !loose(t) && !is(t, "bool") {
1584                    return Err(format!("cannot apply {op} to {}", type_display(t)));
1585                }
1586            }
1587            return Ok(ty("bool"));
1588        }
1589        if op == "==" || op == "!=" {
1590            // The null check: either side may be missing.
1591            let x = required_type(&self.tc(a, scope)?);
1592            let y = required_type(&self.tc(b, scope)?);
1593            let (dx, dy) = (type_display(&x), type_display(&y));
1594            let comparable = loose(&x) || loose(&y) || (is_textual(&x) && is_textual(&y)) || dx == dy;
1595            if !comparable {
1596                return Err(format!("cannot compare {dx} and {dy}"));
1597            }
1598            return Ok(ty("bool"));
1599        }
1600        let x = self.need(a, scope)?;
1601        let y = self.need(b, scope)?;
1602        let (dx, dy) = (type_display(&x), type_display(&y));
1603        let numeric = |t: &Value| is(t, "number") || loose(t);
1604        match op {
1605            "<" | "<=" | ">" | ">=" => {
1606                let ok = |t: &Value| loose(t) || is(t, "number") || is_textual(t);
1607                if !ok(&x) || !ok(&y) || (is(&x, "number") && is_textual(&y)) || (is(&y, "number") && is_textual(&x)) {
1608                    return Err(format!("cannot compare {dx} and {dy}"));
1609                }
1610                Ok(ty("bool"))
1611            }
1612            "+" => {
1613                if loose(&x) && loose(&y) {
1614                    return Ok(ty("dyn"));
1615                }
1616                if numeric(&x) && numeric(&y) {
1617                    return Ok(ty("number"));
1618                }
1619                if (is_textual(&x) || loose(&x)) && (is_textual(&y) || loose(&y)) {
1620                    return Ok(ty("string"));
1621                }
1622                if (x.is_array() || loose(&x)) && (y.is_array() || loose(&y)) {
1623                    return Ok(if x.is_array() && y.is_array() && x == y { x } else { Value::Array(vec![ty("dyn")]) });
1624                }
1625                Err(format!("cannot apply + to {dx} and {dy}"))
1626            }
1627            _ => {
1628                if !(numeric(&x) && numeric(&y)) {
1629                    return Err(format!("cannot apply {op} to {dx} and {dy}"));
1630                }
1631                if (op == "/" || op == "%") && literal(b).is_some_and(|v| v.as_f64() == Some(0.0)) {
1632                    return Err(if op == "/" { "division by zero" } else { "modulo by zero" }.into());
1633                }
1634                Ok(ty("number"))
1635            }
1636        }
1637    }
1638
1639    fn call(&self, name: &str, args: &[Node], scope: Option<&TypeScope>) -> R<Value> {
1640        let d = def(name).ok_or_else(|| format!("unknown function {name}"))?;
1641        if let Some(e) = arity_error(name, &d, args.len()) {
1642            return Err(e);
1643        }
1644        // orElse takes a value that may be missing; every other argument is used.
1645        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<_>>>()?;
1646        for (i, t) in types.iter().enumerate() {
1647            let param = d.params[i.min(d.params.len() - 1)];
1648            if !accepts(param, &required_type(t)) {
1649                return Err(format!("{name} needs {}, got {}", param_name(param), type_display(t)));
1650            }
1651        }
1652        if name == "orElse" {
1653            let (x, y) = (required_type(&types[0]), &types[1]);
1654            if !loose(&x) && !loose(y) && !(is_textual(&x) && is_textual(y)) && type_display(&x) != type_display(y) {
1655                return Err(format!("orElse needs a {} default, got {}", type_display(&x), type_display(y)));
1656            }
1657        }
1658        let lit = |i: usize| args.get(i).and_then(literal);
1659        let unit_at = match name {
1660            "dateAdd" | "dateDiff" => Some(2),
1661            "startOf" | "endOf" => Some(1),
1662            _ => None,
1663        };
1664        if let Some(Value::String(unit)) = unit_at.and_then(lit) {
1665            let units = if unit_at == Some(2) { DATE_UNITS } else { CALENDAR_UNITS };
1666            if !units.contains(&unit.as_str()) {
1667                return Err(format!("unknown unit {unit}"));
1668            }
1669            if name == "dateAdd" && is(&types[0], "date") && unit_ms(unit).is_some() {
1670                return Err(format!("unit {unit} needs a timestamp"));
1671            }
1672        }
1673        if name == "dateAdd" {
1674            if let Some(n) = lit(1).and_then(Value::as_f64) {
1675                if n.fract() != 0.0 {
1676                    return Err(format!("dateAdd needs a whole number, got {}", num_text(n)));
1677                }
1678            }
1679        }
1680        if name == "round" && args.len() > 1 {
1681            if let Some(d) = lit(1).and_then(Value::as_f64) {
1682                if d.fract() != 0.0 || !(0.0..=10.0).contains(&d) {
1683                    return Err("round needs 0 to 10 digits".into());
1684                }
1685            }
1686        }
1687        if name == "div" && lit(1).and_then(Value::as_f64) == Some(0.0) {
1688            return Err("division by zero".into());
1689        }
1690        if name == "mod" && lit(1).and_then(Value::as_f64) == Some(0.0) {
1691            return Err("modulo by zero".into());
1692        }
1693        if name == "formatDuration" {
1694            if let Some(Value::String(f)) = lit(1) {
1695                if !DURATION_FORMATS.contains(&f.as_str()) {
1696                    return Err(format!("unknown duration format {f}"));
1697                }
1698            }
1699        }
1700        Ok(match name {
1701            "dateAdd" | "startOf" | "endOf" => {
1702                if is(&types[0], "date") || is(&types[0], "timestamp") {
1703                    types[0].clone()
1704                } else {
1705                    ty("dyn")
1706                }
1707            }
1708            "today" => ty("date"),
1709            "now" => ty("timestamp"),
1710            "formatDuration" => ty("string"),
1711            "orElse" => {
1712                let x = required_type(&types[0]);
1713                if is(&x, "null") {
1714                    required_type(&types[1])
1715                } else {
1716                    x
1717                }
1718            }
1719            _ => ty("number"),
1720        })
1721    }
1722}
1723
1724/// Type-check an expression against the names it may read (a kind's
1725/// fields, a binding's value): the type, or the error (the parse error, or
1726/// the first type error). Types are spelled as JSON (`"number"`, `[T]`, a
1727/// row object, `{"?": T}` for a value that may be missing).
1728pub fn check_expression(source: &str, types: &Map<String, Value>) -> Result<Value, String> {
1729    let ast = parse_expression(source)?;
1730    Checker { types, present: std::cell::RefCell::new(Vec::new()) }.tc(&ast, None)
1731}