Skip to main content

aura_lang/eval/
mod.rs

1//! Evaluation engine (SPEC §4). Deterministic tree-walking interpreter
2//! with a capability model for effects (D1).
3
4pub 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/// Capability for reading environment variables (D1): denied by default.
32#[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    /// No read capability at all.
43    Denied,
44    /// The capability was granted, but the path resolves outside every allowed
45    /// root. Kept separate from `Denied` because the remedy differs: the caller
46    /// already passed --allow-read, and telling them to pass it again is the
47    /// least useful thing a diagnostic can do. Carries the roots so the message
48    /// can name them.
49    Outside(Vec<PathBuf>),
50    Io(String),
51}
52
53/// File access behind a trait: the VFS builds on this, dry-run wraps it in RecordingFs.
54pub trait FileAccess {
55    fn read(&self, path: &str) -> Result<String, FileError>;
56}
57
58/// D1 default: no I/O without explicit grants.
59pub 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
76/// Dry-run wrapper (SPEC §6.3): reads are performed but recorded into a report.
77pub 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
91/// `--allow-read=<paths>`: canonicalization + prefix check (E0311).
92pub 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    /// Environment overrides for tests/dry-run snapshots; take priority over the real env.
119    pub env_overrides: HashMap<String, String>,
120    pub options: Options,
121    /// --allow-imports-io: grant imported modules the root's I/O capabilities (D1).
122    pub allow_imports_io: bool,
123    /// Set by the VFS loader: false while evaluating an imported module.
124    pub current_root: bool,
125    /// Evaluated modules by alias; populated by the VFS loader (or by tests directly).
126    modules: HashMap<String, Value<'a>>,
127    call_depth: u32,
128    /// Strong owner of every environment created during evaluation. Closures hold
129    /// only a `WeakEnv`, so this arena is what keeps envs alive; it also breaks the
130    /// env<->function Arc cycle (freed with the interpreter, a DAG that drops cleanly).
131    env_arena: Vec<Env<'a>>,
132}
133
134/// The closest member within a small edit distance, for a "did you mean" hint.
135fn 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
145/// Levenshtein distance over chars (inputs here are short enum members).
146fn 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    /// Register a freshly created env in the arena (its strong owner) and return it.
182    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    /// A module evaluates to an Object of its exported properties and domain blocks.
192    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            // D10: `=` is a private computation and is never exported; only
222            // `key:` properties and `domain` blocks are.
223            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                // D12: pub enum is visible to importers via the module object
243                if en.public {
244                    exports.insert(en.name.to_string(), v);
245                }
246            }
247            Stmt::TypeDecl(schema) => {
248                // D18: bind each enum-typed field to the enum visible here.
249                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                // D12: pub type is visible to importers via the module object
264                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                // D12: pub def is visible to importers via the module object
283                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    /// Binding rules D7/E0301 (SPEC §4.2).
334    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    /// A `domain` body → Object (its label becomes the key at the call site).
361    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            // D14: first true arm wins; conditions must be Bool; `else` is the fallback.
416            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                // D12: calling an exported module function — obj.fn(args);
468                // builtin methods take precedence over same-named fields
469                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            // D11: `xs[int]` for lists; `obj."#{key}"` (bracket=false) for objects
496            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                // D12: `new alias.Schema` — a schema from an imported module's object
557                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                // Apply defaults for optional fields omitted here (in schema order,
572                // after the provided fields). Evaluated in the instantiation scope.
573                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    /// A code body (D17): statements in their own scope; `key:` properties are
590    /// exported, `=` bindings stay private — the same rule as a module or block.
591    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    /// `#{expr}` — lazy parse of the raw slice (SPEC §2.1), evaluated in the current scope.
644    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    /// Only scalars are allowed in interpolation and join (E0307).
668    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        // Short-circuit logic
694        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        // Arithmetic and comparisons (SPEC §4.1, D6): Int⊕Int→Int (checked), Float is contagious.
717        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    /// Calls a Value::Function (def / lambda). Extra arguments are ignored
781    /// (map passes elem+index, a lambda may declare only elem); too few — E0312.
782    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        // The closure's env is kept alive by the interpreter's arena for the whole
812        // eval, so this upgrade always succeeds while a call is in flight.
813        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        // D1×D12: the body runs with the capabilities of the function's origin module
822        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    /// Effectful builtins under capability control (D1, SPEC §4.3).
835    fn try_builtin_call(
836        &mut self,
837        name: &str,
838        args: &[Value<'a>],
839        span: Span,
840    ) -> Result<Option<Value<'a>>, Diagnostic> {
841        // D1: imported modules get no I/O without --allow-imports-io (SPEC §4.3).
842        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            // D13: current time does not exist in Aura — configs are deterministic
855            "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            // Pure generator: range(n) = [0, 1, ..., n-1]. Deterministic, no capability.
935            "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                // Guard against accidental OOM; configs never need a huge range.
947                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    /// Schema validation (SPEC §6.2): E0511 missing, E0512 type, E0513 extra (strict only).
962    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                // Defaults are applied before validation, so a still-missing field
974                // has no default and is genuinely required.
975                return Err(rt(
976                    "E0511",
977                    format!("missing field '{}' required by schema {}", f.name, def.name),
978                    span,
979                ));
980            };
981            // D18: a `Custom` name may be an enum — then the field is a plain
982            // String constrained to the declared members.
983            {
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
1104/// Lazy unescaping (SPEC §2.3): zero cost when there is no `\`.
1105fn 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        // D10: only `key:` properties are exported
1156        let v = eval("a: 7 / 2\nb: 7.0 / 2\nc: 2 + 3 * 4").unwrap();
1157        assert_eq!(get(&v, "a"), Value::Int(3)); // integer division
1158        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        // Regression (found by fuzzing under LeakSanitizer): a module's env holds
1188        // its functions, whose closure is that same env. With a strong-Arc closure
1189        // this formed a cycle that leaked every function-bearing env. Closures now
1190        // hold a WeakEnv and the interpreter's arena owns the envs, so dropping the
1191        // interpreter must free them (strong_count -> 0).
1192        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        // Every created env lives in the arena; nothing else should keep them alive.
1201        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        // Multi-line value with interpolation; common indent stripped, joined by '\n'.
1217        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        // `\#{` is a literal `#{`, exactly as in quoted strings.
1228        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        // Fuzz regression: parse_rfc3339 used byte-indexed split_at; a multi-byte
1235        // char straddling the split point must yield E0320, never a panic.
1236        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        // D18: an enum is a constraint, not a wrapper — the value serializes as a
1247        // plain string, so nothing downstream changes.
1248        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        // the hint carries a suggestion and the member list
1287        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        // Like a schema, a top-level enum is API, not output (D12).
1314        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()); // non-strict: the field is kept
1347        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        // dynamic key via brackets on an object is forbidden
1398        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        // Known vectors: NIST for SHA-256, RFC 4648 §10 for base64.
1419        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        // omitted optional fields take their defaults; a default can reference a var.
1491        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)); // default
1516        assert_eq!(get(&d, "tier"), Value::str("backend")); // default
1517        assert_eq!(get(&d, "offset"), Value::Int(8001)); // default references `base`
1518                                                         // a required field (no default) is still E0511 when missing
1519        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        // a default of the wrong type is caught as E0512
1522        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        // non-Bool condition -> E0306 (eval-level)
1544        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        // incomparable sort and empty min are errors
1603        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        // 2000-01-01 UTC — a well-known epoch value
1634        assert_eq!(get(&v, "e"), Value::Int(946684800));
1635        // the +02:00 offset denotes the same instant
1636        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")); // right side wins
1669        assert_eq!(get(&get(&v, "m"), "managed_by"), Value::str("aura"));
1670    }
1671}