1use 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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
14pub enum Ref {
15 Input(String, Vec<String>),
18 Step(String),
19 Item(Vec<String>),
21 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#[derive(Debug, Clone, PartialEq)]
63pub enum StepTy {
64 Exact(Ty),
65 Semantic(crate::sem::SemKind, Vec<String>),
68}
69
70pub struct TyEnv<'a> {
72 pub inputs: &'a BTreeMap<String, Ty>,
73 pub steps: &'a BTreeMap<String, StepTy>,
74 pub item: Option<&'a Ty>,
75 pub v2: bool,
79}
80
81pub 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
88pub fn split_item(path: Vec<String>) -> Vec<String> {
90 path.iter()
91 .flat_map(|p| p.split('/').map(String::from))
92 .collect()
93}
94
95pub 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
104pub 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
113pub 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
161pub 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
180pub 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
192pub 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 Ty::Nullable(inner) => inner.as_ref(),
267 t => t,
268 };
269 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
291pub 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
330pub 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, <) && operand_fits(right, <, &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 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#[derive(Debug, Clone, PartialEq, Eq)]
494pub enum Fault {
495 Invalid(String),
497 Missing(String),
499 Conflict(String),
501 Absent(String),
504}
505
506impl From<ArithError> for Fault {
507 fn from(e: ArithError) -> Self {
508 Fault::Invalid(e.to_string())
509 }
510}
511
512pub 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 pub v2: bool,
522}
523
524fn 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 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
612pub 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 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 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 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 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 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 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}