1use std::collections::HashMap;
41
42use harn_lexer::{Span, StringSegment};
43
44use crate::ast::{BinaryOperator, DictEntry, Node, SNode, UnaryOperator};
45
46pub const MAX_STEPS: u32 = 100_000;
49
50pub const MAX_DEPTH: u32 = 256;
53
54#[derive(Debug, Clone, PartialEq)]
63pub enum ConstValue {
64 Int(i64),
65 Float(f64),
66 Bool(bool),
67 String(String),
68 List(Vec<ConstValue>),
69 Dict(Vec<(String, ConstValue)>),
70 Nil,
71}
72
73impl ConstValue {
74 pub fn display(&self) -> String {
78 match self {
79 ConstValue::Int(n) => n.to_string(),
80 ConstValue::Float(f) => format_float(*f),
81 ConstValue::Bool(b) => b.to_string(),
82 ConstValue::String(s) => s.clone(),
83 ConstValue::Nil => "nil".to_string(),
84 ConstValue::List(items) => {
85 let parts: Vec<String> = items.iter().map(|v| v.display()).collect();
86 format!("[{}]", parts.join(", "))
87 }
88 ConstValue::Dict(entries) => {
89 let parts: Vec<String> = entries
90 .iter()
91 .map(|(k, v)| format!("{k}: {}", v.display()))
92 .collect();
93 format!("{{{}}}", parts.join(", "))
94 }
95 }
96 }
97}
98
99fn format_float(f: f64) -> String {
100 if f.fract() == 0.0 && f.is_finite() {
101 format!("{f:.1}")
102 } else {
103 format!("{f}")
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum ConstEvalErrorKind {
116 Disallowed,
118 StepLimit,
120 RecursionLimit,
122 SandboxViolation,
125 RuntimeError,
128}
129
130#[derive(Debug, Clone)]
134pub struct ConstEvalError {
135 pub kind: ConstEvalErrorKind,
136 pub span: Span,
137 pub detail: String,
138}
139
140impl ConstEvalError {
141 fn disallowed(span: Span, detail: impl Into<String>) -> Self {
142 Self {
143 kind: ConstEvalErrorKind::Disallowed,
144 span,
145 detail: detail.into(),
146 }
147 }
148
149 fn sandbox(span: Span, detail: impl Into<String>) -> Self {
150 Self {
151 kind: ConstEvalErrorKind::SandboxViolation,
152 span,
153 detail: detail.into(),
154 }
155 }
156
157 fn runtime(span: Span, detail: impl Into<String>) -> Self {
158 Self {
159 kind: ConstEvalErrorKind::RuntimeError,
160 span,
161 detail: detail.into(),
162 }
163 }
164
165 fn step_limit(span: Span) -> Self {
166 Self {
167 kind: ConstEvalErrorKind::StepLimit,
168 span,
169 detail: format!("const-eval exceeded the {MAX_STEPS}-step budget"),
170 }
171 }
172
173 fn recursion_limit(span: Span) -> Self {
174 Self {
175 kind: ConstEvalErrorKind::RecursionLimit,
176 span,
177 detail: format!("const-eval exceeded the {MAX_DEPTH}-deep recursion budget"),
178 }
179 }
180}
181
182const SANDBOXED_OBJECT_ROOTS: &[&str] = &[
188 "harness",
189 "host",
190 "transcript",
191 "registry",
192 "process",
193 "fs",
194 "net",
195 "env",
196 "stdio",
197 "log",
198 "agent",
199 "session",
200];
201
202const PURE_BUILTINS: &[&str] = &[
207 "len",
208 "format",
209 "min",
210 "max",
211 "abs",
212 "floor",
213 "ceil",
214 "round",
215 "lowercase",
216 "uppercase",
217 "trim",
218 "concat",
219 "join",
220];
221
222const PURE_BINARY_OPS: &[BinaryOperator] = &[
226 BinaryOperator::Add,
227 BinaryOperator::Sub,
228 BinaryOperator::Mul,
229 BinaryOperator::Div,
230 BinaryOperator::Mod,
231 BinaryOperator::Pow,
232 BinaryOperator::Eq,
233 BinaryOperator::Neq,
234 BinaryOperator::Lt,
235 BinaryOperator::Gt,
236 BinaryOperator::Lte,
237 BinaryOperator::Gte,
238 BinaryOperator::And,
239 BinaryOperator::Or,
240 BinaryOperator::NilCoalesce,
241];
242
243pub type ConstEnv = HashMap<String, ConstValue>;
247
248pub fn const_eval(node: &SNode, env: &ConstEnv) -> Result<ConstValue, ConstEvalError> {
252 const_eval_with_resolver(node, &|name| env.get(name).cloned())
253}
254
255pub(crate) fn const_eval_with_resolver(
257 node: &SNode,
258 resolve: &dyn Fn(&str) -> Option<ConstValue>,
259) -> Result<ConstValue, ConstEvalError> {
260 let mut ctx = EvalCtx {
261 resolve,
262 steps: 0,
263 depth: 0,
264 };
265 ctx.eval_node(node)
266}
267
268struct EvalCtx<'a> {
269 resolve: &'a dyn Fn(&str) -> Option<ConstValue>,
270 steps: u32,
271 depth: u32,
272}
273
274impl<'a> EvalCtx<'a> {
275 fn step(&mut self, span: Span) -> Result<(), ConstEvalError> {
276 self.steps = self.steps.saturating_add(1);
277 if self.steps > MAX_STEPS {
278 return Err(ConstEvalError::step_limit(span));
279 }
280 Ok(())
281 }
282
283 fn enter(&mut self, span: Span) -> Result<(), ConstEvalError> {
284 self.depth = self.depth.saturating_add(1);
285 if self.depth > MAX_DEPTH {
286 self.depth -= 1;
287 return Err(ConstEvalError::recursion_limit(span));
288 }
289 Ok(())
290 }
291
292 fn leave(&mut self) {
293 self.depth = self.depth.saturating_sub(1);
294 }
295
296 fn eval_node(&mut self, node: &SNode) -> Result<ConstValue, ConstEvalError> {
297 self.step(node.span)?;
298 self.enter(node.span)?;
299 let result = self.eval_node_inner(node);
300 self.leave();
301 result
302 }
303
304 fn eval_node_inner(&mut self, node: &SNode) -> Result<ConstValue, ConstEvalError> {
305 let ctx = self;
306 match &node.node {
307 Node::IntLiteral(n) => Ok(ConstValue::Int(*n)),
308 Node::FloatLiteral(f) => Ok(ConstValue::Float(*f)),
309 Node::BoolLiteral(b) => Ok(ConstValue::Bool(*b)),
310 Node::StringLiteral(s) | Node::RawStringLiteral(s) => Ok(ConstValue::String(s.clone())),
311 Node::NilLiteral => Ok(ConstValue::Nil),
312
313 Node::Identifier(name) => (ctx.resolve)(name).ok_or_else(|| {
314 ConstEvalError::runtime(
315 node.span,
316 format!("`{name}` is not a const-known identifier"),
317 )
318 }),
319
320 Node::ListLiteral(items) => {
321 let mut out = Vec::with_capacity(items.len());
322 for item in items {
323 if matches!(&item.node, Node::Spread(_)) {
324 return Err(ConstEvalError::disallowed(
325 item.span,
326 "spread in a const list literal is not supported",
327 ));
328 }
329 out.push(ctx.eval_node(item)?);
330 }
331 Ok(ConstValue::List(out))
332 }
333
334 Node::DictLiteral(entries) => {
335 let mut out: Vec<(String, ConstValue)> = Vec::with_capacity(entries.len());
336 for entry in entries {
337 let key = ctx.dict_key_name(entry)?;
338 let value = ctx.eval_node(&entry.value)?;
339 out.push((key, value));
340 }
341 Ok(ConstValue::Dict(out))
342 }
343
344 Node::InterpolatedString(segments) => {
345 let mut buf = String::new();
346 for seg in segments {
347 match seg {
348 StringSegment::Literal(lit) => buf.push_str(lit),
349 StringSegment::Expression(src, _, _) => {
350 return Err(ConstEvalError::disallowed(
360 node.span,
361 format!("interpolated expression `${{{src}}}` is not supported in a const initializer; use `format(...)` or string concatenation"),
362 ));
363 }
364 }
365 }
366 Ok(ConstValue::String(buf))
367 }
368
369 Node::UnaryOp { op, operand } => {
370 let value = ctx.eval_node(operand)?;
371 match (op, &value) {
372 (UnaryOperator::Neg, ConstValue::Int(n)) => {
373 Ok(ConstValue::Int(n.checked_neg().ok_or_else(|| {
374 ConstEvalError::runtime(node.span, "integer overflow in unary minus")
375 })?))
376 }
377 (UnaryOperator::Neg, ConstValue::Float(f)) => Ok(ConstValue::Float(-f)),
378 (UnaryOperator::Not, ConstValue::Bool(b)) => Ok(ConstValue::Bool(!b)),
379 _ => Err(ConstEvalError::runtime(
380 node.span,
381 format!("unary `{op}` is not defined for the operand"),
382 )),
383 }
384 }
385
386 Node::BinaryOp { op, left, right } => {
387 if !PURE_BINARY_OPS.contains(op) {
388 return Err(ConstEvalError::disallowed(
389 node.span,
390 format!("binary operator `{op}` is not const-evaluable"),
391 ));
392 }
393 let lhs = ctx.eval_node(left)?;
394 let rhs = ctx.eval_node(right)?;
395 ctx.apply_binary(*op, lhs, rhs, node.span)
396 }
397
398 Node::Ternary {
399 condition,
400 true_expr,
401 false_expr,
402 } => {
403 let cond = ctx.eval_node(condition)?;
404 let pick = match cond {
405 ConstValue::Bool(b) => b,
406 _ => {
407 return Err(ConstEvalError::runtime(
408 condition.span,
409 "ternary condition must fold to a bool",
410 ))
411 }
412 };
413 if pick {
414 ctx.eval_node(true_expr)
415 } else {
416 ctx.eval_node(false_expr)
417 }
418 }
419
420 Node::IfElse {
421 condition,
422 then_body,
423 else_body,
424 ..
425 } => {
426 let cond = ctx.eval_node(condition)?;
427 let pick = match cond {
428 ConstValue::Bool(b) => b,
429 _ => {
430 return Err(ConstEvalError::runtime(
431 condition.span,
432 "if-expression condition must fold to a bool",
433 ))
434 }
435 };
436 let branch =
437 if pick {
438 then_body.as_slice()
439 } else {
440 match else_body {
441 Some(body) => body.as_slice(),
442 None => return Err(ConstEvalError::disallowed(
443 node.span,
444 "if-expression without an else branch cannot be const-evaluated",
445 )),
446 }
447 };
448 let Some(last) = branch.last() else {
449 return Err(ConstEvalError::disallowed(
450 node.span,
451 "if-expression branch must produce a value",
452 ));
453 };
454 if let Some(first_pre) = branch[..branch.len().saturating_sub(1)].first() {
455 return Err(ConstEvalError::disallowed(
460 first_pre.span,
461 "multi-statement if-branch is not const-evaluable",
462 ));
463 }
464 ctx.eval_node(last)
465 }
466
467 Node::FunctionCall { name, args, .. } => {
468 if !PURE_BUILTINS.contains(&name.as_str()) {
469 return Err(ConstEvalError::sandbox(
470 node.span,
471 format!(
472 "`{name}(...)` is not on the const-eval allowlist (only pure stdlib builtins may be called from a const initializer)"
473 ),
474 ));
475 }
476 let mut folded = Vec::with_capacity(args.len());
477 for arg in args {
478 folded.push(ctx.eval_node(arg)?);
479 }
480 ctx.apply_builtin(name, folded, node.span)
481 }
482
483 Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
489 if let Node::Identifier(root) = &object.node {
490 if SANDBOXED_OBJECT_ROOTS.contains(&root.as_str()) {
491 return Err(ConstEvalError::sandbox(
492 node.span,
493 format!(
494 "`{root}.*` is a sandboxed capability surface; const-eval refuses fs/net/env/process/host access"
495 ),
496 ));
497 }
498 }
499 Err(ConstEvalError::disallowed(
500 node.span,
501 "property access is not const-evaluable",
502 ))
503 }
504 Node::MethodCall { object, .. } | Node::OptionalMethodCall { object, .. } => {
505 if let Some(root) = leftmost_receiver_identifier(object) {
514 if SANDBOXED_OBJECT_ROOTS.contains(&root) {
515 return Err(ConstEvalError::sandbox(
516 node.span,
517 format!(
518 "`{root}.*(...)` is a sandboxed capability surface; const-eval refuses fs/net/env/process/host access"
519 ),
520 ));
521 }
522 }
523 Err(ConstEvalError::disallowed(
524 node.span,
525 "method call is not const-evaluable",
526 ))
527 }
528 Node::SubscriptAccess { object, index } => {
529 let recv = ctx.eval_node(object)?;
530 let idx = ctx.eval_node(index)?;
531 match (recv, idx) {
532 (ConstValue::List(items), ConstValue::Int(i)) => {
533 let pos = if i < 0 { i + items.len() as i64 } else { i };
534 usize::try_from(pos)
535 .ok()
536 .and_then(|p| items.get(p).cloned())
537 .ok_or_else(|| {
538 ConstEvalError::runtime(
539 node.span,
540 format!("index {i} out of bounds"),
541 )
542 })
543 }
544 (ConstValue::Dict(entries), ConstValue::String(k)) => entries
545 .into_iter()
546 .find(|(name, _)| *name == k)
547 .map(|(_, v)| v)
548 .ok_or_else(|| {
549 ConstEvalError::runtime(node.span, format!("unknown key `{k}`"))
550 }),
551 _ => Err(ConstEvalError::runtime(
552 node.span,
553 "subscript receiver and index types are incompatible",
554 )),
555 }
556 }
557 Node::Block(_) => Err(ConstEvalError::disallowed(
558 node.span,
559 "block expression is not const-evaluable",
560 )),
561 Node::Closure { .. } => Err(ConstEvalError::disallowed(
562 node.span,
563 "closure is not const-evaluable",
564 )),
565
566 Node::SpawnExpr { .. }
568 | Node::SelectExpr { .. }
569 | Node::Parallel { .. }
570 | Node::MutexBlock { .. }
571 | Node::DeferStmt { .. }
572 | Node::YieldExpr { .. }
573 | Node::EmitExpr { .. }
574 | Node::TryCatch { .. }
575 | Node::TryExpr { .. }
576 | Node::TryOperator { .. }
577 | Node::TryStar { .. }
578 | Node::DeadlineBlock { .. }
579 | Node::CostRoute { .. }
580 | Node::WhileLoop { .. }
581 | Node::ForIn { .. }
582 | Node::Retry { .. }
583 | Node::GuardStmt { .. }
584 | Node::RequireStmt { .. }
585 | Node::Assignment { .. }
586 | Node::ThrowStmt { .. }
587 | Node::ReturnStmt { .. }
588 | Node::BreakStmt
589 | Node::ContinueStmt => Err(ConstEvalError::sandbox(
590 node.span,
591 "runtime construct is not permitted in a const initializer",
592 )),
593
594 _ => Err(ConstEvalError::disallowed(
596 node.span,
597 "expression shape is not on the const-eval allowlist",
598 )),
599 }
600 }
601
602 fn dict_key_name(&self, entry: &DictEntry) -> Result<String, ConstEvalError> {
603 match &entry.key.node {
604 Node::Identifier(name) => Ok(name.clone()),
605 Node::StringLiteral(s) | Node::RawStringLiteral(s) => Ok(s.clone()),
606 _ => Err(ConstEvalError::disallowed(
607 entry.key.span,
608 "dict keys in a const dict literal must be identifiers or string literals",
609 )),
610 }
611 }
612
613 fn apply_binary(
614 &self,
615 op: BinaryOperator,
616 lhs: ConstValue,
617 rhs: ConstValue,
618 span: Span,
619 ) -> Result<ConstValue, ConstEvalError> {
620 use ConstValue::*;
621
622 if op.is_logical() {
624 let (Bool(l), Bool(r)) = (&lhs, &rhs) else {
625 return Err(ConstEvalError::runtime(
626 span,
627 format!("`{op}` requires bool operands"),
628 ));
629 };
630 return Ok(Bool(if op == BinaryOperator::And {
631 *l && *r
632 } else {
633 *l || *r
634 }));
635 }
636 if op == BinaryOperator::NilCoalesce {
637 return Ok(match lhs {
638 Nil => rhs,
639 other => other,
640 });
641 }
642 if op == BinaryOperator::Eq {
643 return Ok(Bool(lhs == rhs));
644 }
645 if op == BinaryOperator::Neq {
646 return Ok(Bool(lhs != rhs));
647 }
648
649 if op == BinaryOperator::Add {
651 if let (String(a), String(b)) = (&lhs, &rhs) {
652 return Ok(String(format!("{a}{b}")));
653 }
654 }
655
656 let (lhs_num, rhs_num) = match (&lhs, &rhs) {
658 (Int(_) | Float(_), Int(_) | Float(_)) => (lhs.clone(), rhs.clone()),
659 _ => {
660 return Err(ConstEvalError::runtime(
661 span,
662 format!(
663 "`{op}` requires numeric operands, got {} and {}",
664 value_kind(&lhs),
665 value_kind(&rhs)
666 ),
667 ))
668 }
669 };
670
671 if op.precedence() == crate::operators::Precedence::Comparison {
673 let (l, r) = (as_float(&lhs_num), as_float(&rhs_num));
674 let out = match op {
675 BinaryOperator::Lt => l < r,
676 BinaryOperator::Gt => l > r,
677 BinaryOperator::Lte => l <= r,
678 BinaryOperator::Gte => l >= r,
679 _ => unreachable!(),
680 };
681 return Ok(Bool(out));
682 }
683
684 if let (Int(a), Int(b)) = (&lhs_num, &rhs_num) {
686 let result = match op {
687 BinaryOperator::Add => a.checked_add(*b),
688 BinaryOperator::Sub => a.checked_sub(*b),
689 BinaryOperator::Mul => a.checked_mul(*b),
690 BinaryOperator::Div => {
691 if *b == 0 {
692 return Err(ConstEvalError::runtime(span, "division by zero"));
693 }
694 a.checked_div(*b)
695 }
696 BinaryOperator::Mod => {
697 if *b == 0 {
698 return Err(ConstEvalError::runtime(span, "modulo by zero"));
699 }
700 a.checked_rem(*b)
701 }
702 BinaryOperator::Pow => {
703 if *b < 0 || *b > u32::MAX as i64 {
704 return Err(ConstEvalError::runtime(
705 span,
706 "exponent must be a non-negative i64 within u32 range",
707 ));
708 }
709 a.checked_pow(*b as u32)
710 }
711 _ => unreachable!(),
712 };
713 return result
714 .map(Int)
715 .ok_or_else(|| ConstEvalError::runtime(span, "integer overflow"));
716 }
717
718 let (l, r) = (as_float(&lhs_num), as_float(&rhs_num));
719 let value = match op {
720 BinaryOperator::Add => l + r,
721 BinaryOperator::Sub => l - r,
722 BinaryOperator::Mul => l * r,
723 BinaryOperator::Div => {
724 if r == 0.0 {
725 return Err(ConstEvalError::runtime(span, "division by zero"));
726 }
727 l / r
728 }
729 BinaryOperator::Mod => {
730 if r == 0.0 {
731 return Err(ConstEvalError::runtime(span, "modulo by zero"));
732 }
733 l % r
734 }
735 BinaryOperator::Pow => l.powf(r),
736 _ => unreachable!(),
737 };
738 Ok(Float(value))
739 }
740
741 fn apply_builtin(
742 &self,
743 name: &str,
744 args: Vec<ConstValue>,
745 span: Span,
746 ) -> Result<ConstValue, ConstEvalError> {
747 match name {
748 "len" => match args.as_slice() {
749 [ConstValue::String(s)] => Ok(ConstValue::Int(s.chars().count() as i64)),
750 [ConstValue::List(items)] => Ok(ConstValue::Int(items.len() as i64)),
751 [ConstValue::Dict(entries)] => Ok(ConstValue::Int(entries.len() as i64)),
752 _ => Err(ConstEvalError::runtime(
753 span,
754 "len() expects a single string / list / dict argument",
755 )),
756 },
757 "format" => format_call(span, args),
758 "concat" => {
759 let mut out = String::new();
760 for arg in &args {
761 match arg {
762 ConstValue::String(s) => out.push_str(s),
763 _ => {
764 return Err(ConstEvalError::runtime(
765 span,
766 "concat() expects string arguments",
767 ))
768 }
769 }
770 }
771 Ok(ConstValue::String(out))
772 }
773 "join" => match args.as_slice() {
774 [ConstValue::List(items), ConstValue::String(sep)] => {
775 let mut parts = Vec::with_capacity(items.len());
776 for item in items {
777 match item {
778 ConstValue::String(s) => parts.push(s.clone()),
779 other => parts.push(other.display()),
780 }
781 }
782 Ok(ConstValue::String(parts.join(sep)))
783 }
784 _ => Err(ConstEvalError::runtime(
785 span,
786 "join() expects (list, string)",
787 )),
788 },
789 "min" | "max" => apply_min_max(name, &args, span),
790 "abs" => match args.as_slice() {
791 [ConstValue::Int(n)] => {
792 Ok(ConstValue::Int(n.checked_abs().ok_or_else(|| {
793 ConstEvalError::runtime(span, "integer overflow in abs()")
794 })?))
795 }
796 [ConstValue::Float(f)] => Ok(ConstValue::Float(f.abs())),
797 _ => Err(ConstEvalError::runtime(
798 span,
799 "abs() expects a single numeric argument",
800 )),
801 },
802 "floor" => unary_float(span, &args, |f| f.floor()),
803 "ceil" => unary_float(span, &args, |f| f.ceil()),
804 "round" => match args.as_slice() {
805 [ConstValue::Float(f), ConstValue::Int(digits)] => {
809 Ok(ConstValue::Float(round_float_to_digits(*f, *digits)))
810 }
811 [ConstValue::Int(n), ConstValue::Int(digits)] => {
812 Ok(round_int_to_digits(*n, *digits)
813 .map_or_else(ConstValue::Float, ConstValue::Int))
814 }
815 _ => unary_float(span, &args, |f| f.round()),
816 },
817 "lowercase" => match args.as_slice() {
818 [ConstValue::String(s)] => Ok(ConstValue::String(s.to_lowercase())),
819 _ => Err(ConstEvalError::runtime(
820 span,
821 "lowercase() expects a string",
822 )),
823 },
824 "uppercase" => match args.as_slice() {
825 [ConstValue::String(s)] => Ok(ConstValue::String(s.to_uppercase())),
826 _ => Err(ConstEvalError::runtime(
827 span,
828 "uppercase() expects a string",
829 )),
830 },
831 "trim" => match args.as_slice() {
832 [ConstValue::String(s)] => Ok(ConstValue::String(s.trim().to_string())),
833 _ => Err(ConstEvalError::runtime(span, "trim() expects a string")),
834 },
835 _ => Err(ConstEvalError::sandbox(
840 span,
841 format!("`{name}(...)` lacks a const-eval implementation"),
842 )),
843 }
844 }
845}
846
847fn leftmost_receiver_identifier(node: &SNode) -> Option<&str> {
851 let mut current = node;
852 loop {
853 match ¤t.node {
854 Node::Identifier(name) => return Some(name.as_str()),
855 Node::PropertyAccess { object, .. }
856 | Node::OptionalPropertyAccess { object, .. }
857 | Node::SubscriptAccess { object, .. }
858 | Node::OptionalSubscriptAccess { object, .. } => {
859 current = object;
860 }
861 _ => return None,
862 }
863 }
864}
865
866fn value_kind(v: &ConstValue) -> &'static str {
867 match v {
868 ConstValue::Int(_) => "int",
869 ConstValue::Float(_) => "float",
870 ConstValue::Bool(_) => "bool",
871 ConstValue::String(_) => "string",
872 ConstValue::List(_) => "list",
873 ConstValue::Dict(_) => "dict",
874 ConstValue::Nil => "nil",
875 }
876}
877
878fn as_float(v: &ConstValue) -> f64 {
879 match v {
880 ConstValue::Int(n) => *n as f64,
881 ConstValue::Float(f) => *f,
882 _ => 0.0,
883 }
884}
885
886fn format_call(span: Span, args: Vec<ConstValue>) -> Result<ConstValue, ConstEvalError> {
887 let mut iter = args.into_iter();
888 let template = match iter.next() {
889 Some(ConstValue::String(s)) => s,
890 Some(_) => {
891 return Err(ConstEvalError::runtime(
892 span,
893 "format() template must be a string literal",
894 ))
895 }
896 None => {
897 return Err(ConstEvalError::runtime(
898 span,
899 "format() requires at least a template argument",
900 ))
901 }
902 };
903 let rest: Vec<ConstValue> = iter.collect();
904
905 if let [ConstValue::Dict(entries)] = rest.as_slice() {
908 let mut result = String::with_capacity(template.len());
909 let mut rest_str = template.as_str();
910 while let Some((head, after_open)) = rest_str.split_once('{') {
911 result.push_str(head);
912 let hit = after_open.split_once('}').and_then(|(key, after_close)| {
913 let (_, val) = entries.iter().find(|(k, _)| k == key)?;
914 Some((val, after_close))
915 });
916 if let Some((val, after_close)) = hit {
917 result.push_str(&val.display());
918 rest_str = after_close;
919 } else {
920 result.push('{');
921 rest_str = after_open;
922 }
923 }
924 result.push_str(rest_str);
925 return Ok(ConstValue::String(result));
926 }
927
928 let mut result = String::with_capacity(template.len());
929 let mut rest_iter = rest.iter();
930 let mut tail = template.as_str();
931 while let Some((head, rest_of_template)) = tail.split_once("{}") {
932 result.push_str(head);
933 if let Some(arg) = rest_iter.next() {
934 result.push_str(&arg.display());
935 } else {
936 result.push_str("{}");
937 }
938 tail = rest_of_template;
939 }
940 result.push_str(tail);
941 Ok(ConstValue::String(result))
942}
943
944fn apply_min_max(
945 name: &str,
946 args: &[ConstValue],
947 span: Span,
948) -> Result<ConstValue, ConstEvalError> {
949 if args.is_empty() {
950 return Err(ConstEvalError::runtime(
951 span,
952 format!("{name}() requires at least one argument"),
953 ));
954 }
955 let mut all_int = true;
956 for arg in args {
957 match arg {
958 ConstValue::Int(_) => {}
959 ConstValue::Float(_) => all_int = false,
960 _ => {
961 return Err(ConstEvalError::runtime(
962 span,
963 format!("{name}() expects numeric arguments"),
964 ))
965 }
966 }
967 }
968 if all_int {
969 let nums: Vec<i64> = args
970 .iter()
971 .map(|v| match v {
972 ConstValue::Int(n) => *n,
973 _ => unreachable!(),
974 })
975 .collect();
976 let pick = if name == "min" {
977 nums.iter().copied().min().unwrap()
978 } else {
979 nums.iter().copied().max().unwrap()
980 };
981 Ok(ConstValue::Int(pick))
982 } else {
983 let nums: Vec<f64> = args.iter().map(as_float).collect();
984 let pick = if name == "min" {
985 nums.iter().copied().fold(f64::INFINITY, f64::min)
986 } else {
987 nums.iter().copied().fold(f64::NEG_INFINITY, f64::max)
988 };
989 Ok(ConstValue::Float(pick))
990 }
991}
992
993pub fn round_float_to_digits(x: f64, digits: i64) -> f64 {
997 if !x.is_finite() {
998 return x;
999 }
1000 if digits == 0 {
1001 return x.round();
1002 }
1003 if digits > 308 {
1008 return x;
1009 }
1010 if digits < -308 {
1011 return 0.0 * x.signum();
1012 }
1013 if digits < 0 {
1014 let bucket = 10f64.powi((-digits) as i32);
1017 return (x / bucket).round() * bucket;
1018 }
1019 let factor = 10f64.powi(digits as i32);
1020 let scaled = x * factor;
1021 if !scaled.is_finite() {
1022 return x;
1025 }
1026 scaled.round() / factor
1027}
1028
1029pub fn round_int_to_digits(n: i64, digits: i64) -> Result<i64, f64> {
1035 if digits >= 0 || n == 0 {
1036 return Ok(n);
1037 }
1038 if digits <= -20 {
1039 return Ok(0);
1042 }
1043 let factor = 10i128.pow((-digits) as u32);
1044 let n128 = n as i128;
1045 let rem = n128 % factor;
1046 let base = n128 - rem;
1047 let rounded = if rem.abs() * 2 >= factor {
1048 base + factor * n128.signum()
1049 } else {
1050 base
1051 };
1052 i64::try_from(rounded).map_err(|_| rounded as f64)
1053}
1054
1055fn unary_float(
1056 span: Span,
1057 args: &[ConstValue],
1058 op: impl Fn(f64) -> f64,
1059) -> Result<ConstValue, ConstEvalError> {
1060 match args {
1061 [ConstValue::Int(n)] => Ok(ConstValue::Float(op(*n as f64))),
1062 [ConstValue::Float(f)] => Ok(ConstValue::Float(op(*f))),
1063 _ => Err(ConstEvalError::runtime(
1064 span,
1065 "expected a single numeric argument",
1066 )),
1067 }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072 use super::*;
1073 use crate::parse_source;
1074
1075 fn fold(source: &str) -> Result<ConstValue, ConstEvalError> {
1076 let program = parse_source(source).expect("parse");
1080 let mut env = ConstEnv::new();
1081 let mut last = None;
1082 for snode in &program {
1083 if let Node::ConstBinding {
1084 pattern: crate::ast::BindingPattern::Identifier(name),
1085 value,
1086 ..
1087 } = &snode.node
1088 {
1089 let folded = const_eval(value, &env)?;
1090 env.insert(name.clone(), folded.clone());
1091 last = Some(folded);
1092 }
1093 }
1094 Ok(last.expect("no const binding in source"))
1095 }
1096
1097 #[test]
1098 fn arithmetic_literals_fold() {
1099 assert_eq!(fold("const X = 1 + 2").unwrap(), ConstValue::Int(3));
1100 assert_eq!(fold("const Y = 5 * (3 + 2)").unwrap(), ConstValue::Int(25));
1101 assert_eq!(fold("const Z = 2 ** 10").unwrap(), ConstValue::Int(1024));
1102 }
1103
1104 #[test]
1105 fn string_concat_folds() {
1106 assert_eq!(
1107 fold(r#"const S = "foo" + "-" + "bar""#).unwrap(),
1108 ConstValue::String("foo-bar".to_string())
1109 );
1110 }
1111
1112 #[test]
1113 fn earlier_const_visible_to_later() {
1114 let src = "const A = 10\nconst B = A * 2";
1115 assert_eq!(fold(src).unwrap(), ConstValue::Int(20));
1116 }
1117
1118 #[test]
1119 fn len_of_literal_list() {
1120 assert_eq!(
1121 fold("const N = len([1, 2, 3, 4])").unwrap(),
1122 ConstValue::Int(4)
1123 );
1124 }
1125
1126 #[test]
1127 fn format_positional_placeholders() {
1128 let src = r#"const G = format("{}-{}", "hello", 42)"#;
1129 assert_eq!(
1130 fold(src).unwrap(),
1131 ConstValue::String("hello-42".to_string())
1132 );
1133 }
1134
1135 #[test]
1136 fn host_property_access_is_sandboxed() {
1137 let err = fold("const Z = harness.clock.now()").unwrap_err();
1138 assert!(matches!(
1139 err.kind,
1140 ConstEvalErrorKind::SandboxViolation | ConstEvalErrorKind::Disallowed
1141 ));
1142 }
1143
1144 #[test]
1145 fn division_by_zero_is_runtime_error() {
1146 let err = fold("const Z = 1 / 0").unwrap_err();
1147 assert!(matches!(err.kind, ConstEvalErrorKind::RuntimeError));
1148 }
1149
1150 #[test]
1151 fn unknown_identifier_is_runtime_error() {
1152 let err = fold("const Z = NOPE + 1").unwrap_err();
1153 assert!(matches!(err.kind, ConstEvalErrorKind::RuntimeError));
1154 }
1155
1156 #[test]
1157 fn spawn_is_sandbox_violation() {
1158 let err = fold("const Z = spawn { 1 }").unwrap_err();
1159 assert!(matches!(err.kind, ConstEvalErrorKind::SandboxViolation));
1160 }
1161
1162 #[test]
1163 fn user_function_call_is_sandboxed() {
1164 let err = fold("const Z = some_user_fn()").unwrap_err();
1165 assert!(matches!(err.kind, ConstEvalErrorKind::SandboxViolation));
1166 }
1167
1168 #[test]
1169 fn ternary_picks_branch() {
1170 assert_eq!(fold("const T = true ? 1 : 2").unwrap(), ConstValue::Int(1));
1171 assert_eq!(fold("const T = false ? 1 : 2").unwrap(), ConstValue::Int(2));
1172 }
1173
1174 #[test]
1175 fn list_subscript_folds() {
1176 assert_eq!(
1177 fold("const N = [10, 20, 30][1]").unwrap(),
1178 ConstValue::Int(20)
1179 );
1180 }
1181
1182 #[test]
1183 fn list_subscript_out_of_bounds_is_runtime_error() {
1184 let err = fold("const N = [1, 2][9]").unwrap_err();
1185 assert!(matches!(err.kind, ConstEvalErrorKind::RuntimeError));
1186 }
1187
1188 #[test]
1189 fn recursion_depth_is_bounded() {
1190 let env = ConstEnv::new();
1197 let mut ctx = EvalCtx {
1198 resolve: &|name| env.get(name).cloned(),
1199 steps: 0,
1200 depth: MAX_DEPTH,
1201 };
1202 let err = ctx.enter(Span::dummy()).unwrap_err();
1203 assert!(matches!(err.kind, ConstEvalErrorKind::RecursionLimit));
1204 assert_eq!(ctx.depth, MAX_DEPTH);
1208 }
1209
1210 #[test]
1211 fn step_budget_is_bounded() {
1212 let env = ConstEnv::new();
1218 let mut ctx = EvalCtx {
1219 resolve: &|name| env.get(name).cloned(),
1220 steps: MAX_STEPS,
1221 depth: 0,
1222 };
1223 let err = ctx.step(Span::dummy()).unwrap_err();
1224 assert!(matches!(err.kind, ConstEvalErrorKind::StepLimit));
1225 }
1226
1227 #[test]
1228 fn step_counter_is_not_amortized() {
1229 let env = ConstEnv::new();
1235 let mut ctx = EvalCtx {
1239 resolve: &|name| env.get(name).cloned(),
1240 steps: MAX_STEPS - 4,
1241 depth: 0,
1242 };
1243 let span = Span::dummy();
1244 for _ in 0..4 {
1245 ctx.step(span).expect("inside budget");
1246 }
1247 let err = ctx.step(span).unwrap_err();
1248 assert!(matches!(err.kind, ConstEvalErrorKind::StepLimit));
1249 }
1250
1251 #[test]
1252 fn index_and_round_fold_like_the_runtime() {
1253 assert_eq!(
1254 fold("const A = [10, 20, 30][-1]").unwrap(),
1255 ConstValue::Int(30)
1256 );
1257 assert!(fold("const B = [10, 20, 30][-4]").is_err());
1258 assert_eq!(
1259 fold("const C = round(123456.0, -5)").unwrap(),
1260 ConstValue::Float(100000.0)
1261 );
1262 assert_eq!(
1263 fold("const D = round(9000000000000000000, -19)").unwrap(),
1264 ConstValue::Float(1e19)
1265 );
1266 }
1267}