1pub mod env;
5pub mod methods;
6pub mod value;
7
8use indexmap::IndexMap;
9use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::Arc;
12
13use crate::error::Diagnostic;
14use crate::lexer::token::StrPart;
15use crate::lexer::Lexer;
16use crate::parser::ast::*;
17use crate::parser::Parser;
18use crate::span::Span;
19use env::{Env, Environment};
20use methods::MethodRegistry;
21use value::{EnumDef, FuncBody, FunctionDef, SchemaDef, Value};
22
23const MAX_CALL_DEPTH: u32 = 256;
24
25#[derive(Debug, Clone, Copy, Default)]
26pub struct Options {
27 pub strict: bool,
28 pub dry_run: bool,
29}
30
31#[derive(Debug, Clone, Default)]
33pub enum EnvCap {
34 #[default]
35 Deny,
36 AllowAll,
37 Allow(Vec<String>),
38}
39
40#[derive(Debug)]
41pub enum FileError {
42 Denied,
44 Outside(Vec<PathBuf>),
50 Io(String),
51}
52
53pub trait FileAccess {
55 fn read(&self, path: &str) -> Result<String, FileError>;
56}
57
58pub struct DenyFs;
60impl FileAccess for DenyFs {
61 fn read(&self, _path: &str) -> Result<String, FileError> {
62 Err(FileError::Denied)
63 }
64}
65
66pub struct MemFs(pub HashMap<String, String>);
67impl FileAccess for MemFs {
68 fn read(&self, path: &str) -> Result<String, FileError> {
69 self.0
70 .get(path)
71 .cloned()
72 .ok_or_else(|| FileError::Io(format!("not found: {path}")))
73 }
74}
75
76pub struct RecordingFs {
78 pub inner: Box<dyn FileAccess>,
79 pub log: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
80}
81impl FileAccess for RecordingFs {
82 fn read(&self, path: &str) -> Result<String, FileError> {
83 let result = self.inner.read(path);
84 if result.is_ok() {
85 self.log.borrow_mut().push(path.to_string());
86 }
87 result
88 }
89}
90
91pub struct RealFs {
93 pub allowed: Vec<PathBuf>,
94}
95impl FileAccess for RealFs {
96 fn read(&self, path: &str) -> Result<String, FileError> {
97 let canon = std::fs::canonicalize(path).map_err(|e| FileError::Io(e.to_string()))?;
98 let permitted = self.allowed.iter().any(|root| {
99 std::fs::canonicalize(root)
100 .map(|r| canon.starts_with(r))
101 .unwrap_or(false)
102 });
103 if !permitted {
104 return Err(if self.allowed.is_empty() {
105 FileError::Denied
106 } else {
107 FileError::Outside(self.allowed.clone())
108 });
109 }
110 std::fs::read_to_string(&canon).map_err(|e| FileError::Io(e.to_string()))
111 }
112}
113
114pub struct Interpreter<'a> {
115 pub registry: MethodRegistry<'a>,
116 pub fs: Box<dyn FileAccess>,
117 pub env_cap: EnvCap,
118 pub env_overrides: HashMap<String, String>,
120 pub options: Options,
121 pub allow_imports_io: bool,
123 pub current_root: bool,
125 modules: HashMap<String, Value<'a>>,
127 call_depth: u32,
128 env_arena: Vec<Env<'a>>,
132}
133
134fn nearest<'m>(members: &[&'m str], got: &str) -> Option<&'m str> {
136 let limit = 1 + got.len() / 4;
137 members
138 .iter()
139 .map(|m| (*m, edit_distance(m, got)))
140 .filter(|(_, d)| *d <= limit)
141 .min_by_key(|(_, d)| *d)
142 .map(|(m, _)| m)
143}
144
145fn edit_distance(a: &str, b: &str) -> usize {
147 let b: Vec<char> = b.chars().collect();
148 let mut prev: Vec<usize> = (0..=b.len()).collect();
149 let mut cur = vec![0usize; b.len() + 1];
150 for (i, ca) in a.chars().enumerate() {
151 cur[0] = i + 1;
152 for (j, cb) in b.iter().enumerate() {
153 let cost = usize::from(ca != *cb);
154 cur[j + 1] = (prev[j] + cost).min(prev[j + 1] + 1).min(cur[j] + 1);
155 }
156 std::mem::swap(&mut prev, &mut cur);
157 }
158 prev[b.len()]
159}
160
161fn rt(code: &'static str, msg: impl Into<String>, span: Span) -> Diagnostic {
162 Diagnostic::error(code, msg, span, "evaluated here")
163}
164
165impl<'a> Interpreter<'a> {
166 pub fn new(options: Options) -> Self {
167 Interpreter {
168 registry: MethodRegistry::builtin(),
169 fs: Box::new(DenyFs),
170 env_cap: EnvCap::Deny,
171 env_overrides: HashMap::new(),
172 options,
173 allow_imports_io: false,
174 current_root: true,
175 modules: HashMap::new(),
176 call_depth: 0,
177 env_arena: Vec::new(),
178 }
179 }
180
181 fn track(&mut self, env: Env<'a>) -> Env<'a> {
183 self.env_arena.push(env.clone());
184 env
185 }
186
187 pub fn provide_module(&mut self, alias: impl Into<String>, value: Value<'a>) {
188 self.modules.insert(alias.into(), value);
189 }
190
191 pub fn eval_module(&mut self, module: &Module<'a>) -> Result<Value<'a>, Diagnostic> {
193 let env = self.track(Environment::root());
194 for imp in &module.imports {
195 let v = self.modules.get(imp.alias).cloned().ok_or_else(|| {
196 rt(
197 "E0410",
198 format!(
199 "unresolved import '{}': module loading arrives in Phase 4 (VFS)",
200 imp.alias
201 ),
202 imp.span,
203 )
204 })?;
205 env.insert(imp.alias, v);
206 }
207 let mut exports: IndexMap<String, Value<'a>> = IndexMap::new();
208 for stmt in &module.stmts {
209 self.exec_stmt(&env, stmt, &mut exports)?;
210 }
211 Ok(Value::object(exports))
212 }
213
214 fn exec_stmt(
215 &mut self,
216 env: &Env<'a>,
217 stmt: &Stmt<'a>,
218 exports: &mut IndexMap<String, Value<'a>>,
219 ) -> Result<(), Diagnostic> {
220 match stmt {
221 Stmt::Assign {
224 name,
225 shadow,
226 value,
227 span,
228 } => {
229 let v = self.eval_expr(env, value)?;
230 self.define(env, name, v, *shadow, *span)?;
231 }
232 Stmt::Property { key, value, .. } => {
233 let v = self.eval_expr(env, value)?;
234 exports.insert(key.to_string(), v);
235 }
236 Stmt::EnumDecl(en) => {
237 let v = Value::Enum(Arc::new(EnumDef {
238 name: en.name,
239 members: en.members.clone(),
240 }));
241 self.define(env, en.name, v.clone(), false, en.span)?;
242 if en.public {
244 exports.insert(en.name.to_string(), v);
245 }
246 }
247 Stmt::TypeDecl(schema) => {
248 let mut field_enums = HashMap::new();
250 for f in &schema.fields {
251 if let TypeName::Custom(tname) = f.ty {
252 if let Some(Value::Enum(en)) = env.get(tname) {
253 field_enums.insert(f.name, en);
254 }
255 }
256 }
257 let v = Value::Schema(Arc::new(SchemaDef {
258 name: schema.name,
259 fields: schema.fields.clone(),
260 field_enums,
261 }));
262 self.define(env, schema.name, v.clone(), false, schema.span)?;
263 if schema.public {
265 exports.insert(schema.name.to_string(), v);
266 }
267 }
268 Stmt::FuncDecl {
269 name,
270 params,
271 body,
272 public,
273 span,
274 } => {
275 let v = Value::Function(Arc::new(FunctionDef {
276 params: params.clone(),
277 body: FuncBody::Block(body.clone()),
278 closure: Arc::downgrade(env),
279 defined_in_root: self.current_root,
280 }));
281 self.define(env, name, v.clone(), false, *span)?;
282 if *public {
284 exports.insert(name.to_string(), v);
285 }
286 }
287 Stmt::Block(block) => {
288 let label = self.eval_expr(env, &block.label)?;
289 let obj = self.eval_block(env, block)?;
290 let key = match &label {
291 Value::Str(s) => s.to_string(),
292 other => {
293 return Err(rt(
294 "E0306",
295 format!("block label must be String, got {}", other.type_name()),
296 block.span,
297 ))
298 }
299 };
300 exports.insert(key, obj);
301 }
302 Stmt::Assert {
303 cond,
304 message,
305 span,
306 } => match self.eval_expr(env, cond)? {
307 Value::Bool(true) => {}
308 Value::Bool(false) => {
309 let msg = match message {
310 Some(m) => {
311 let v = self.eval_expr(env, m)?;
312 self.display(&v, *span)?
313 }
314 None => "assertion failed".to_string(),
315 };
316 return Err(rt("E0530", msg, *span));
317 }
318 other => {
319 return Err(rt(
320 "E0306",
321 format!("assert condition must be Bool, got {}", other.type_name()),
322 *span,
323 ))
324 }
325 },
326 Stmt::Expr(e) => {
327 self.eval_expr(env, e)?;
328 }
329 }
330 Ok(())
331 }
332
333 fn define(
335 &self,
336 env: &Env<'a>,
337 name: &str,
338 v: Value<'a>,
339 shadow: bool,
340 span: Span,
341 ) -> Result<(), Diagnostic> {
342 if env.defined_here(name) {
343 return Err(rt(
344 "E0301",
345 format!("variable '{name}' is immutable and already defined in this scope"),
346 span,
347 ));
348 }
349 if !shadow && env.defined_in_ancestors(name) {
350 let mut d = rt("E0302", format!("'{name}' shadows an outer variable"), span);
351 d.help = Some(format!(
352 "write `shadow {name} = ...` to make the shadowing explicit"
353 ));
354 return Err(d);
355 }
356 env.insert(name, v);
357 Ok(())
358 }
359
360 fn eval_block(
362 &mut self,
363 outer: &Env<'a>,
364 block: &BlockDeclaration<'a>,
365 ) -> Result<Value<'a>, Diagnostic> {
366 let env = self.track(Environment::child(outer));
367 let mut exports: IndexMap<String, Value<'a>> = IndexMap::new();
368 for stmt in &block.body {
369 self.exec_stmt(&env, stmt, &mut exports)?;
370 }
371 Ok(Value::object(exports))
372 }
373
374 pub fn eval_expr(&mut self, env: &Env<'a>, e: &Expr<'a>) -> Result<Value<'a>, Diagnostic> {
375 match e {
376 Expr::Literal(lit, span) => self.eval_literal(env, lit, *span),
377 Expr::Variable(name, span) => env.get(name).ok_or_else(|| {
378 rt(
379 "E0504",
380 format!("use of undefined variable '{name}'"),
381 *span,
382 )
383 }),
384 Expr::Unary { op, rhs, span } => {
385 let v = self.eval_expr(env, rhs)?;
386 match (op, v) {
387 (UnaryOp::Neg, Value::Int(n)) => n
388 .checked_neg()
389 .map(Value::Int)
390 .ok_or_else(|| rt("E0304", "integer overflow in negation", *span)),
391 (UnaryOp::Neg, Value::Float(n)) => Ok(Value::Float(-n)),
392 (UnaryOp::Not, Value::Bool(b)) => Ok(Value::Bool(!b)),
393 (_, v) => Err(rt(
394 "E0306",
395 format!("invalid operand type {}", v.type_name()),
396 *span,
397 )),
398 }
399 }
400 Expr::Binary { op, lhs, rhs, span } => self.eval_binary(env, *op, lhs, rhs, *span),
401 Expr::Ternary {
402 cond,
403 then,
404 otherwise,
405 span,
406 } => match self.eval_expr(env, cond)? {
407 Value::Bool(true) => self.eval_expr(env, then),
408 Value::Bool(false) => self.eval_expr(env, otherwise),
409 other => Err(rt(
410 "E0306",
411 format!("ternary condition must be Bool, got {}", other.type_name()),
412 *span,
413 )),
414 },
415 Expr::Cond {
417 arms,
418 otherwise,
419 span,
420 } => {
421 for (condition, value) in arms {
422 match self.eval_expr(env, condition)? {
423 Value::Bool(true) => return self.eval_expr(env, value),
424 Value::Bool(false) => {}
425 other => {
426 return Err(rt(
427 "E0306",
428 format!("cond condition must be Bool, got {}", other.type_name()),
429 *span,
430 ))
431 }
432 }
433 }
434 self.eval_expr(env, otherwise)
435 }
436 Expr::Call { callee, args, span } => {
437 let argv: Vec<Value<'a>> = args
438 .iter()
439 .map(|a| self.eval_expr(env, a))
440 .collect::<Result<_, _>>()?;
441 if let Expr::Variable(name, _) = callee.as_ref() {
442 if let Some(r) = self.try_builtin_call(name, &argv, *span)? {
443 return Ok(r);
444 }
445 }
446 let f = self.eval_expr(env, callee)?;
447 self.call_value(&f, &argv, *span)
448 }
449 Expr::MethodCall {
450 recv,
451 method,
452 args,
453 lambda,
454 span,
455 } => {
456 let recv_v = self.eval_expr(env, recv)?;
457 let mut argv: Vec<Value<'a>> = args
458 .iter()
459 .map(|a| self.eval_expr(env, a))
460 .collect::<Result<_, _>>()?;
461 if let Some(l) = lambda {
462 argv.push(self.eval_expr(env, l)?);
463 }
464 if let Some(f) = self.registry.get(recv_v.tag(), method) {
465 return f(self, &recv_v, &argv, *span);
466 }
467 if let Value::Object(m) = &recv_v {
470 if let Some(f @ Value::Function(_)) = m.get(*method) {
471 let f = f.clone();
472 return self.call_value(&f, &argv, *span);
473 }
474 }
475 Err(rt(
476 "E0309",
477 format!("unknown method '{}' on {}", method, recv_v.type_name()),
478 *span,
479 ))
480 }
481 Expr::FieldAccess { recv, field, span } => {
482 let v = self.eval_expr(env, recv)?;
483 match v {
484 Value::Object(m) => m
485 .get(*field)
486 .cloned()
487 .ok_or_else(|| rt("E0308", format!("unknown field '{field}'"), *span)),
488 other => Err(rt(
489 "E0306",
490 format!("cannot access field '{}' on {}", field, other.type_name()),
491 *span,
492 )),
493 }
494 }
495 Expr::Index {
497 recv,
498 key,
499 bracket,
500 span,
501 } => {
502 let r = self.eval_expr(env, recv)?;
503 let k = self.eval_expr(env, key)?;
504 match (&r, &k) {
505 (Value::List(xs), Value::Int(i)) => {
506 let i = *i;
507 if i < 0 || i as usize >= xs.len() {
508 return Err(rt(
509 "E0317",
510 format!("index {i} out of bounds (list has {} elements)", xs.len()),
511 *span,
512 ));
513 }
514 Ok(xs[i as usize].clone())
515 }
516 (Value::Object(m), Value::Str(s)) => {
517 if *bracket {
518 let mut d =
519 rt("E0318", "bracket access on objects is not supported", *span);
520 d.help = Some(format!(
521 "use dot access instead: `.\"{s}\"` or `.get(\"{s}\", default)`"
522 ));
523 return Err(d);
524 }
525 m.get(s.as_ref())
526 .cloned()
527 .ok_or_else(|| rt("E0308", format!("unknown field '{s}'"), *span))
528 }
529 _ => Err(rt(
530 "E0306",
531 format!("cannot index {} with {}", r.type_name(), k.type_name()),
532 *span,
533 )),
534 }
535 }
536 Expr::ObjectLiteral(body) => self.eval_object_body(env, body),
537 Expr::ListLiteral(items, _) => {
538 let vs: Vec<Value<'a>> = items
539 .iter()
540 .map(|i| self.eval_expr(env, i))
541 .collect::<Result<_, _>>()?;
542 Ok(Value::list(vs))
543 }
544 Expr::Lambda { params, body, .. } => Ok(Value::Function(Arc::new(FunctionDef {
545 params: params.clone(),
546 body: FuncBody::Lambda(body.clone()),
547 closure: Arc::downgrade(env),
548 defined_in_root: self.current_root,
549 }))),
550 Expr::SchemaInstance {
551 schema,
552 schema_alias,
553 body,
554 span,
555 } => {
556 let resolved = match schema_alias {
558 Some(alias) => match env.get(alias) {
559 Some(Value::Object(m)) => m.get(*schema).cloned(),
560 _ => None,
561 },
562 None => env.get(schema),
563 };
564 let Some(Value::Schema(def)) = resolved else {
565 return Err(rt("E0504", format!("unknown schema '{schema}'"), *span));
566 };
567 let provided = self.eval_object_body(env, body)?;
568 let Value::Object(pmap) = &provided else {
569 unreachable!()
570 };
571 let mut map: IndexMap<String, Value<'a>> = (**pmap).clone();
574 for f in &def.fields {
575 if !map.contains_key(f.name) {
576 if let Some(default_expr) = &f.default {
577 let v = self.eval_expr(env, default_expr)?;
578 map.insert(f.name.to_string(), v);
579 }
580 }
581 }
582 let obj = Value::object(map);
583 self.validate_schema(&def, &obj, *span)?;
584 Ok(obj)
585 }
586 }
587 }
588
589 fn eval_stmt_body(
592 &mut self,
593 env: &Env<'a>,
594 body: &[Stmt<'a>],
595 ) -> Result<Value<'a>, Diagnostic> {
596 let mut exports: IndexMap<String, Value<'a>> = IndexMap::new();
597 for stmt in body {
598 self.exec_stmt(env, stmt, &mut exports)?;
599 }
600 Ok(Value::object(exports))
601 }
602
603 fn eval_object_body(
604 &mut self,
605 env: &Env<'a>,
606 body: &ObjectBody<'a>,
607 ) -> Result<Value<'a>, Diagnostic> {
608 let mut map: IndexMap<String, Value<'a>> = IndexMap::with_capacity(body.props.len());
609 for (key, expr, _) in &body.props {
610 map.insert(key.to_string(), self.eval_expr(env, expr)?);
611 }
612 Ok(Value::object(map))
613 }
614
615 fn eval_literal(
616 &mut self,
617 env: &Env<'a>,
618 lit: &LitValue<'a>,
619 span: Span,
620 ) -> Result<Value<'a>, Diagnostic> {
621 Ok(match lit {
622 LitValue::Int(n) => Value::Int(*n),
623 LitValue::Float(n) => Value::Float(*n),
624 LitValue::Bool(b) => Value::Bool(*b),
625 LitValue::Null => Value::Null,
626 LitValue::Str(s) => Value::Str(unescape(s)),
627 LitValue::InterpStr(parts) => {
628 let mut out = String::new();
629 for part in parts {
630 match part {
631 StrPart::Lit(s) => out.push_str(&unescape(s)),
632 StrPart::Interp(src) => {
633 let v = self.eval_interp(env, src, span)?;
634 out.push_str(&self.display(&v, span)?);
635 }
636 }
637 }
638 Value::str(out)
639 }
640 })
641 }
642
643 fn eval_interp(
645 &mut self,
646 env: &Env<'a>,
647 src: &'a str,
648 span: Span,
649 ) -> Result<Value<'a>, Diagnostic> {
650 let toks = Lexer::new(src, span.source).tokenize().map_err(|d| {
651 rt(
652 "E0316",
653 format!("invalid interpolation: {}", d.message),
654 span,
655 )
656 })?;
657 let expr = Parser::new(toks).parse_expression().map_err(|d| {
658 rt(
659 "E0316",
660 format!("invalid interpolation: {}", d.message),
661 span,
662 )
663 })?;
664 self.eval_expr(env, &expr)
665 }
666
667 pub(crate) fn display(&self, v: &Value<'a>, span: Span) -> Result<String, Diagnostic> {
669 Ok(match v {
670 Value::Str(s) => s.to_string(),
671 Value::Int(n) => n.to_string(),
672 Value::Float(n) => n.to_string(),
673 Value::Bool(b) => b.to_string(),
674 Value::Null => "null".to_string(),
675 other => {
676 return Err(rt(
677 "E0307",
678 format!("cannot interpolate {}", other.type_name()),
679 span,
680 ))
681 }
682 })
683 }
684
685 fn eval_binary(
686 &mut self,
687 env: &Env<'a>,
688 op: BinOp,
689 lhs: &Expr<'a>,
690 rhs: &Expr<'a>,
691 span: Span,
692 ) -> Result<Value<'a>, Diagnostic> {
693 if matches!(op, BinOp::And | BinOp::Or) {
695 let Value::Bool(l) = self.eval_expr(env, lhs)? else {
696 return Err(rt("E0306", "logical operand must be Bool", span));
697 };
698 return match (op, l) {
699 (BinOp::And, false) => Ok(Value::Bool(false)),
700 (BinOp::Or, true) => Ok(Value::Bool(true)),
701 _ => match self.eval_expr(env, rhs)? {
702 Value::Bool(r) => Ok(Value::Bool(r)),
703 _ => Err(rt("E0306", "logical operand must be Bool", span)),
704 },
705 };
706 }
707 let l = self.eval_expr(env, lhs)?;
708 let r = self.eval_expr(env, rhs)?;
709 use BinOp::*;
710 use Value::*;
711 match op {
712 Eq => return Ok(Bool(l == r)),
713 Ne => return Ok(Bool(l != r)),
714 _ => {}
715 }
716 match (op, &l, &r) {
718 (Add, Int(a), Int(b)) => a
719 .checked_add(*b)
720 .map(Int)
721 .ok_or_else(|| rt("E0304", "integer overflow", span)),
722 (Sub, Int(a), Int(b)) => a
723 .checked_sub(*b)
724 .map(Int)
725 .ok_or_else(|| rt("E0304", "integer overflow", span)),
726 (Mul, Int(a), Int(b)) => a
727 .checked_mul(*b)
728 .map(Int)
729 .ok_or_else(|| rt("E0304", "integer overflow", span)),
730 (Div, Int(a), Int(b)) => {
731 if *b == 0 {
732 Err(rt("E0305", "division by zero", span))
733 } else {
734 Ok(Int(a / b))
735 }
736 }
737 (Rem, Int(a), Int(b)) => {
738 if *b == 0 {
739 Err(rt("E0305", "division by zero", span))
740 } else {
741 Ok(Int(a % b))
742 }
743 }
744 (Add | Sub | Mul | Div | Rem, _, _)
745 if l.tag() == value::TypeTag::Float || r.tag() == value::TypeTag::Float =>
746 {
747 let (a, b) = (as_float(&l, span)?, as_float(&r, span)?);
748 Ok(Float(match op {
749 Add => a + b,
750 Sub => a - b,
751 Mul => a * b,
752 Div => a / b,
753 Rem => a % b,
754 _ => unreachable!(),
755 }))
756 }
757 (Lt | Gt | Le | Ge, Str(a), Str(b)) => {
758 Ok(Bool(cmp_ord(op, a.as_ref().cmp(b.as_ref()))))
759 }
760 (Lt | Gt | Le | Ge, _, _) => {
761 let (a, b) = (as_float(&l, span)?, as_float(&r, span)?);
762 let ord = a
763 .partial_cmp(&b)
764 .ok_or_else(|| rt("E0306", "NaN comparison", span))?;
765 Ok(Bool(cmp_ord(op, ord)))
766 }
767 _ => Err(rt(
768 "E0306",
769 format!(
770 "invalid operand types: {} {} {}",
771 l.type_name(),
772 op_name(op),
773 r.type_name()
774 ),
775 span,
776 )),
777 }
778 }
779
780 pub fn call_value(
783 &mut self,
784 f: &Value<'a>,
785 args: &[Value<'a>],
786 span: Span,
787 ) -> Result<Value<'a>, Diagnostic> {
788 let Value::Function(def) = f else {
789 return Err(rt(
790 "E0306",
791 format!("{} is not callable", f.type_name()),
792 span,
793 ));
794 };
795 if args.len() < def.params.len() {
796 return Err(rt(
797 "E0312",
798 format!(
799 "function expects {} arguments, got {}",
800 def.params.len(),
801 args.len()
802 ),
803 span,
804 ));
805 }
806 self.call_depth += 1;
807 if self.call_depth > MAX_CALL_DEPTH {
808 self.call_depth -= 1;
809 return Err(rt("E0399", "maximum call depth (256) exceeded", span));
810 }
811 let closure = def.closure.upgrade().ok_or_else(|| {
814 self.call_depth -= 1;
815 rt("E0398", "internal: closure environment was dropped", span)
816 })?;
817 let env = self.track(Environment::child(&closure));
818 for (p, a) in def.params.iter().zip(args) {
819 env.insert(p, a.clone());
820 }
821 let saved_root = self.current_root;
823 self.current_root = def.defined_in_root;
824 let result = match &def.body {
825 FuncBody::Block(body) => self.eval_stmt_body(&env, body),
826 FuncBody::Lambda(LambdaBody::Expr(e)) => self.eval_expr(&env, e),
827 FuncBody::Lambda(LambdaBody::Block(body)) => self.eval_stmt_body(&env, body),
828 };
829 self.current_root = saved_root;
830 self.call_depth -= 1;
831 result
832 }
833
834 fn try_builtin_call(
836 &mut self,
837 name: &str,
838 args: &[Value<'a>],
839 span: Span,
840 ) -> Result<Option<Value<'a>>, Diagnostic> {
841 if matches!(name, "env" | "read_file") && !self.current_root && !self.allow_imports_io {
843 let mut d = rt(
844 "E0310",
845 format!("imported module has no capability to call {name}()"),
846 span,
847 );
848 d.help = Some(
849 "pass --allow-imports-io to grant imports the root I/O capabilities".to_string(),
850 );
851 return Err(d);
852 }
853 match name {
854 "now" | "timestamp" => {
856 let mut d = rt(
857 "E0533",
858 format!("{name}() does not exist: Aura configs are reproducible by design"),
859 span,
860 );
861 d.help = Some(
862 "pass a build timestamp from the host instead: env(\"BUILD_TIME\", ...) with --allow-env=BUILD_TIME"
863 .to_string(),
864 );
865 Err(d)
866 }
867 "env" => {
868 let Some(Value::Str(var)) = args.first() else {
869 return Err(rt("E0306", "env() expects a String name", span));
870 };
871 let allowed = match &self.env_cap {
872 EnvCap::Deny => false,
873 EnvCap::AllowAll => true,
874 EnvCap::Allow(list) => list.iter().any(|n| n == var.as_ref()),
875 };
876 if !allowed {
877 let mut d = rt(
878 "E0310",
879 format!("no capability to read env var '{var}'"),
880 span,
881 );
882 d.help = Some(format!("pass --allow-env={var} to grant access"));
883 return Err(d);
884 }
885 let value = self
886 .env_overrides
887 .get(var.as_ref())
888 .cloned()
889 .or_else(|| std::env::var(var.as_ref()).ok());
890 Ok(Some(match value {
891 Some(s) => Value::str(s),
892 None => args.get(1).cloned().unwrap_or(Value::Null),
893 }))
894 }
895 "read_file" => {
896 let Some(Value::Str(path)) = args.first() else {
897 return Err(rt("E0306", "read_file() expects a String path", span));
898 };
899 match self.fs.read(path) {
900 Ok(s) => Ok(Some(Value::str(s))),
901 Err(FileError::Denied) => {
902 let mut d = rt("E0310", format!("no capability to read '{path}'"), span);
903 d.help = Some("pass --allow-read=<dir> to grant access".to_string());
904 Err(d)
905 }
906 Err(FileError::Outside(roots)) => {
907 let list = roots
908 .iter()
909 .map(|r| r.display().to_string())
910 .collect::<Vec<_>>()
911 .join(", ");
912 let mut d = rt(
913 "E0311",
914 format!("'{path}' is outside every directory allowed by --allow-read"),
915 span,
916 );
917 d.help = Some(format!(
918 "allowed: {list}. Paths are canonicalised before the check, so `..` cannot step out of a granted directory."
919 ));
920 Err(d)
921 }
922 Err(FileError::Io(e)) => {
923 Err(rt("E0313", format!("cannot read '{path}': {e}"), span))
924 }
925 }
926 }
927 "fail" => {
928 let msg = match args.first() {
929 Some(v) => self.display(v, span)?,
930 None => "fail() called".to_string(),
931 };
932 Err(rt("E0531", msg, span))
933 }
934 "range" => {
936 let Some(Value::Int(n)) = args.first() else {
937 return Err(rt("E0306", "range() expects an Int argument", span));
938 };
939 if *n < 0 {
940 return Err(rt(
941 "E0306",
942 format!("range() argument must be non-negative, got {n}"),
943 span,
944 ));
945 }
946 const RANGE_LIMIT: i64 = 1_000_000;
948 if *n > RANGE_LIMIT {
949 return Err(rt(
950 "E0306",
951 format!("range() argument {n} exceeds the limit of {RANGE_LIMIT}"),
952 span,
953 ));
954 }
955 Ok(Some(Value::list((0..*n).map(Value::Int).collect())))
956 }
957 _ => Ok(None),
958 }
959 }
960
961 fn validate_schema(
963 &self,
964 def: &SchemaDef<'a>,
965 obj: &Value<'a>,
966 span: Span,
967 ) -> Result<(), Diagnostic> {
968 let Value::Object(map) = obj else {
969 unreachable!()
970 };
971 for f in &def.fields {
972 let Some(v) = map.get(f.name) else {
973 return Err(rt(
976 "E0511",
977 format!("missing field '{}' required by schema {}", f.name, def.name),
978 span,
979 ));
980 };
981 {
984 if let Some(en) = def.field_enums.get(f.name) {
985 let Value::Str(got) = v else {
986 return Err(rt(
987 "E0512",
988 format!(
989 "field '{}' of schema {} expects enum {}, got {}",
990 f.name,
991 def.name,
992 en.name,
993 v.type_name()
994 ),
995 span,
996 ));
997 };
998 if !en.members.iter().any(|m| *m == got.as_ref()) {
999 let listed = en
1000 .members
1001 .iter()
1002 .map(|m| format!("\"{m}\""))
1003 .collect::<Vec<_>>()
1004 .join(", ");
1005 let mut d = rt(
1006 "E0514",
1007 format!(
1008 "'{got}' is not a member of enum {} (field '{}')",
1009 en.name, f.name
1010 ),
1011 span,
1012 );
1013 d.help = Some(match nearest(&en.members, got.as_ref()) {
1014 Some(sug) => format!("did you mean \"{sug}\"? members: {listed}"),
1015 None => format!("members: {listed}"),
1016 });
1017 return Err(d);
1018 }
1019 continue;
1020 }
1021 }
1022 let ok = match f.ty {
1023 TypeName::String => matches!(v, Value::Str(_)),
1024 TypeName::Int => matches!(v, Value::Int(_)),
1025 TypeName::Float => matches!(v, Value::Float(_)),
1026 TypeName::Bool => matches!(v, Value::Bool(_)),
1027 TypeName::List => matches!(v, Value::List(_)),
1028 TypeName::Object | TypeName::Custom(_) => matches!(v, Value::Object(_)),
1029 };
1030 if !ok {
1031 return Err(rt(
1032 "E0512",
1033 format!(
1034 "field '{}' of schema {} expects {:?}, got {}",
1035 f.name,
1036 def.name,
1037 f.ty,
1038 v.type_name()
1039 ),
1040 span,
1041 ));
1042 }
1043 }
1044 if self.options.strict {
1045 for key in map.keys() {
1046 if !def.fields.iter().any(|f| f.name == key) {
1047 return Err(rt(
1048 "E0513",
1049 format!(
1050 "unknown field '{}' not declared in schema {}",
1051 key, def.name
1052 ),
1053 span,
1054 ));
1055 }
1056 }
1057 }
1058 Ok(())
1059 }
1060}
1061
1062fn as_float(v: &Value<'_>, span: Span) -> Result<f64, Diagnostic> {
1063 match v {
1064 Value::Int(n) => Ok(*n as f64),
1065 Value::Float(n) => Ok(*n),
1066 other => Err(rt(
1067 "E0306",
1068 format!("expected a number, got {}", other.type_name()),
1069 span,
1070 )),
1071 }
1072}
1073
1074fn cmp_ord(op: BinOp, ord: std::cmp::Ordering) -> bool {
1075 use std::cmp::Ordering::*;
1076 match op {
1077 BinOp::Lt => ord == Less,
1078 BinOp::Gt => ord == Greater,
1079 BinOp::Le => ord != Greater,
1080 BinOp::Ge => ord != Less,
1081 _ => unreachable!(),
1082 }
1083}
1084
1085fn op_name(op: BinOp) -> &'static str {
1086 use BinOp::*;
1087 match op {
1088 Add => "+",
1089 Sub => "-",
1090 Mul => "*",
1091 Div => "/",
1092 Rem => "%",
1093 Eq => "==",
1094 Ne => "!=",
1095 Lt => "<",
1096 Gt => ">",
1097 Le => "<=",
1098 Ge => ">=",
1099 And => "&&",
1100 Or => "||",
1101 }
1102}
1103
1104fn unescape(s: &str) -> Arc<str> {
1106 if !s.contains('\\') {
1107 return Arc::from(s);
1108 }
1109 let mut out = String::with_capacity(s.len());
1110 let mut chars = s.chars();
1111 while let Some(c) = chars.next() {
1112 if c != '\\' {
1113 out.push(c);
1114 continue;
1115 }
1116 match chars.next() {
1117 Some('n') => out.push('\n'),
1118 Some('t') => out.push('\t'),
1119 Some('"') => out.push('"'),
1120 Some('\\') => out.push('\\'),
1121 Some('#') => out.push('#'),
1122 Some(other) => {
1123 out.push('\\');
1124 out.push(other);
1125 }
1126 None => out.push('\\'),
1127 }
1128 }
1129 Arc::from(out.as_str())
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135
1136 fn eval(src: &str) -> Result<Value<'_>, Diagnostic> {
1137 eval_with(src, Options::default())
1138 }
1139
1140 fn eval_with(src: &str, options: Options) -> Result<Value<'_>, Diagnostic> {
1141 let toks = Lexer::new(src, 0).tokenize().expect("lex ok");
1142 let module = Parser::new(toks).parse_module().expect("parse ok");
1143 Interpreter::new(options).eval_module(&module)
1144 }
1145
1146 fn get<'a>(v: &Value<'a>, key: &str) -> Value<'a> {
1147 let Value::Object(m) = v else {
1148 panic!("not an object: {v:?}")
1149 };
1150 m.get(key).unwrap_or_else(|| panic!("no key {key}")).clone()
1151 }
1152
1153 #[test]
1154 fn arithmetic_int_float_d6() {
1155 let v = eval("a: 7 / 2\nb: 7.0 / 2\nc: 2 + 3 * 4").unwrap();
1157 assert_eq!(get(&v, "a"), Value::Int(3)); assert_eq!(get(&v, "b"), Value::Float(3.5));
1159 assert_eq!(get(&v, "c"), Value::Int(14));
1160 }
1161
1162 #[test]
1163 fn int_overflow_is_e0304() {
1164 assert_eq!(
1165 eval("x = 9223372036854775807 + 1").unwrap_err().code,
1166 "E0304"
1167 );
1168 assert_eq!(eval("x = 1 / 0").unwrap_err().code, "E0305");
1169 }
1170
1171 #[test]
1172 fn redefine_in_same_scope_is_e0301() {
1173 assert_eq!(eval("x = 1\nx = 2").unwrap_err().code, "E0301");
1174 }
1175
1176 #[test]
1177 fn shadow_requires_keyword_d7() {
1178 let src_bad = "x = 1\ndomain \"d\"\n x = 2\nend";
1179 assert_eq!(eval(src_bad).unwrap_err().code, "E0302");
1180 let src_ok = "x = 1\ndomain \"d\"\n shadow x = 2\n y: x\nend";
1181 let v = eval(src_ok).unwrap();
1182 assert_eq!(get(&get(&v, "d"), "y"), Value::Int(2));
1183 }
1184
1185 #[test]
1186 fn function_envs_are_freed_no_arc_cycle() {
1187 let src =
1193 "def greet(who)\n msg: who\nend\ng = (x) -> x + 1 end\nx: greet(\"hi\").msg\ny: g(2)";
1194 let toks = Lexer::new(src, 0).tokenize().unwrap();
1195 let module = Parser::new(toks).parse_module().unwrap();
1196 let mut interp = Interpreter::new(Options::default());
1197 let v = interp.eval_module(&module).unwrap();
1198 assert_eq!(get(&v, "x"), Value::Str("hi".into()));
1199 assert_eq!(get(&v, "y"), Value::Int(3));
1200 let weaks: Vec<_> = interp.env_arena.iter().map(Arc::downgrade).collect();
1202 assert!(!weaks.is_empty(), "no envs were tracked");
1203 drop(v);
1204 drop(interp);
1205 for w in &weaks {
1206 assert_eq!(
1207 w.strong_count(),
1208 0,
1209 "an env survived interpreter drop -> Arc cycle leak is back"
1210 );
1211 }
1212 }
1213
1214 #[test]
1215 fn block_string_d16_evaluates() {
1216 let src = "app = \"web\"\nscript: text\n #!/bin/sh\n echo \"#{app}\"\nend";
1218 let v = eval(src).unwrap();
1219 assert_eq!(
1220 get(&v, "script"),
1221 Value::Str("#!/bin/sh\necho \"web\"".into())
1222 );
1223 }
1224
1225 #[test]
1226 fn block_string_d16_escaped_interpolation() {
1227 let v = eval("s: text\n price is \\#{5}\nend").unwrap();
1229 assert_eq!(get(&v, "s"), Value::Str("price is #{5}".into()));
1230 }
1231
1232 #[test]
1233 fn parse_datetime_multibyte_is_error_not_panic() {
1234 assert_eq!(
1237 eval("x: \"2026-07-18TxxxxxxxЫ\".parse_datetime()")
1238 .unwrap_err()
1239 .code,
1240 "E0320"
1241 );
1242 }
1243
1244 #[test]
1245 fn enum_field_accepts_a_member_and_stays_a_string() {
1246 let v = eval(concat!(
1249 "enum Tier
1250 \"frontend\"
1251 \"backend\"
1252end
1253",
1254 "type Service
1255 tier: Tier
1256end
1257",
1258 "svc: new Service
1259 tier: \"backend\"
1260end
1261",
1262 ))
1263 .unwrap();
1264 assert_eq!(get(&get(&v, "svc"), "tier"), Value::str("backend"));
1265 }
1266
1267 #[test]
1268 fn enum_field_rejects_a_non_member_with_e0514() {
1269 let err = eval(concat!(
1270 "enum Tier
1271 \"frontend\"
1272 \"backend\"
1273end
1274",
1275 "type Service
1276 tier: Tier
1277end
1278",
1279 "svc: new Service
1280 tier: \"backand\"
1281end
1282",
1283 ))
1284 .unwrap_err();
1285 assert_eq!(err.code, "E0514");
1286 let help = err.help.unwrap_or_default();
1288 assert!(help.contains("backend"), "{help}");
1289 }
1290
1291 #[test]
1292 fn enum_field_rejects_a_non_string() {
1293 let err = eval(concat!(
1294 "enum Tier
1295 \"a\"
1296end
1297",
1298 "type S
1299 t: Tier
1300end
1301",
1302 "x: new S
1303 t: 42
1304end
1305",
1306 ))
1307 .unwrap_err();
1308 assert_eq!(err.code, "E0512");
1309 }
1310
1311 #[test]
1312 fn enum_declaration_is_not_serializable_data() {
1313 let v = eval(
1315 "enum Tier
1316 \"a\"
1317end
1318x: 1
1319",
1320 )
1321 .unwrap();
1322 let json = crate::serialize::to_json(&v).unwrap();
1323 assert!(json.get("Tier").is_none(), "{json}");
1324 assert_eq!(json.get("x").and_then(|x| x.as_i64()), Some(1));
1325 }
1326
1327 #[test]
1328 fn capability_denied_by_default_d1() {
1329 assert_eq!(
1330 eval("x = read_file(\"/etc/passwd\")").unwrap_err().code,
1331 "E0310"
1332 );
1333 assert_eq!(eval("x = env(\"HOME\", \"\")").unwrap_err().code, "E0310");
1334 }
1335
1336 #[test]
1337 fn schema_validation() {
1338 let base = "type M\n name: String\n port: Int\nend\n";
1339 let ok = format!("{base}m = new M\n name: \"a\"\n port: 1\nend");
1340 assert!(eval(&ok).is_ok());
1341 let missing = format!("{base}m = new M\n name: \"a\"\nend");
1342 assert_eq!(eval(&missing).unwrap_err().code, "E0511");
1343 let wrong_ty = format!("{base}m = new M\n name: \"a\"\n port: \"x\"\nend");
1344 assert_eq!(eval(&wrong_ty).unwrap_err().code, "E0512");
1345 let extra = format!("{base}m = new M\n name: \"a\"\n port: 1\n zzz: 1\nend");
1346 assert!(eval(&extra).is_ok()); assert_eq!(
1348 eval_with(
1349 &extra,
1350 Options {
1351 strict: true,
1352 dry_run: false
1353 }
1354 )
1355 .unwrap_err()
1356 .code,
1357 "E0513"
1358 );
1359 }
1360
1361 #[test]
1362 fn list_methods_and_lambda() {
1363 let v = eval("xs = [\"b\"\n\"a\"\nnull\n\"b\"]\nys = xs.compact().uniq()\nn: ys.len()\nup: ys.map (s, i) -> s.upper() end").unwrap();
1364 assert_eq!(get(&v, "n"), Value::Int(2));
1365 let Value::List(up) = get(&v, "up") else {
1366 panic!()
1367 };
1368 assert_eq!(up[0], Value::str("B"));
1369 assert_eq!(up[1], Value::str("A"));
1370 }
1371
1372 #[test]
1373 fn interpolation_and_ternary() {
1374 let v = eval("name = \"auth\"\nport = 8000\ns: \"svc-#{name}:#{port + 1}\"\nt: port > 100 ? \"big\" : \"small\"").unwrap();
1375 assert_eq!(get(&v, "s"), Value::str("svc-auth:8001"));
1376 assert_eq!(get(&v, "t"), Value::str("big"));
1377 }
1378
1379 #[test]
1380 fn indexing_and_get_d11() {
1381 let v = eval("xs = [10, 20, 30]\na: xs[1]\nb: xs.get(9, 99)\nc: xs.first()\nd: xs.last()")
1382 .unwrap();
1383 assert_eq!(get(&v, "a"), Value::Int(20));
1384 assert_eq!(get(&v, "b"), Value::Int(99));
1385 assert_eq!(get(&v, "c"), Value::Int(10));
1386 assert_eq!(get(&v, "d"), Value::Int(30));
1387 assert_eq!(eval("x: [1][5]").unwrap_err().code, "E0317");
1388 }
1389
1390 #[test]
1391 fn string_keys_and_dynamic_access_d11() {
1392 let src = "def mk()\n \"eu west\": 8080\nend\nregion = \"eu west\"\np: mk().\"eu west\"\nq: mk().\"#{region}\"\ng: mk().get(\"nope\", 0)";
1393 let v = eval(src).unwrap();
1394 assert_eq!(get(&v, "p"), Value::Int(8080));
1395 assert_eq!(get(&v, "q"), Value::Int(8080));
1396 assert_eq!(get(&v, "g"), Value::Int(0));
1397 let bad = "def mk()\n a: 1\nend\nk = \"a\"\nx: mk()[k]";
1399 assert_eq!(eval(bad).unwrap_err().code, "E0318");
1400 }
1401
1402 #[test]
1403 fn keys_values_contains_join() {
1404 let src = "def mk()\n a: 1\n b: 2\nend\nks: mk().keys().join(\",\")\nvs: mk().values()\nhk: mk().contains(\"a\")\nhe: [1, 2].contains(3)\nhs: \"hello\".contains(\"ell\")";
1405 let v = eval(src).unwrap();
1406 assert_eq!(get(&v, "ks"), Value::str("a,b"));
1407 assert_eq!(
1408 get(&v, "vs"),
1409 Value::list(vec![Value::Int(1), Value::Int(2)])
1410 );
1411 assert_eq!(get(&v, "hk"), Value::Bool(true));
1412 assert_eq!(get(&v, "he"), Value::Bool(false));
1413 assert_eq!(get(&v, "hs"), Value::Bool(true));
1414 }
1415
1416 #[test]
1417 fn digest_and_codec_methods() {
1418 let v = eval(concat!(
1420 "empty: \"\".sha256()\n",
1421 "abc: \"abc\".sha256()\n",
1422 "b0: \"\".base64()\n",
1423 "b1: \"f\".base64()\n",
1424 "b2: \"fo\".base64()\n",
1425 "b3: \"foo\".base64()\n",
1426 "b6: \"foobar\".base64()\n",
1427 "round: \"hello world\".base64().base64_decode()\n",
1428 "utf8: \"привет\".base64().base64_decode()\n",
1429 ))
1430 .unwrap();
1431 assert_eq!(
1432 get(&v, "empty"),
1433 Value::str("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
1434 );
1435 assert_eq!(
1436 get(&v, "abc"),
1437 Value::str("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
1438 );
1439 assert_eq!(get(&v, "b0"), Value::str(""));
1440 assert_eq!(get(&v, "b1"), Value::str("Zg=="));
1441 assert_eq!(get(&v, "b2"), Value::str("Zm8="));
1442 assert_eq!(get(&v, "b3"), Value::str("Zm9v"));
1443 assert_eq!(get(&v, "b6"), Value::str("Zm9vYmFy"));
1444 assert_eq!(get(&v, "round"), Value::str("hello world"));
1445 assert_eq!(get(&v, "utf8"), Value::str("привет"));
1446 }
1447
1448 #[test]
1449 fn invalid_base64_is_e0321() {
1450 for bad in ["\"Zg\"", "\"Zg=x\"", "\"====\"", "\"Z!==\""] {
1451 assert_eq!(
1452 eval(&format!("x: {bad}.base64_decode()")).unwrap_err().code,
1453 "E0321",
1454 "expected E0321 for {bad}"
1455 );
1456 }
1457 }
1458
1459 #[test]
1460 fn stdlib_string_and_numeric_methods() {
1461 let src = concat!(
1462 "parts: \"a,b,c\".split(\",\")\n",
1463 "trimmed: \" hi \".trim()\n",
1464 "repl: \"a-b-c\".replace(\"-\", \"_\")\n",
1465 "sw: \"hello\".starts_with(\"he\")\n",
1466 "ew: \"hello\".ends_with(\"lo\")\n",
1467 "n: \" 42 \".to_int()\n",
1468 "f: \"3.5\".to_float()\n",
1469 "neg: (0 - 7).abs()\n",
1470 "s: 123.to_str()\n"
1471 );
1472 let v = eval(src).unwrap();
1473 assert_eq!(
1474 get(&v, "parts"),
1475 Value::list(vec![Value::str("a"), Value::str("b"), Value::str("c")])
1476 );
1477 assert_eq!(get(&v, "trimmed"), Value::str("hi"));
1478 assert_eq!(get(&v, "repl"), Value::str("a_b_c"));
1479 assert_eq!(get(&v, "sw"), Value::Bool(true));
1480 assert_eq!(get(&v, "ew"), Value::Bool(true));
1481 assert_eq!(get(&v, "n"), Value::Int(42));
1482 assert_eq!(get(&v, "f"), Value::Float(3.5));
1483 assert_eq!(get(&v, "neg"), Value::Int(7));
1484 assert_eq!(get(&v, "s"), Value::str("123"));
1485 assert_eq!(eval("x: \"nope\".to_int()").unwrap_err().code, "E0314");
1486 }
1487
1488 #[test]
1489 fn schema_optional_fields_with_defaults() {
1490 let src = concat!(
1492 "base = 8000\n",
1493 "type Service\n",
1494 " name: String\n",
1495 " port: Int = 8080\n",
1496 " tier: String = \"backend\"\n",
1497 " offset: Int = base + 1\n",
1498 "end\n",
1499 "full: new Service\n",
1500 " name: \"a\"\n",
1501 " port: 9090\n",
1502 " tier: \"frontend\"\n",
1503 " offset: 5\n",
1504 "end\n",
1505 "defaulted: new Service\n",
1506 " name: \"b\"\n",
1507 "end\n"
1508 );
1509 let v = eval(src).unwrap();
1510 let full = get(&v, "full");
1511 assert_eq!(get(&full, "port"), Value::Int(9090));
1512 assert_eq!(get(&full, "tier"), Value::str("frontend"));
1513 let d = get(&v, "defaulted");
1514 assert_eq!(get(&d, "name"), Value::str("b"));
1515 assert_eq!(get(&d, "port"), Value::Int(8080)); assert_eq!(get(&d, "tier"), Value::str("backend")); assert_eq!(get(&d, "offset"), Value::Int(8001)); let missing = "type S\n name: String\n port: Int\nend\nx: new S\n name: \"z\"\nend";
1520 assert_eq!(eval(missing).unwrap_err().code, "E0511");
1521 let badty = "type S\n n: Int = \"oops\"\nend\nx: new S\nend";
1523 assert_eq!(eval(badty).unwrap_err().code, "E0512");
1524 }
1525
1526 #[test]
1527 fn cond_expression() {
1528 let src = concat!(
1529 "region = \"us\"\n",
1530 "tier: cond\n",
1531 " region == \"eu\" -> \"frankfurt\"\n",
1532 " region == \"us\" -> \"virginia\"\n",
1533 " else -> \"singapore\"\n",
1534 "end\n",
1535 "fallback: cond\n",
1536 " false -> 1\n",
1537 " else -> 42\n",
1538 "end\n"
1539 );
1540 let v = eval(src).unwrap();
1541 assert_eq!(get(&v, "tier"), Value::str("virginia"));
1542 assert_eq!(get(&v, "fallback"), Value::Int(42));
1543 let bad = "x: cond\n \"nope\" -> 1\n else -> 2\nend";
1545 assert_eq!(eval(bad).unwrap_err().code, "E0306");
1546 }
1547
1548 #[test]
1549 fn range_builtin() {
1550 let v = eval("a: range(3)\nb: range(0)\nc: range(2).map (i, _) -> i * 10 end").unwrap();
1551 assert_eq!(
1552 get(&v, "a"),
1553 Value::list(vec![Value::Int(0), Value::Int(1), Value::Int(2)])
1554 );
1555 assert_eq!(get(&v, "b"), Value::list(vec![]));
1556 assert_eq!(
1557 get(&v, "c"),
1558 Value::list(vec![Value::Int(0), Value::Int(10)])
1559 );
1560 assert_eq!(eval("x: range(-1)").unwrap_err().code, "E0306");
1561 assert_eq!(eval("x: range(\"3\")").unwrap_err().code, "E0306");
1562 }
1563
1564 #[test]
1565 fn stdlib_list_methods() {
1566 let src = concat!(
1567 "sorted: [3, 1, 2].sort()\n",
1568 "rev: [1, 2, 3].reverse()\n",
1569 "total: [1, 2, 3].sum()\n",
1570 "totalf: [1, 2.5].sum()\n",
1571 "lo: [3, 1, 2].min()\n",
1572 "hi: [3, 1, 2].max()\n",
1573 "flat: [[1, 2], [3], 4].flatten()\n",
1574 "sl: [10, 20, 30, 40].slice(1, 3)\n"
1575 );
1576 let v = eval(src).unwrap();
1577 assert_eq!(
1578 get(&v, "sorted"),
1579 Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)])
1580 );
1581 assert_eq!(
1582 get(&v, "rev"),
1583 Value::list(vec![Value::Int(3), Value::Int(2), Value::Int(1)])
1584 );
1585 assert_eq!(get(&v, "total"), Value::Int(6));
1586 assert_eq!(get(&v, "totalf"), Value::Float(3.5));
1587 assert_eq!(get(&v, "lo"), Value::Int(1));
1588 assert_eq!(get(&v, "hi"), Value::Int(3));
1589 assert_eq!(
1590 get(&v, "flat"),
1591 Value::list(vec![
1592 Value::Int(1),
1593 Value::Int(2),
1594 Value::Int(3),
1595 Value::Int(4)
1596 ])
1597 );
1598 assert_eq!(
1599 get(&v, "sl"),
1600 Value::list(vec![Value::Int(20), Value::Int(30)])
1601 );
1602 assert_eq!(eval("x: [1, \"a\"].sort()").unwrap_err().code, "E0306");
1604 assert_eq!(eval("x: [].min()").unwrap_err().code, "E0317");
1605 }
1606
1607 #[test]
1608 fn format_parsing_and_emitting() {
1609 let v = eval("d = \"{\\\"a\\\": [1, 2]}\".parse_json()\nx: d.a[1]").unwrap();
1610 assert_eq!(get(&v, "x"), Value::Int(2));
1611
1612 let v = eval("d = \"a: 1\\nb:\\n - x\\n\".parse_yaml()\nfirst_b: d.b[0]").unwrap();
1613 assert_eq!(get(&v, "first_b"), Value::str("x"));
1614
1615 let v =
1616 eval("def mk()\n a: 1\nend\nj: mk().to_json()\ny: mk().to_yaml()\nt: mk().to_toml()")
1617 .unwrap();
1618 assert_eq!(get(&v, "j"), Value::str("{\"a\":1}"));
1619 assert_eq!(get(&v, "y"), Value::str("a: 1\n"));
1620 assert_eq!(get(&v, "t"), Value::str("a = 1\n"));
1621 }
1622
1623 #[test]
1624 fn durations_and_datetimes() {
1625 let v = eval(
1626 "a: \"1h30m\".parse_duration()\nb: \"2d\".parse_duration()\nc: 5400.format_duration()\nd: 0.format_duration()\ne: \"2000-01-01T00:00:00Z\".parse_datetime()\nf: \"2000-01-01T02:00:00+02:00\".parse_datetime()\ng: 946684800.format_datetime()\nh: \"2026-07-18\".parse_datetime().format_datetime()",
1627 )
1628 .unwrap();
1629 assert_eq!(get(&v, "a"), Value::Int(5400));
1630 assert_eq!(get(&v, "b"), Value::Int(172800));
1631 assert_eq!(get(&v, "c"), Value::str("1h30m"));
1632 assert_eq!(get(&v, "d"), Value::str("0s"));
1633 assert_eq!(get(&v, "e"), Value::Int(946684800));
1635 assert_eq!(get(&v, "f"), Value::Int(946684800));
1637 assert_eq!(get(&v, "g"), Value::str("2000-01-01T00:00:00Z"));
1638 assert_eq!(get(&v, "h"), Value::str("2026-07-18T00:00:00Z"));
1639
1640 assert_eq!(
1641 eval("x: \"90 minutes\".parse_duration()").unwrap_err().code,
1642 "E0319"
1643 );
1644 assert_eq!(
1645 eval("x: \"2026-13-01\".parse_datetime()").unwrap_err().code,
1646 "E0320"
1647 );
1648 }
1649
1650 #[test]
1651 fn now_is_banned_d13() {
1652 let d = eval("x: now()").unwrap_err();
1653 assert_eq!(d.code, "E0533");
1654 assert!(d.help.is_some());
1655 }
1656
1657 #[test]
1658 fn assert_failure_is_e0530() {
1659 let d = eval("assert 1 > 2, \"nope\"").unwrap_err();
1660 assert_eq!(d.code, "E0530");
1661 assert_eq!(d.message, "nope");
1662 }
1663
1664 #[test]
1665 fn def_call_and_object_merge() {
1666 let v = eval("def labels(app)\n app: app\n managed_by: \"aura\"\nend\na = labels(\"x\")\nb = labels(\"y\")\norig: a\nm: a.merge(b)").unwrap();
1667 assert_eq!(get(&get(&v, "orig"), "app"), Value::str("x"));
1668 assert_eq!(get(&get(&v, "m"), "app"), Value::str("y")); assert_eq!(get(&get(&v, "m"), "managed_by"), Value::str("aura"));
1670 }
1671}