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