1use petgraph::algo::{is_cyclic_directed, toposort};
11use std::collections::HashMap;
12use std::time::Instant;
13use thiserror::Error;
14
15use crate::diag::Span;
16use crate::diag::{Diagnostic, Severity};
17use crate::ir::*;
18use crate::parser::{validate_patch_path, PathSeg};
19use crate::types::Unit;
20
21#[derive(Debug, Error)]
22pub enum EvalError {
23 #[error("cycle detected in dependency graph")]
24 CycleDetected,
25 #[error("missing root")]
26 MissingRoot,
27 #[error("node {0:?} not found")]
28 MissingNode(NodeId),
29 #[error("type error: {message}")]
30 TypeError {
31 message: String,
32 span: Option<Span>,
33 file: Option<String>,
34 },
35 #[error("patch error: {message}")]
39 PatchError {
40 message: String,
41 span: Option<Span>,
42 file: Option<String>,
43 },
44 #[error("secret used in unsafe context")]
45 SecretLeak { file: Option<String> },
46 #[error("validation failed")]
52 ValidationFailed { diagnostics: Vec<Diagnostic> },
53 #[error("evaluation budget exceeded: {kind} (limit {limit})")]
57 BudgetExceeded {
58 kind: BudgetKind,
59 limit: u64,
60 span: Option<Span>,
61 file: Option<String>,
62 },
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum BudgetKind {
69 Nodes,
70 WallClock,
71 Depth,
72 ArrayLength,
73 Keys,
74 ValueSize,
76}
77
78impl std::fmt::Display for BudgetKind {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 BudgetKind::Nodes => write!(f, "node budget"),
82 BudgetKind::WallClock => write!(f, "time budget"),
83 BudgetKind::Depth => write!(f, "depth budget"),
84 BudgetKind::ArrayLength => write!(f, "array budget"),
85 BudgetKind::Keys => write!(f, "key budget"),
86 BudgetKind::ValueSize => write!(f, "value size budget"),
87 }
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct Budget {
96 pub max_nodes: u64,
98 pub max_depth: u64,
100 pub max_array_len: u64,
102 pub max_keys: u64,
105 pub max_wall_ms: u64,
107 pub max_value_size: u64,
112 pub max_total_value_size: u64,
117}
118
119impl Default for Budget {
120 fn default() -> Self {
123 Self {
124 max_nodes: 100_000,
125 max_depth: 256,
126 max_array_len: 100_000,
127 max_keys: 100_000,
128 max_wall_ms: 100,
129 max_value_size: 10_000_000,
130 max_total_value_size: 100_000_000,
131 }
132 }
133}
134
135impl Budget {
136 pub fn wall_deadline(&self) -> Option<Instant> {
141 if self.max_wall_ms == 0 {
142 None
143 } else {
144 Instant::now().checked_add(std::time::Duration::from_millis(self.max_wall_ms))
145 }
146 }
147}
148
149impl EvalError {
150 pub fn code(&self) -> &'static str {
152 match self {
153 EvalError::CycleDetected => "S001",
154 EvalError::MissingRoot => "S002",
155 EvalError::MissingNode(_) => "S003",
156 EvalError::TypeError { .. } => "T001",
157 EvalError::PatchError { .. } => "E019",
158 EvalError::SecretLeak { .. } => "S004",
159 EvalError::BudgetExceeded { kind, .. } => match kind {
160 BudgetKind::Nodes => "X001",
161 BudgetKind::WallClock => "X002",
162 BudgetKind::Depth => "X003",
163 BudgetKind::ArrayLength => "X004",
164 BudgetKind::Keys => "X005",
165 BudgetKind::ValueSize => "X006",
166 },
167 EvalError::ValidationFailed { diagnostics } => {
168 match diagnostics.first().map(|d| d.code.as_str()) {
169 Some("T003") => "T003",
170 Some("T004") => "T004",
171 _ => "T002",
172 }
173 }
174 }
175 }
176
177 pub fn span(&self) -> Option<&Span> {
179 match self {
180 EvalError::TypeError { span, .. } | EvalError::PatchError { span, .. } => span.as_ref(),
181 EvalError::BudgetExceeded { span, .. } => span.as_ref(),
182 EvalError::ValidationFailed { diagnostics } => {
183 diagnostics.first().and_then(|d| d.span.as_ref())
184 }
185 _ => None,
186 }
187 }
188
189 pub fn file(&self) -> Option<&str> {
192 match self {
193 EvalError::TypeError { file, .. } | EvalError::PatchError { file, .. } => {
194 file.as_deref()
195 }
196 EvalError::SecretLeak { file } => file.as_deref(),
197 EvalError::BudgetExceeded { file, .. } => file.as_deref(),
198 EvalError::ValidationFailed { diagnostics } => {
199 diagnostics.first().and_then(|d| d.file.as_deref())
200 }
201 _ => None,
202 }
203 }
204
205 fn type_error(message: impl Into<String>, module: &Module, node: NodeId) -> Self {
208 EvalError::TypeError {
209 message: message.into(),
210 span: module.node_spans.get(&node).cloned(),
211 file: module.node_files.get(&node).cloned(),
212 }
213 }
214
215 pub(crate) fn budget_exceeded(
219 kind: BudgetKind,
220 limit: u64,
221 module: &Module,
222 node: Option<NodeId>,
223 ) -> Self {
224 EvalError::BudgetExceeded {
225 kind,
226 limit,
227 span: node.and_then(|n| module.node_spans.get(&n).cloned()),
228 file: node.and_then(|n| module.node_files.get(&n).cloned()),
229 }
230 }
231 fn patch_error(message: impl Into<String>, module: &Module, node: NodeId) -> Self {
234 EvalError::PatchError {
235 message: message.into(),
236 span: module.node_spans.get(&node).cloned(),
237 file: module.node_files.get(&node).cloned(),
238 }
239 }
240
241 fn secret_leak(module: &Module, node: NodeId) -> Self {
243 EvalError::SecretLeak {
244 file: module.node_files.get(&node).cloned(),
245 }
246 }
247}
248
249pub struct Engine {
250 pub resilient: bool,
251 pub profile_override: Option<String>,
254 pub budget: Budget,
257 pub env: std::collections::HashMap<String, String>,
262}
263
264impl Default for Engine {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270fn check_result_depth(result: &Value, max_depth: u64) -> Result<(), EvalError> {
276 let mut stack: Vec<(&Value, u64)> = vec![(result, 0)];
277 while let Some((value, depth)) = stack.pop() {
278 if depth > max_depth {
279 return Err(EvalError::BudgetExceeded {
280 kind: BudgetKind::Depth,
281 limit: max_depth,
282 span: None,
283 file: None,
284 });
285 }
286 match value {
287 Value::Table(map) => {
288 for v in map.values() {
289 stack.push((v, depth + 1));
290 }
291 }
292 Value::Array(elements) => {
293 for v in elements {
294 stack.push((v, depth + 1));
295 }
296 }
297 _ => {}
298 }
299 }
300 Ok(())
301}
302
303pub(crate) struct Accountant {
308 spent: u64,
309 limit: u64,
310}
311
312impl Accountant {
313 pub(crate) fn charge(
314 &mut self,
315 module: &Module,
316 node: Option<NodeId>,
317 amount: u64,
318 ) -> Result<(), EvalError> {
319 self.spent = self.spent.saturating_add(amount);
320 if self.limit != 0 && self.spent >= self.limit.saturating_add(1) {
324 return Err(EvalError::budget_exceeded(
325 BudgetKind::ValueSize,
326 self.limit,
327 module,
328 node,
329 ));
330 }
331 Ok(())
332 }
333}
334
335pub(crate) struct BudgetCx<'a> {
340 pub deadline: Option<Instant>,
341 pub wall_limit: u64,
342 pub account: &'a mut Accountant,
343}
344
345pub(crate) fn measure_value(value: &Value) -> u64 {
349 let mut total = 0u64;
350 let mut stack = vec![value];
351 while let Some(v) = stack.pop() {
352 total = total.saturating_add(1);
353 match v {
354 Value::String(s) | Value::Secret(s) => {
355 total = total.saturating_add(s.len() as u64);
356 }
357 Value::Array(items) => stack.extend(items.iter()),
358 Value::Table(map) => stack.extend(map.values()),
359 _ => {}
360 }
361 }
362 total
363}
364
365fn check_static_budgets(module: &Module, budget: &Budget) -> Result<(), EvalError> {
373 let over = |limit: u64, used: u64| limit != 0 && used > limit;
374 if over(budget.max_nodes, module.arena.len() as u64) {
375 return Err(EvalError::BudgetExceeded {
376 kind: BudgetKind::Nodes,
377 limit: budget.max_nodes,
378 span: None,
379 file: None,
380 });
381 }
382 if budget.max_array_len != 0 || budget.max_keys != 0 {
383 let mut keys = 0u64;
384 for (i, node) in module.arena.iter().enumerate() {
385 let id = NodeId(i as u32);
386 match node {
387 Node::Table { fields } => {
390 keys += fields.len() as u64;
391 if over(budget.max_keys, keys) {
392 return Err(EvalError::BudgetExceeded {
393 kind: BudgetKind::Keys,
394 limit: budget.max_keys,
395 span: None,
396 file: None,
397 });
398 }
399 }
400 Node::Array { elements } if over(budget.max_array_len, elements.len() as u64) => {
401 return Err(EvalError::budget_exceeded(
402 BudgetKind::ArrayLength,
403 budget.max_array_len,
404 module,
405 Some(id),
406 ));
407 }
408 _ => {}
409 }
410 }
411 }
412 Ok(())
413}
414
415fn check_dp_bounds(
428 module: &Module,
429 order: &[petgraph::graph::NodeIndex],
430 budget: &Budget,
431) -> Result<(u64, Vec<u64>), EvalError> {
432 if budget.max_value_size == 0 && budget.max_depth == 0 && budget.max_total_value_size == 0 {
433 return Ok((0, vec![1u64; module.arena.len()]));
434 }
435 let size_cap = if budget.max_value_size == 0 {
441 u64::MAX
442 } else {
443 budget.max_value_size.saturating_add(1)
444 };
445 let depth_cap = if budget.max_depth == 0 {
446 u64::MAX
447 } else {
448 budget.max_depth.saturating_add(1)
449 };
450 let mut size = vec![1u64; module.arena.len()];
451 let mut depth = vec![0u64; module.arena.len()];
452 let sat_size = |a: u64, b: u64| a.saturating_add(b).min(size_cap);
453 let mut total = 0u64;
458 for node_idx in order {
459 let id = module.graph[*node_idx];
460 let i = id.0 as usize;
461 if i >= size.len() {
462 continue;
463 }
464 let mut child_size = 0u64;
470 let mut child_depth = 0u64;
471 match module.get(id) {
472 Some(Node::Table { fields }) => {
473 for child in fields.values() {
474 let c = child.0 as usize;
475 if c < size.len() {
476 child_size = sat_size(child_size, size[c]);
477 child_depth = child_depth.max(depth[c]);
478 }
479 }
480 size[i] = sat_size(1, child_size);
481 depth[i] = child_depth.saturating_add(1).min(depth_cap);
482 }
483 Some(Node::Array { elements }) => {
484 for child in elements {
485 let c = child.0 as usize;
486 if c < size.len() {
487 child_size = sat_size(child_size, size[c]);
488 child_depth = child_depth.max(depth[c]);
489 }
490 }
491 size[i] = sat_size(1, child_size);
492 depth[i] = child_depth.saturating_add(1).min(depth_cap);
493 }
494 Some(Node::Ref(target)) => {
495 let c = target.0 as usize;
496 if c < size.len() {
497 size[i] = size[c];
498 depth[i] = depth[c];
499 }
500 }
501 Some(Node::Literal(Value::String(s))) => {
502 size[i] = sat_size(1, s.len() as u64);
503 }
504 Some(Node::Interpolate { parts }) => {
505 for part in parts {
506 match part {
507 InterpPart::Expr(e) => {
508 let c = e.0 as usize;
509 if c < size.len() {
510 child_size = sat_size(child_size, size[c]);
511 child_depth = child_depth.max(depth[c]);
512 }
513 }
514 InterpPart::Literal(s) => {
515 child_size = sat_size(child_size, s.len() as u64);
516 }
517 }
518 }
519 size[i] = sat_size(1, child_size);
520 }
521 Some(Node::Binary { lhs, rhs, .. }) => {
522 for child in [*lhs, *rhs] {
523 let c = child.0 as usize;
524 if c < size.len() {
525 child_size = sat_size(child_size, size[c]);
526 }
527 }
528 size[i] = sat_size(1, child_size);
529 }
530 Some(Node::Unary { expr, .. }) => {
531 let c = expr.0 as usize;
532 if c < size.len() {
533 size[i] = sat_size(1, size[c]);
534 }
535 }
536 Some(Node::Semantic { inner, .. }) => {
539 let c = inner.0 as usize;
540 if c < size.len() {
541 size[i] = size[c];
542 depth[i] = depth[c];
543 }
544 }
545 _ => {}
546 }
547 if budget.max_value_size != 0 && size[i] >= size_cap {
551 return Err(EvalError::budget_exceeded(
552 BudgetKind::ValueSize,
553 budget.max_value_size,
554 module,
555 Some(id),
556 ));
557 }
558 if budget.max_depth != 0 && depth[i] >= depth_cap {
559 return Err(EvalError::budget_exceeded(
560 BudgetKind::Depth,
561 budget.max_depth,
562 module,
563 Some(id),
564 ));
565 }
566 total = total.saturating_add(size[i]);
567 if budget.max_total_value_size != 0
568 && total >= budget.max_total_value_size.saturating_add(1)
569 {
570 return Err(EvalError::budget_exceeded(
571 BudgetKind::ValueSize,
572 budget.max_total_value_size,
573 module,
574 Some(id),
575 ));
576 }
577 }
578 Ok((total, size))
579}
580
581pub struct IncrementalMemo {
595 map: HashMap<u64, (Value, Vec<Diagnostic>)>,
596 config: Option<(bool, Option<String>, Budget)>,
597 salt: u64,
598 pub last_eval_count: u64,
599 pub last_memo_hits: u64,
600}
601
602impl std::fmt::Debug for IncrementalMemo {
603 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
604 f.debug_struct("IncrementalMemo")
605 .field("entries", &self.map.len())
606 .field("last_eval_count", &self.last_eval_count)
607 .field("last_memo_hits", &self.last_memo_hits)
608 .finish()
609 }
610}
611
612impl Default for IncrementalMemo {
613 fn default() -> Self {
614 Self {
615 map: HashMap::new(),
616 config: None,
617 salt: memo_salt(),
618 last_eval_count: 0,
619 last_memo_hits: 0,
620 }
621 }
622}
623
624fn memo_salt() -> u64 {
629 let mut buf = [0u8; 8];
630 if getrandom::getrandom(&mut buf).is_ok() {
631 u64::from_le_bytes(buf)
632 } else {
633 0x9E37_79B9_7F4A_7C15
634 }
635}
636
637struct FpHasher(u64);
649
650impl FpHasher {
651 fn new(salt: u64) -> Self {
652 Self(14_695_981_039_346_656_037 ^ salt)
658 }
659}
660
661impl std::hash::Hasher for FpHasher {
662 fn write(&mut self, bytes: &[u8]) {
663 for &byte in bytes {
664 self.0 ^= u64::from(byte);
665 self.0 = self.0.wrapping_mul(1_099_511_628_211);
666 }
667 }
668
669 fn finish(&self) -> u64 {
670 self.0
671 }
672}
673
674fn fp_value(hasher: &mut FpHasher, value: &Value) {
681 use std::hash::Hash;
682 match value {
683 Value::Null => 0u8.hash(hasher),
684 Value::Bool(flag) => {
685 1u8.hash(hasher);
686 flag.hash(hasher);
687 }
688 Value::Int(num) => {
689 2u8.hash(hasher);
690 num.hash(hasher);
691 }
692 Value::Float(num) => {
693 3u8.hash(hasher);
694 num.to_bits().hash(hasher);
695 }
696 Value::String(text) => {
697 4u8.hash(hasher);
698 text.hash(hasher);
699 }
700 Value::DurationMs(millis) => {
701 5u8.hash(hasher);
702 millis.hash(hasher);
703 }
704 Value::Quantity(amount, unit) => {
705 6u8.hash(hasher);
706 amount.to_bits().hash(hasher);
707 unit.hash(hasher);
708 }
709 Value::Array(items) => {
710 7u8.hash(hasher);
711 items.len().hash(hasher);
712 for item in items {
713 fp_value(hasher, item);
714 }
715 }
716 Value::Table(fields) => {
717 8u8.hash(hasher);
718 fields.len().hash(hasher);
719 for (key, item) in fields {
720 key.hash(hasher);
721 fp_value(hasher, item);
722 }
723 }
724 Value::Secret(text) => {
727 9u8.hash(hasher);
728 text.hash(hasher);
729 }
730 }
731}
732
733fn fingerprint_one(
746 engine: &Engine,
747 module: &Module,
748 id: NodeId,
749 cache: &[Option<u64>],
750 salt: u64,
751) -> Option<u64> {
752 use std::hash::{Hash, Hasher};
753 let child = |dep: &NodeId| cache.get(dep.0 as usize).copied().flatten();
754 let mut hasher = FpHasher::new(salt);
755 match module.get(id)? {
756 Node::Literal(v) => {
757 0u8.hash(&mut hasher);
758 fp_value(&mut hasher, v);
759 }
760 Node::Env { name, default } => {
761 1u8.hash(&mut hasher);
762 name.hash(&mut hasher);
763 engine.env.get(name).hash(&mut hasher);
764 match default {
765 Some(d) => child(d)?.hash(&mut hasher),
766 None => 0u8.hash(&mut hasher),
767 }
768 }
769 Node::Secret { locator } => {
770 2u8.hash(&mut hasher);
771 child(locator)?.hash(&mut hasher);
772 }
773 Node::Ref(target) => {
774 3u8.hash(&mut hasher);
775 child(target)?.hash(&mut hasher);
776 }
777 Node::Ident(name) => {
778 4u8.hash(&mut hasher);
779 name.hash(&mut hasher);
780 }
781 Node::Interpolate { parts } => {
782 5u8.hash(&mut hasher);
783 parts.len().hash(&mut hasher);
784 for part in parts {
785 match part {
786 crate::ir::InterpPart::Literal(s) => {
787 0u8.hash(&mut hasher);
788 s.hash(&mut hasher);
789 }
790 crate::ir::InterpPart::Expr(e) => {
791 1u8.hash(&mut hasher);
792 child(e)?.hash(&mut hasher);
793 }
794 }
795 }
796 }
797 Node::Binary { op, lhs, rhs } => {
798 6u8.hash(&mut hasher);
799 op.hash(&mut hasher);
800 child(lhs)?.hash(&mut hasher);
801 child(rhs)?.hash(&mut hasher);
802 }
803 Node::Unary { op, expr } => {
804 7u8.hash(&mut hasher);
805 op.hash(&mut hasher);
806 child(expr)?.hash(&mut hasher);
807 }
808 Node::Array { elements } => {
809 8u8.hash(&mut hasher);
810 elements.len().hash(&mut hasher);
811 for element in elements {
812 child(element)?.hash(&mut hasher);
813 }
814 }
815 Node::Table { fields } => {
816 9u8.hash(&mut hasher);
817 fields.len().hash(&mut hasher);
818 for (key, child_id) in fields {
819 key.hash(&mut hasher);
820 child(child_id)?.hash(&mut hasher);
821 }
822 }
823 Node::Quantity { value, unit } => {
824 10u8.hash(&mut hasher);
825 unit.hash(&mut hasher);
826 child(value)?.hash(&mut hasher);
827 }
828 Node::Semantic { tag, inner } => {
829 11u8.hash(&mut hasher);
830 tag.as_str().hash(&mut hasher);
831 child(inner)?.hash(&mut hasher);
832 }
833 _ => return None,
834 }
835 Some(hasher.finish())
836}
837
838impl Engine {
839 pub fn new() -> Self {
840 Self {
841 resilient: true,
842 profile_override: None,
843 budget: Budget::default(),
844 env: std::env::vars_os()
849 .filter_map(|(key, value)| {
850 Some((key.into_string().ok()?, value.into_string().ok()?))
851 })
852 .collect(),
853 }
854 }
855
856 pub fn with_env(mut self, env: std::collections::HashMap<String, String>) -> Self {
858 self.env = env;
859 self
860 }
861
862 pub fn evaluate(&self, module: &Module) -> Result<(Value, Vec<Diagnostic>), EvalError> {
863 self.evaluate_inner(module, None)
864 }
865
866 pub fn evaluate_incremental(
877 &self,
878 module: &Module,
879 memo: &mut IncrementalMemo,
880 ) -> Result<(Value, Vec<Diagnostic>), EvalError> {
881 let token = (self.resilient, self.profile_override.clone(), self.budget);
884 if memo.config.as_ref() != Some(&token) {
885 memo.map.clear();
886 memo.config = Some(token);
887 }
888 memo.last_eval_count = 0;
889 memo.last_memo_hits = 0;
890 let outcome = self.evaluate_inner(module, Some(memo));
891 let cap = module.arena.len().saturating_mul(4).max(1024);
899 if memo.map.len() > cap {
900 memo.map.clear();
901 }
902 outcome
903 }
904
905 fn evaluate_inner(
906 &self,
907 module: &Module,
908 mut memo: Option<&mut IncrementalMemo>,
909 ) -> Result<(Value, Vec<Diagnostic>), EvalError> {
910 let deadline = self.budget.wall_deadline();
914 check_static_budgets(module, &self.budget)?;
916 if is_cyclic_directed(&module.graph) {
917 return Err(EvalError::CycleDetected);
918 }
919
920 let order = toposort(&module.graph, None).map_err(|_| EvalError::CycleDetected)?;
921 let (dp_total, dp_bounds) = check_dp_bounds(module, &order, &self.budget)?;
927 let mut account = Accountant {
928 spent: dp_total,
929 limit: self.budget.max_total_value_size,
930 };
931 let mut actuals: HashMap<NodeId, u64> = HashMap::new();
937 let mut values: HashMap<NodeId, Value> = HashMap::new();
938 let mut diagnostics = module.diagnostics.clone();
939 diagnostics.extend(module.healing.iter().cloned());
940 let mut visited = 0u64;
945
946 let mut fp_cache: Vec<Option<u64>> = if memo.is_some() {
954 vec![None; module.arena.len()]
955 } else {
956 Vec::new()
957 };
958 for node_idx in order {
959 visited += 1;
960 if visited % 1024 == 0 {
961 if let Some(end) = deadline {
962 if Instant::now() >= end {
963 let node_id = module.graph[node_idx];
964 return Err(EvalError::budget_exceeded(
965 BudgetKind::WallClock,
966 self.budget.max_wall_ms,
967 module,
968 Some(node_id),
969 ));
970 }
971 }
972 }
973 let node_id = module.graph[node_idx];
974 let node = module.get(node_id).ok_or(EvalError::MissingNode(node_id))?;
975 let mut memo_fp = None;
988 if let Some(memo) = memo.as_mut() {
989 let salt = memo.salt;
990 if let Some(fp) = fingerprint_one(self, module, node_id, &fp_cache, salt) {
991 if let Ok(index) = usize::try_from(node_id.0) {
992 if let Some(slot) = fp_cache.get_mut(index) {
993 *slot = Some(fp);
994 }
995 }
996 memo_fp = Some(fp);
997 if let Some((value, diags)) = memo.map.get(&fp) {
998 values.insert(node_id, value.clone());
999 let span = module.node_spans.get(&node_id).cloned();
1000 let file = module.node_files.get(&node_id).cloned();
1001 diagnostics.extend(diags.iter().map(|d| {
1002 let mut fresh = d.clone();
1003 fresh.span = span.clone();
1008 fresh.file = file.clone();
1009 fresh
1010 }));
1011 memo.last_memo_hits += 1;
1012 continue;
1013 }
1014 }
1015 }
1016
1017 let diag_mark = diagnostics.len();
1018 let value = match node {
1019 Node::Literal(v) => v.clone(),
1020
1021 Node::Env { name, default } => match self.env.get(name) {
1022 Some(s) => {
1023 actuals.insert(node_id, 1u64.saturating_add(s.len() as u64));
1027 account.charge(module, Some(node_id), s.len() as u64)?;
1028 Value::String(s.clone())
1029 }
1030 None => {
1031 if let Some(def) = default {
1032 values.get(def).cloned().unwrap_or(Value::Null)
1033 } else {
1034 Value::Null
1035 }
1036 }
1037 },
1038
1039 Node::Secret { locator } => {
1040 let loc = values.get(locator).cloned().unwrap_or(Value::Null);
1041 let secret = match loc {
1042 Value::String(s) => Value::Secret(s),
1043 _ => Value::Secret(String::new()),
1044 };
1045 if let Value::Secret(s) = &secret {
1049 let actual = 1u64.saturating_add(s.len() as u64);
1050 actuals.insert(node_id, actual);
1051 account.charge(module, Some(node_id), s.len() as u64)?;
1052 }
1053 secret
1054 }
1055
1056 Node::Ref(id) => {
1057 let v = values.get(id).cloned().unwrap_or(Value::Null);
1058 if let Some(actual) = actuals.get(id) {
1063 let bound = dp_bounds.get(id.0 as usize).copied().unwrap_or(1);
1064 account.charge(module, Some(node_id), actual.saturating_sub(bound))?;
1065 }
1066 v
1067 }
1068
1069 Node::Semantic { inner, .. } => values.get(inner).cloned().unwrap_or(Value::Null),
1071
1072 Node::SchemaField { .. } | Node::SchemaTable { .. } | Node::SchemaArray { .. } => {
1077 Value::Null
1078 }
1079 Node::Ident(name) => {
1080 diagnostics.push(Diagnostic {
1081 severity: Severity::Warning,
1082 code: "W001".into(),
1083 message: format!("unresolved identifier '{}'", name),
1084 path: Some(name.clone()),
1085 span: module.node_spans.get(&node_id).cloned(),
1086 repair: None,
1087 confidence: 1.0,
1088 file: module.node_files.get(&node_id).cloned(),
1089 });
1090 Value::Null
1091 }
1092
1093 Node::Interpolate { parts } => {
1094 let mut out = String::new();
1095 for part in parts {
1096 match part {
1097 InterpPart::Literal(s) => out.push_str(s),
1098 InterpPart::Expr(id) => {
1099 if let Some(v) = values.get(id) {
1100 match v {
1101 Value::String(s) => out.push_str(s),
1102 Value::Int(i) => out.push_str(&i.to_string()),
1103 Value::Float(f) => out.push_str(&f.to_string()),
1104 Value::Bool(b) => out.push_str(&b.to_string()),
1105 Value::DurationMs(ms) => out.push_str(&format!("{}ms", ms)),
1106 Value::Quantity(n, unit) => {
1107 out.push_str(&unit.format_amount(*n))
1108 }
1109 Value::Secret(_) => {
1110 return Err(EvalError::secret_leak(module, node_id));
1111 }
1112 Value::Null => out.push_str("null"),
1113 Value::Array(_) | Value::Table(_) => {
1117 return Err(EvalError::type_error(
1118 format!(
1119 "cannot interpolate a {} into a string",
1120 match v {
1121 Value::Array(_) => "array",
1122 _ => "table",
1123 }
1124 ),
1125 module,
1126 node_id,
1127 ));
1128 }
1129 }
1130 } else {
1131 out.push_str("null");
1132 }
1133 }
1134 }
1135 }
1136 Value::String(out)
1137 }
1138
1139 Node::Binary { op, lhs, rhs } => {
1140 let l = values.get(lhs).cloned().unwrap_or(Value::Null);
1141 let r = values.get(rhs).cloned().unwrap_or(Value::Null);
1142 eval_binary(*op, &l, &r)
1143 .map_err(|m| EvalError::type_error(m, module, node_id))?
1144 }
1145
1146 Node::Unary { op, expr } => {
1147 let v = values.get(expr).cloned().unwrap_or(Value::Null);
1148 eval_unary(*op, &v).map_err(|m| EvalError::type_error(m, module, node_id))?
1149 }
1150
1151 Node::Array { elements } => {
1152 let els: Vec<Value> = elements
1153 .iter()
1154 .map(|e| values.get(e).cloned().unwrap_or(Value::Null))
1155 .collect();
1156 Value::Array(els)
1157 }
1158
1159 Node::Table { fields } => {
1160 let mut map = indexmap::IndexMap::new();
1161 for (k, id) in fields {
1162 map.insert(k.clone(), values.get(id).cloned().unwrap_or(Value::Null));
1163 }
1164 Value::Table(map)
1165 }
1166
1167 Node::Quantity { value, unit } => {
1168 let v = values.get(value).cloned().unwrap_or(Value::Null);
1169 normalize_quantity(&v, unit)
1170 .map_err(|m| EvalError::type_error(m, module, node_id))?
1171 }
1172
1173 _ => Value::Null,
1174 };
1175 if let Some(memo) = memo.as_mut() {
1178 memo.last_eval_count += 1;
1179 if let Some(fp) = memo_fp {
1180 let fresh = diagnostics[diag_mark..].to_vec();
1181 memo.map.insert(fp, (value.clone(), fresh));
1182 }
1183 }
1184
1185 values.insert(node_id, value);
1186 }
1187
1188 let root = module.root.ok_or(EvalError::MissingRoot)?;
1189 let result = values.get(&root).cloned().unwrap_or(Value::Null);
1190 let result = self.apply_profile(module, &values, result, &mut diagnostics, &mut account)?;
1191 let mut result = result;
1192 self.apply_patches(module, &values, &mut result, &mut diagnostics, &mut account)?;
1193 if !module.patches.is_empty() && self.budget.max_depth != 0 {
1199 check_result_depth(&result, self.budget.max_depth)?;
1200 }
1201 self.validate_schema(
1209 module,
1210 &values,
1211 &mut result,
1212 &mut diagnostics,
1213 BudgetCx {
1214 deadline,
1215 wall_limit: self.budget.max_wall_ms,
1216 account: &mut account,
1217 },
1218 )?;
1219 self.validate_annotations(
1220 module,
1221 &values,
1222 &result,
1223 &mut diagnostics,
1224 deadline,
1225 self.budget.max_wall_ms,
1226 )?;
1227 if let Some(end) = deadline {
1230 if Instant::now() >= end {
1231 return Err(EvalError::budget_exceeded(
1232 BudgetKind::WallClock,
1233 self.budget.max_wall_ms,
1234 module,
1235 None,
1236 ));
1237 }
1238 }
1239 Ok((result, diagnostics))
1240 }
1241
1242 fn check_immutable(
1248 module: &Module,
1249 patch_id: NodeId,
1250 targets: &[&str],
1251 ) -> Result<(), EvalError> {
1252 for target in targets {
1253 if let Some(immutable) = Self::immutable_overlap(module, target) {
1254 return Err(EvalError::patch_error(
1255 format!("patch target `{target}` overlaps ai_immutable `{immutable}`"),
1256 module,
1257 patch_id,
1258 ));
1259 }
1260 }
1261 Ok(())
1262 }
1263
1264 fn immutable_overlap(module: &Module, target: &str) -> Option<String> {
1271 let target_segs = validate_patch_path(target).ok()?;
1272 let mut paths: Vec<&String> = module
1273 .annotations
1274 .values()
1275 .flatten()
1276 .filter(|a| matches!(&a.kind, crate::ir::AnnotationKind::AiImmutable(true)))
1277 .map(|a| &a.path)
1278 .collect();
1279 paths.sort();
1280 for apath in paths {
1281 let Ok(asegs) = validate_patch_path(apath) else {
1282 continue;
1283 };
1284 let shared = target_segs.len().min(asegs.len());
1285 if shared > 0 && target_segs[..shared] == asegs[..shared] {
1286 return Some(apath.clone());
1287 }
1288 }
1289 None
1290 }
1291 fn apply_patches(
1297 &self,
1298 module: &Module,
1299 values: &HashMap<NodeId, Value>,
1300 result: &mut Value,
1301 diagnostics: &mut Vec<Diagnostic>,
1302 account: &mut Accountant,
1303 ) -> Result<(), EvalError> {
1304 use crate::ir::PatchOp;
1305
1306 for &patch_id in &module.patches {
1307 let (op, span, file) = match module.get(patch_id) {
1308 Some(Node::PatchDirective { op }) => (
1309 op.clone(),
1310 module.node_spans.get(&patch_id).cloned(),
1311 module.node_files.get(&patch_id).cloned(),
1312 ),
1313 _ => continue,
1314 };
1315 let mut warn = |code: &str, message: String| {
1316 diagnostics.push(Diagnostic {
1317 severity: Severity::Warning,
1318 code: code.into(),
1319 message,
1320 path: None,
1321 span: span.clone(),
1322 repair: None,
1323 confidence: 1.0,
1324 file: file.clone(),
1325 });
1326 };
1327 match op {
1328 PatchOp::Set { path, value } => {
1329 Self::check_immutable(module, patch_id, &[&path])?;
1330 let segs = validate_patch_path(&path).map_err(|why| {
1331 EvalError::patch_error(
1332 format!("invalid patch path `{path}`: {why}"),
1333 module,
1334 patch_id,
1335 )
1336 })?;
1337 let v = values.get(&value).cloned().unwrap_or(Value::Null);
1338 account.charge(
1343 module,
1344 Some(patch_id),
1345 measure_value(&v).saturating_add(segs.len() as u64),
1346 )?;
1347 let snapshot = result.clone();
1351 if !patch_set(result, &segs, v) {
1352 *result = snapshot;
1353 warn(
1354 "W004",
1355 format!("patch target `{path}` is blocked or out of bounds"),
1356 );
1357 }
1358 }
1359 PatchOp::Remove { path } => {
1360 Self::check_immutable(module, patch_id, &[&path])?;
1361 let segs = validate_patch_path(&path).map_err(|why| {
1362 EvalError::patch_error(
1363 format!("invalid patch path `{path}`: {why}"),
1364 module,
1365 patch_id,
1366 )
1367 })?;
1368 if !patch_remove(result, &segs) {
1369 warn("W004", format!("remove target `{path}` does not exist"));
1370 }
1371 }
1372 PatchOp::Rename { from, to } => {
1373 Self::check_immutable(module, patch_id, &[&from, &to])?;
1374 let segs = validate_patch_path(&from).map_err(|why| {
1375 EvalError::patch_error(
1376 format!("invalid patch path `{from}`: {why}"),
1377 module,
1378 patch_id,
1379 )
1380 })?;
1381 match patch_rename(result, &segs, &to) {
1382 RenameOutcome::Ok => {}
1383 RenameOutcome::Missing => {
1384 warn("W004", format!("rename source `{from}` does not exist"));
1385 }
1386 RenameOutcome::Exists => {
1387 return Err(EvalError::patch_error(
1388 format!("rename target `{to}` already exists"),
1389 module,
1390 patch_id,
1391 ));
1392 }
1393 }
1394 }
1395 }
1396 }
1397 Ok(())
1398 }
1399
1400 fn apply_profile(
1407 &self,
1408 module: &Module,
1409 values: &HashMap<NodeId, Value>,
1410 result: Value,
1411 diagnostics: &mut Vec<Diagnostic>,
1412 account: &mut Accountant,
1413 ) -> Result<Value, EvalError> {
1414 if module.profiles.is_empty() {
1415 let selected: Option<String> = match &self.profile_override {
1418 Some(name) => Some(name.clone()),
1419 None => match &result {
1420 Value::Table(map) => match map.get("active_profile") {
1421 Some(Value::String(s)) => Some(s.clone()),
1422 Some(_) => {
1423 diagnostics.push(Diagnostic {
1424 severity: Severity::Warning,
1425 code: "W002".into(),
1426 message: "active_profile must be a string".into(),
1427 path: Some("active_profile".into()),
1428 span: if self.profile_override.is_some() {
1429 None
1430 } else {
1431 active_key_span(module, &result)
1432 },
1433 repair: None,
1434 confidence: 1.0,
1435 file: if self.profile_override.is_some() {
1436 None
1437 } else {
1438 active_key_file(module, &result)
1439 },
1440 });
1441 return Ok(result);
1442 }
1443 None => None,
1444 },
1445 _ => None,
1446 },
1447 };
1448 if let Some(name) = selected {
1449 diagnostics.push(Diagnostic {
1451 severity: Severity::Warning,
1452 code: "W002".into(),
1453 message: format!("unknown profile '{name}'"),
1454 path: Some("active_profile".into()),
1455 span: if self.profile_override.is_some() {
1456 None
1457 } else {
1458 active_key_span(module, &result)
1459 },
1460 repair: None,
1461 confidence: 1.0,
1462 file: if self.profile_override.is_some() {
1463 None
1464 } else {
1465 active_key_file(module, &result)
1466 },
1467 });
1468 }
1469 return Ok(result);
1470 }
1471 let base_table = match &result {
1472 Value::Table(map) => map.clone(),
1473 _ => return Ok(result),
1474 };
1475 let selected: Option<String> = match &self.profile_override {
1476 Some(name) => Some(name.clone()),
1477 None => match base_table.get("active_profile") {
1478 None => return Ok(result),
1479 Some(Value::String(s)) => Some(s.clone()),
1480 Some(other) => {
1481 diagnostics.push(Diagnostic {
1482 severity: Severity::Warning,
1483 code: "W002".into(),
1484 message: format!(
1485 "active_profile must be a string, got {}",
1486 value_kind_name(other)
1487 ),
1488 path: Some("active_profile".into()),
1489 span: if self.profile_override.is_some() {
1490 None
1491 } else {
1492 active_key_span(module, &result)
1493 },
1494 repair: None,
1495 confidence: 1.0,
1496 file: if self.profile_override.is_some() {
1497 None
1498 } else {
1499 active_key_file(module, &result)
1500 },
1501 });
1502 return Ok(result);
1503 }
1504 },
1505 };
1506 let name = match selected {
1507 Some(n) => n,
1508 None => return Ok(result),
1509 };
1510 let Some(&profile_id) = module.profiles.get(&name) else {
1511 diagnostics.push(Diagnostic {
1512 severity: Severity::Warning,
1513 code: "W002".into(),
1514 message: format!("unknown profile '{name}'"),
1515 path: Some("active_profile".into()),
1516 span: if self.profile_override.is_some() {
1517 None
1518 } else {
1519 active_key_span(module, &result)
1520 },
1521 repair: None,
1522 confidence: 1.0,
1523 file: if self.profile_override.is_some() {
1524 None
1525 } else {
1526 active_key_file(module, &result)
1527 },
1528 });
1529 return Ok(result);
1530 };
1531 let profile_value = values.get(&profile_id).cloned().unwrap_or(Value::Null);
1532 account.charge(module, Some(profile_id), measure_value(&profile_value))?;
1536 let mut merged = base_table;
1537 merge_profile_value(
1538 &mut merged,
1539 profile_value,
1540 &name,
1541 String::new(),
1542 module.node_spans.get(&profile_id).cloned(),
1543 module.node_files.get(&profile_id).cloned(),
1544 diagnostics,
1545 );
1546 Ok(Value::Table(merged))
1547 }
1548}
1549
1550fn active_key_span(module: &Module, result: &Value) -> Option<crate::diag::Span> {
1552 let Value::Table(_) = result else {
1553 return None;
1554 };
1555 let root = module.root?;
1556 let Node::Table { fields } = module.get(root)? else {
1557 return None;
1558 };
1559 let id = fields.get("active_profile")?;
1560 module.node_spans.get(id).cloned()
1561}
1562
1563fn active_key_file(module: &Module, result: &Value) -> Option<String> {
1565 let Value::Table(_) = result else {
1566 return None;
1567 };
1568 let root = module.root?;
1569 let Node::Table { fields } = module.get(root)? else {
1570 return None;
1571 };
1572 let id = fields.get("active_profile")?;
1573 module.node_files.get(id).cloned()
1574}
1575
1576pub(crate) fn value_kind_name(v: &Value) -> String {
1580 match v {
1581 Value::Null => "null".into(),
1582 Value::Bool(_) => "boolean".into(),
1583 Value::Int(_) | Value::Float(_) => "number".into(),
1584 Value::String(_) => "string".into(),
1585 Value::DurationMs(_) => "duration".into(),
1586 Value::Quantity(_, unit) => format!("quantity:{}", unit.suffix()),
1587 Value::Array(_) => "array".into(),
1588 Value::Table(_) => "table".into(),
1589 Value::Secret(_) => "secret".into(),
1590 }
1591}
1592
1593fn merge_profile_value(
1599 base: &mut indexmap::IndexMap<String, Value>,
1600 overlay: Value,
1601 profile: &str,
1602 path: String,
1603 span: Option<crate::diag::Span>,
1604 file: Option<String>,
1605 diagnostics: &mut Vec<Diagnostic>,
1606) {
1607 let Value::Table(fields) = overlay else {
1608 return;
1609 };
1610 for (k, v) in fields {
1611 let child = if path.is_empty() {
1612 k.clone()
1613 } else {
1614 format!("{path}.{k}")
1615 };
1616 match base.get(&k) {
1617 None => {
1618 base.insert(k, v);
1619 }
1620 Some(existing) => match (existing, &v) {
1621 (Value::Table(_), Value::Table(_)) => {
1622 if let Some(Value::Table(base_sub)) = base.get_mut(&k) {
1623 merge_profile_value(
1624 base_sub,
1625 v,
1626 profile,
1627 child,
1628 span.clone(),
1629 file.clone(),
1630 diagnostics,
1631 );
1632 }
1633 }
1634 _ => {
1635 if value_kind_name(existing) != value_kind_name(&v) {
1636 diagnostics.push(Diagnostic {
1637 severity: Severity::Warning,
1638 code: "W003".into(),
1639 message: format!(
1640 "profile '{profile}' overrides '{child}' with a different type"
1641 ),
1642 path: Some(child),
1643 span: span.clone(),
1644 repair: None,
1645 confidence: 1.0,
1646 file: file.clone(),
1647 });
1648 }
1649 base.insert(k, v);
1650 }
1651 },
1652 }
1653 }
1654}
1655
1656#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1658enum RenameOutcome {
1659 Ok,
1660 Missing,
1661 Exists,
1662}
1663
1664fn patch_parent<'a>(root: &'a mut Value, segs: &[PathSeg], create: bool) -> Option<&'a mut Value> {
1672 let mut cur = root;
1673 let last = segs.len().checked_sub(1)?;
1674 let mut i = 0;
1675 while i < last {
1676 match &segs[i] {
1677 PathSeg::Key(k) => {
1678 let need_array = matches!(segs.get(i + 1), Some(PathSeg::Index(_)));
1679 cur = match cur {
1680 Value::Table(map) => {
1681 if !create && !map.contains_key(k) {
1682 return None;
1683 }
1684 map.entry(k.clone()).or_insert_with(|| {
1685 if need_array {
1686 Value::Array(Vec::new())
1687 } else {
1688 Value::Table(indexmap::IndexMap::new())
1689 }
1690 })
1691 }
1692 _ => return None,
1693 };
1694 }
1695 PathSeg::Index(idx) => {
1696 cur = match cur {
1697 Value::Array(items) => items.get_mut(*idx)?,
1698 _ => return None,
1699 };
1700 }
1701 }
1702 i += 1;
1703 }
1704 Some(cur)
1705}
1706
1707fn patch_set(root: &mut Value, segs: &[PathSeg], value: Value) -> bool {
1711 let Some(last) = segs.last() else {
1712 return false;
1713 };
1714 let Some(parent) = patch_parent(root, segs, true) else {
1715 return false;
1716 };
1717 match (parent, last) {
1718 (Value::Table(map), PathSeg::Key(k)) => {
1719 map.insert(k.clone(), value);
1720 true
1721 }
1722 (Value::Array(items), PathSeg::Index(i)) => {
1723 if let Some(slot) = items.get_mut(*i) {
1724 *slot = value;
1725 true
1726 } else {
1727 false
1728 }
1729 }
1730 _ => false,
1731 }
1732}
1733
1734fn patch_remove(root: &mut Value, segs: &[PathSeg]) -> bool {
1737 let Some(last) = segs.last() else {
1738 return false;
1739 };
1740 let Some(parent) = patch_parent(root, segs, false) else {
1741 return false;
1742 };
1743 match (parent, last) {
1744 (Value::Table(map), PathSeg::Key(k)) => map.shift_remove(k).is_some(),
1745 (Value::Array(items), PathSeg::Index(i)) if *i < items.len() => {
1746 items.remove(*i);
1747 true
1748 }
1749 _ => false,
1750 }
1751}
1752
1753fn patch_rename(root: &mut Value, segs: &[PathSeg], new_name: &str) -> RenameOutcome {
1758 let Some(last) = segs.last() else {
1759 return RenameOutcome::Missing;
1760 };
1761 if !matches!(last, PathSeg::Key(_)) {
1763 return RenameOutcome::Missing;
1764 }
1765 let Some(parent) = patch_parent(root, segs, false) else {
1766 return RenameOutcome::Missing;
1767 };
1768 let Value::Table(map) = parent else {
1769 return RenameOutcome::Missing;
1770 };
1771 let PathSeg::Key(old) = last else {
1772 return RenameOutcome::Missing;
1773 };
1774 if !map.contains_key(old) {
1775 return RenameOutcome::Missing;
1776 }
1777 if map.contains_key(new_name) {
1778 return RenameOutcome::Exists;
1779 }
1780 match map.shift_remove(old) {
1781 Some(v) => {
1782 map.insert(new_name.to_string(), v);
1783 RenameOutcome::Ok
1784 }
1785 None => RenameOutcome::Missing,
1786 }
1787}
1788
1789fn normalize_quantity(v: &Value, unit: &Unit) -> Result<Value, String> {
1790 let num = match v {
1791 Value::Int(n) => *n as f64,
1792 Value::Float(f) => *f,
1793 _ => {
1794 return Err(format!("quantity requires numeric value, got {v:?}"));
1795 }
1796 };
1797
1798 if let Some(ms) = unit.normalize_time_to_ms(num) {
1799 return Ok(Value::DurationMs(ms));
1800 }
1801 if let Some(bytes) = unit.normalize_to_bytes(num) {
1802 return Ok(Value::Int(i64::try_from(bytes).unwrap_or(i64::MAX)));
1806 }
1807 if *unit == Unit::Percent {
1812 return Ok(Value::Quantity(num / 100.0, Unit::Percent));
1813 }
1814 let hz_factor = match unit {
1818 Unit::Hertz => Some(1.0),
1819 Unit::Kilohertz => Some(1_000.0),
1820 Unit::Megahertz => Some(1_000_000.0),
1821 Unit::Gigahertz => Some(1_000_000_000.0),
1822 Unit::Terahertz => Some(1_000_000_000_000.0),
1823 _ => None,
1824 };
1825 if let Some(factor) = hz_factor {
1826 return Ok(Value::Quantity((num * factor).max(0.0), Unit::Hertz));
1827 }
1828 let bit_factor = match unit {
1832 Unit::Bits => Some((1.0, false)),
1833 Unit::Kilobits => Some((1_000.0, false)),
1834 Unit::Megabits => Some((1_000_000.0, false)),
1835 Unit::Gigabits => Some((1_000_000_000.0, false)),
1836 Unit::BitsPerSecond => Some((1.0, true)),
1837 Unit::KilobitsPerSecond => Some((1_000.0, true)),
1838 Unit::MegabitsPerSecond => Some((1_000_000.0, true)),
1839 Unit::GigabitsPerSecond => Some((1_000_000_000.0, true)),
1840 Unit::MegabytesPerSecond => Some((8_000_000.0, true)),
1841 _ => None,
1842 };
1843 if let Some((factor, is_rate)) = bit_factor {
1844 let canonical = if is_rate {
1845 Unit::BitsPerSecond
1846 } else {
1847 Unit::Bits
1848 };
1849 return Ok(Value::Quantity((num * factor).max(0.0), canonical));
1850 }
1851 Ok(Value::Quantity(num, unit.clone()))
1852}
1853
1854pub(crate) fn eval_binary(op: BinOp, lhs: &Value, rhs: &Value) -> Result<Value, String> {
1855 use BinOp::*;
1856 match (op, lhs, rhs) {
1857 (Add, Value::Int(a), Value::Int(b)) => Ok(Value::Int(a.saturating_add(*b))),
1862 (Sub, Value::Int(a), Value::Int(b)) => Ok(Value::Int(a.saturating_sub(*b))),
1863 (Mul, Value::Int(a), Value::Int(b)) => Ok(Value::Int(a.saturating_mul(*b))),
1864 (Div, Value::Int(a), Value::Int(b)) if *b != 0 => {
1865 Ok(Value::Int(a.checked_div(*b).unwrap_or(i64::MAX)))
1866 }
1867
1868 (Add, Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
1869 (Sub, Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)),
1870 (Mul, Value::Float(a), Value::Float(b)) => Ok(Value::Float(a * b)),
1871 (Div, Value::Float(a), Value::Float(b)) if *b != 0.0 => Ok(Value::Float(a / b)),
1872
1873 (Add, Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)),
1875 (Add, Value::Float(a), Value::Int(b)) => Ok(Value::Float(a + *b as f64)),
1876 (Mul, Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 * b)),
1877 (Mul, Value::Float(a), Value::Int(b)) => Ok(Value::Float(a * *b as f64)),
1878
1879 (Eq, a, b) => Ok(Value::Bool(value_eq(a, b))),
1880 (Ne, a, b) => Ok(Value::Bool(!value_eq(a, b))),
1881
1882 (Lt, Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
1883 (Le, Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
1884 (Gt, Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
1885 (Ge, Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
1886
1887 (Add, Value::DurationMs(a), Value::DurationMs(b)) => {
1892 Ok(Value::DurationMs(a.saturating_add(*b)))
1893 }
1894 (Sub, Value::DurationMs(a), Value::DurationMs(b)) => {
1895 Ok(Value::DurationMs(a.saturating_sub(*b)))
1896 }
1897 (Mul, Value::DurationMs(a), Value::Int(b)) => {
1898 Ok(Value::DurationMs(scale_dur(*a, *b as f64)))
1899 }
1900 (Mul, Value::Int(a), Value::DurationMs(b)) => {
1901 Ok(Value::DurationMs(scale_dur(*b, *a as f64)))
1902 }
1903 (Mul, Value::DurationMs(a), Value::Float(b)) => Ok(Value::DurationMs(scale_dur(*a, *b))),
1904 (Mul, Value::Float(a), Value::DurationMs(b)) => Ok(Value::DurationMs(scale_dur(*b, *a))),
1905 (Div, Value::DurationMs(a), Value::Int(b)) if *b != 0 => {
1906 Ok(Value::DurationMs(scale_dur(*a, 1.0 / *b as f64)))
1907 }
1908 (Div, Value::DurationMs(a), Value::Float(b)) if *b != 0.0 => {
1909 Ok(Value::DurationMs(scale_dur(*a, 1.0 / *b)))
1910 }
1911 (Div, Value::DurationMs(a), Value::DurationMs(b)) if *b != 0 => {
1912 Ok(Value::Int((*a / *b) as i64))
1913 }
1914 (Lt, Value::DurationMs(a), Value::DurationMs(b)) => Ok(Value::Bool(a < b)),
1915 (Le, Value::DurationMs(a), Value::DurationMs(b)) => Ok(Value::Bool(a <= b)),
1916 (Gt, Value::DurationMs(a), Value::DurationMs(b)) => Ok(Value::Bool(a > b)),
1917 (Ge, Value::DurationMs(a), Value::DurationMs(b)) => Ok(Value::Bool(a >= b)),
1918
1919 (Add, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => {
1924 Ok(Value::Quantity(a + b, ua.clone()))
1925 }
1926 (Sub, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => {
1927 Ok(Value::Quantity(a - b, ua.clone()))
1928 }
1929 (Mul, Value::Quantity(a, u), Value::Int(n)) => {
1930 Ok(Value::Quantity(a * *n as f64, u.clone()))
1931 }
1932 (Mul, Value::Quantity(a, u), Value::Float(f)) => Ok(Value::Quantity(a * f, u.clone())),
1933 (Div, Value::Quantity(a, u), Value::Int(n)) if *n != 0 => {
1934 Ok(Value::Quantity(a / *n as f64, u.clone()))
1935 }
1936 (Div, Value::Quantity(a, u), Value::Float(f)) if *f != 0.0 => {
1937 Ok(Value::Quantity(a / f, u.clone()))
1938 }
1939 (Div, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub && *b != 0.0 => {
1940 Ok(Value::Float(a / b))
1941 }
1942 (Lt, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => Ok(Value::Bool(a < b)),
1943 (Le, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => Ok(Value::Bool(a <= b)),
1944 (Gt, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => Ok(Value::Bool(a > b)),
1945 (Ge, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => Ok(Value::Bool(a >= b)),
1946
1947 (And, Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(*a && *b)),
1948 (Or, Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(*a || *b)),
1949
1950 _ => Err(format!(
1951 "unsupported binary op {op:?} on {lhs:?} and {rhs:?}"
1952 )),
1953 }
1954}
1955
1956fn scale_dur(ms: u64, factor: f64) -> u64 {
1958 (ms as f64 * factor).round().max(0.0) as u64
1959}
1960
1961pub(crate) fn eval_unary(op: UnOp, v: &Value) -> Result<Value, String> {
1962 match (op, v) {
1963 (UnOp::Neg, Value::Int(n)) => Ok(Value::Int(n.saturating_neg())),
1964 (UnOp::Neg, Value::Float(f)) => Ok(Value::Float(-f)),
1965 (UnOp::Neg, Value::Quantity(a, u)) => Ok(Value::Quantity(-a, u.clone())),
1969 (UnOp::Not, Value::Bool(b)) => Ok(Value::Bool(!b)),
1970 _ => Err(format!("unsupported unary op {op:?} on {v:?}")),
1971 }
1972}
1973
1974fn value_eq(a: &Value, b: &Value) -> bool {
1975 match (a, b) {
1976 (Value::Null, Value::Null) => true,
1977 (Value::Bool(x), Value::Bool(y)) => x == y,
1978 (Value::Int(x), Value::Int(y)) => x == y,
1979 (Value::Float(x), Value::Float(y)) => (x - y).abs() < f64::EPSILON,
1980 (Value::String(x), Value::String(y)) => x == y,
1981 (Value::DurationMs(x), Value::DurationMs(y)) => x == y,
1982 (Value::Quantity(x, ux), Value::Quantity(y, uy)) => x == y && ux == uy,
1983 _ => false,
1984 }
1985}