Skip to main content

rustev_core/
expr.rs

1//! References, expressions and predicates of registry `rustev.exact/1`:
2//! typing (compile phase e) and evaluation (spec 002, 3.4 and 3.8).
3
4use std::collections::{BTreeMap, BTreeSet};
5
6use rustev_contract::decimal::{ArithError, Decimal, Rounding};
7use rustev_contract::definition::{CmpOp, Expr, Literal, Predicate};
8use rustev_contract::time::{DurationMs, Timestamp};
9
10use crate::value::{Ty, Value};
11
12/// A parsed reference.
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
14pub enum Ref {
15    /// An input, optionally followed by a record field path
16    /// (`input:trip.dates/check_in`).
17    Input(String, Vec<String>),
18    Step(String),
19    /// A field path inside the current item of a per-item operator.
20    Item(Vec<String>),
21    /// The item bound to a fan-out instance key (projections only).
22    Bind(String),
23    Now,
24}
25
26impl Ref {
27    pub fn parse(s: &str) -> Result<Ref, String> {
28        if s == "now" {
29            return Ok(Ref::Now);
30        }
31        let (kind, rest) = s
32            .split_once(':')
33            .ok_or_else(|| format!("reference {s:?} has no kind"))?;
34        if rest.is_empty() {
35            return Err(format!("reference {s:?} names nothing"));
36        }
37        match kind {
38            "input" => {
39                let mut parts = rest.split('/');
40                let name = parts.next().unwrap_or_default().to_string();
41                let path: Vec<String> = parts.map(String::from).collect();
42                if name.is_empty() || path.iter().any(String::is_empty) {
43                    return Err(format!("reference {s:?} has an empty part"));
44                }
45                Ok(Ref::Input(name, path))
46            }
47            "step" => Ok(Ref::Step(rest.into())),
48            "bind" => Ok(Ref::Bind(rest.into())),
49            "item" => {
50                let path: Vec<String> = rest.split('.').map(String::from).collect();
51                if path.iter().any(String::is_empty) {
52                    return Err(format!("reference {s:?} has an empty field"));
53                }
54                Ok(Ref::Item(path))
55            }
56            _ => Err(format!("reference {s:?} has unknown kind {kind:?}")),
57        }
58    }
59}
60
61/// The type of a step's result as expressions see it.
62#[derive(Debug, Clone, PartialEq)]
63pub enum StepTy {
64    Exact(Ty),
65    /// A semantic value and its fan-out binds; exact expressions cannot read
66    /// it, only `weighted_rank@1` components and the policy can.
67    Semantic(crate::sem::SemKind, Vec<String>),
68}
69
70/// Types visible while typing an expression.
71pub struct TyEnv<'a> {
72    pub inputs: &'a BTreeMap<String, Ty>,
73    pub steps: &'a BTreeMap<String, StepTy>,
74    pub item: Option<&'a Ty>,
75    /// A version 2 definition: step references may carry a field path
76    /// (`step:<id>/<field>`) and item paths may use `/` (spec 032, 3.4.4
77    /// and 3.5.4). Version 2 step ids and field names hold no `/` or `.`.
78    pub v2: bool,
79}
80
81/// A version 2 step reference's id and field path.
82pub fn split_step(s: &str) -> (String, Vec<String>) {
83    let mut parts = s.split('/');
84    let id = parts.next().unwrap_or_default().to_string();
85    (id, parts.map(String::from).collect())
86}
87
88/// A version 2 item path, with `/` as well as `.` separating fields.
89pub fn split_item(path: Vec<String>) -> Vec<String> {
90    path.iter()
91        .flat_map(|p| p.split('/').map(String::from))
92        .collect()
93}
94
95/// The dependency a reference names: `input:<name>` or `step:<id>` without
96/// any field path.
97pub fn base_ref(r: &str) -> String {
98    match r.strip_prefix("input:") {
99        Some(rest) => format!("input:{}", rest.split('/').next().unwrap_or_default()),
100        None => r.to_string(),
101    }
102}
103
104/// [`base_ref`] for a version 2 definition, whose step references may carry
105/// a field path.
106pub fn base_ref_v2(r: &str) -> String {
107    match r.strip_prefix("step:") {
108        Some(s) => format!("step:{}", split_step(s).0),
109        None => base_ref(r),
110    }
111}
112
113/// Every reference an expression or predicate reads, in reading order.
114pub fn expr_refs(e: &Expr, out: &mut Vec<String>) {
115    match e {
116        Expr::Ref(r) => out.push(r.clone()),
117        Expr::Lit(_) => {}
118        Expr::Add { left, right }
119        | Expr::Sub { left, right }
120        | Expr::Mul { left, right }
121        | Expr::Div { left, right } => {
122            expr_refs(left, out);
123            expr_refs(right, out);
124        }
125        Expr::Round { value, .. } | Expr::Len(value) | Expr::ToDecimal(value) => {
126            expr_refs(value, out)
127        }
128        Expr::Fx {
129            amount,
130            from,
131            to,
132            rates,
133            ..
134        } => {
135            expr_refs(amount, out);
136            expr_refs(from, out);
137            expr_refs(to, out);
138            out.push(rates.clone());
139        }
140        Expr::Graded { reference, .. } => out.push(reference.clone()),
141    }
142}
143
144pub fn predicate_refs(p: &Predicate, out: &mut Vec<String>) {
145    match p {
146        Predicate::Cmp { left, right, .. } => {
147            expr_refs(left, out);
148            expr_refs(right, out);
149        }
150        Predicate::All(ps) | Predicate::Any(ps) => ps.iter().for_each(|p| predicate_refs(p, out)),
151        Predicate::Not(p) => predicate_refs(p, out),
152        Predicate::Member { value, .. } => expr_refs(value, out),
153        Predicate::Present(r) | Predicate::IsConstructed { reference: r } => out.push(r.clone()),
154        Predicate::MemberRef { value, set } => {
155            expr_refs(value, out);
156            out.push(set.clone());
157        }
158    }
159}
160
161/// Whether an expression uses a construct of `rustev.exact/2` that a
162/// version 1 definition cannot carry (spec 032, 3.1.3).
163pub fn expr_uses_v2(e: &Expr) -> bool {
164    match e {
165        Expr::Graded { .. } => true,
166        Expr::Ref(_) | Expr::Lit(_) => false,
167        Expr::Add { left, right }
168        | Expr::Sub { left, right }
169        | Expr::Mul { left, right }
170        | Expr::Div { left, right } => expr_uses_v2(left) || expr_uses_v2(right),
171        Expr::Round { value, .. } | Expr::Len(value) | Expr::ToDecimal(value) => {
172            expr_uses_v2(value)
173        }
174        Expr::Fx {
175            amount, from, to, ..
176        } => expr_uses_v2(amount) || expr_uses_v2(from) || expr_uses_v2(to),
177    }
178}
179
180/// Whether a predicate uses a construct of `rustev.exact/2`.
181pub fn predicate_uses_v2(p: &Predicate) -> bool {
182    match p {
183        Predicate::MemberRef { .. } | Predicate::IsConstructed { .. } => true,
184        Predicate::Cmp { left, right, .. } => expr_uses_v2(left) || expr_uses_v2(right),
185        Predicate::All(ps) | Predicate::Any(ps) => ps.iter().any(predicate_uses_v2),
186        Predicate::Not(p) => predicate_uses_v2(p),
187        Predicate::Member { value, .. } => expr_uses_v2(value),
188        Predicate::Present(_) => false,
189    }
190}
191
192/// Structural checks of compile phase b: scales in range.
193pub fn expr_structure(e: &Expr) -> Result<(), String> {
194    match e {
195        Expr::Ref(_) | Expr::Lit(_) => Ok(()),
196        Expr::Add { left, right }
197        | Expr::Sub { left, right }
198        | Expr::Mul { left, right }
199        | Expr::Div { left, right } => {
200            expr_structure(left)?;
201            expr_structure(right)
202        }
203        Expr::Round { value, scale } => {
204            if *scale > 9 {
205                return Err(format!("scale {scale} exceeds 9"));
206            }
207            expr_structure(value)
208        }
209        Expr::Fx {
210            amount,
211            from,
212            to,
213            scale,
214            ..
215        } => {
216            if *scale > 9 {
217                return Err(format!("scale {scale} exceeds 9"));
218            }
219            expr_structure(amount)?;
220            expr_structure(from)?;
221            expr_structure(to)
222        }
223        Expr::Len(v) | Expr::ToDecimal(v) => expr_structure(v),
224        Expr::Graded { reference, .. } => match Ref::parse(reference)? {
225            Ref::Input(..) | Ref::Step(_) | Ref::Item(_) => Ok(()),
226            _ => Err(format!("a grade requirement reads {reference:?}")),
227        },
228    }
229}
230
231pub fn predicate_structure(p: &Predicate) -> Result<(), String> {
232    match p {
233        Predicate::Cmp { left, right, .. } => {
234            expr_structure(left)?;
235            expr_structure(right)
236        }
237        Predicate::All(ps) | Predicate::Any(ps) => ps.iter().try_for_each(predicate_structure),
238        Predicate::Not(p) => predicate_structure(p),
239        Predicate::Member { value, set } => {
240            if set.is_empty() {
241                return Err("member set is empty".into());
242            }
243            expr_structure(value)
244        }
245        Predicate::IsConstructed { reference } => Ref::parse(reference).map(|_| ()),
246        Predicate::Present(r) => match Ref::parse(r)? {
247            Ref::Step(s) if !s.contains('/') => Ok(()),
248            Ref::Input(_, p) if p.is_empty() => Ok(()),
249            _ => Err(format!(
250                "present() takes an input or a bare step reference, not {r:?}"
251            )),
252        },
253        Predicate::MemberRef { value, set } => {
254            expr_structure(value)?;
255            match Ref::parse(set)? {
256                Ref::Input(..) | Ref::Step(_) => Ok(()),
257                _ => Err(format!("member_ref set {set:?} must be an input or step")),
258            }
259        }
260    }
261}
262
263fn field_ty<'t>(ty: &'t Ty, path: &[String]) -> Result<&'t Ty, String> {
264    let unwrap = |t: &'t Ty| match t {
265        // Reading a nullable field yields its type; `null` is missing.
266        Ty::Nullable(inner) => inner.as_ref(),
267        t => t,
268    };
269    // A root is never nullable (`nullable` is a record field type only,
270    // spec 032 3.4.1), so only a field read is unwrapped.
271    let mut cur = ty;
272    for f in path {
273        if f.is_empty() {
274            return Err("a path has an empty field name".into());
275        }
276        match cur {
277            Ty::Record(fields) => {
278                cur = fields
279                    .iter()
280                    .find(|(n, _)| n == f)
281                    .map(|(_, t)| t)
282                    .ok_or_else(|| format!("no field {f:?}"))?;
283                cur = unwrap(cur);
284            }
285            other => return Err(format!("{} has no field {f:?}", other.name())),
286        }
287    }
288    Ok(cur)
289}
290
291/// The type of a reference, or a kind mismatch.
292pub fn ref_ty(r: &str, env: &TyEnv<'_>) -> Result<Ty, String> {
293    match Ref::parse(r)? {
294        Ref::Input(n, path) => {
295            let t = env
296                .inputs
297                .get(&n)
298                .ok_or_else(|| format!("unknown input {n:?}"))?;
299            field_ty(t, &path).cloned()
300        }
301        Ref::Step(s) => {
302            let (s, path) = if env.v2 { split_step(&s) } else { (s, vec![]) };
303            if s.is_empty() {
304                return Err(format!("{r:?} names no step"));
305            }
306            match env.steps.get(&s) {
307                Some(StepTy::Exact(t)) => field_ty(t, &path).cloned(),
308                Some(StepTy::Semantic(..)) => Err(format!(
309                    "step {s:?} is semantic; exact operators cannot read it"
310                )),
311                None => Err(format!("unknown step {s:?}")),
312            }
313        }
314        Ref::Item(path) => {
315            let item = env
316                .item
317                .ok_or_else(|| format!("{r:?} outside a per-item operator"))?;
318            let path = if env.v2 { split_item(path) } else { path };
319            field_ty(item, &path).cloned()
320        }
321        Ref::Now => Ok(Ty::Timestamp),
322        Ref::Bind(_) => Err(format!("{r:?} is only valid in a projection")),
323    }
324}
325
326fn is_currency(t: &Ty) -> bool {
327    matches!(t, Ty::Text { .. } | Ty::Enum(_))
328}
329
330/// The static type of an expression (compile phase e). Values never convert
331/// silently: integer and decimal mix only through `to_decimal`.
332pub fn type_expr(e: &Expr, env: &TyEnv<'_>) -> Result<Ty, String> {
333    let bin = |l: &Expr, r: &Expr| -> Result<(Ty, Ty), String> {
334        Ok((type_expr(l, env)?, type_expr(r, env)?))
335    };
336    match e {
337        Expr::Ref(r) => ref_ty(r, env),
338        Expr::Lit(l) => Ok(Value::literal_ty(l)),
339        Expr::Add { left, right } => match bin(left, right)? {
340            (Ty::Integer, Ty::Integer) => Ok(Ty::Integer),
341            (Ty::Decimal, Ty::Decimal) => Ok(Ty::Decimal),
342            (Ty::Timestamp, Ty::Duration) => Ok(Ty::Timestamp),
343            (Ty::Duration, Ty::Duration) => Ok(Ty::Duration),
344            (a, b) => Err(format!("cannot add {} and {}", a.name(), b.name())),
345        },
346        Expr::Sub { left, right } => match bin(left, right)? {
347            (Ty::Integer, Ty::Integer) => Ok(Ty::Integer),
348            (Ty::Decimal, Ty::Decimal) => Ok(Ty::Decimal),
349            (Ty::Timestamp, Ty::Duration) => Ok(Ty::Timestamp),
350            (Ty::Timestamp, Ty::Timestamp) => Ok(Ty::Duration),
351            (Ty::Duration, Ty::Duration) => Ok(Ty::Duration),
352            (a, b) => Err(format!("cannot subtract {} from {}", b.name(), a.name())),
353        },
354        Expr::Mul { left, right } => match bin(left, right)? {
355            (Ty::Integer, Ty::Integer) => Ok(Ty::Integer),
356            (Ty::Decimal, Ty::Decimal) => Ok(Ty::Decimal),
357            (a, b) => Err(format!("cannot multiply {} by {}", a.name(), b.name())),
358        },
359        Expr::Div { left, right } => match bin(left, right)? {
360            (Ty::Decimal, Ty::Decimal) => Ok(Ty::Decimal),
361            (a, b) => Err(format!(
362                "cannot divide {} by {} (only decimals divide)",
363                a.name(),
364                b.name()
365            )),
366        },
367        Expr::Round { value, .. } => match type_expr(value, env)? {
368            Ty::Decimal => Ok(Ty::Decimal),
369            t => Err(format!("cannot round {}", t.name())),
370        },
371        Expr::Fx {
372            amount,
373            from,
374            to,
375            rates,
376            ..
377        } => {
378            if type_expr(amount, env)? != Ty::Decimal {
379                return Err("fx amount must be a decimal".into());
380            }
381            if !is_currency(&type_expr(from, env)?) || !is_currency(&type_expr(to, env)?) {
382                return Err("fx currencies must be text or enum".into());
383            }
384            match ref_ty(rates, env)? {
385                Ty::List { item, .. } => match item.as_ref() {
386                    Ty::Record(f)
387                        if f.iter().any(|(n, t)| n == "currency" && is_currency(t))
388                            && f.iter().any(|(n, t)| n == "rate" && *t == Ty::Decimal) =>
389                    {
390                        Ok(Ty::Decimal)
391                    }
392                    _ => Err(
393                        "fx rates must be a list of records with `currency` and decimal `rate`"
394                            .into(),
395                    ),
396                },
397                t => Err(format!("fx rates must be a list, not {}", t.name())),
398            }
399        }
400        Expr::Len(v) => match type_expr(v, env)? {
401            Ty::List { .. } => Ok(Ty::Integer),
402            t => Err(format!("len() of {}", t.name())),
403        },
404        Expr::ToDecimal(v) => match type_expr(v, env)? {
405            Ty::Integer => Ok(Ty::Decimal),
406            t => Err(format!("to_decimal() of {}", t.name())),
407        },
408        Expr::Graded { reference, .. } => ref_ty(reference, env),
409    }
410}
411
412fn literal_fits(l: &Literal, t: &Ty) -> bool {
413    match (l, t) {
414        (Literal::Enum(s), Ty::Enum(opts)) => opts.contains(s),
415        _ => Value::literal_ty(l).same_shape(t),
416    }
417}
418
419fn operand_fits(e: &Expr, t_other: &Ty, t_self: &Ty) -> bool {
420    match e {
421        Expr::Lit(l) => literal_fits(l, t_other),
422        _ => t_self.same_shape(t_other),
423    }
424}
425
426pub fn type_predicate(p: &Predicate, env: &TyEnv<'_>) -> Result<(), String> {
427    match p {
428        Predicate::Cmp { left, op, right } => {
429            let (lt, rt) = (type_expr(left, env)?, type_expr(right, env)?);
430            if matches!(lt, Ty::Maybe(_)) || matches!(rt, Ty::Maybe(_)) {
431                return Err("a maybe value cannot be compared".into());
432            }
433            if !(operand_fits(left, &rt, &lt) && operand_fits(right, &lt, &rt)) {
434                return Err(format!("cannot compare {} with {}", lt.name(), rt.name()));
435            }
436            let ordered = !matches!(op, CmpOp::Eq | CmpOp::Ne);
437            if ordered && !lt.is_orderable() {
438                return Err(format!("{} has no order", lt.name()));
439            }
440            if !ordered
441                && !matches!(
442                    lt,
443                    Ty::Bool
444                        | Ty::Integer
445                        | Ty::Decimal
446                        | Ty::Text { .. }
447                        | Ty::Enum(_)
448                        | Ty::Timestamp
449                        | Ty::Duration
450                )
451            {
452                return Err(format!("{} cannot be tested for equality", lt.name()));
453            }
454            Ok(())
455        }
456        Predicate::All(ps) | Predicate::Any(ps) => {
457            ps.iter().try_for_each(|p| type_predicate(p, env))
458        }
459        Predicate::Not(p) => type_predicate(p, env),
460        Predicate::Member { value, set } => {
461            let t = type_expr(value, env)?;
462            if set.iter().all(|l| literal_fits(l, &t)) {
463                Ok(())
464            } else {
465                Err(format!("member set does not match {}", t.name()))
466            }
467        }
468        Predicate::IsConstructed { reference } => ref_ty(reference, env).map(|_| ()),
469        Predicate::Present(r) => match Ref::parse(r)? {
470            Ref::Step(s) if env.v2 && !s.contains('/') => ref_ty(r, env).map(|_| ()),
471            Ref::Input(n, p) if p.is_empty() && env.inputs.contains_key(&n) => Ok(()),
472            _ => Err(format!("present() of unknown input {r:?}")),
473        },
474        Predicate::MemberRef { value, set } => {
475            let t = type_expr(value, env)?;
476            let textual = matches!(t, Ty::Text { .. } | Ty::Enum(_));
477            match ref_ty(set, env)? {
478                Ty::List { item, .. } if textual && item.same_shape(&t) => Ok(()),
479                // A map's keys are its members, never its values (spec 032,
480                // 3.7.2).
481                Ty::Map { .. } if textual => Ok(()),
482                s => Err(format!(
483                    "member_ref of {} in {}: the set is a list of text or enum values, or a map",
484                    t.name(),
485                    s.name()
486                )),
487            }
488        }
489    }
490}
491
492/// Why evaluation produced no value; becomes the step's unresolved reason.
493#[derive(Debug, Clone, PartialEq, Eq)]
494pub enum Fault {
495    /// Overflow, division by zero, an out-of-range timestamp.
496    Invalid(String),
497    /// A missing fact, e.g. `fx.rates.EUR`.
498    Missing(String),
499    /// Two different values for one fact.
500    Conflict(String),
501    /// A read of an absent value or a `null` field, naming its path
502    /// (spec 032, 3.4.4); only plan 3 values can be absent.
503    Absent(String),
504}
505
506impl From<ArithError> for Fault {
507    fn from(e: ArithError) -> Self {
508        Fault::Invalid(e.to_string())
509    }
510}
511
512/// Values visible while evaluating.
513pub struct Scope<'a> {
514    pub constructed: Option<&'a BTreeSet<String>>,
515    pub now: Timestamp,
516    pub inputs: &'a BTreeMap<String, Value>,
517    pub steps: &'a BTreeMap<String, Value>,
518    pub present: &'a BTreeSet<String>,
519    pub item: Option<&'a Value>,
520    /// See [`TyEnv::v2`].
521    pub v2: bool,
522}
523
524/// Reads `path` from `v`. A `null` or absent value on the way is missing
525/// evidence named by `name`. That includes the root: `field_ty` does not
526/// unwrap a root because no root is typed nullable (spec 032 3.4.1), so a
527/// null root can only be a fault, never a value.
528fn field<'v>(v: &'v Value, path: &[String], name: &dyn Fn() -> String) -> Result<&'v Value, Fault> {
529    let present = |v: &'v Value| match v {
530        Value::Null | Value::Absent(_) => Err(Fault::Absent(name())),
531        v => Ok(v),
532    };
533    let mut cur = present(v)?;
534    for f in path {
535        match cur {
536            Value::Record(m) => {
537                cur = present(
538                    m.get(f)
539                        .ok_or_else(|| Fault::Invalid(format!("no field {f:?}")))?,
540                )?
541            }
542            _ => return Err(Fault::Invalid(format!("no field {f:?}"))),
543        }
544    }
545    Ok(cur)
546}
547
548pub fn eval_ref(r: &str, s: &Scope<'_>) -> Result<Value, Fault> {
549    match Ref::parse(r).map_err(Fault::Invalid)? {
550        Ref::Input(n, path) => {
551            let v = s.inputs.get(&n).ok_or_else(|| Fault::Missing(n.clone()))?;
552            field(v, &path, &|| {
553                [vec![n.clone()], path.clone()].concat().join("/")
554            })
555            .cloned()
556        }
557        Ref::Step(n) if s.v2 => {
558            let (id, path) = split_step(&n);
559            if id.is_empty() {
560                return Err(Fault::Invalid(format!("{r:?} names no step")));
561            }
562            let v = s.steps.get(&id).ok_or_else(|| Fault::Missing(id.clone()))?;
563            field(v, &path, &|| format!("step:{n}")).cloned()
564        }
565        Ref::Step(n) => s.steps.get(&n).cloned().ok_or(Fault::Missing(n)),
566        Ref::Item(path) => {
567            let item = s
568                .item
569                .ok_or_else(|| Fault::Invalid("no current item".into()))?;
570            let path = if s.v2 { split_item(path) } else { path };
571            // `Ref::parse` refuses a bare `item:`; the fallback keeps a
572            // missing-evidence name non-empty regardless.
573            let name = || {
574                if path.is_empty() {
575                    "item".to_string()
576                } else {
577                    path.join("/")
578                }
579            };
580            field(item, &path, &name).cloned()
581        }
582        Ref::Now => Ok(Value::Timestamp(s.now)),
583        Ref::Bind(b) => Err(Fault::Invalid(format!("bind:{b} outside a projection"))),
584    }
585}
586
587fn time_err(_: rustev_contract::time::TimeRangeError) -> Fault {
588    Fault::Invalid("timestamp or duration out of range".into())
589}
590
591fn rate(rates: &[Value], currency: &str, table: &str) -> Result<Decimal, Fault> {
592    let mut found: Option<Decimal> = None;
593    for r in rates {
594        let (Ok(Value::Text(c) | Value::Enum(c)), Ok(Value::Decimal(v))) = (
595            field(r, &["currency".into()], &String::new),
596            field(r, &["rate".into()], &String::new),
597        ) else {
598            continue;
599        };
600        if c == currency {
601            match found {
602                Some(prev) if prev != *v => {
603                    return Err(Fault::Conflict(format!("{table}.{currency}")));
604                }
605                _ => found = Some(*v),
606            }
607        }
608    }
609    found.ok_or_else(|| Fault::Missing(format!("{table}.{currency}")))
610}
611
612/// `amount / rate(from) * rate(to)`, each step half-even at 10^-9, then
613/// rescaled half-even to `scale` (`fx_convert@1`).
614pub fn fx(
615    amount: Decimal,
616    from: &str,
617    to: &str,
618    rates: &[Value],
619    table: &str,
620    scale: u32,
621) -> Result<Decimal, Fault> {
622    let rf = rate(rates, from, table)?;
623    let rt = rate(rates, to, table)?;
624    let base = amount.checked_div(rf, Rounding::HalfEven)?;
625    Ok(base
626        .checked_mul(rt, Rounding::HalfEven)?
627        .round_to_scale(scale, Rounding::HalfEven)?)
628}
629
630fn text_of(v: Value) -> Result<String, Fault> {
631    match v {
632        Value::Text(s) | Value::Enum(s) => Ok(s),
633        _ => Err(Fault::Invalid("expected text".into())),
634    }
635}
636
637pub fn eval_expr(e: &Expr, s: &Scope<'_>) -> Result<Value, Fault> {
638    let bin = |l: &Expr, r: &Expr| -> Result<(Value, Value), Fault> {
639        Ok((eval_expr(l, s)?, eval_expr(r, s)?))
640    };
641    let overflow = || Fault::Invalid("integer overflow".into());
642    match e {
643        Expr::Ref(r) => eval_ref(r, s),
644        Expr::Lit(l) => Ok(Value::from_literal(l)),
645        Expr::Add { left, right } => match bin(left, right)? {
646            (Value::Integer(a), Value::Integer(b)) => {
647                a.checked_add(b).map(Value::Integer).ok_or_else(overflow)
648            }
649            (Value::Decimal(a), Value::Decimal(b)) => Ok(Value::Decimal(a.checked_add(b)?)),
650            (Value::Timestamp(a), Value::Duration(b)) => {
651                a.checked_add(b).map(Value::Timestamp).map_err(time_err)
652            }
653            (Value::Duration(a), Value::Duration(b)) => {
654                a.0.checked_add(b.0)
655                    .map(|d| Value::Duration(DurationMs(d)))
656                    .ok_or_else(overflow)
657            }
658            _ => Err(Fault::Invalid("ill-typed add".into())),
659        },
660        Expr::Sub { left, right } => match bin(left, right)? {
661            (Value::Integer(a), Value::Integer(b)) => {
662                a.checked_sub(b).map(Value::Integer).ok_or_else(overflow)
663            }
664            (Value::Decimal(a), Value::Decimal(b)) => Ok(Value::Decimal(a.checked_sub(b)?)),
665            (Value::Timestamp(a), Value::Duration(b)) => {
666                a.checked_sub(b).map(Value::Timestamp).map_err(time_err)
667            }
668            (Value::Timestamp(a), Value::Timestamp(b)) => {
669                a.since(b).map(Value::Duration).map_err(time_err)
670            }
671            (Value::Duration(a), Value::Duration(b)) => {
672                a.0.checked_sub(b.0)
673                    .map(|d| Value::Duration(DurationMs(d)))
674                    .ok_or_else(|| Fault::Invalid("negative duration".into()))
675            }
676            _ => Err(Fault::Invalid("ill-typed sub".into())),
677        },
678        Expr::Mul { left, right } => match bin(left, right)? {
679            (Value::Integer(a), Value::Integer(b)) => {
680                a.checked_mul(b).map(Value::Integer).ok_or_else(overflow)
681            }
682            (Value::Decimal(a), Value::Decimal(b)) => {
683                Ok(Value::Decimal(a.checked_mul(b, Rounding::HalfEven)?))
684            }
685            _ => Err(Fault::Invalid("ill-typed mul".into())),
686        },
687        Expr::Div { left, right } => match bin(left, right)? {
688            (Value::Decimal(a), Value::Decimal(b)) => {
689                Ok(Value::Decimal(a.checked_div(b, Rounding::HalfEven)?))
690            }
691            _ => Err(Fault::Invalid("ill-typed div".into())),
692        },
693        Expr::Round { value, scale } => match eval_expr(value, s)? {
694            Value::Decimal(d) => Ok(Value::Decimal(
695                d.round_to_scale(*scale, Rounding::HalfEven)?,
696            )),
697            _ => Err(Fault::Invalid("ill-typed round".into())),
698        },
699        Expr::Fx {
700            amount,
701            from,
702            to,
703            rates,
704            scale,
705        } => {
706            let Value::Decimal(a) = eval_expr(amount, s)? else {
707                return Err(Fault::Invalid("ill-typed fx".into()));
708            };
709            let from = text_of(eval_expr(from, s)?)?;
710            let to = text_of(eval_expr(to, s)?)?;
711            let Value::List(table) = eval_ref(rates, s)? else {
712                return Err(Fault::Invalid("ill-typed fx rates".into()));
713            };
714            let name = rates.strip_prefix("input:").unwrap_or(rates);
715            Ok(Value::Decimal(fx(a, &from, &to, &table, name, *scale)?))
716        }
717        Expr::Len(v) => match eval_expr(v, s)? {
718            Value::List(items) => i64::try_from(items.len())
719                .map(Value::Integer)
720                .map_err(|_| overflow()),
721            _ => Err(Fault::Invalid("ill-typed len".into())),
722        },
723        Expr::ToDecimal(v) => match eval_expr(v, s)? {
724            Value::Integer(i) => Ok(Value::Decimal(Decimal::from_i64(i))),
725            _ => Err(Fault::Invalid("ill-typed to_decimal".into())),
726        },
727        // The grade requirement is checked by the evaluator before the
728        // expression is evaluated (spec 032, 3.9.1); the value is the
729        // reference's own.
730        Expr::Graded { reference, .. } => eval_ref(reference, s),
731    }
732}
733
734fn cmp_values(a: &Value, op: CmpOp, b: &Value) -> Result<bool, Fault> {
735    use std::cmp::Ordering;
736    let ord: Option<Ordering> = match (a, b) {
737        (Value::Integer(x), Value::Integer(y)) => Some(x.cmp(y)),
738        (Value::Decimal(x), Value::Decimal(y)) => Some(x.cmp(y)),
739        (Value::Timestamp(x), Value::Timestamp(y)) => Some(x.cmp(y)),
740        (Value::Duration(x), Value::Duration(y)) => Some(x.cmp(y)),
741        _ => None,
742    };
743    Ok(match (op, ord) {
744        (CmpOp::Eq, _) => {
745            a == b
746                || matches!((a, b), (Value::Text(x), Value::Text(y)) | (Value::Enum(x), Value::Enum(y)) if x == y)
747        }
748        (CmpOp::Ne, _) => a != b,
749        (CmpOp::Lt, Some(o)) => o == Ordering::Less,
750        (CmpOp::Le, Some(o)) => o != Ordering::Greater,
751        (CmpOp::Gt, Some(o)) => o == Ordering::Greater,
752        (CmpOp::Ge, Some(o)) => o != Ordering::Less,
753        _ => return Err(Fault::Invalid("ill-typed comparison".into())),
754    })
755}
756
757pub fn eval_predicate(p: &Predicate, s: &Scope<'_>) -> Result<bool, Fault> {
758    match p {
759        Predicate::Cmp { left, op, right } => {
760            cmp_values(&eval_expr(left, s)?, *op, &eval_expr(right, s)?)
761        }
762        Predicate::All(ps) => {
763            for p in ps {
764                if !eval_predicate(p, s)? {
765                    return Ok(false);
766                }
767            }
768            Ok(true)
769        }
770        Predicate::Any(ps) => {
771            for p in ps {
772                if eval_predicate(p, s)? {
773                    return Ok(true);
774                }
775            }
776            Ok(false)
777        }
778        Predicate::Not(p) => Ok(!eval_predicate(p, s)?),
779        Predicate::Member { value, set } => {
780            let v = eval_expr(value, s)?;
781            Ok(set.iter().any(|l| Value::from_literal(l) == v))
782        }
783        Predicate::IsConstructed { reference } => Ok(s
784            .constructed
785            .is_some_and(|set| set.contains(&base_ref_v2(reference)))
786            && eval_ref(reference, s).is_ok()),
787        Predicate::Present(r) if s.v2 => Ok(eval_ref(r, s).is_ok()),
788        Predicate::Present(r) => Ok(r
789            .strip_prefix("input:")
790            .is_some_and(|n| s.present.contains(n))),
791        Predicate::MemberRef { value, set } => {
792            let v = match eval_expr(value, s)? {
793                Value::Text(x) | Value::Enum(x) => x,
794                _ => return Err(Fault::Invalid("member_ref of a non-text value".into())),
795            };
796            match eval_ref(set, s)? {
797                Value::List(items) => Ok(items
798                    .iter()
799                    .any(|i| matches!(i, Value::Text(x) | Value::Enum(x) if *x == v))),
800                Value::Map(m) => Ok(m.contains_key(&v)),
801                _ => Err(Fault::Invalid("member_ref of a non-set".into())),
802            }
803        }
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810    use rustev_contract::definition::Literal as L;
811
812    fn dec(s: &str) -> Decimal {
813        Decimal::parse(s).unwrap()
814    }
815
816    fn rates(v: &[(&str, &str)]) -> Vec<Value> {
817        v.iter()
818            .map(|(c, r)| {
819                Value::Record(
820                    [
821                        ("currency".to_string(), Value::Text(c.to_string())),
822                        ("rate".to_string(), Value::Decimal(dec(r))),
823                    ]
824                    .into(),
825                )
826            })
827            .collect()
828    }
829
830    #[test]
831    fn fx_converts_through_the_base_and_rounds_half_even() {
832        let t = rates(&[("USD", "1"), ("EUR", "0.8"), ("JPY", "150")]);
833        assert_eq!(fx(dec("100"), "EUR", "USD", &t, "fx", 2), Ok(dec("125")));
834        assert_eq!(fx(dec("1"), "USD", "JPY", &t, "fx", 0), Ok(dec("150")));
835        // 0.125 at scale 2 is a tie: half-even gives 0.12; 0.135 gives 0.14.
836        let t2 = rates(&[("A", "1"), ("B", "0.125"), ("C", "0.135")]);
837        assert_eq!(fx(dec("1"), "A", "B", &t2, "fx", 2), Ok(dec("0.12")));
838        assert_eq!(fx(dec("1"), "A", "C", &t2, "fx", 2), Ok(dec("0.14")));
839    }
840
841    #[test]
842    fn fx_missing_conflicting_and_zero_rates_are_faults() {
843        let t = rates(&[("USD", "1"), ("EUR", "0.8"), ("EUR", "0.9"), ("ZZZ", "0")]);
844        assert_eq!(
845            fx(dec("1"), "GBP", "USD", &t, "fx", 2),
846            Err(Fault::Missing("fx.GBP".into()))
847        );
848        assert_eq!(
849            fx(dec("1"), "EUR", "USD", &t, "fx", 2),
850            Err(Fault::Conflict("fx.EUR".into()))
851        );
852        assert!(matches!(
853            fx(dec("1"), "ZZZ", "USD", &t, "fx", 2),
854            Err(Fault::Invalid(_))
855        ));
856        // Duplicate but equal rates are not a conflict.
857        let same = rates(&[("USD", "1"), ("EUR", "0.8"), ("EUR", "0.8")]);
858        assert!(fx(dec("1"), "EUR", "USD", &same, "fx", 2).is_ok());
859    }
860
861    #[test]
862    fn arithmetic_overflow_is_a_fault_not_a_wrap() {
863        let inputs = BTreeMap::new();
864        let steps = BTreeMap::new();
865        let present = BTreeSet::new();
866        let s = Scope {
867            constructed: None,
868            now: Timestamp::from_ms(0).unwrap(),
869            inputs: &inputs,
870            steps: &steps,
871            present: &present,
872            item: None,
873            v2: false,
874        };
875        let big = Expr::Lit(L::Integer(i64::MAX));
876        let add = Expr::Add {
877            left: Box::new(big.clone()),
878            right: Box::new(Expr::Lit(L::Integer(1))),
879        };
880        assert!(matches!(eval_expr(&add, &s), Err(Fault::Invalid(_))));
881        let div0 = Expr::Div {
882            left: Box::new(Expr::Lit(L::Decimal(dec("1")))),
883            right: Box::new(Expr::Lit(L::Decimal(dec("0")))),
884        };
885        assert!(matches!(eval_expr(&div0, &s), Err(Fault::Invalid(_))));
886        let back = Expr::Sub {
887            left: Box::new(Expr::Ref("now".into())),
888            right: Box::new(Expr::Lit(L::DurationMs(1))),
889        };
890        assert!(matches!(eval_expr(&back, &s), Err(Fault::Invalid(_))));
891        // Negative control: in-range arithmetic succeeds.
892        let ok = Expr::Add {
893            left: Box::new(Expr::Lit(L::Integer(1))),
894            right: Box::new(Expr::Lit(L::Integer(1))),
895        };
896        assert_eq!(eval_expr(&ok, &s), Ok(Value::Integer(2)));
897    }
898
899    #[test]
900    fn an_empty_field_name_in_a_path_is_refused() {
901        let rec = Ty::Record(vec![("a".into(), Ty::Integer)]);
902        assert_eq!(field_ty(&rec, &["a".into()]), Ok(&Ty::Integer));
903        let e = field_ty(&rec, &["".into(), "a".into()]).unwrap_err();
904        assert!(e.contains("empty field name"), "{e}");
905    }
906
907    #[test]
908    fn version_2_step_paths_split_at_the_step_id() {
909        assert_eq!(
910            split_step("join/match/total"),
911            (
912                "join".to_string(),
913                vec!["match".to_string(), "total".to_string()]
914            )
915        );
916        assert_eq!(split_step("plain"), ("plain".to_string(), vec![]));
917        // `Ref::parse` takes a step reference whole, so a version 2 path
918        // with an empty id is refused where it is split.
919        assert!(Ref::parse("step:/a").is_ok());
920        let inputs = BTreeMap::new();
921        let steps = BTreeMap::from([("".to_string(), StepTy::Exact(Ty::Bool))]);
922        let env = TyEnv {
923            inputs: &inputs,
924            steps: &steps,
925            item: None,
926            v2: true,
927        };
928        let e = ref_ty("step:/a", &env).unwrap_err();
929        assert!(e.contains("names no step"), "{e}");
930        let values = BTreeMap::from([("".to_string(), Value::Bool(true))]);
931        let present = BTreeSet::new();
932        let s = Scope {
933            constructed: None,
934            now: Timestamp::from_ms(0).unwrap(),
935            inputs: &BTreeMap::new(),
936            steps: &values,
937            present: &present,
938            item: None,
939            v2: true,
940        };
941        assert!(
942            matches!(eval_ref("step:/a", &s), Err(Fault::Invalid(e)) if e.contains("names no step"))
943        );
944        assert_eq!(base_ref_v2("step:join/match"), "step:join");
945        assert_eq!(base_ref_v2("input:a/b"), "input:a");
946        // Version 1 keeps a step reference whole.
947        assert_eq!(base_ref("step:join/match"), "step:join/match");
948        assert_eq!(
949            split_item(vec!["match/total".into(), "unit".into()]),
950            vec!["match", "total", "unit"]
951        );
952    }
953
954    #[test]
955    fn member_ref_of_a_map_tests_its_keys() {
956        let map_ty = Ty::Map {
957            key_max: 8,
958            value: Box::new(Ty::Text { max_bytes: 8 }),
959            max: 4,
960        };
961        let tys = BTreeMap::from([
962            ("codes".to_string(), map_ty),
963            (
964                "tier".to_string(),
965                Ty::Enum(vec!["gold".into(), "eu".into()]),
966            ),
967            ("n".to_string(), Ty::Integer),
968        ]);
969        let step_tys = BTreeMap::new();
970        let env = TyEnv {
971            inputs: &tys,
972            steps: &step_tys,
973            item: None,
974            v2: true,
975        };
976        let member = |value: &str| Predicate::MemberRef {
977            value: Expr::Ref(value.into()),
978            set: "input:codes".into(),
979        };
980        assert!(type_predicate(&member("input:tier"), &env).is_ok());
981        assert!(
982            type_predicate(&member("input:n"), &env).is_err(),
983            "a non-text value"
984        );
985
986        let inputs = BTreeMap::from([
987            (
988                "codes".to_string(),
989                Value::Map(BTreeMap::from([(
990                    "eu".to_string(),
991                    Value::Text("gold".into()),
992                )])),
993            ),
994            ("tier".to_string(), Value::Enum("eu".into())),
995            ("other".to_string(), Value::Enum("gold".into())),
996        ]);
997        let steps = BTreeMap::new();
998        let present = BTreeSet::new();
999        let s = Scope {
1000            constructed: None,
1001            now: Timestamp::from_ms(0).unwrap(),
1002            inputs: &inputs,
1003            steps: &steps,
1004            present: &present,
1005            item: None,
1006            v2: true,
1007        };
1008        assert_eq!(eval_predicate(&member("input:tier"), &s), Ok(true));
1009        assert_eq!(
1010            eval_predicate(&member("input:other"), &s),
1011            Ok(false),
1012            "a map's values are not its members"
1013        );
1014    }
1015
1016    #[test]
1017    fn a_reference_that_names_nothing_is_refused() {
1018        for r in ["item:", "input:", "step:", "bind:"] {
1019            assert!(Ref::parse(r).is_err(), "{r}");
1020        }
1021        let item = Value::Null;
1022        let inputs = BTreeMap::new();
1023        let steps = BTreeMap::new();
1024        let present = BTreeSet::new();
1025        let s = Scope {
1026            constructed: None,
1027            now: Timestamp::from_ms(0).unwrap(),
1028            inputs: &inputs,
1029            steps: &steps,
1030            present: &present,
1031            item: Some(&item),
1032            v2: true,
1033        };
1034        assert_eq!(eval_ref("item:a", &s), Err(Fault::Absent("a".into())));
1035    }
1036
1037    #[test]
1038    fn integers_and_decimals_do_not_mix_silently() {
1039        let inputs = BTreeMap::new();
1040        let steps = BTreeMap::new();
1041        let env = TyEnv {
1042            inputs: &inputs,
1043            steps: &steps,
1044            item: None,
1045            v2: false,
1046        };
1047        let mixed = Expr::Add {
1048            left: Box::new(Expr::Lit(L::Integer(1))),
1049            right: Box::new(Expr::Lit(L::Decimal(dec("1")))),
1050        };
1051        assert!(type_expr(&mixed, &env).is_err());
1052        let explicit = Expr::Add {
1053            left: Box::new(Expr::ToDecimal(Box::new(Expr::Lit(L::Integer(1))))),
1054            right: Box::new(Expr::Lit(L::Decimal(dec("1")))),
1055        };
1056        assert_eq!(type_expr(&explicit, &env), Ok(Ty::Decimal));
1057        let int_div = Expr::Div {
1058            left: Box::new(Expr::Lit(L::Integer(1))),
1059            right: Box::new(Expr::Lit(L::Integer(1))),
1060        };
1061        assert!(type_expr(&int_div, &env).is_err());
1062    }
1063
1064    #[test]
1065    fn enum_literals_must_be_declared_options() {
1066        let inputs: BTreeMap<String, Ty> = [(
1067            "tier".to_string(),
1068            Ty::Enum(vec!["free".into(), "enterprise".into()]),
1069        )]
1070        .into();
1071        let steps = BTreeMap::new();
1072        let env = TyEnv {
1073            inputs: &inputs,
1074            steps: &steps,
1075            item: None,
1076            v2: false,
1077        };
1078        let ok = Predicate::Cmp {
1079            left: Expr::Ref("input:tier".into()),
1080            op: CmpOp::Eq,
1081            right: Expr::Lit(L::Enum("enterprise".into())),
1082        };
1083        assert!(type_predicate(&ok, &env).is_ok());
1084        let bad = Predicate::Cmp {
1085            left: Expr::Ref("input:tier".into()),
1086            op: CmpOp::Eq,
1087            right: Expr::Lit(L::Enum("gold".into())),
1088        };
1089        assert!(type_predicate(&bad, &env).is_err());
1090        let text = Predicate::Cmp {
1091            left: Expr::Ref("input:tier".into()),
1092            op: CmpOp::Eq,
1093            right: Expr::Lit(L::Text("enterprise".into())),
1094        };
1095        assert!(type_predicate(&text, &env).is_err(), "text is not an enum");
1096        let ordered = Predicate::Cmp {
1097            left: Expr::Ref("input:tier".into()),
1098            op: CmpOp::Lt,
1099            right: Expr::Lit(L::Enum("free".into())),
1100        };
1101        assert!(
1102            type_predicate(&ordered, &env).is_err(),
1103            "enums have no order"
1104        );
1105    }
1106
1107    #[test]
1108    fn references_parse_strictly() {
1109        assert_eq!(
1110            Ref::parse("input:a.b"),
1111            Ok(Ref::Input("a.b".into(), vec![]))
1112        );
1113        assert_eq!(
1114            Ref::parse("input:a.b/c/d"),
1115            Ok(Ref::Input("a.b".into(), vec!["c".into(), "d".into()]))
1116        );
1117        assert_eq!(
1118            Ref::parse("item:price.amount"),
1119            Ok(Ref::Item(vec!["price".into(), "amount".into()]))
1120        );
1121        assert_eq!(Ref::parse("now"), Ok(Ref::Now));
1122        for bad in [
1123            "",
1124            "input:",
1125            "x:y",
1126            "item:a..b",
1127            "plain",
1128            "input:a//b",
1129            "input:/b",
1130        ] {
1131            assert!(Ref::parse(bad).is_err(), "{bad:?}");
1132        }
1133    }
1134}