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