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#[allow(clippy::arc_with_non_send_sync)]
31pub type EnvLookup = std::sync::Arc<dyn Fn(&str) -> Option<String>>;
32
33#[derive(Debug, Error)]
34pub enum EvalError {
35 #[error("cycle detected in dependency graph")]
36 CycleDetected,
37 #[error("missing root")]
38 MissingRoot,
39 #[error("node {0:?} not found")]
40 MissingNode(NodeId),
41 #[error("type error: {message}")]
42 TypeError {
43 message: String,
44 span: Option<Span>,
45 file: Option<String>,
46 },
47 #[error("patch error: {message}")]
51 PatchError {
52 message: String,
53 span: Option<Span>,
54 file: Option<String>,
55 },
56 #[error("secret used in unsafe context")]
57 SecretLeak { file: Option<String> },
58 #[error("validation failed")]
64 ValidationFailed { diagnostics: Vec<Diagnostic> },
65 #[error("evaluation budget exceeded: {kind} (limit {limit})")]
69 BudgetExceeded {
70 kind: BudgetKind,
71 limit: u64,
72 span: Option<Span>,
73 file: Option<String>,
74 },
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum BudgetKind {
81 Nodes,
82 WallClock,
83 Depth,
84 ArrayLength,
85 Keys,
86 ValueSize,
88}
89
90impl std::fmt::Display for BudgetKind {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 BudgetKind::Nodes => write!(f, "node budget"),
94 BudgetKind::WallClock => write!(f, "time budget"),
95 BudgetKind::Depth => write!(f, "depth budget"),
96 BudgetKind::ArrayLength => write!(f, "array budget"),
97 BudgetKind::Keys => write!(f, "key budget"),
98 BudgetKind::ValueSize => write!(f, "value size budget"),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct Budget {
108 pub max_nodes: u64,
110 pub max_depth: u64,
112 pub max_array_len: u64,
114 pub max_keys: u64,
117 pub max_wall_ms: u64,
119 pub max_value_size: u64,
124 pub max_total_value_size: u64,
129}
130
131impl Default for Budget {
132 fn default() -> Self {
135 Self {
136 max_nodes: 100_000,
137 max_depth: 256,
138 max_array_len: 100_000,
139 max_keys: 100_000,
140 max_wall_ms: 100,
141 max_value_size: 10_000_000,
142 max_total_value_size: 100_000_000,
143 }
144 }
145}
146
147impl Budget {
148 pub fn wall_deadline(&self) -> Option<Instant> {
161 if self.max_wall_ms == 0 || !Self::wall_supported() {
162 None
163 } else {
164 Instant::now().checked_add(std::time::Duration::from_millis(self.max_wall_ms))
165 }
166 }
167
168 pub fn wall_supported() -> bool {
175 #[cfg(any(target_arch = "wasm32", miri))]
176 {
177 false
178 }
179 #[cfg(not(any(target_arch = "wasm32", miri)))]
180 {
181 true
182 }
183 }
184}
185
186impl EvalError {
187 pub fn code(&self) -> &'static str {
189 match self {
190 EvalError::CycleDetected => "S001",
191 EvalError::MissingRoot => "S002",
192 EvalError::MissingNode(_) => "S003",
193 EvalError::TypeError { .. } => "T001",
194 EvalError::PatchError { .. } => "E019",
195 EvalError::SecretLeak { .. } => "S004",
196 EvalError::BudgetExceeded { kind, .. } => match kind {
197 BudgetKind::Nodes => "X001",
198 BudgetKind::WallClock => "X002",
199 BudgetKind::Depth => "X003",
200 BudgetKind::ArrayLength => "X004",
201 BudgetKind::Keys => "X005",
202 BudgetKind::ValueSize => "X006",
203 },
204 EvalError::ValidationFailed { diagnostics } => {
205 match diagnostics.first().map(|d| d.code.as_str()) {
206 Some("T003") => "T003",
207 Some("T004") => "T004",
208 _ => "T002",
209 }
210 }
211 }
212 }
213
214 pub fn span(&self) -> Option<&Span> {
216 match self {
217 EvalError::TypeError { span, .. } | EvalError::PatchError { span, .. } => span.as_ref(),
218 EvalError::BudgetExceeded { span, .. } => span.as_ref(),
219 EvalError::ValidationFailed { diagnostics } => {
220 diagnostics.first().and_then(|d| d.span.as_ref())
221 }
222 _ => None,
223 }
224 }
225
226 pub fn file(&self) -> Option<&str> {
229 match self {
230 EvalError::TypeError { file, .. } | EvalError::PatchError { file, .. } => {
231 file.as_deref()
232 }
233 EvalError::SecretLeak { file } => file.as_deref(),
234 EvalError::BudgetExceeded { file, .. } => file.as_deref(),
235 EvalError::ValidationFailed { diagnostics } => {
236 diagnostics.first().and_then(|d| d.file.as_deref())
237 }
238 _ => None,
239 }
240 }
241
242 fn type_error(message: impl Into<String>, module: &Module, node: NodeId) -> Self {
245 EvalError::TypeError {
246 message: message.into(),
247 span: module.node_spans.get(&node).cloned(),
248 file: module.node_files.get(&node).cloned(),
249 }
250 }
251
252 pub(crate) fn budget_exceeded(
256 kind: BudgetKind,
257 limit: u64,
258 module: &Module,
259 node: Option<NodeId>,
260 ) -> Self {
261 EvalError::BudgetExceeded {
262 kind,
263 limit,
264 span: node.and_then(|n| module.node_spans.get(&n).cloned()),
265 file: node.and_then(|n| module.node_files.get(&n).cloned()),
266 }
267 }
268 fn patch_error(message: impl Into<String>, module: &Module, node: NodeId) -> Self {
271 EvalError::PatchError {
272 message: message.into(),
273 span: module.node_spans.get(&node).cloned(),
274 file: module.node_files.get(&node).cloned(),
275 }
276 }
277
278 fn secret_leak(module: &Module, node: NodeId) -> Self {
280 EvalError::SecretLeak {
281 file: module.node_files.get(&node).cloned(),
282 }
283 }
284}
285
286pub struct Engine {
287 pub resilient: bool,
288 pub profile_override: Option<String>,
291 pub budget: Budget,
294 pub env: std::collections::HashMap<String, String>,
299 pub env_fallback: Option<EnvLookup>,
302}
303
304impl Default for Engine {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309
310fn check_result_depth(result: &Value, max_depth: u64) -> Result<(), EvalError> {
316 let mut stack: Vec<(&Value, u64)> = vec![(result, 0)];
317 while let Some((value, depth)) = stack.pop() {
318 if depth > max_depth {
319 return Err(EvalError::BudgetExceeded {
320 kind: BudgetKind::Depth,
321 limit: max_depth,
322 span: None,
323 file: None,
324 });
325 }
326 match value {
327 Value::Table(map) => {
328 for v in map.values() {
329 stack.push((v, depth + 1));
330 }
331 }
332 Value::Array(elements) => {
333 for v in elements {
334 stack.push((v, depth + 1));
335 }
336 }
337 _ => {}
338 }
339 }
340 Ok(())
341}
342
343pub(crate) struct Accountant {
348 spent: u64,
349 limit: u64,
350}
351
352impl Accountant {
353 pub(crate) fn charge(
354 &mut self,
355 module: &Module,
356 node: Option<NodeId>,
357 amount: u64,
358 ) -> Result<(), EvalError> {
359 self.spent = self.spent.saturating_add(amount);
360 if self.limit != 0 && self.spent >= self.limit.saturating_add(1) {
364 return Err(EvalError::budget_exceeded(
365 BudgetKind::ValueSize,
366 self.limit,
367 module,
368 node,
369 ));
370 }
371 Ok(())
372 }
373}
374
375pub(crate) struct BudgetCx<'a> {
380 pub deadline: Option<Instant>,
381 pub wall_limit: u64,
382 pub account: &'a mut Accountant,
383}
384
385pub(crate) fn measure_value(value: &Value) -> u64 {
389 let mut total = 0u64;
390 let mut stack = vec![value];
391 while let Some(v) = stack.pop() {
392 total = total.saturating_add(1);
393 match v {
394 Value::String(s) | Value::Secret(s) => {
395 total = total.saturating_add(s.len() as u64);
396 }
397 Value::Array(items) => stack.extend(items.iter()),
398 Value::Table(map) => stack.extend(map.values()),
399 _ => {}
400 }
401 }
402 total
403}
404
405fn check_static_budgets(module: &Module, budget: &Budget) -> Result<(), EvalError> {
413 let over = |limit: u64, used: u64| limit != 0 && used > limit;
414 if over(budget.max_nodes, module.arena.len() as u64) {
415 return Err(EvalError::BudgetExceeded {
416 kind: BudgetKind::Nodes,
417 limit: budget.max_nodes,
418 span: None,
419 file: None,
420 });
421 }
422 if budget.max_array_len != 0 || budget.max_keys != 0 {
423 let mut keys = 0u64;
424 for (i, node) in module.arena.iter().enumerate() {
425 let id = NodeId(i as u32);
426 match node {
427 Node::Table { fields } => {
430 keys += fields.len() as u64;
431 if over(budget.max_keys, keys) {
432 return Err(EvalError::BudgetExceeded {
433 kind: BudgetKind::Keys,
434 limit: budget.max_keys,
435 span: None,
436 file: None,
437 });
438 }
439 }
440 Node::Array { elements } if over(budget.max_array_len, elements.len() as u64) => {
441 return Err(EvalError::budget_exceeded(
442 BudgetKind::ArrayLength,
443 budget.max_array_len,
444 module,
445 Some(id),
446 ));
447 }
448 _ => {}
449 }
450 }
451 }
452 Ok(())
453}
454
455fn check_dp_bounds(
468 module: &Module,
469 order: &[petgraph::graph::NodeIndex],
470 budget: &Budget,
471) -> Result<(u64, Vec<u64>), EvalError> {
472 if budget.max_value_size == 0 && budget.max_depth == 0 && budget.max_total_value_size == 0 {
473 return Ok((0, vec![1u64; module.arena.len()]));
474 }
475 let size_cap = if budget.max_value_size == 0 {
481 u64::MAX
482 } else {
483 budget.max_value_size.saturating_add(1)
484 };
485 let depth_cap = if budget.max_depth == 0 {
486 u64::MAX
487 } else {
488 budget.max_depth.saturating_add(1)
489 };
490 let mut size = vec![1u64; module.arena.len()];
491 let mut depth = vec![0u64; module.arena.len()];
492 let sat_size = |a: u64, b: u64| a.saturating_add(b).min(size_cap);
493 let mut total = 0u64;
498 for node_idx in order {
499 let id = module.graph[*node_idx];
500 let i = id.0 as usize;
501 if i >= size.len() {
502 continue;
503 }
504 let mut child_size = 0u64;
510 let mut child_depth = 0u64;
511 match module.get(id) {
512 Some(Node::Table { fields }) => {
513 for child in fields.values() {
514 let c = child.0 as usize;
515 if c < size.len() {
516 child_size = sat_size(child_size, size[c]);
517 child_depth = child_depth.max(depth[c]);
518 }
519 }
520 size[i] = sat_size(1, child_size);
521 depth[i] = child_depth.saturating_add(1).min(depth_cap);
522 }
523 Some(Node::Array { elements }) => {
524 for child in elements {
525 let c = child.0 as usize;
526 if c < size.len() {
527 child_size = sat_size(child_size, size[c]);
528 child_depth = child_depth.max(depth[c]);
529 }
530 }
531 size[i] = sat_size(1, child_size);
532 depth[i] = child_depth.saturating_add(1).min(depth_cap);
533 }
534 Some(Node::Ref(target)) => {
535 let c = target.0 as usize;
536 if c < size.len() {
537 size[i] = size[c];
538 depth[i] = depth[c];
539 }
540 }
541 Some(Node::Literal(Value::String(s))) => {
542 size[i] = sat_size(1, s.len() as u64);
543 }
544 Some(Node::Interpolate { parts }) => {
545 for part in parts {
546 match part {
547 InterpPart::Expr(e) => {
548 let c = e.0 as usize;
549 if c < size.len() {
550 child_size = sat_size(child_size, size[c]);
551 child_depth = child_depth.max(depth[c]);
552 }
553 }
554 InterpPart::Literal(s) => {
555 child_size = sat_size(child_size, s.len() as u64);
556 }
557 }
558 }
559 size[i] = sat_size(1, child_size);
560 }
561 Some(Node::Binary { lhs, rhs, .. }) => {
562 for child in [*lhs, *rhs] {
563 let c = child.0 as usize;
564 if c < size.len() {
565 child_size = sat_size(child_size, size[c]);
566 }
567 }
568 size[i] = sat_size(1, child_size);
569 }
570 Some(Node::Unary { expr, .. }) => {
571 let c = expr.0 as usize;
572 if c < size.len() {
573 size[i] = sat_size(1, size[c]);
574 }
575 }
576 Some(Node::Semantic { inner, .. }) => {
579 let c = inner.0 as usize;
580 if c < size.len() {
581 size[i] = size[c];
582 depth[i] = depth[c];
583 }
584 }
585 _ => {}
586 }
587 if budget.max_value_size != 0 && size[i] >= size_cap {
591 return Err(EvalError::budget_exceeded(
592 BudgetKind::ValueSize,
593 budget.max_value_size,
594 module,
595 Some(id),
596 ));
597 }
598 if budget.max_depth != 0 && depth[i] >= depth_cap {
599 return Err(EvalError::budget_exceeded(
600 BudgetKind::Depth,
601 budget.max_depth,
602 module,
603 Some(id),
604 ));
605 }
606 total = total.saturating_add(size[i]);
607 if budget.max_total_value_size != 0
608 && total >= budget.max_total_value_size.saturating_add(1)
609 {
610 return Err(EvalError::budget_exceeded(
611 BudgetKind::ValueSize,
612 budget.max_total_value_size,
613 module,
614 Some(id),
615 ));
616 }
617 }
618 Ok((total, size))
619}
620
621pub struct IncrementalMemo {
635 map: HashMap<u64, (Value, Vec<Diagnostic>)>,
636 config: Option<(bool, Option<String>, Budget)>,
637 salt: u64,
638 pub last_eval_count: u64,
639 pub last_memo_hits: u64,
640}
641
642impl std::fmt::Debug for IncrementalMemo {
643 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644 f.debug_struct("IncrementalMemo")
645 .field("entries", &self.map.len())
646 .field("last_eval_count", &self.last_eval_count)
647 .field("last_memo_hits", &self.last_memo_hits)
648 .finish()
649 }
650}
651
652impl Default for IncrementalMemo {
653 fn default() -> Self {
654 Self {
655 map: HashMap::new(),
656 config: None,
657 salt: memo_salt(),
658 last_eval_count: 0,
659 last_memo_hits: 0,
660 }
661 }
662}
663
664fn memo_salt() -> u64 {
669 let mut buf = [0u8; 8];
670 if getrandom::getrandom(&mut buf).is_ok() {
671 u64::from_le_bytes(buf)
672 } else {
673 0x9E37_79B9_7F4A_7C15
674 }
675}
676
677struct FpHasher(u64);
689
690impl FpHasher {
691 fn new(salt: u64) -> Self {
692 Self(14_695_981_039_346_656_037 ^ salt)
698 }
699}
700
701impl std::hash::Hasher for FpHasher {
702 fn write(&mut self, bytes: &[u8]) {
703 for &byte in bytes {
704 self.0 ^= u64::from(byte);
705 self.0 = self.0.wrapping_mul(1_099_511_628_211);
706 }
707 }
708
709 fn finish(&self) -> u64 {
710 self.0
711 }
712}
713
714fn fp_value(hasher: &mut FpHasher, value: &Value) {
721 use std::hash::Hash;
722 match value {
723 Value::Null => 0u8.hash(hasher),
724 Value::Bool(flag) => {
725 1u8.hash(hasher);
726 flag.hash(hasher);
727 }
728 Value::Int(num) => {
729 2u8.hash(hasher);
730 num.hash(hasher);
731 }
732 Value::Float(num) => {
733 3u8.hash(hasher);
734 num.to_bits().hash(hasher);
735 }
736 Value::String(text) => {
737 4u8.hash(hasher);
738 text.hash(hasher);
739 }
740 Value::DurationMs(millis) => {
741 5u8.hash(hasher);
742 millis.hash(hasher);
743 }
744 Value::Quantity(amount, unit) => {
745 6u8.hash(hasher);
746 amount.to_bits().hash(hasher);
747 unit.hash(hasher);
748 }
749 Value::Array(items) => {
750 7u8.hash(hasher);
751 items.len().hash(hasher);
752 for item in items {
753 fp_value(hasher, item);
754 }
755 }
756 Value::Table(fields) => {
757 8u8.hash(hasher);
758 fields.len().hash(hasher);
759 for (key, item) in fields {
760 key.hash(hasher);
761 fp_value(hasher, item);
762 }
763 }
764 Value::Secret(text) => {
767 9u8.hash(hasher);
768 text.hash(hasher);
769 }
770 }
771}
772
773fn fingerprint_one(
786 engine: &Engine,
787 module: &Module,
788 id: NodeId,
789 cache: &[Option<u64>],
790 salt: u64,
791) -> Option<u64> {
792 use std::hash::{Hash, Hasher};
793 let child = |dep: &NodeId| cache.get(dep.0 as usize).copied().flatten();
794 let mut hasher = FpHasher::new(salt);
795 match module.get(id)? {
796 Node::Literal(v) => {
797 0u8.hash(&mut hasher);
798 fp_value(&mut hasher, v);
799 }
800 Node::Env { name, default } => {
801 1u8.hash(&mut hasher);
802 name.hash(&mut hasher);
803 engine.env.get(name).hash(&mut hasher);
804 match default {
805 Some(d) => child(d)?.hash(&mut hasher),
806 None => 0u8.hash(&mut hasher),
807 }
808 }
809 Node::Secret { locator } => {
810 2u8.hash(&mut hasher);
811 child(locator)?.hash(&mut hasher);
812 }
813 Node::Ref(target) => {
814 3u8.hash(&mut hasher);
815 child(target)?.hash(&mut hasher);
816 }
817 Node::Ident(name) => {
818 4u8.hash(&mut hasher);
819 name.hash(&mut hasher);
820 }
821 Node::Interpolate { parts } => {
822 5u8.hash(&mut hasher);
823 parts.len().hash(&mut hasher);
824 for part in parts {
825 match part {
826 crate::ir::InterpPart::Literal(s) => {
827 0u8.hash(&mut hasher);
828 s.hash(&mut hasher);
829 }
830 crate::ir::InterpPart::Expr(e) => {
831 1u8.hash(&mut hasher);
832 child(e)?.hash(&mut hasher);
833 }
834 }
835 }
836 }
837 Node::Binary { op, lhs, rhs } => {
838 6u8.hash(&mut hasher);
839 op.hash(&mut hasher);
840 child(lhs)?.hash(&mut hasher);
841 child(rhs)?.hash(&mut hasher);
842 }
843 Node::Unary { op, expr } => {
844 7u8.hash(&mut hasher);
845 op.hash(&mut hasher);
846 child(expr)?.hash(&mut hasher);
847 }
848 Node::Array { elements } => {
849 8u8.hash(&mut hasher);
850 elements.len().hash(&mut hasher);
851 for element in elements {
852 child(element)?.hash(&mut hasher);
853 }
854 }
855 Node::Table { fields } => {
856 9u8.hash(&mut hasher);
857 fields.len().hash(&mut hasher);
858 for (key, child_id) in fields {
859 key.hash(&mut hasher);
860 child(child_id)?.hash(&mut hasher);
861 }
862 }
863 Node::Quantity { value, unit } => {
864 10u8.hash(&mut hasher);
865 unit.hash(&mut hasher);
866 child(value)?.hash(&mut hasher);
867 }
868 Node::Semantic { tag, inner } => {
869 11u8.hash(&mut hasher);
870 tag.as_str().hash(&mut hasher);
871 child(inner)?.hash(&mut hasher);
872 }
873 _ => return None,
874 }
875 Some(hasher.finish())
876}
877
878impl Engine {
879 pub fn new() -> Self {
880 let mut engine = Self::hermetic();
881 engine.env = std::env::vars_os()
886 .filter_map(|(key, value)| Some((key.into_string().ok()?, value.into_string().ok()?)))
887 .collect();
888 engine
889 }
890
891 pub fn hermetic() -> Self {
899 Self {
900 resilient: true,
901 profile_override: None,
902 budget: Budget::default(),
903 env: std::collections::HashMap::new(),
904 env_fallback: None,
905 }
906 }
907
908 pub fn with_env(mut self, env: std::collections::HashMap<String, String>) -> Self {
910 self.env = env;
911 self
912 }
913
914 pub fn with_env_fallback(mut self, lookup: EnvLookup) -> Self {
916 self.env_fallback = Some(lookup);
917 self
918 }
919
920 pub fn evaluate(&self, module: &Module) -> Result<(Value, Vec<Diagnostic>), EvalError> {
921 self.evaluate_inner(module, None)
922 }
923
924 pub fn evaluate_incremental(
935 &self,
936 module: &Module,
937 memo: &mut IncrementalMemo,
938 ) -> Result<(Value, Vec<Diagnostic>), EvalError> {
939 let token = (self.resilient, self.profile_override.clone(), self.budget);
942 if memo.config.as_ref() != Some(&token) {
943 memo.map.clear();
944 memo.config = Some(token);
945 }
946 memo.last_eval_count = 0;
947 memo.last_memo_hits = 0;
948 let outcome = self.evaluate_inner(module, Some(memo));
949 let cap = module.arena.len().saturating_mul(4).max(1024);
957 if memo.map.len() > cap {
958 memo.map.clear();
959 }
960 outcome
961 }
962
963 fn evaluate_inner(
964 &self,
965 module: &Module,
966 mut memo: Option<&mut IncrementalMemo>,
967 ) -> Result<(Value, Vec<Diagnostic>), EvalError> {
968 let deadline = self.budget.wall_deadline();
972 check_static_budgets(module, &self.budget)?;
974 if is_cyclic_directed(&module.graph) {
975 return Err(EvalError::CycleDetected);
976 }
977
978 let order = toposort(&module.graph, None).map_err(|_| EvalError::CycleDetected)?;
979 let (dp_total, dp_bounds) = check_dp_bounds(module, &order, &self.budget)?;
985 let mut account = Accountant {
986 spent: dp_total,
987 limit: self.budget.max_total_value_size,
988 };
989 let mut actuals: HashMap<NodeId, u64> = HashMap::new();
995 let mut values: HashMap<NodeId, Value> = HashMap::new();
996 let mut diagnostics = module.diagnostics.clone();
997 diagnostics.extend(module.healing.iter().cloned());
998 let mut visited = 0u64;
1003
1004 let mut fp_cache: Vec<Option<u64>> = if memo.is_some() {
1012 vec![None; module.arena.len()]
1013 } else {
1014 Vec::new()
1015 };
1016 for node_idx in order {
1017 visited += 1;
1018 if visited % 1024 == 0 {
1019 if let Some(end) = deadline {
1020 if Instant::now() >= end {
1021 let node_id = module.graph[node_idx];
1022 return Err(EvalError::budget_exceeded(
1023 BudgetKind::WallClock,
1024 self.budget.max_wall_ms,
1025 module,
1026 Some(node_id),
1027 ));
1028 }
1029 }
1030 }
1031 let node_id = module.graph[node_idx];
1032 let node = module.get(node_id).ok_or(EvalError::MissingNode(node_id))?;
1033 let mut memo_fp = None;
1046 if let Some(memo) = memo.as_mut() {
1047 let salt = memo.salt;
1048 if let Some(fp) = fingerprint_one(self, module, node_id, &fp_cache, salt) {
1049 if let Ok(index) = usize::try_from(node_id.0) {
1050 if let Some(slot) = fp_cache.get_mut(index) {
1051 *slot = Some(fp);
1052 }
1053 }
1054 memo_fp = Some(fp);
1055 if let Some((value, diags)) = memo.map.get(&fp) {
1056 values.insert(node_id, value.clone());
1057 let span = module.node_spans.get(&node_id).cloned();
1058 let file = module.node_files.get(&node_id).cloned();
1059 diagnostics.extend(diags.iter().map(|d| {
1060 let mut fresh = d.clone();
1061 fresh.span = span.clone();
1066 fresh.file = file.clone();
1067 fresh
1068 }));
1069 memo.last_memo_hits += 1;
1070 continue;
1071 }
1072 }
1073 }
1074
1075 let diag_mark = diagnostics.len();
1076 let value = match node {
1077 Node::Literal(v) => v.clone(),
1078
1079 Node::Env { name, default } => match self.env.get(name) {
1080 Some(s) => {
1081 actuals.insert(node_id, 1u64.saturating_add(s.len() as u64));
1085 account.charge(module, Some(node_id), s.len() as u64)?;
1086 Value::String(s.clone())
1087 }
1088 None => {
1089 if let Some(lookup) = self.env_fallback.clone() {
1094 if let Some(s) = lookup(name) {
1095 actuals.insert(node_id, 1u64.saturating_add(s.len() as u64));
1096 account.charge(module, Some(node_id), s.len() as u64)?;
1097 Value::String(s)
1098 } else if let Some(def) = default {
1099 values.get(def).cloned().unwrap_or(Value::Null)
1100 } else {
1101 Value::Null
1102 }
1103 } else if let Some(def) = default {
1104 values.get(def).cloned().unwrap_or(Value::Null)
1105 } else {
1106 Value::Null
1107 }
1108 }
1109 },
1110
1111 Node::Secret { locator } => {
1112 let loc = values.get(locator).cloned().unwrap_or(Value::Null);
1113 let secret = match loc {
1114 Value::String(s) => Value::Secret(s),
1115 _ => Value::Secret(String::new()),
1116 };
1117 if let Value::Secret(s) = &secret {
1121 let actual = 1u64.saturating_add(s.len() as u64);
1122 actuals.insert(node_id, actual);
1123 account.charge(module, Some(node_id), s.len() as u64)?;
1124 }
1125 secret
1126 }
1127
1128 Node::Ref(id) => {
1129 let v = values.get(id).cloned().unwrap_or(Value::Null);
1130 if let Some(actual) = actuals.get(id) {
1135 let bound = dp_bounds.get(id.0 as usize).copied().unwrap_or(1);
1136 account.charge(module, Some(node_id), actual.saturating_sub(bound))?;
1137 }
1138 v
1139 }
1140
1141 Node::Semantic { inner, .. } => values.get(inner).cloned().unwrap_or(Value::Null),
1143
1144 Node::SchemaField { .. } | Node::SchemaTable { .. } | Node::SchemaArray { .. } => {
1149 Value::Null
1150 }
1151 Node::Ident(name) => {
1152 diagnostics.push(Diagnostic {
1153 severity: Severity::Warning,
1154 code: "W001".into(),
1155 message: format!("unresolved identifier '{}'", name),
1156 path: Some(name.clone()),
1157 span: module.node_spans.get(&node_id).cloned(),
1158 repair: None,
1159 confidence: 1.0,
1160 file: module.node_files.get(&node_id).cloned(),
1161 });
1162 Value::Null
1163 }
1164
1165 Node::Interpolate { parts } => {
1166 let mut out = String::new();
1167 for part in parts {
1168 match part {
1169 InterpPart::Literal(s) => out.push_str(s),
1170 InterpPart::Expr(id) => {
1171 if let Some(v) = values.get(id) {
1172 match v {
1173 Value::String(s) => out.push_str(s),
1174 Value::Int(i) => out.push_str(&i.to_string()),
1175 Value::Float(f) => out.push_str(&f.to_string()),
1176 Value::Bool(b) => out.push_str(&b.to_string()),
1177 Value::DurationMs(ms) => out.push_str(&format!("{}ms", ms)),
1178 Value::Quantity(n, unit) => {
1179 out.push_str(&unit.format_amount(*n))
1180 }
1181 Value::Secret(_) => {
1182 return Err(EvalError::secret_leak(module, node_id));
1183 }
1184 Value::Null => out.push_str("null"),
1185 Value::Array(_) | Value::Table(_) => {
1189 return Err(EvalError::type_error(
1190 format!(
1191 "cannot interpolate a {} into a string",
1192 match v {
1193 Value::Array(_) => "array",
1194 _ => "table",
1195 }
1196 ),
1197 module,
1198 node_id,
1199 ));
1200 }
1201 }
1202 } else {
1203 out.push_str("null");
1204 }
1205 }
1206 }
1207 }
1208 Value::String(out)
1209 }
1210
1211 Node::Binary { op, lhs, rhs } => {
1212 let l = values.get(lhs).cloned().unwrap_or(Value::Null);
1213 let r = values.get(rhs).cloned().unwrap_or(Value::Null);
1214 eval_binary(*op, &l, &r)
1215 .map_err(|m| EvalError::type_error(m, module, node_id))?
1216 }
1217
1218 Node::Unary { op, expr } => {
1219 let v = values.get(expr).cloned().unwrap_or(Value::Null);
1220 eval_unary(*op, &v).map_err(|m| EvalError::type_error(m, module, node_id))?
1221 }
1222
1223 Node::Array { elements } => {
1224 let els: Vec<Value> = elements
1225 .iter()
1226 .map(|e| values.get(e).cloned().unwrap_or(Value::Null))
1227 .collect();
1228 Value::Array(els)
1229 }
1230
1231 Node::Table { fields } => {
1232 let mut map = indexmap::IndexMap::new();
1233 for (k, id) in fields {
1234 map.insert(k.clone(), values.get(id).cloned().unwrap_or(Value::Null));
1235 }
1236 Value::Table(map)
1237 }
1238
1239 Node::Quantity { value, unit } => {
1240 let v = values.get(value).cloned().unwrap_or(Value::Null);
1241 normalize_quantity(&v, unit)
1242 .map_err(|m| EvalError::type_error(m, module, node_id))?
1243 }
1244
1245 _ => Value::Null,
1246 };
1247 if let Some(memo) = memo.as_mut() {
1250 memo.last_eval_count += 1;
1251 if let Some(fp) = memo_fp {
1252 let fresh = diagnostics[diag_mark..].to_vec();
1253 memo.map.insert(fp, (value.clone(), fresh));
1254 }
1255 }
1256
1257 values.insert(node_id, value);
1258 }
1259
1260 let root = module.root.ok_or(EvalError::MissingRoot)?;
1261 let result = values.get(&root).cloned().unwrap_or(Value::Null);
1262 let result = self.apply_profile(module, &values, result, &mut diagnostics, &mut account)?;
1263 let mut result = result;
1264 self.apply_patches(module, &values, &mut result, &mut diagnostics, &mut account)?;
1265 if !module.patches.is_empty() && self.budget.max_depth != 0 {
1271 check_result_depth(&result, self.budget.max_depth)?;
1272 }
1273 self.validate_schema(
1281 module,
1282 &values,
1283 &mut result,
1284 &mut diagnostics,
1285 BudgetCx {
1286 deadline,
1287 wall_limit: self.budget.max_wall_ms,
1288 account: &mut account,
1289 },
1290 )?;
1291 self.validate_annotations(
1292 module,
1293 &values,
1294 &result,
1295 &mut diagnostics,
1296 deadline,
1297 self.budget.max_wall_ms,
1298 )?;
1299 if let Some(end) = deadline {
1302 if Instant::now() >= end {
1303 return Err(EvalError::budget_exceeded(
1304 BudgetKind::WallClock,
1305 self.budget.max_wall_ms,
1306 module,
1307 None,
1308 ));
1309 }
1310 }
1311 Ok((result, diagnostics))
1312 }
1313
1314 fn check_immutable(
1320 module: &Module,
1321 patch_id: NodeId,
1322 targets: &[&str],
1323 ) -> Result<(), EvalError> {
1324 for target in targets {
1325 if let Some(immutable) = Self::immutable_overlap(module, target) {
1326 return Err(EvalError::patch_error(
1327 format!("patch target `{target}` overlaps ai_immutable `{immutable}`"),
1328 module,
1329 patch_id,
1330 ));
1331 }
1332 }
1333 Ok(())
1334 }
1335
1336 fn immutable_overlap(module: &Module, target: &str) -> Option<String> {
1343 let target_segs = validate_patch_path(target).ok()?;
1344 let mut paths: Vec<&String> = module
1345 .annotations
1346 .values()
1347 .flatten()
1348 .filter(|a| matches!(&a.kind, crate::ir::AnnotationKind::AiImmutable(true)))
1349 .map(|a| &a.path)
1350 .collect();
1351 paths.sort();
1352 for apath in paths {
1353 let Ok(asegs) = validate_patch_path(apath) else {
1354 continue;
1355 };
1356 let shared = target_segs.len().min(asegs.len());
1357 if shared > 0 && target_segs[..shared] == asegs[..shared] {
1358 return Some(apath.clone());
1359 }
1360 }
1361 None
1362 }
1363 fn apply_patches(
1369 &self,
1370 module: &Module,
1371 values: &HashMap<NodeId, Value>,
1372 result: &mut Value,
1373 diagnostics: &mut Vec<Diagnostic>,
1374 account: &mut Accountant,
1375 ) -> Result<(), EvalError> {
1376 use crate::ir::PatchOp;
1377
1378 for &patch_id in &module.patches {
1379 let (op, span, file) = match module.get(patch_id) {
1380 Some(Node::PatchDirective { op }) => (
1381 op.clone(),
1382 module.node_spans.get(&patch_id).cloned(),
1383 module.node_files.get(&patch_id).cloned(),
1384 ),
1385 _ => continue,
1386 };
1387 let mut warn = |code: &str, message: String| {
1388 diagnostics.push(Diagnostic {
1389 severity: Severity::Warning,
1390 code: code.into(),
1391 message,
1392 path: None,
1393 span: span.clone(),
1394 repair: None,
1395 confidence: 1.0,
1396 file: file.clone(),
1397 });
1398 };
1399 match op {
1400 PatchOp::Set { path, value } => {
1401 Self::check_immutable(module, patch_id, &[&path])?;
1402 let segs = validate_patch_path(&path).map_err(|why| {
1403 EvalError::patch_error(
1404 format!("invalid patch path `{path}`: {why}"),
1405 module,
1406 patch_id,
1407 )
1408 })?;
1409 let v = values.get(&value).cloned().unwrap_or(Value::Null);
1410 account.charge(
1415 module,
1416 Some(patch_id),
1417 measure_value(&v).saturating_add(segs.len() as u64),
1418 )?;
1419 let snapshot = result.clone();
1423 if !patch_set(result, &segs, v) {
1424 *result = snapshot;
1425 warn(
1426 "W004",
1427 format!("patch target `{path}` is blocked or out of bounds"),
1428 );
1429 }
1430 }
1431 PatchOp::Remove { path } => {
1432 Self::check_immutable(module, patch_id, &[&path])?;
1433 let segs = validate_patch_path(&path).map_err(|why| {
1434 EvalError::patch_error(
1435 format!("invalid patch path `{path}`: {why}"),
1436 module,
1437 patch_id,
1438 )
1439 })?;
1440 if !patch_remove(result, &segs) {
1441 warn("W004", format!("remove target `{path}` does not exist"));
1442 }
1443 }
1444 PatchOp::Rename { from, to } => {
1445 Self::check_immutable(module, patch_id, &[&from, &to])?;
1446 let segs = validate_patch_path(&from).map_err(|why| {
1447 EvalError::patch_error(
1448 format!("invalid patch path `{from}`: {why}"),
1449 module,
1450 patch_id,
1451 )
1452 })?;
1453 match patch_rename(result, &segs, &to) {
1454 RenameOutcome::Ok => {}
1455 RenameOutcome::Missing => {
1456 warn("W004", format!("rename source `{from}` does not exist"));
1457 }
1458 RenameOutcome::Exists => {
1459 return Err(EvalError::patch_error(
1460 format!("rename target `{to}` already exists"),
1461 module,
1462 patch_id,
1463 ));
1464 }
1465 }
1466 }
1467 }
1468 }
1469 Ok(())
1470 }
1471
1472 fn apply_profile(
1479 &self,
1480 module: &Module,
1481 values: &HashMap<NodeId, Value>,
1482 result: Value,
1483 diagnostics: &mut Vec<Diagnostic>,
1484 account: &mut Accountant,
1485 ) -> Result<Value, EvalError> {
1486 if module.profiles.is_empty() {
1487 let selected: Option<String> = match &self.profile_override {
1490 Some(name) => Some(name.clone()),
1491 None => match &result {
1492 Value::Table(map) => match map.get("active_profile") {
1493 Some(Value::String(s)) => Some(s.clone()),
1494 Some(_) => {
1495 diagnostics.push(Diagnostic {
1496 severity: Severity::Warning,
1497 code: "W002".into(),
1498 message: "active_profile must be a string".into(),
1499 path: Some("active_profile".into()),
1500 span: if self.profile_override.is_some() {
1501 None
1502 } else {
1503 active_key_span(module, &result)
1504 },
1505 repair: None,
1506 confidence: 1.0,
1507 file: if self.profile_override.is_some() {
1508 None
1509 } else {
1510 active_key_file(module, &result)
1511 },
1512 });
1513 return Ok(result);
1514 }
1515 None => None,
1516 },
1517 _ => None,
1518 },
1519 };
1520 if let Some(name) = selected {
1521 diagnostics.push(Diagnostic {
1523 severity: Severity::Warning,
1524 code: "W002".into(),
1525 message: format!("unknown profile '{name}'"),
1526 path: Some("active_profile".into()),
1527 span: if self.profile_override.is_some() {
1528 None
1529 } else {
1530 active_key_span(module, &result)
1531 },
1532 repair: None,
1533 confidence: 1.0,
1534 file: if self.profile_override.is_some() {
1535 None
1536 } else {
1537 active_key_file(module, &result)
1538 },
1539 });
1540 }
1541 return Ok(result);
1542 }
1543 let base_table = match &result {
1544 Value::Table(map) => map.clone(),
1545 _ => return Ok(result),
1546 };
1547 let selected: Option<String> = match &self.profile_override {
1548 Some(name) => Some(name.clone()),
1549 None => match base_table.get("active_profile") {
1550 None => return Ok(result),
1551 Some(Value::String(s)) => Some(s.clone()),
1552 Some(other) => {
1553 diagnostics.push(Diagnostic {
1554 severity: Severity::Warning,
1555 code: "W002".into(),
1556 message: format!(
1557 "active_profile must be a string, got {}",
1558 value_kind_name(other)
1559 ),
1560 path: Some("active_profile".into()),
1561 span: if self.profile_override.is_some() {
1562 None
1563 } else {
1564 active_key_span(module, &result)
1565 },
1566 repair: None,
1567 confidence: 1.0,
1568 file: if self.profile_override.is_some() {
1569 None
1570 } else {
1571 active_key_file(module, &result)
1572 },
1573 });
1574 return Ok(result);
1575 }
1576 },
1577 };
1578 let name = match selected {
1579 Some(n) => n,
1580 None => return Ok(result),
1581 };
1582 let Some(&profile_id) = module.profiles.get(&name) else {
1583 diagnostics.push(Diagnostic {
1584 severity: Severity::Warning,
1585 code: "W002".into(),
1586 message: format!("unknown profile '{name}'"),
1587 path: Some("active_profile".into()),
1588 span: if self.profile_override.is_some() {
1589 None
1590 } else {
1591 active_key_span(module, &result)
1592 },
1593 repair: None,
1594 confidence: 1.0,
1595 file: if self.profile_override.is_some() {
1596 None
1597 } else {
1598 active_key_file(module, &result)
1599 },
1600 });
1601 return Ok(result);
1602 };
1603 let profile_value = values.get(&profile_id).cloned().unwrap_or(Value::Null);
1604 account.charge(module, Some(profile_id), measure_value(&profile_value))?;
1608 let mut merged = base_table;
1609 merge_profile_value(
1610 &mut merged,
1611 profile_value,
1612 &name,
1613 String::new(),
1614 module.node_spans.get(&profile_id).cloned(),
1615 module.node_files.get(&profile_id).cloned(),
1616 diagnostics,
1617 );
1618 Ok(Value::Table(merged))
1619 }
1620}
1621
1622fn active_key_span(module: &Module, result: &Value) -> Option<crate::diag::Span> {
1624 let Value::Table(_) = result else {
1625 return None;
1626 };
1627 let root = module.root?;
1628 let Node::Table { fields } = module.get(root)? else {
1629 return None;
1630 };
1631 let id = fields.get("active_profile")?;
1632 module.node_spans.get(id).cloned()
1633}
1634
1635fn active_key_file(module: &Module, result: &Value) -> Option<String> {
1637 let Value::Table(_) = result else {
1638 return None;
1639 };
1640 let root = module.root?;
1641 let Node::Table { fields } = module.get(root)? else {
1642 return None;
1643 };
1644 let id = fields.get("active_profile")?;
1645 module.node_files.get(id).cloned()
1646}
1647
1648pub(crate) fn value_kind_name(v: &Value) -> String {
1652 match v {
1653 Value::Null => "null".into(),
1654 Value::Bool(_) => "boolean".into(),
1655 Value::Int(_) | Value::Float(_) => "number".into(),
1656 Value::String(_) => "string".into(),
1657 Value::DurationMs(_) => "duration".into(),
1658 Value::Quantity(_, unit) => format!("quantity:{}", unit.suffix()),
1659 Value::Array(_) => "array".into(),
1660 Value::Table(_) => "table".into(),
1661 Value::Secret(_) => "secret".into(),
1662 }
1663}
1664
1665fn merge_profile_value(
1671 base: &mut indexmap::IndexMap<String, Value>,
1672 overlay: Value,
1673 profile: &str,
1674 path: String,
1675 span: Option<crate::diag::Span>,
1676 file: Option<String>,
1677 diagnostics: &mut Vec<Diagnostic>,
1678) {
1679 let Value::Table(fields) = overlay else {
1680 return;
1681 };
1682 for (k, v) in fields {
1683 let child = if path.is_empty() {
1684 k.clone()
1685 } else {
1686 format!("{path}.{k}")
1687 };
1688 match base.get(&k) {
1689 None => {
1690 base.insert(k, v);
1691 }
1692 Some(existing) => match (existing, &v) {
1693 (Value::Table(_), Value::Table(_)) => {
1694 if let Some(Value::Table(base_sub)) = base.get_mut(&k) {
1695 merge_profile_value(
1696 base_sub,
1697 v,
1698 profile,
1699 child,
1700 span.clone(),
1701 file.clone(),
1702 diagnostics,
1703 );
1704 }
1705 }
1706 _ => {
1707 if value_kind_name(existing) != value_kind_name(&v) {
1708 diagnostics.push(Diagnostic {
1709 severity: Severity::Warning,
1710 code: "W003".into(),
1711 message: format!(
1712 "profile '{profile}' overrides '{child}' with a different type"
1713 ),
1714 path: Some(child),
1715 span: span.clone(),
1716 repair: None,
1717 confidence: 1.0,
1718 file: file.clone(),
1719 });
1720 }
1721 base.insert(k, v);
1722 }
1723 },
1724 }
1725 }
1726}
1727
1728#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1730enum RenameOutcome {
1731 Ok,
1732 Missing,
1733 Exists,
1734}
1735
1736fn patch_parent<'a>(root: &'a mut Value, segs: &[PathSeg], create: bool) -> Option<&'a mut Value> {
1744 let mut cur = root;
1745 let last = segs.len().checked_sub(1)?;
1746 let mut i = 0;
1747 while i < last {
1748 match &segs[i] {
1749 PathSeg::Key(k) => {
1750 let need_array = matches!(segs.get(i + 1), Some(PathSeg::Index(_)));
1751 cur = match cur {
1752 Value::Table(map) => {
1753 if !create && !map.contains_key(k) {
1754 return None;
1755 }
1756 map.entry(k.clone()).or_insert_with(|| {
1757 if need_array {
1758 Value::Array(Vec::new())
1759 } else {
1760 Value::Table(indexmap::IndexMap::new())
1761 }
1762 })
1763 }
1764 _ => return None,
1765 };
1766 }
1767 PathSeg::Index(idx) => {
1768 cur = match cur {
1769 Value::Array(items) => items.get_mut(*idx)?,
1770 _ => return None,
1771 };
1772 }
1773 }
1774 i += 1;
1775 }
1776 Some(cur)
1777}
1778
1779fn patch_set(root: &mut Value, segs: &[PathSeg], value: Value) -> bool {
1783 let Some(last) = segs.last() else {
1784 return false;
1785 };
1786 let Some(parent) = patch_parent(root, segs, true) else {
1787 return false;
1788 };
1789 match (parent, last) {
1790 (Value::Table(map), PathSeg::Key(k)) => {
1791 map.insert(k.clone(), value);
1792 true
1793 }
1794 (Value::Array(items), PathSeg::Index(i)) => {
1795 if let Some(slot) = items.get_mut(*i) {
1796 *slot = value;
1797 true
1798 } else {
1799 false
1800 }
1801 }
1802 _ => false,
1803 }
1804}
1805
1806fn patch_remove(root: &mut Value, segs: &[PathSeg]) -> bool {
1809 let Some(last) = segs.last() else {
1810 return false;
1811 };
1812 let Some(parent) = patch_parent(root, segs, false) else {
1813 return false;
1814 };
1815 match (parent, last) {
1816 (Value::Table(map), PathSeg::Key(k)) => map.shift_remove(k).is_some(),
1817 (Value::Array(items), PathSeg::Index(i)) if *i < items.len() => {
1818 items.remove(*i);
1819 true
1820 }
1821 _ => false,
1822 }
1823}
1824
1825fn patch_rename(root: &mut Value, segs: &[PathSeg], new_name: &str) -> RenameOutcome {
1830 let Some(last) = segs.last() else {
1831 return RenameOutcome::Missing;
1832 };
1833 if !matches!(last, PathSeg::Key(_)) {
1835 return RenameOutcome::Missing;
1836 }
1837 let Some(parent) = patch_parent(root, segs, false) else {
1838 return RenameOutcome::Missing;
1839 };
1840 let Value::Table(map) = parent else {
1841 return RenameOutcome::Missing;
1842 };
1843 let PathSeg::Key(old) = last else {
1844 return RenameOutcome::Missing;
1845 };
1846 if !map.contains_key(old) {
1847 return RenameOutcome::Missing;
1848 }
1849 if map.contains_key(new_name) {
1850 return RenameOutcome::Exists;
1851 }
1852 match map.shift_remove(old) {
1853 Some(v) => {
1854 map.insert(new_name.to_string(), v);
1855 RenameOutcome::Ok
1856 }
1857 None => RenameOutcome::Missing,
1858 }
1859}
1860
1861fn normalize_quantity(v: &Value, unit: &Unit) -> Result<Value, String> {
1862 let num = match v {
1863 Value::Int(n) => *n as f64,
1864 Value::Float(f) => *f,
1865 _ => {
1866 return Err(format!("quantity requires numeric value, got {v:?}"));
1867 }
1868 };
1869
1870 if let Some(ms) = unit.normalize_time_to_ms(num) {
1871 return Ok(Value::DurationMs(ms));
1872 }
1873 if let Some(bytes) = unit.normalize_to_bytes(num) {
1874 return Ok(Value::Int(i64::try_from(bytes).unwrap_or(i64::MAX)));
1878 }
1879 if *unit == Unit::Percent {
1884 return Ok(Value::Quantity(num / 100.0, Unit::Percent));
1885 }
1886 let hz_factor = match unit {
1890 Unit::Hertz => Some(1.0),
1891 Unit::Kilohertz => Some(1_000.0),
1892 Unit::Megahertz => Some(1_000_000.0),
1893 Unit::Gigahertz => Some(1_000_000_000.0),
1894 Unit::Terahertz => Some(1_000_000_000_000.0),
1895 _ => None,
1896 };
1897 if let Some(factor) = hz_factor {
1898 return Ok(Value::Quantity((num * factor).max(0.0), Unit::Hertz));
1899 }
1900 let bit_factor = match unit {
1904 Unit::Bits => Some((1.0, false)),
1905 Unit::Kilobits => Some((1_000.0, false)),
1906 Unit::Megabits => Some((1_000_000.0, false)),
1907 Unit::Gigabits => Some((1_000_000_000.0, false)),
1908 Unit::BitsPerSecond => Some((1.0, true)),
1909 Unit::KilobitsPerSecond => Some((1_000.0, true)),
1910 Unit::MegabitsPerSecond => Some((1_000_000.0, true)),
1911 Unit::GigabitsPerSecond => Some((1_000_000_000.0, true)),
1912 Unit::MegabytesPerSecond => Some((8_000_000.0, true)),
1913 _ => None,
1914 };
1915 if let Some((factor, is_rate)) = bit_factor {
1916 let canonical = if is_rate {
1917 Unit::BitsPerSecond
1918 } else {
1919 Unit::Bits
1920 };
1921 return Ok(Value::Quantity((num * factor).max(0.0), canonical));
1922 }
1923 let ai_factor = match unit {
1927 Unit::Tokens => Some((1.0, Unit::Tokens)),
1928 Unit::KiloTokens => Some((1_000.0, Unit::Tokens)),
1929 Unit::MegaTokens => Some((1_000_000.0, Unit::Tokens)),
1930 Unit::TokensPerSecond => Some((1.0, Unit::TokensPerSecond)),
1931 Unit::Flops => Some((1.0, Unit::Flops)),
1932 Unit::TeraFlops => Some((1_000_000_000_000.0, Unit::Flops)),
1933 Unit::PetaFlops => Some((1_000_000_000_000_000.0, Unit::Flops)),
1934 _ => None,
1935 };
1936 if let Some((factor, canonical)) = ai_factor {
1937 return Ok(Value::Quantity((num * factor).max(0.0), canonical));
1938 }
1939 let si_factor = match unit {
1943 Unit::Pascal => Some((1.0, Unit::Pascal)),
1944 Unit::Bar => Some((100_000.0, Unit::Pascal)),
1945 Unit::Psi => Some((6_894.757_293_168, Unit::Pascal)),
1946 Unit::Volts => Some((1.0, Unit::Volts)),
1947 Unit::Amperes => Some((1.0, Unit::Amperes)),
1948 Unit::Watts => Some((1.0, Unit::Watts)),
1949 Unit::Joules => Some((1.0, Unit::Joules)),
1950 Unit::KilowattHours => Some((3_600_000.0, Unit::Joules)),
1951 Unit::Newtons => Some((1.0, Unit::Newtons)),
1952 _ => None,
1953 };
1954 if let Some((factor, canonical)) = si_factor {
1955 return Ok(Value::Quantity((num * factor).max(0.0), canonical));
1956 }
1957 let mech_factor = match unit {
1959 Unit::Degrees => Some((std::f64::consts::PI / 180.0, Unit::Radians)),
1960 Unit::Radians => Some((1.0, Unit::Radians)),
1961 Unit::Rpm => Some((1.0, Unit::Rpm)),
1962 Unit::Meters => Some((1.0, Unit::Meters)),
1963 Unit::Kilometers => Some((1_000.0, Unit::Meters)),
1964 Unit::Kilograms => Some((1.0, Unit::Kilograms)),
1965 Unit::Grams => Some((0.001, Unit::Kilograms)),
1966 _ => None,
1967 };
1968 if let Some((factor, canonical)) = mech_factor {
1969 return Ok(Value::Quantity((num * factor).max(0.0), canonical));
1970 }
1971 match unit {
1975 Unit::Kelvin => return Ok(Value::Quantity(num.max(0.0), Unit::Kelvin)),
1976 Unit::Celsius => return Ok(Value::Quantity((num + 273.15).max(0.0), Unit::Kelvin)),
1977 Unit::Fahrenheit => {
1978 return Ok(Value::Quantity(
1979 ((num - 32.0) * 5.0 / 9.0 + 273.15).max(0.0),
1980 Unit::Kelvin,
1981 ))
1982 }
1983 Unit::Decibel => return Ok(Value::Quantity(num, Unit::Decibel)),
1984 _ => {}
1985 }
1986 Ok(Value::Quantity(num, unit.clone()))
1987}
1988
1989pub(crate) fn eval_binary(op: BinOp, lhs: &Value, rhs: &Value) -> Result<Value, String> {
1990 use BinOp::*;
1991 match (op, lhs, rhs) {
1992 (Add, Value::Int(a), Value::Int(b)) => Ok(Value::Int(a.saturating_add(*b))),
1997 (Sub, Value::Int(a), Value::Int(b)) => Ok(Value::Int(a.saturating_sub(*b))),
1998 (Mul, Value::Int(a), Value::Int(b)) => Ok(Value::Int(a.saturating_mul(*b))),
1999 (Div, Value::Int(a), Value::Int(b)) if *b != 0 => {
2000 Ok(Value::Int(a.checked_div(*b).unwrap_or(i64::MAX)))
2001 }
2002
2003 (Add, Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
2004 (Sub, Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)),
2005 (Mul, Value::Float(a), Value::Float(b)) => Ok(Value::Float(a * b)),
2006 (Div, Value::Float(a), Value::Float(b)) if *b != 0.0 => Ok(Value::Float(a / b)),
2007
2008 (Add, Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)),
2010 (Add, Value::Float(a), Value::Int(b)) => Ok(Value::Float(a + *b as f64)),
2011 (Mul, Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 * b)),
2012 (Mul, Value::Float(a), Value::Int(b)) => Ok(Value::Float(a * *b as f64)),
2013
2014 (Eq, a, b) => Ok(Value::Bool(value_eq(a, b))),
2015 (Ne, a, b) => Ok(Value::Bool(!value_eq(a, b))),
2016
2017 (Lt, Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
2018 (Le, Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
2019 (Gt, Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
2020 (Ge, Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
2021
2022 (Add, Value::DurationMs(a), Value::DurationMs(b)) => {
2027 Ok(Value::DurationMs(a.saturating_add(*b)))
2028 }
2029 (Sub, Value::DurationMs(a), Value::DurationMs(b)) => {
2030 Ok(Value::DurationMs(a.saturating_sub(*b)))
2031 }
2032 (Mul, Value::DurationMs(a), Value::Int(b)) => {
2033 Ok(Value::DurationMs(scale_dur(*a, *b as f64)))
2034 }
2035 (Mul, Value::Int(a), Value::DurationMs(b)) => {
2036 Ok(Value::DurationMs(scale_dur(*b, *a as f64)))
2037 }
2038 (Mul, Value::DurationMs(a), Value::Float(b)) => Ok(Value::DurationMs(scale_dur(*a, *b))),
2039 (Mul, Value::Float(a), Value::DurationMs(b)) => Ok(Value::DurationMs(scale_dur(*b, *a))),
2040 (Div, Value::DurationMs(a), Value::Int(b)) if *b != 0 => {
2041 Ok(Value::DurationMs(scale_dur(*a, 1.0 / *b as f64)))
2042 }
2043 (Div, Value::DurationMs(a), Value::Float(b)) if *b != 0.0 => {
2044 Ok(Value::DurationMs(scale_dur(*a, 1.0 / *b)))
2045 }
2046 (Div, Value::DurationMs(a), Value::DurationMs(b)) if *b != 0 => {
2047 Ok(Value::Int((*a / *b) as i64))
2048 }
2049 (Lt, Value::DurationMs(a), Value::DurationMs(b)) => Ok(Value::Bool(a < b)),
2050 (Le, Value::DurationMs(a), Value::DurationMs(b)) => Ok(Value::Bool(a <= b)),
2051 (Gt, Value::DurationMs(a), Value::DurationMs(b)) => Ok(Value::Bool(a > b)),
2052 (Ge, Value::DurationMs(a), Value::DurationMs(b)) => Ok(Value::Bool(a >= b)),
2053
2054 (Add, Value::Quantity(a, ua), Value::Quantity(b, ub))
2062 if ua == ub && !ua.is_non_arithmetic() =>
2063 {
2064 Ok(Value::Quantity(a + b, ua.clone()))
2065 }
2066 (Sub, Value::Quantity(a, ua), Value::Quantity(b, ub))
2067 if ua == ub && !ua.is_non_arithmetic() =>
2068 {
2069 Ok(Value::Quantity(a - b, ua.clone()))
2070 }
2071 (Mul, Value::Quantity(a, u), Value::Int(n)) if !u.is_non_arithmetic() => {
2072 Ok(Value::Quantity(a * *n as f64, u.clone()))
2073 }
2074 (Mul, Value::Quantity(a, u), Value::Float(f)) if !u.is_non_arithmetic() => {
2075 Ok(Value::Quantity(a * f, u.clone()))
2076 }
2077 (Div, Value::Quantity(a, u), Value::Int(n)) if *n != 0 && !u.is_non_arithmetic() => {
2078 Ok(Value::Quantity(a / *n as f64, u.clone()))
2079 }
2080 (Div, Value::Quantity(a, u), Value::Float(f)) if *f != 0.0 && !u.is_non_arithmetic() => {
2081 Ok(Value::Quantity(a / f, u.clone()))
2082 }
2083 (Div, Value::Quantity(a, ua), Value::Quantity(b, ub))
2084 if ua == ub && *b != 0.0 && !ua.is_non_arithmetic() =>
2085 {
2086 Ok(Value::Float(a / b))
2087 }
2088 (Lt, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => Ok(Value::Bool(a < b)),
2089 (Le, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => Ok(Value::Bool(a <= b)),
2090 (Gt, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => Ok(Value::Bool(a > b)),
2091 (Ge, Value::Quantity(a, ua), Value::Quantity(b, ub)) if ua == ub => Ok(Value::Bool(a >= b)),
2092
2093 (And, Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(*a && *b)),
2094 (Or, Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(*a || *b)),
2095
2096 _ => Err(format!(
2097 "unsupported binary op {op:?} on {lhs:?} and {rhs:?}"
2098 )),
2099 }
2100}
2101
2102fn scale_dur(ms: u64, factor: f64) -> u64 {
2104 (ms as f64 * factor).round().max(0.0) as u64
2105}
2106
2107pub(crate) fn eval_unary(op: UnOp, v: &Value) -> Result<Value, String> {
2108 match (op, v) {
2109 (UnOp::Neg, Value::Int(n)) => Ok(Value::Int(n.saturating_neg())),
2110 (UnOp::Neg, Value::Float(f)) => Ok(Value::Float(-f)),
2111 (UnOp::Neg, Value::Quantity(a, u)) => Ok(Value::Quantity(-a, u.clone())),
2115 (UnOp::Not, Value::Bool(b)) => Ok(Value::Bool(!b)),
2116 _ => Err(format!("unsupported unary op {op:?} on {v:?}")),
2117 }
2118}
2119
2120fn value_eq(a: &Value, b: &Value) -> bool {
2121 match (a, b) {
2122 (Value::Null, Value::Null) => true,
2123 (Value::Bool(x), Value::Bool(y)) => x == y,
2124 (Value::Int(x), Value::Int(y)) => x == y,
2125 (Value::Float(x), Value::Float(y)) => (x - y).abs() < f64::EPSILON,
2126 (Value::String(x), Value::String(y)) => x == y,
2127 (Value::DurationMs(x), Value::DurationMs(y)) => x == y,
2128 (Value::Quantity(x, ux), Value::Quantity(y, uy)) => x == y && ux == uy,
2129 _ => false,
2130 }
2131}