Skip to main content

bop/
evaluator.rs

1#[cfg(feature = "no_std")]
2use alloc::{format, string::{String, ToString}, vec, vec::Vec};
3
4use alloc_import::collections::BTreeMap;
5
6#[cfg(not(feature = "no_std"))]
7use std as alloc_import;
8#[cfg(feature = "no_std")]
9use alloc as alloc_import;
10
11#[cfg(feature = "no_std")]
12use alloc::rc::Rc;
13
14#[cfg(not(feature = "no_std"))]
15use std::rc::Rc;
16
17use core::cell::RefCell;
18
19use crate::builtins::{
20    self, error, error_at, error_fatal_with_hint, error_with_hint, error_with_hint_at,
21};
22use crate::error::BopError;
23use crate::lexer::StringPart;
24use crate::methods;
25use crate::ops;
26use crate::parser::*;
27use crate::value::{BopFn, EnumPayload, FnBody, Value};
28use crate::{BopHost, BopLimits};
29
30/// What the tree-walker stores for each imported module once it
31/// has been loaded. Cached by dot-joined path so the same module
32/// imported twice in one `run` only evaluates once.
33///
34/// `Loading` is the in-progress sentinel; if an import request
35/// sees a module already in this state it's a circular import and
36/// halts with a clear error.
37enum ImportSlot {
38    Loading,
39    Loaded(ModuleBindings),
40}
41
42#[derive(Clone)]
43struct ModuleBindings {
44    /// `(name, value)` pairs for every top-level `let` in the
45    /// module (fns are handled separately — they also need to
46    /// land in `self.functions` so cross-fn calls within the
47    /// module resolve).
48    bindings: Vec<(String, Value)>,
49    /// Top-level `fn` declarations, keyed by name. The importer
50    /// registers each both in its `self.functions` table
51    /// (for nested call resolution) and as a scope binding
52    /// (so the fn is also usable as a first-class value).
53    fn_decls: Vec<(String, FnDef)>,
54    /// Struct type declarations the module introduces, already
55    /// qualified with their full identity `(module_path,
56    /// type_name)`. The importer copies these into its own
57    /// `struct_defs` without rewriting keys, so type identity
58    /// stays pinned to the declaring module across the import
59    /// boundary.
60    struct_defs: Vec<((String, String), Vec<String>)>,
61    /// Enum type declarations, qualified the same way as
62    /// `struct_defs` above.
63    enum_defs: Vec<((String, String), Vec<crate::parser::VariantDecl>)>,
64    /// User methods the module declared, keyed by the *full
65    /// type identity* of the receiver. `((module_path,
66    /// type_name), method_name, fn_def)` — the importer merges
67    /// these directly into its own `methods` table.
68    methods: Vec<((String, String), String, FnDef)>,
69}
70
71type ImportCache = Rc<RefCell<alloc_import::collections::BTreeMap<String, ImportSlot>>>;
72
73const MAX_CALL_DEPTH: usize = 64;
74
75// `try` unwinds via `BopError::try_return_signal` — a proper
76// sentinel with `is_try_return = true` rather than the older
77// magic-string approach. The value travels on
78// `Evaluator::pending_try_return` so the `BopError` itself
79// doesn't need to carry a `Value` (which would introduce a
80// module cycle between `bop::error` and `bop::value`).
81
82// ─── Control flow signals ──────────────────────────────────────────────────
83
84enum Signal {
85    None,
86    Break,
87    Continue,
88    Return(Value),
89}
90
91#[derive(Clone)]
92struct FnDef {
93    params: Vec<String>,
94    body: Vec<Stmt>,
95}
96
97// ─── Evaluator ─────────────────────────────────────────────────────────────
98
99pub struct Evaluator<'h, H: BopHost> {
100    scopes: Vec<BTreeMap<String, Value>>,
101    functions: BTreeMap<String, FnDef>,
102    /// Module this evaluator is running. Used to tag newly
103    /// declared types with the module they live in, so two
104    /// modules that declare `struct Color { ... }` produce
105    /// distinct types rather than colliding by name. `<root>`
106    /// for the top-level program, the dot-joined module path
107    /// for a sub-evaluator loading a `use`'d module.
108    current_module: String,
109    /// User-defined struct types, keyed by their full identity
110    /// `(module_path, type_name)` — the same pair the runtime
111    /// values carry. Two modules declaring the same struct name
112    /// coexist at different keys with independently-validated
113    /// field lists.
114    struct_defs: BTreeMap<(String, String), Vec<String>>,
115    /// User-defined enum types, same `(module_path, type_name)`
116    /// keying scheme as [`Self::struct_defs`].
117    enum_defs: BTreeMap<(String, String), Vec<VariantDecl>>,
118    /// User-defined methods. Outer key is the *full type
119    /// identity* `(module_path, type_name)`; inner key is the
120    /// method name. Methods receive the receiver as their first
121    /// parameter (conventionally `self`). Dispatch looks up the
122    /// receiver's own `(module_path, type_name)` so a method
123    /// declared for `paint.Color` isn't accidentally called on
124    /// `other.Color`.
125    methods: BTreeMap<(String, String), BTreeMap<String, FnDef>>,
126    /// Per-scope bare-name → module_path bindings for types
127    /// *and* module aliases. Parallels `scopes`: a declared
128    /// type or an aliased `use` binds `name → module_path` in
129    /// the current scope. Bare-name lookup (`Color::Red`,
130    /// `m.Color::Red`) walks this stack inside-out. `<builtin>`
131    /// types (`Result`, `RuntimeError`) are seeded into scope
132    /// 0 at evaluator construction so they're visible
133    /// everywhere.
134    ///
135    /// Unlike `scopes`, this stack is *preserved* across
136    /// function calls — type identity follows lexical scoping
137    /// at the module level, so a `fn` declared inside the root
138    /// program can still match patterns against types + aliases
139    /// that were visible at its declaration site. Function
140    /// bodies push a fresh frame on entry so any locally-
141    /// declared types are discarded on return; module-scope
142    /// entries below stay put.
143    type_bindings: Vec<BTreeMap<String, String>>,
144    /// Module-level aliases `m → Rc<BopModule>`, populated by
145    /// aliased `use` statements. Separate from `scopes` so
146    /// aliases remain reachable from inside function bodies
147    /// (where `self.scopes` is fresh per call). Field access /
148    /// method dispatch on `m.foo` falls back to this map when
149    /// `m` isn't a local binding.
150    module_aliases: BTreeMap<String, Rc<crate::value::BopModule>>,
151    host: &'h mut H,
152    steps: u64,
153    call_depth: usize,
154    limits: BopLimits,
155    rand_state: u64,
156    /// Shared across nested evaluators so recursive imports see
157    /// the same cache — every sub-evaluator inherits the parent's
158    /// `Rc` clone in `new_nested`.
159    imports: ImportCache,
160    /// Paths already injected into *this* evaluator's scope (not
161    /// shared with sub-evaluators). Re-importing the same path at
162    /// the same level is a no-op — matches Python's `import x;
163    /// import x` behaviour.
164    imported_here: alloc_import::collections::BTreeSet<String>,
165    /// Set by `try` when it sees an `Err(...)` variant and wants
166    /// the enclosing fn to early-return with that value. The
167    /// expression that raised stuffs the value here and returns
168    /// a sentinel `BopError`; the fn-call wrapper detects the
169    /// sentinel, takes the value, and converts it to
170    /// `Signal::Return`. Always `None` outside the narrow
171    /// unwinding window.
172    pending_try_return: Option<Value>,
173    /// Non-fatal runtime warnings accumulated during execution —
174    /// currently only `use`-time name-shadowing events (glob
175    /// imports bringing in a name that's already bound). Read
176    /// after `run()` returns via [`Self::take_warnings`].
177    runtime_warnings: Vec<crate::error::BopWarning>,
178}
179
180impl<'h, H: BopHost> Evaluator<'h, H> {
181    pub fn new(host: &'h mut H, limits: BopLimits) -> Self {
182        crate::memory::bop_memory_init(limits.max_memory);
183        let (struct_defs, enum_defs, builtin_bindings) = seed_builtin_types();
184        Self {
185            scopes: vec![BTreeMap::new()],
186            functions: BTreeMap::new(),
187            current_module: String::from(crate::value::ROOT_MODULE_PATH),
188            struct_defs,
189            enum_defs,
190            methods: BTreeMap::new(),
191            type_bindings: vec![builtin_bindings],
192            module_aliases: BTreeMap::new(),
193            host,
194            steps: 0,
195            call_depth: 0,
196            limits,
197            rand_state: 0,
198            imports: Rc::new(RefCell::new(alloc_import::collections::BTreeMap::new())),
199            imported_here: alloc_import::collections::BTreeSet::new(),
200            pending_try_return: None,
201            runtime_warnings: Vec::new(),
202        }
203    }
204
205    /// Build a sub-evaluator for loading a module — inherits the
206    /// parent's import cache, memory ceiling, and step budget, but
207    /// runs with a fresh scope stack so module code can't see the
208    /// importer's locals. `module_path` is the dot-joined name the
209    /// module is being loaded under; types it declares tag their
210    /// runtime values with this path.
211    fn new_for_module(
212        host: &'h mut H,
213        limits: BopLimits,
214        imports: ImportCache,
215        module_path: String,
216    ) -> Self {
217        let (struct_defs, enum_defs, builtin_bindings) = seed_builtin_types();
218        Self {
219            scopes: vec![BTreeMap::new()],
220            functions: BTreeMap::new(),
221            current_module: module_path,
222            struct_defs,
223            enum_defs,
224            methods: BTreeMap::new(),
225            type_bindings: vec![builtin_bindings],
226            module_aliases: BTreeMap::new(),
227            host,
228            steps: 0,
229            call_depth: 0,
230            limits,
231            rand_state: 0,
232            imports,
233            imported_here: alloc_import::collections::BTreeSet::new(),
234            pending_try_return: None,
235            runtime_warnings: Vec::new(),
236        }
237    }
238
239    pub fn run(mut self, stmts: &[Stmt]) -> Result<(), BopError> {
240        let result = self.exec_block(stmts);
241        #[cfg(not(feature = "no_std"))]
242        {
243            // Surface any runtime warnings accumulated during the
244            // run (currently: glob-import shadowing). They land on
245            // stderr with the standard `warning:` prefix so
246            // terminal users see them — no public API change
247            // needed. Embedders that want structured access can
248            // use `run_with_warnings` (future) or implement their
249            // own evaluation loop.
250            for w in &self.runtime_warnings {
251                eprintln!("warning: {}", w.message);
252            }
253        }
254        match result? {
255            Signal::Break => {
256                return Err(error(0, "break used outside of a loop"));
257            }
258            Signal::Continue => {
259                return Err(error(0, "continue used outside of a loop"));
260            }
261            _ => {}
262        }
263        Ok(())
264    }
265
266    fn tick(&mut self, line: u32) -> Result<(), BopError> {
267        self.steps += 1;
268        if self.steps > self.limits.max_steps {
269            // Fatal — `try_call` must not swallow this, or the
270            // sandbox invariant breaks.
271            return Err(error_fatal_with_hint(
272                line,
273                "Your code took too many steps (possible infinite loop)",
274                "Check your loops — make sure they have a condition that eventually stops them.",
275            ));
276        }
277        if crate::memory::bop_memory_exceeded() {
278            return Err(error_fatal_with_hint(
279                line,
280                "Memory limit exceeded",
281                "Your code is using too much memory. Check for large strings or arrays growing in loops.",
282            ));
283        }
284        self.host.on_tick()?;
285        Ok(())
286    }
287
288    // ─── Scope ─────────────────────────────────────────────────────
289
290    fn push_scope(&mut self) {
291        self.scopes.push(BTreeMap::new());
292        // Type bindings parallel the value scopes — same push /
293        // pop rhythm keeps `use`-injected type names stack-scoped.
294        self.type_bindings.push(BTreeMap::new());
295    }
296
297    fn pop_scope(&mut self) {
298        self.scopes.pop();
299        self.type_bindings.pop();
300    }
301
302    /// Resolve a type reference to the module it was declared
303    /// in. `namespace` is the explicit qualifier (from
304    /// `m.Color::Red`) or `None` for bare names. For bare names
305    /// the scope stack is walked inside-out; for namespaced
306    /// references the alias is resolved via
307    /// `validate_namespaced_type` and its backing module path
308    /// returned. Returns `None` when no matching type is visible.
309    fn resolve_type_ref(&self, namespace: Option<&str>, type_name: &str) -> Option<String> {
310        resolve_type_in(
311            &self.scopes,
312            &self.type_bindings,
313            &self.module_aliases,
314            namespace,
315            type_name,
316        )
317    }
318
319    /// Record a type declaration in the current module.
320    /// Registers `(current_module, name)` in the appropriate
321    /// table and binds the bare name in the current scope so
322    /// subsequent references resolve to *this* module's version.
323    /// Same-shape redeclarations in the same module are a no-op;
324    /// different-shape ones error.
325    fn bind_local_type(&mut self, name: &str) {
326        if let Some(top) = self.type_bindings.last_mut() {
327            top.insert(name.to_string(), self.current_module.clone());
328        }
329    }
330
331    fn define(&mut self, name: String, value: Value) {
332        if let Some(scope) = self.scopes.last_mut() {
333            scope.insert(name, value);
334        }
335    }
336
337    fn get_var(&self, name: &str) -> Option<&Value> {
338        for scope in self.scopes.iter().rev() {
339            if let Some(val) = scope.get(name) {
340                return Some(val);
341            }
342        }
343        None
344    }
345
346    fn set_var(&mut self, name: &str, value: Value) -> bool {
347        for scope in self.scopes.iter_mut().rev() {
348            if scope.contains_key(name) {
349                scope.insert(name.to_string(), value);
350                return true;
351            }
352        }
353        false
354    }
355
356    // ─── "Did you mean?" candidate collectors ─────────────────
357    //
358    // Every name the user could reasonably have meant, gathered
359    // into a single list so `bop::suggest::did_you_mean` picks
360    // the closest match. Separate methods for the "ident used
361    // as a value" and "ident called as a function" paths since
362    // the reachable sets differ (builtins are callable but
363    // aren't scope values).
364
365    /// Names reachable when an identifier is used as a value:
366    /// any local from the enclosing scopes plus any top-level
367    /// fn declaration (fns are first-class — `let g = some_fn`
368    /// works, so they count as value-like).
369    fn value_candidates_hint(&self, target: &str) -> Option<String> {
370        let mut candidates: Vec<String> = Vec::new();
371        for scope in &self.scopes {
372            for k in scope.keys() {
373                candidates.push(k.clone());
374            }
375        }
376        for name in self.functions.keys() {
377            candidates.push(name.clone());
378        }
379        crate::suggest::did_you_mean(target, candidates)
380    }
381
382    /// Names reachable in a call position: user fns, core
383    /// builtins, plus `try_call`. Host builtins stay with the
384    /// host's own `function_hint()` path — embedders often want
385    /// to list theirs differently.
386    fn callable_candidates_hint(&self, target: &str) -> Option<String> {
387        let mut candidates: Vec<String> =
388            self.functions.keys().cloned().collect();
389        for builtin in crate::suggest::CORE_CALLABLE_BUILTINS {
390            candidates.push((*builtin).to_string());
391        }
392        crate::suggest::did_you_mean(target, candidates)
393    }
394
395    // ─── Statements ────────────────────────────────────────────────
396
397    fn exec_block(&mut self, stmts: &[Stmt]) -> Result<Signal, BopError> {
398        for stmt in stmts {
399            let signal = self.exec_stmt(stmt)?;
400            match signal {
401                Signal::None => {}
402                other => return Ok(other),
403            }
404        }
405        Ok(Signal::None)
406    }
407
408    fn exec_stmt(&mut self, stmt: &Stmt) -> Result<Signal, BopError> {
409        self.tick(stmt.line)?;
410
411        match &stmt.kind {
412            StmtKind::Let { name, value, is_const: _ } => {
413                let val = self.eval_expr(value)?;
414                self.define(name.clone(), val);
415                Ok(Signal::None)
416            }
417
418            StmtKind::Assign { target, op, value } => {
419                let new_val = self.eval_expr(value)?;
420                self.exec_assign(target, op, new_val, stmt.line)?;
421                Ok(Signal::None)
422            }
423
424            StmtKind::If {
425                condition,
426                body,
427                else_ifs,
428                else_body,
429            } => {
430                if self.eval_expr(condition)?.is_truthy() {
431                    self.push_scope();
432                    let sig = self.exec_block(body)?;
433                    self.pop_scope();
434                    return Ok(sig);
435                }
436                for (elif_cond, elif_body) in else_ifs {
437                    if self.eval_expr(elif_cond)?.is_truthy() {
438                        self.push_scope();
439                        let sig = self.exec_block(elif_body)?;
440                        self.pop_scope();
441                        return Ok(sig);
442                    }
443                }
444                if let Some(else_body) = else_body {
445                    self.push_scope();
446                    let sig = self.exec_block(else_body)?;
447                    self.pop_scope();
448                    return Ok(sig);
449                }
450                Ok(Signal::None)
451            }
452
453            StmtKind::While { condition, body } => {
454                loop {
455                    self.tick(stmt.line)?;
456                    if !self.eval_expr(condition)?.is_truthy() {
457                        break;
458                    }
459                    self.push_scope();
460                    let sig = self.exec_block(body)?;
461                    self.pop_scope();
462                    match sig {
463                        Signal::Break => break,
464                        Signal::Continue => continue,
465                        Signal::Return(v) => return Ok(Signal::Return(v)),
466                        Signal::None => {}
467                    }
468                }
469                Ok(Signal::None)
470            }
471
472            StmtKind::Repeat { count, body } => {
473                let n = match self.eval_expr(count)? {
474                    Value::Int(n) => n,
475                    Value::Number(n) => n as i64,
476                    other => {
477                        return Err(error(
478                            stmt.line,
479                            format!("repeat needs a number, but got {}", other.type_name()),
480                        ));
481                    }
482                };
483                for _ in 0..n.max(0) {
484                    self.tick(stmt.line)?;
485                    self.push_scope();
486                    let sig = self.exec_block(body)?;
487                    self.pop_scope();
488                    match sig {
489                        Signal::Break => break,
490                        Signal::Continue => continue,
491                        Signal::Return(v) => return Ok(Signal::Return(v)),
492                        Signal::None => {}
493                    }
494                }
495                Ok(Signal::None)
496            }
497
498            StmtKind::ForIn {
499                var,
500                iterable,
501                body,
502            } => {
503                let val = self.eval_expr(iterable)?;
504                // Fast path for the three built-in iterables:
505                // materialise up-front and loop over the Vec
506                // directly, skipping the method-dispatch cost of
507                // the iterator protocol. Semantically identical
508                // to `for x in v.iter()` for these types.
509                if matches!(
510                    &val,
511                    Value::Array(_) | Value::Str(_) | Value::Dict(_)
512                ) {
513                    let mut val = val;
514                    let items: Vec<Value> = match &mut val {
515                        Value::Array(arr) => arr.take(),
516                        Value::Str(s) => s
517                            .chars()
518                            .map(|c| Value::new_str(c.to_string()))
519                            .collect(),
520                        Value::Dict(d) => d
521                            .iter()
522                            .map(|(k, _)| Value::new_str(k.clone()))
523                            .collect(),
524                        _ => unreachable!(),
525                    };
526                    for item in items {
527                        self.tick(stmt.line)?;
528                        self.push_scope();
529                        self.define(var.clone(), item);
530                        let sig = self.exec_block(body)?;
531                        self.pop_scope();
532                        match sig {
533                            Signal::Break => break,
534                            Signal::Continue => continue,
535                            Signal::Return(v) => return Ok(Signal::Return(v)),
536                            Signal::None => {}
537                        }
538                    }
539                    return Ok(Signal::None);
540                }
541                // Protocol path: anything else must either be an
542                // iterator already (Value::Iter, or a user value
543                // with a `.next()` method that returns
544                // `Iter::Next/Done`) or an iterable — a value
545                // whose `.iter()` method returns an iterator.
546                // Primitives and callables that don't fit get a
547                // clean "can't iterate over X" error instead of
548                // a raw "no such method" surface from the
549                // dispatcher below.
550                let iterator = match &val {
551                    Value::Iter(_) | Value::Struct(_) | Value::EnumVariant(_) => {
552                        // Ask the value for an iterator. User
553                        // structs typically implement `iter` to
554                        // return either a built-in iterator or
555                        // themselves.
556                        self.call_method_full(&val, "iter", Vec::new(), stmt.line)?
557                    }
558                    other => {
559                        return Err(error(
560                            stmt.line,
561                            crate::error_messages::cant_iterate_over(
562                                other.type_name(),
563                            ),
564                        ));
565                    }
566                };
567                loop {
568                    self.tick(stmt.line)?;
569                    let next_val =
570                        self.call_method_full(&iterator, "next", Vec::new(), stmt.line)?;
571                    let item = match unwrap_iter_result(&next_val) {
572                        Some(IterStep::Next(v)) => v,
573                        Some(IterStep::Done) => break,
574                        None => {
575                            return Err(error(
576                                stmt.line,
577                                format!(
578                                    "`.next()` on a `for` iterator must return `Iter::Next(v)` or `Iter::Done`, got {}",
579                                    next_val.inspect()
580                                ),
581                            ));
582                        }
583                    };
584                    self.push_scope();
585                    self.define(var.clone(), item);
586                    let sig = self.exec_block(body)?;
587                    self.pop_scope();
588                    match sig {
589                        Signal::Break => break,
590                        Signal::Continue => continue,
591                        Signal::Return(v) => return Ok(Signal::Return(v)),
592                        Signal::None => {}
593                    }
594                }
595                Ok(Signal::None)
596            }
597
598            StmtKind::FnDecl { name, params, body } => {
599                self.functions.insert(
600                    name.clone(),
601                    FnDef {
602                        params: params.clone(),
603                        body: body.clone(),
604                    },
605                );
606                Ok(Signal::None)
607            }
608
609            StmtKind::MethodDecl {
610                type_name,
611                method_name,
612                params,
613                body,
614            } => {
615                // Methods belong to a *specific* type identity —
616                // `fn Color.area(self)` inside module `paint`
617                // registers under `(paint, Color)` and doesn't
618                // leak to `other.Color`. If the named type isn't
619                // declared in this module, that's an error at
620                // parse/emit consistency — fall back to the
621                // current module as the owner.
622                let type_key = (self.current_module.clone(), type_name.clone());
623                self.methods
624                    .entry(type_key)
625                    .or_default()
626                    .insert(
627                        method_name.clone(),
628                        FnDef {
629                            params: params.clone(),
630                            body: body.clone(),
631                        },
632                    );
633                Ok(Signal::None)
634            }
635
636            StmtKind::Return { value } => {
637                let val = match value {
638                    Some(expr) => self.eval_expr(expr)?,
639                    None => Value::None,
640                };
641                Ok(Signal::Return(val))
642            }
643
644            StmtKind::Break => Ok(Signal::Break),
645            StmtKind::Continue => Ok(Signal::Continue),
646
647            StmtKind::Use { path, items, alias } => {
648                self.exec_import(
649                    path,
650                    items.as_deref(),
651                    alias.as_deref(),
652                    stmt.line,
653                )?;
654                Ok(Signal::None)
655            }
656
657            StmtKind::StructDecl { name, fields } => {
658                // Reject duplicate field names at decl time so
659                // downstream code doesn't have to re-check. Walker,
660                // VM, and AOT all assume unique fields per struct.
661                let mut seen = alloc_import::collections::BTreeSet::new();
662                for f in fields {
663                    if !seen.insert(f.clone()) {
664                        return Err(error(
665                            stmt.line,
666                            format!("Struct `{}` has duplicate field `{}`", name, f),
667                        ));
668                    }
669                }
670                // Type identity is `(current_module, name)` —
671                // two different modules declaring the same name
672                // coexist because their keys differ. A redecl
673                // *in the same module* is a no-op when the shape
674                // matches (mirrors the same-module idempotency
675                // rule for re-imports) and a clash otherwise.
676                let key = (self.current_module.clone(), name.clone());
677                if let Some(existing) = self.struct_defs.get(&key) {
678                    if existing == fields {
679                        self.bind_local_type(name);
680                        return Ok(Signal::None);
681                    }
682                    return Err(error(
683                        stmt.line,
684                        format!("Struct `{}` is already declared", name),
685                    ));
686                }
687                self.struct_defs.insert(key, fields.clone());
688                self.bind_local_type(name);
689                Ok(Signal::None)
690            }
691
692            StmtKind::EnumDecl { name, variants } => {
693                let key = (self.current_module.clone(), name.clone());
694                if let Some(existing) = self.enum_defs.get(&key) {
695                    // Same rule as `StructDecl` above — a
696                    // same-module same-shape redeclaration is a
697                    // no-op, anything else is a clash.
698                    if variants_equivalent(existing, variants) {
699                        self.bind_local_type(name);
700                        return Ok(Signal::None);
701                    }
702                    return Err(error(
703                        stmt.line,
704                        format!("Enum `{}` is already declared", name),
705                    ));
706                }
707                let mut seen_variants = alloc_import::collections::BTreeSet::new();
708                for v in variants {
709                    if !seen_variants.insert(v.name.clone()) {
710                        return Err(error(
711                            stmt.line,
712                            format!(
713                                "Enum `{}` has duplicate variant `{}`",
714                                name, v.name
715                            ),
716                        ));
717                    }
718                    if let VariantKind::Struct(fields) | VariantKind::Tuple(fields) =
719                        &v.kind
720                    {
721                        let mut seen_fields = alloc_import::collections::BTreeSet::new();
722                        for f in fields {
723                            if !seen_fields.insert(f.clone()) {
724                                return Err(error(
725                                    stmt.line,
726                                    format!(
727                                        "Enum variant `{}::{}` has duplicate field `{}`",
728                                        name, v.name, f
729                                    ),
730                                ));
731                            }
732                        }
733                    }
734                }
735                self.enum_defs.insert(key, variants.clone());
736                self.bind_local_type(name);
737                Ok(Signal::None)
738            }
739
740            StmtKind::ExprStmt(expr) => {
741                self.eval_expr(expr)?;
742                Ok(Signal::None)
743            }
744        }
745    }
746
747    /// Execute a `use` statement.
748    ///
749    /// Four shapes are dispatched from the parser:
750    ///
751    /// - `use foo`                 — glob: inject all public
752    ///   (non-`_`-prefixed) exports into the caller's scope.
753    ///   Name collisions emit a `BopWarning` and the first
754    ///   binding wins (already-present beats newcomer).
755    /// - `use foo.{a, b}`          — selective: inject only the
756    ///   listed names. Missing names raise a clear error. Names
757    ///   that start with `_` are accepted when listed
758    ///   explicitly (the selective form is how you opt-in to
759    ///   private bindings).
760    /// - `use foo as m`            — aliased: every export
761    ///   (including `_`-prefixed) hangs off a `Value::Module`
762    ///   bound as `m`. Access via `m.binding` or
763    ///   `m.Type { ... }` / `m.Type::Variant(...)`.
764    /// - `use foo.{a, b} as m`     — selective + aliased: same
765    ///   filtering, and the resulting module only exposes the
766    ///   listed names.
767    ///
768    /// Struct / enum / method declarations from the imported
769    /// module still register globally on every form — types
770    /// are first-come-first-served across the engine. Conflicts
771    /// there produce the same "clashes with existing" error as
772    /// before. (Qualified type names that genuinely disambiguate
773    /// same-named types across modules are a future extension.)
774    fn exec_import(
775        &mut self,
776        path: &str,
777        items: Option<&[String]>,
778        alias: Option<&str>,
779        line: u32,
780    ) -> Result<(), BopError> {
781        // Idempotent at the glob injection site: re-importing a
782        // module already applied is a no-op. Aliased / selective
783        // forms don't enter this cache — they always run, because
784        // the same module imported with different shapes can
785        // legitimately produce different scope effects.
786        let is_plain_glob = items.is_none() && alias.is_none();
787        if is_plain_glob && self.imported_here.contains(path) {
788            return Ok(());
789        }
790
791        let bindings = self.load_module(path, line)?;
792
793        // Types always register under their *full identity*
794        // `(module_path, type_name)`. That means two modules
795        // declaring `struct Color { ... }` with different fields
796        // coexist at different registry keys — no clash. Same-
797        // identity reinsertion is a no-op; same key + different
798        // shape would mean the same module got loaded twice with
799        // a different source, which we treat as a hard error.
800        for (key, fields) in &bindings.struct_defs {
801            if let Some(existing) = self.struct_defs.get(key) {
802                if existing == fields {
803                    continue;
804                }
805                return Err(error(
806                    line,
807                    format!(
808                        "Type `{}` from `{}` reloaded with different fields",
809                        key.1, key.0
810                    ),
811                ));
812            }
813            self.struct_defs.insert(key.clone(), fields.clone());
814        }
815        for (key, variants) in &bindings.enum_defs {
816            if let Some(existing) = self.enum_defs.get(key) {
817                if variants_equivalent(existing, variants) {
818                    continue;
819                }
820                return Err(error(
821                    line,
822                    format!(
823                        "Type `{}` from `{}` reloaded with different variants",
824                        key.1, key.0
825                    ),
826                ));
827            }
828            self.enum_defs.insert(key.clone(), variants.clone());
829        }
830        for (type_key, method_name, fn_def) in &bindings.methods {
831            let slot = self.methods.entry(type_key.clone()).or_default();
832            slot.insert(method_name.clone(), fn_def.clone());
833        }
834
835        // Figure out which name-value pairs to surface based on
836        // the (items, alias) combination. Fn declarations and
837        // plain `let` bindings are threaded together so the
838        // caller-visible order matches the module's declaration
839        // order.
840        let mut exports: Vec<(String, Value)> =
841            Vec::with_capacity(bindings.fn_decls.len() + bindings.bindings.len());
842        let mut fn_entries: Vec<(String, FnDef)> =
843            Vec::with_capacity(bindings.fn_decls.len());
844        for (name, fn_def) in &bindings.fn_decls {
845            let value = Value::new_fn(
846                fn_def.params.clone(),
847                Vec::new(),
848                fn_def.body.clone(),
849                Some(name.clone()),
850            );
851            exports.push((name.clone(), value));
852            fn_entries.push((name.clone(), fn_def.clone()));
853        }
854        for (name, value) in &bindings.bindings {
855            exports.push((name.clone(), value.clone()));
856        }
857
858        // Selective filter: restrict to the listed names.
859        // Missing names error loudly — a silent skip would make
860        // typos hard to spot.
861        if let Some(list) = items {
862            let available: alloc_import::collections::BTreeSet<&str> =
863                exports.iter().map(|(k, _)| k.as_str()).collect();
864            for wanted in list {
865                if !available.contains(wanted.as_str())
866                    && !bindings
867                        .struct_defs
868                        .iter()
869                        .any(|((_, n), _)| n == wanted)
870                    && !bindings
871                        .enum_defs
872                        .iter()
873                        .any(|((_, n), _)| n == wanted)
874                {
875                    return Err(error(
876                        line,
877                        format!(
878                            "`{}` isn't exported from `{}` (selective import)",
879                            wanted, path
880                        ),
881                    ));
882                }
883            }
884            let listed: alloc_import::collections::BTreeSet<String> =
885                list.iter().cloned().collect();
886            exports.retain(|(k, _)| listed.contains(k));
887            fn_entries.retain(|(k, _)| listed.contains(k));
888        }
889
890        // Figure out which of the module's types the caller
891        // should see *by bare name*. The selective form picks
892        // exactly the listed names; the glob form takes
893        // everything public; the aliased form never binds bare
894        // names (the alias is the only way in).
895        let module_type_names: Vec<String> = bindings
896            .struct_defs
897            .iter()
898            .map(|((_, n), _)| n.clone())
899            .chain(bindings.enum_defs.iter().map(|((_, n), _)| n.clone()))
900            .collect();
901        let exposed_types: Vec<String> = match items {
902            Some(list) => module_type_names
903                .into_iter()
904                .filter(|n| list.iter().any(|i| i == n))
905                .collect(),
906            None => module_type_names
907                .into_iter()
908                .filter(|n| !crate::naming::is_private(n))
909                .collect(),
910        };
911
912        if let Some(alias_name) = alias {
913            // Aliased form: build a `Value::Module` and bind it
914            // under the alias. The module carries its bindings
915            // (let + fn) and the names of declared types so
916            // namespaced constructors like `m.Entity { ... }`
917            // can verify they're reaching for something the
918            // module actually exports.
919            if self
920                .scopes
921                .last()
922                .map(|s| s.contains_key(alias_name))
923                .unwrap_or(false)
924                || self.functions.contains_key(alias_name)
925            {
926                return Err(error(
927                    line,
928                    format!(
929                        "`{}` is already bound — can't use it as a module alias",
930                        alias_name
931                    ),
932                ));
933            }
934            // Fn entries imported via alias also register in
935            // `self.functions` so `m.foo()` (which lowers to
936            // `Value::Fn` lookup in the module) and `foo()`
937            // (bare call, never reaches the alias) stay
938            // consistent when the module itself has sibling fn
939            // calls inside its own body. Selective + alias:
940            // only the listed fns register.
941            for (name, fn_def) in fn_entries {
942                if !self.functions.contains_key(&name) {
943                    self.functions.insert(name, fn_def);
944                }
945            }
946            let module_rc = Rc::new(crate::value::BopModule {
947                path: path.to_string(),
948                bindings: exports,
949                types: exposed_types,
950            });
951            // Bind the alias three ways:
952            //   1. as a Value::Module in the current value
953            //      scope (for `m.helper(x)` style calls that
954            //      happen directly at the callsite);
955            //   2. in `module_aliases` so it survives the
956            //      fresh-scope reset at function boundaries and
957            //      stays reachable for field access inside fns;
958            //   3. in `type_bindings` so patterns + construction
959            //      can resolve `m.Type` inside function bodies
960            //      without needing the Value::Module in value
961            //      scope.
962            self.define(
963                alias_name.to_string(),
964                Value::Module(Rc::clone(&module_rc)),
965            );
966            self.module_aliases
967                .insert(alias_name.to_string(), Rc::clone(&module_rc));
968            if let Some(scope) = self.type_bindings.last_mut() {
969                scope.insert(alias_name.to_string(), path.to_string());
970            }
971        } else {
972            // Glob / selective without alias — flat injection.
973            // Privacy: glob-only drops `_`-prefixed names.
974            // Selective lets the user reach into private
975            // bindings explicitly.
976            let skip_private = items.is_none();
977            for (name, fn_def) in fn_entries {
978                if skip_private && crate::naming::is_private(&name) {
979                    continue;
980                }
981                if self
982                    .scopes
983                    .last()
984                    .map(|s| s.contains_key(&name))
985                    .unwrap_or(false)
986                    || self.functions.contains_key(&name)
987                {
988                    self.runtime_warnings.push(crate::error::BopWarning::at(
989                        format!(
990                            "`{}` from `{}` shadowed by an existing binding — the first definition wins",
991                            name, path
992                        ),
993                        line,
994                    ));
995                    continue;
996                }
997                self.functions.insert(name, fn_def);
998            }
999            for (name, value) in exports {
1000                if skip_private && crate::naming::is_private(&name) {
1001                    continue;
1002                }
1003                if self
1004                    .scopes
1005                    .last()
1006                    .map(|s| s.contains_key(&name))
1007                    .unwrap_or(false)
1008                {
1009                    self.runtime_warnings.push(crate::error::BopWarning::at(
1010                        format!(
1011                            "`{}` from `{}` shadowed by an existing binding — the first definition wins",
1012                            name, path
1013                        ),
1014                        line,
1015                    ));
1016                    continue;
1017                }
1018                self.define(name, value);
1019            }
1020            // Bind the bare type names in the current scope's
1021            // type_bindings so subsequent `Color::Red` /
1022            // `Color { ... }` resolves to the module the name
1023            // came from. Shadowing here is silent (same as
1024            // values): first definition wins. Types without an
1025            // explicit bare binding remain reachable through
1026            // the alias form only.
1027            for tn in &exposed_types {
1028                let already_bound = self
1029                    .type_bindings
1030                    .last()
1031                    .map(|s| s.contains_key(tn))
1032                    .unwrap_or(false);
1033                if already_bound {
1034                    continue;
1035                }
1036                if let Some(scope) = self.type_bindings.last_mut() {
1037                    scope.insert(tn.clone(), path.to_string());
1038                }
1039            }
1040        }
1041
1042        if is_plain_glob {
1043            self.imported_here.insert(path.to_string());
1044        }
1045        Ok(())
1046    }
1047
1048    /// Resolve and evaluate a module, caching the result. Returns
1049    /// the module's exported bindings.
1050    fn load_module(&mut self, path: &str, line: u32) -> Result<ModuleBindings, BopError> {
1051        // Fast path: already loaded.
1052        {
1053            let cache = self.imports.borrow();
1054            if let Some(ImportSlot::Loaded(bindings)) = cache.get(path) {
1055                return Ok(bindings.clone());
1056            }
1057            if let Some(ImportSlot::Loading) = cache.get(path) {
1058                return Err(error(
1059                    line,
1060                    format!("Circular import: module `{}` is still loading", path),
1061                ));
1062            }
1063        }
1064
1065        // Ask the host for source.
1066        let source = match self.host.resolve_module(path) {
1067            Some(Ok(s)) => s,
1068            Some(Err(e)) => return Err(e),
1069            None => {
1070                return Err(error(
1071                    line,
1072                    format!("Module `{}` not found", path),
1073                ));
1074            }
1075        };
1076
1077        // Mark in-progress before we start evaluating so a
1078        // circular import surfaces as a clean error.
1079        self.imports
1080            .borrow_mut()
1081            .insert(path.to_string(), ImportSlot::Loading);
1082
1083        let result = self.evaluate_module(path, &source, line);
1084
1085        match result {
1086            Ok(bindings) => {
1087                self.imports
1088                    .borrow_mut()
1089                    .insert(path.to_string(), ImportSlot::Loaded(bindings.clone()));
1090                Ok(bindings)
1091            }
1092            Err(e) => {
1093                // Drop the Loading marker so a subsequent, non-
1094                // broken context could retry.
1095                self.imports.borrow_mut().remove(path);
1096                Err(e)
1097            }
1098        }
1099    }
1100
1101    /// Parse and walk a module source in a fresh scope, returning
1102    /// its top-level bindings as a `ModuleBindings`. Reuses the
1103    /// parent evaluator's host, limits, and import cache.
1104    /// `module_path` is the dot-joined name the importer used —
1105    /// the sub-evaluator tags its own declared types with this
1106    /// path so runtime values keep a stable identity across the
1107    /// import boundary.
1108    fn evaluate_module(
1109        &mut self,
1110        module_path: &str,
1111        source: &str,
1112        line: u32,
1113    ) -> Result<ModuleBindings, BopError> {
1114        let _ = line;
1115        let stmts = crate::parse(source)?;
1116        let imports = Rc::clone(&self.imports);
1117        let limits = self.limits.clone();
1118        let mut sub = Evaluator::new_for_module(
1119            self.host,
1120            limits,
1121            imports,
1122            module_path.to_string(),
1123        );
1124        // Run the module body to top — errors propagate as-is.
1125        match sub.exec_block(&stmts)? {
1126            Signal::Return(_) | Signal::None => {}
1127            Signal::Break => {
1128                return Err(error(0, "break used outside of a loop"));
1129            }
1130            Signal::Continue => {
1131                return Err(error(0, "continue used outside of a loop"));
1132            }
1133        }
1134        // Collect top-level `let` bindings from the module's
1135        // one remaining scope. Fns are handled separately so
1136        // the importer can register them in `self.functions`
1137        // as well as in the scope (see `exec_import`).
1138        let mut bindings: Vec<(String, Value)> = Vec::new();
1139        if let Some(top_scope) = sub.scopes.into_iter().next() {
1140            for (k, v) in top_scope {
1141                bindings.push((k, v));
1142            }
1143        }
1144        let fn_decls: Vec<(String, FnDef)> =
1145            sub.functions.into_iter().collect();
1146        // Type decls and methods transfer with their full
1147        // identity. Engine builtins (`<builtin>` module path)
1148        // are seeded into every evaluator anyway, so we filter
1149        // those out here to avoid duplicating them in the
1150        // importer's merge step.
1151        let builtin_mp = crate::value::BUILTIN_MODULE_PATH;
1152        let struct_defs: Vec<((String, String), Vec<String>)> = sub
1153            .struct_defs
1154            .into_iter()
1155            .filter(|((mp, _), _)| mp != builtin_mp)
1156            .collect();
1157        let enum_defs: Vec<((String, String), Vec<crate::parser::VariantDecl>)> = sub
1158            .enum_defs
1159            .into_iter()
1160            .filter(|((mp, _), _)| mp != builtin_mp)
1161            .collect();
1162        let mut methods: Vec<((String, String), String, FnDef)> = Vec::new();
1163        for (type_key, by_method) in sub.methods {
1164            for (method_name, fn_def) in by_method {
1165                methods.push((type_key.clone(), method_name, fn_def));
1166            }
1167        }
1168        Ok(ModuleBindings {
1169            bindings,
1170            fn_decls,
1171            struct_defs,
1172            enum_defs,
1173            methods,
1174        })
1175    }
1176
1177    /// Validate `ns.Type` — the caller wrote `ns.Type { ... }`
1178    /// or `ns.Type::Variant(...)`. `ns` must be a
1179    /// `Value::Module` in scope, and `Type` must appear in that
1180    /// module's list of exported type names. The return is
1181    /// `Ok(())` for now because types still register under their
1182    /// bare names globally; once qualified type names land, this
1183    /// is where we translate `ns.Type` into its qualified form.
1184    fn validate_namespaced_type(
1185        &self,
1186        ns: &str,
1187        type_name: &str,
1188        line: u32,
1189    ) -> Result<(), BopError> {
1190        // Prefer the local value scope so a shadowing binding
1191        // (`let p = 3` after `use paint as p`) is caught — but
1192        // fall back to the evaluator-level alias map so
1193        // namespaced references inside function bodies still
1194        // resolve.
1195        if let Some(v) = self.get_var(ns) {
1196            let module = match v {
1197                Value::Module(m) => m,
1198                _ => {
1199                    return Err(error(
1200                        line,
1201                        format!(
1202                            "`{}` is a {}, not a module alias — can't reach `{}` through it",
1203                            ns,
1204                            v.type_name(),
1205                            type_name
1206                        ),
1207                    ));
1208                }
1209            };
1210            if !module.types.iter().any(|t| t == type_name) {
1211                return Err(error(
1212                    line,
1213                    format!(
1214                        "`{}` isn't a type exported from `{}`",
1215                        type_name, module.path
1216                    ),
1217                ));
1218            }
1219            return Ok(());
1220        }
1221        if let Some(module) = self.module_aliases.get(ns) {
1222            if !module.types.iter().any(|t| t == type_name) {
1223                return Err(error(
1224                    line,
1225                    format!(
1226                        "`{}` isn't a type exported from `{}`",
1227                        type_name, module.path
1228                    ),
1229                ));
1230            }
1231            return Ok(());
1232        }
1233        Err(error(
1234            line,
1235            format!("`{}` isn't a module alias in scope", ns),
1236        ))
1237    }
1238
1239    fn eval_enum_construct(
1240        &mut self,
1241        namespace: Option<&str>,
1242        type_name: &str,
1243        variant: &str,
1244        payload: &VariantPayload,
1245        line: u32,
1246    ) -> Result<Value, BopError> {
1247        // Resolve the type reference to its full
1248        // `(module_path, type_name)` identity before validating
1249        // the variant shape. `namespace` means the source wrote
1250        // `ns.Type::Variant(...)`; bare means we look the name up
1251        // in the current scope's type bindings.
1252        let module_path = match namespace {
1253            Some(ns) => {
1254                self.validate_namespaced_type(ns, type_name, line)?;
1255                // validate_namespaced_type guaranteed the alias
1256                // is a Module and the type is in its exports —
1257                // pick up the module path for the full identity.
1258                self.resolve_type_ref(Some(ns), type_name)
1259                    .unwrap_or_else(|| self.current_module.clone())
1260            }
1261            None => self
1262                .resolve_type_ref(None, type_name)
1263                .ok_or_else(|| {
1264                    error(line, crate::error_messages::enum_not_declared(type_name))
1265                })?,
1266        };
1267        let key = (module_path.clone(), type_name.to_string());
1268        let variants = self.enum_defs.get(&key).ok_or_else(|| {
1269            error(line, crate::error_messages::enum_not_declared(type_name))
1270        })?
1271        .clone();
1272        let decl = variants.iter().find(|v| v.name == variant).ok_or_else(|| {
1273            let msg = crate::error_messages::enum_has_no_variant(type_name, variant);
1274            let names = variants.iter().map(|v| v.name.as_str());
1275            match crate::suggest::did_you_mean(variant, names) {
1276                Some(hint) => error_with_hint(line, msg, hint),
1277                None => error(line, msg),
1278            }
1279        })?
1280        .clone();
1281
1282        match (&decl.kind, payload) {
1283            (VariantKind::Unit, VariantPayload::Unit) => Ok(Value::new_enum_unit(
1284                module_path,
1285                type_name.to_string(),
1286                variant.to_string(),
1287            )),
1288            (VariantKind::Tuple(fields), VariantPayload::Tuple(args)) => {
1289                if args.len() != fields.len() {
1290                    return Err(error(
1291                        line,
1292                        format!(
1293                            "`{}::{}` expects {} argument{}, but got {}",
1294                            type_name,
1295                            variant,
1296                            fields.len(),
1297                            if fields.len() == 1 { "" } else { "s" },
1298                            args.len()
1299                        ),
1300                    ));
1301                }
1302                let mut items = Vec::with_capacity(args.len());
1303                for arg in args {
1304                    items.push(self.eval_expr(arg)?);
1305                }
1306                Ok(Value::new_enum_tuple(
1307                    module_path,
1308                    type_name.to_string(),
1309                    variant.to_string(),
1310                    items,
1311                ))
1312            }
1313            (VariantKind::Struct(decl_fields), VariantPayload::Struct(provided)) => {
1314                let mut seen = alloc_import::collections::BTreeSet::new();
1315                let mut provided_map: BTreeMap<String, Value> = BTreeMap::new();
1316                for (fname, fexpr) in provided {
1317                    if !seen.insert(fname.clone()) {
1318                        return Err(error(
1319                            line,
1320                            format!(
1321                                "Field `{}` specified twice in `{}::{}`",
1322                                fname, type_name, variant
1323                            ),
1324                        ));
1325                    }
1326                    if !decl_fields.iter().any(|d| d == fname) {
1327                        return Err(error(
1328                            line,
1329                            crate::error_messages::variant_has_no_field(
1330                                type_name, variant, fname,
1331                            ),
1332                        ));
1333                    }
1334                    provided_map.insert(fname.clone(), self.eval_expr(fexpr)?);
1335                }
1336                let mut values: Vec<(String, Value)> =
1337                    Vec::with_capacity(decl_fields.len());
1338                for decl_field in decl_fields {
1339                    match provided_map.remove(decl_field) {
1340                        Some(v) => values.push((decl_field.clone(), v)),
1341                        None => {
1342                            return Err(error(
1343                                line,
1344                                format!(
1345                                    "Missing field `{}` in `{}::{}` construction",
1346                                    decl_field, type_name, variant
1347                                ),
1348                            ));
1349                        }
1350                    }
1351                }
1352                Ok(Value::new_enum_struct(
1353                    module_path,
1354                    type_name.to_string(),
1355                    variant.to_string(),
1356                    values,
1357                ))
1358            }
1359            (VariantKind::Unit, _) => Err(error(
1360                line,
1361                format!(
1362                    "Variant `{}::{}` takes no payload",
1363                    type_name, variant
1364                ),
1365            )),
1366            (VariantKind::Tuple(_), _) => Err(error(
1367                line,
1368                format!(
1369                    "Variant `{}::{}` expects positional arguments `(…)`",
1370                    type_name, variant
1371                ),
1372            )),
1373            (VariantKind::Struct(_), _) => Err(error(
1374                line,
1375                format!(
1376                    "Variant `{}::{}` expects named fields `{{ … }}`",
1377                    type_name, variant
1378                ),
1379            )),
1380        }
1381    }
1382
1383    fn exec_assign(
1384        &mut self,
1385        target: &AssignTarget,
1386        op: &AssignOp,
1387        new_val: Value,
1388        line: u32,
1389    ) -> Result<(), BopError> {
1390        match target {
1391            AssignTarget::Variable(name) => {
1392                let final_val = match op {
1393                    AssignOp::Eq => new_val,
1394                    _ => {
1395                        let current = self
1396                            .get_var(name)
1397                            .ok_or_else(|| {
1398                                error(line, format!("Variable `{}` doesn't exist yet", name))
1399                            })?
1400                            .clone();
1401                        self.apply_compound_op(&current, op, &new_val, line)?
1402                    }
1403                };
1404                if !self.set_var(name, final_val) {
1405                    return Err(error_with_hint(
1406                        line,
1407                        format!("Variable `{}` doesn't exist yet", name),
1408                        format!("Use `let` to create a new variable: let {} = ...", name),
1409                    ));
1410                }
1411                Ok(())
1412            }
1413            AssignTarget::Index { object, index } => {
1414                let idx = self.eval_expr(index)?;
1415                let val_to_set = match op {
1416                    AssignOp::Eq => new_val,
1417                    _ => {
1418                        let obj = self.eval_expr(object)?;
1419                        let current = ops::index_get(&obj, &idx, line)?;
1420                        self.apply_compound_op(&current, op, &new_val, line)?
1421                    }
1422                };
1423                if let ExprKind::Ident(name) = &object.kind {
1424                    let mut obj = self
1425                        .get_var(name)
1426                        .ok_or_else(|| {
1427                            error(line, format!("Variable `{}` doesn't exist", name))
1428                        })?
1429                        .clone();
1430                    ops::index_set(&mut obj, &idx, val_to_set, line)?;
1431                    self.set_var(name, obj);
1432                    Ok(())
1433                } else {
1434                    Err(error(
1435                        line,
1436                        "Can only assign to indexed variables (like `arr[0] = val`)",
1437                    ))
1438                }
1439            }
1440            AssignTarget::Field { object, field } => {
1441                // Support assignment into a named struct field.
1442                // Only bare-`Ident` objects are assignable — the
1443                // writeback goes through `set_var`, so chains like
1444                // `foo().x = 1` or `arr[0].x = 1` aren't
1445                // supported yet (matches index-assign behaviour).
1446                let name = match &object.kind {
1447                    ExprKind::Ident(n) => n.clone(),
1448                    _ => {
1449                        return Err(error(
1450                            line,
1451                            "Can only assign to fields of named variables (like `p.x = val`)",
1452                        ));
1453                    }
1454                };
1455                let mut obj = self
1456                    .get_var(&name)
1457                    .ok_or_else(|| {
1458                        error(line, format!("Variable `{}` doesn't exist", name))
1459                    })?
1460                    .clone();
1461                let val_to_set = match op {
1462                    AssignOp::Eq => new_val,
1463                    _ => {
1464                        let current = match &obj {
1465                            Value::Struct(s) => s.field(field).cloned().ok_or_else(|| {
1466                                error(
1467                                    line,
1468                                    crate::error_messages::struct_has_no_field(
1469                                        s.type_name(),
1470                                        field,
1471                                    ),
1472                                )
1473                            })?,
1474                            other => {
1475                                return Err(error(
1476                                    line,
1477                                    crate::error_messages::cant_assign_field(
1478                                        field,
1479                                        other.type_name(),
1480                                    ),
1481                                ));
1482                            }
1483                        };
1484                        self.apply_compound_op(&current, op, &new_val, line)?
1485                    }
1486                };
1487                match &mut obj {
1488                    Value::Struct(s) => {
1489                        let struct_type = s.type_name().to_string();
1490                        if !s.set_field(field, val_to_set) {
1491                            return Err(error(
1492                                line,
1493                                crate::error_messages::struct_has_no_field(&struct_type, field),
1494                            ));
1495                        }
1496                    }
1497                    other => {
1498                        return Err(error(
1499                            line,
1500                            crate::error_messages::cant_assign_field(
1501                                field,
1502                                other.type_name(),
1503                            ),
1504                        ));
1505                    }
1506                }
1507                self.set_var(&name, obj);
1508                Ok(())
1509            }
1510        }
1511    }
1512
1513    fn apply_compound_op(
1514        &self,
1515        left: &Value,
1516        op: &AssignOp,
1517        right: &Value,
1518        line: u32,
1519    ) -> Result<Value, BopError> {
1520        match op {
1521            AssignOp::Eq => Ok(right.clone()),
1522            AssignOp::AddEq => ops::add(left, right, line),
1523            AssignOp::SubEq => ops::sub(left, right, line),
1524            AssignOp::MulEq => ops::mul(left, right, line),
1525            AssignOp::DivEq => ops::div(left, right, line),
1526            AssignOp::ModEq => ops::rem(left, right, line),
1527        }
1528    }
1529
1530    // ─── Expressions ───────────────────────────────────────────────
1531
1532    fn eval_expr(&mut self, expr: &Expr) -> Result<Value, BopError> {
1533        match &expr.kind {
1534            ExprKind::Int(n) => Ok(Value::Int(*n)),
1535            ExprKind::Number(n) => Ok(Value::Number(*n)),
1536            ExprKind::Str(s) => Ok(Value::new_str(s.clone())),
1537            ExprKind::Bool(b) => Ok(Value::Bool(*b)),
1538            ExprKind::None => Ok(Value::None),
1539
1540            ExprKind::StringInterp(parts) => {
1541                let mut result = String::new();
1542                for part in parts {
1543                    match part {
1544                        StringPart::Literal(s) => result.push_str(s),
1545                        StringPart::Variable(name) => {
1546                            let val = self
1547                                .get_var(name)
1548                                .ok_or_else(|| {
1549                                    error_at(expr.line, expr.column, crate::error_messages::variable_not_found(name))
1550                                })?
1551                                .clone();
1552                            result.push_str(&format!("{}", val));
1553                        }
1554                    }
1555                }
1556                Ok(Value::new_str(result))
1557            }
1558
1559            ExprKind::Ident(name) => {
1560                // Lexical lookup first — matches the intuition that
1561                // `let x = ...` locally shadows everything else.
1562                if let Some(v) = self.get_var(name) {
1563                    return Ok(v.clone());
1564                }
1565                // Fall back to named `fn` declarations so they can
1566                // be passed around as first-class values. The
1567                // synthesised `Value::Fn` carries `self_name` so
1568                // recursive lookups inside the body still resolve
1569                // through `self.functions` (see `call_bop_fn`).
1570                if let Some(f) = self.functions.get(name) {
1571                    return Ok(Value::new_fn(
1572                        f.params.clone(),
1573                        Vec::new(),
1574                        f.body.clone(),
1575                        Some(name.to_string()),
1576                    ));
1577                }
1578                // Typo? Offer a "did you mean" hint if something
1579                // close is visible in the current scope / fn
1580                // registry. Falls back to the original "did you
1581                // forget `let`" when no candidate is similar
1582                // enough.
1583                let hint = self
1584                    .value_candidates_hint(name)
1585                    .unwrap_or_else(|| "Did you forget to create it with `let`?".to_string());
1586                Err(error_with_hint_at(
1587                    expr.line,
1588                    expr.column,
1589                    crate::error_messages::variable_not_found(name),
1590                    hint,
1591                ))
1592            }
1593
1594            ExprKind::BinaryOp { left, op, right } => {
1595                // Short-circuit for && and ||
1596                if matches!(op, BinOp::And) {
1597                    let lval = self.eval_expr(left)?;
1598                    if !lval.is_truthy() {
1599                        return Ok(Value::Bool(false));
1600                    }
1601                    let rval = self.eval_expr(right)?;
1602                    return Ok(Value::Bool(rval.is_truthy()));
1603                }
1604                if matches!(op, BinOp::Or) {
1605                    let lval = self.eval_expr(left)?;
1606                    if lval.is_truthy() {
1607                        return Ok(Value::Bool(true));
1608                    }
1609                    let rval = self.eval_expr(right)?;
1610                    return Ok(Value::Bool(rval.is_truthy()));
1611                }
1612
1613                let lval = self.eval_expr(left)?;
1614                let rval = self.eval_expr(right)?;
1615                self.binary_op(&lval, op, &rval, expr.line)
1616            }
1617
1618            ExprKind::UnaryOp { op, expr: inner } => {
1619                let val = self.eval_expr(inner)?;
1620                match op {
1621                    UnaryOp::Neg => ops::neg(&val, expr.line),
1622                    UnaryOp::Not => Ok(ops::not(&val)),
1623                }
1624            }
1625
1626            ExprKind::Call { callee, args } => {
1627                let mut eval_args = Vec::new();
1628                for arg in args {
1629                    eval_args.push(self.eval_expr(arg)?);
1630                }
1631                if let ExprKind::Ident(name) = &callee.kind {
1632                    // Lexical callable first: if the name is bound
1633                    // to a `Value::Fn` in the current scope, call
1634                    // it. A bound non-callable is an explicit
1635                    // error — "shadowing a builtin with a number
1636                    // then calling it" should fail loudly, not
1637                    // silently dispatch to the builtin.
1638                    if let Some(v) = self.get_var(name).cloned() {
1639                        return self.call_value(v, eval_args, expr.line, Some(name));
1640                    }
1641                    // Otherwise fall through to the original
1642                    // name-based dispatch (builtins → host → named
1643                    // fns). This is what keeps `print(x)` /
1644                    // `range(n)` / `my_user_fn(x)` working.
1645                    return self.call_function(name, eval_args, expr.line);
1646                }
1647                // Non-Ident callee: evaluate the expression; it
1648                // must produce a `Value::Fn`.
1649                let callee_val = self.eval_expr(callee)?;
1650                self.call_value(callee_val, eval_args, expr.line, None)
1651            }
1652
1653            ExprKind::Lambda { params, body } => {
1654                let captures = self.snapshot_captures();
1655                Ok(Value::new_fn(
1656                    params.clone(),
1657                    captures,
1658                    body.clone(),
1659                    None,
1660                ))
1661            }
1662
1663            ExprKind::MethodCall {
1664                object,
1665                method,
1666                args,
1667            } => {
1668                let mut eval_args = Vec::new();
1669                for arg in args {
1670                    eval_args.push(self.eval_expr(arg)?);
1671                }
1672                let obj_val = self.eval_expr(object)?;
1673
1674                // `m.foo(args)` on a module alias: this parsed as
1675                // a `MethodCall`, but there's no struct/enum
1676                // receiver — `m` is a `Value::Module` whose
1677                // `foo` export is a callable value. Look it up,
1678                // then treat the result as a regular value call.
1679                //
1680                // The common methods (`type`, `to_str`,
1681                // `inspect`) still win over export lookup, so
1682                // `m.type()` returns `"module"` instead of
1683                // complaining that `type` isn't exported.
1684                if let Value::Module(m) = &obj_val {
1685                    if let Some(result) =
1686                        methods::common_method(&obj_val, method, &eval_args, expr.line)?
1687                    {
1688                        return Ok(result.0);
1689                    }
1690                    if let Some((_, v)) = m.bindings.iter().find(|(k, _)| k == method) {
1691                        let callee = v.clone();
1692                        return self.call_value(callee, eval_args, expr.line, Some(method));
1693                    }
1694                    return Err(error(
1695                        expr.line,
1696                        format!(
1697                            "`{}` isn't exported from `{}`",
1698                            method, m.path
1699                        ),
1700                    ));
1701                }
1702
1703                // User-defined method dispatch comes first — any
1704                // user method registered against the receiver's
1705                // *full* type identity `(module_path, type_name)`
1706                // wins over built-in methods of the same name.
1707                // Enums dispatch on the enum's type, not the
1708                // variant's, so all variants of `paint.Shape`
1709                // share `fn Shape.area(self)` from the paint
1710                // module. A method declared for `paint.Shape`
1711                // deliberately does not fire on `other.Shape`.
1712                let type_key: Option<(String, String)> = match &obj_val {
1713                    Value::Struct(s) => Some((
1714                        s.module_path().to_string(),
1715                        s.type_name().to_string(),
1716                    )),
1717                    Value::EnumVariant(e) => Some((
1718                        e.module_path().to_string(),
1719                        e.type_name().to_string(),
1720                    )),
1721                    _ => None,
1722                };
1723                if let Some(key) = type_key {
1724                    let user = self
1725                        .methods
1726                        .get(&key)
1727                        .and_then(|ms| ms.get(method))
1728                        .cloned();
1729                    if let Some(m) = user {
1730                        if m.params.len() != eval_args.len() + 1 {
1731                            return Err(error(
1732                                expr.line,
1733                                format!(
1734                                    "`{}.{}` expects {} argument{} (including `self`), but got {}",
1735                                    key.1,
1736                                    method,
1737                                    m.params.len(),
1738                                    if m.params.len() == 1 { "" } else { "s" },
1739                                    eval_args.len() + 1
1740                                ),
1741                            ));
1742                        }
1743                        // Prepend receiver as the first parameter
1744                        // (`self` by convention).
1745                        let mut full_args = Vec::with_capacity(eval_args.len() + 1);
1746                        full_args.push(obj_val);
1747                        full_args.extend(eval_args);
1748                        let bop_fn = Rc::new(BopFn {
1749                            params: m.params,
1750                            captures: Vec::new(),
1751                            body: FnBody::Ast(m.body),
1752                            self_name: None,
1753                        });
1754                        return self.call_bop_fn(&bop_fn, full_args, expr.line);
1755                    }
1756                }
1757
1758                // Callable-taking Result methods — `r.map(f)`,
1759                // `r.map_err(f)`, `r.and_then(f)`. These need the
1760                // evaluator's call primitive to invoke `f`, which
1761                // `call_method` doesn't have access to, so they
1762                // dispatch inline before the pure `call_method`
1763                // fall-through. Receiver must be the built-in
1764                // Result; user enums named `Result` don't qualify.
1765                if methods::is_builtin_result(&obj_val) {
1766                    if let Some(kind) = methods::is_result_callable_method(method) {
1767                        return self.call_result_callable_method(
1768                            &obj_val,
1769                            kind,
1770                            method,
1771                            eval_args,
1772                            expr.line,
1773                        );
1774                    }
1775                }
1776
1777                let (ret, mutated) = self.call_method(&obj_val, method, &eval_args, expr.line)?;
1778
1779                if methods::is_mutating_method(method) {
1780                    if let ExprKind::Ident(name) = &object.kind {
1781                        if let Some(new_obj) = mutated {
1782                            self.set_var(name, new_obj);
1783                        }
1784                    }
1785                }
1786                Ok(ret)
1787            }
1788
1789            ExprKind::Index { object, index } => {
1790                let obj = self.eval_expr(object)?;
1791                let idx = self.eval_expr(index)?;
1792                ops::index_get(&obj, &idx, expr.line)
1793            }
1794
1795            ExprKind::FieldAccess { object, field } => {
1796                let obj = self.eval_expr(object)?;
1797                match &obj {
1798                    Value::Struct(s) => s.field(field).cloned().ok_or_else(|| {
1799                        let msg =
1800                            crate::error_messages::struct_has_no_field(s.type_name(), field);
1801                        // Suggest from the struct's own declared
1802                        // field list — `p.z` when `Point` has `x`
1803                        // and `y` should point to `x`/`y`.
1804                        let field_names: Vec<&str> =
1805                            s.fields().iter().map(|(k, _)| k.as_str()).collect();
1806                        match crate::suggest::did_you_mean(field, field_names) {
1807                            Some(hint) => error_with_hint_at(expr.line, expr.column, msg, hint),
1808                            None => error_at(expr.line, expr.column, msg),
1809                        }
1810                    }),
1811                    Value::EnumVariant(e) => e.field(field).cloned().ok_or_else(|| {
1812                        error(
1813                            expr.line,
1814                            crate::error_messages::variant_has_no_field(
1815                                e.type_name(),
1816                                e.variant(),
1817                                field,
1818                            ),
1819                        )
1820                    }),
1821                    Value::Module(m) => {
1822                        // `alias.name` — look up an export in the
1823                        // aliased module.
1824                        if let Some((_, v)) = m.bindings.iter().find(|(k, _)| k == field) {
1825                            return Ok(v.clone());
1826                        }
1827                        if m.types.iter().any(|t| t == field) {
1828                            // Types aren't first-class values; the
1829                            // parser should have taken the namespaced
1830                            // struct-lit / variant-ctor path
1831                            // already. Reaching a FieldAccess here
1832                            // means the user wrote `m.Type` in a
1833                            // value-returning position.
1834                            return Err(error_with_hint(
1835                                expr.line,
1836                                format!("`{}` in `{}` is a type, not a value", field, m.path),
1837                                format!(
1838                                    "construct through the alias: `{}.{} {{ ... }}` or `{}.{}::Variant(...)`",
1839                                    m.path.split('.').last().unwrap_or(&m.path),
1840                                    field,
1841                                    m.path.split('.').last().unwrap_or(&m.path),
1842                                    field,
1843                                ),
1844                            ));
1845                        }
1846                        Err(error(
1847                            expr.line,
1848                            format!(
1849                                "`{}` isn't exported from `{}`",
1850                                field, m.path
1851                            ),
1852                        ))
1853                    }
1854                    other => Err(error(
1855                        expr.line,
1856                        crate::error_messages::cant_read_field(field, other.type_name()),
1857                    )),
1858                }
1859            }
1860
1861            ExprKind::EnumConstruct {
1862                namespace,
1863                type_name,
1864                variant,
1865                payload,
1866            } => self.eval_enum_construct(
1867                namespace.as_deref(),
1868                type_name,
1869                variant,
1870                payload,
1871                expr.line,
1872            ),
1873
1874            ExprKind::StructConstruct {
1875                namespace,
1876                type_name,
1877                fields,
1878            } => {
1879                let module_path = match namespace {
1880                    Some(ns) => {
1881                        self.validate_namespaced_type(ns, type_name, expr.line)?;
1882                        self.resolve_type_ref(Some(ns), type_name)
1883                            .unwrap_or_else(|| self.current_module.clone())
1884                    }
1885                    None => self
1886                        .resolve_type_ref(None, type_name)
1887                        .ok_or_else(|| {
1888                            error(
1889                                expr.line,
1890                                crate::error_messages::struct_not_declared(type_name),
1891                            )
1892                        })?,
1893                };
1894                let key = (module_path.clone(), type_name.clone());
1895                let decl_fields = self.struct_defs.get(&key).ok_or_else(|| {
1896                    error(
1897                        expr.line,
1898                        crate::error_messages::struct_not_declared(type_name),
1899                    )
1900                })?
1901                .clone();
1902                // Enforce exactly-this-field-set at construction:
1903                // no duplicates, no unknown fields, no missing
1904                // fields. The per-field loops below produce the
1905                // specific messages the tests assert on.
1906                let mut seen = alloc_import::collections::BTreeSet::new();
1907                let mut values: Vec<(String, Value)> =
1908                    Vec::with_capacity(decl_fields.len());
1909                // Evaluate provided fields by name, then emit them
1910                // in *declaration* order so the `Value::Struct`'s
1911                // field list is stable across construction sites.
1912                let mut provided: BTreeMap<String, Value> = BTreeMap::new();
1913                for (fname, fexpr) in fields {
1914                    if !seen.insert(fname.clone()) {
1915                        return Err(error(
1916                            expr.line,
1917                            format!(
1918                                "Field `{}` specified twice in `{}` construction",
1919                                fname, type_name
1920                            ),
1921                        ));
1922                    }
1923                    if !decl_fields.iter().any(|d| d == fname) {
1924                        let msg = crate::error_messages::struct_has_no_field(type_name, fname);
1925                        let err = match crate::suggest::did_you_mean(
1926                            fname,
1927                            decl_fields.iter().map(|s| s.as_str()),
1928                        ) {
1929                            Some(hint) => error_with_hint_at(expr.line, expr.column, msg, hint),
1930                            None => error_at(expr.line, expr.column, msg),
1931                        };
1932                        return Err(err);
1933                    }
1934                    provided.insert(fname.clone(), self.eval_expr(fexpr)?);
1935                }
1936                for decl in &decl_fields {
1937                    match provided.remove(decl) {
1938                        Some(v) => values.push((decl.clone(), v)),
1939                        None => {
1940                            return Err(error(
1941                                expr.line,
1942                                format!(
1943                                    "Missing field `{}` in `{}` construction",
1944                                    decl, type_name
1945                                ),
1946                            ));
1947                        }
1948                    }
1949                }
1950                Ok(Value::new_struct(module_path, type_name.clone(), values))
1951            }
1952
1953            ExprKind::Array(elements) => {
1954                let mut items = Vec::new();
1955                for elem in elements {
1956                    items.push(self.eval_expr(elem)?);
1957                }
1958                Ok(Value::new_array(items))
1959            }
1960
1961            ExprKind::Dict(entries) => {
1962                let mut result = Vec::new();
1963                for (key, value_expr) in entries {
1964                    let val = self.eval_expr(value_expr)?;
1965                    result.push((key.clone(), val));
1966                }
1967                Ok(Value::new_dict(result))
1968            }
1969
1970            ExprKind::IfExpr {
1971                condition,
1972                then_expr,
1973                else_expr,
1974            } => {
1975                if self.eval_expr(condition)?.is_truthy() {
1976                    self.eval_expr(then_expr)
1977                } else {
1978                    self.eval_expr(else_expr)
1979                }
1980            }
1981
1982            ExprKind::Match { scrutinee, arms } => self.eval_match(scrutinee, arms, expr.line),
1983
1984            ExprKind::Try(inner) => self.eval_try(inner, expr.line),
1985        }
1986    }
1987
1988    /// `try` expression handler. Evaluates the inner expression,
1989    /// matches the conventional Result-shape (any enum variant
1990    /// named `Ok` or `Err`), and either unwraps `Ok` or stashes
1991    /// the `Err` value into `pending_try_return` so the enclosing
1992    /// `call_bop_fn` can convert it to `Signal::Return`.
1993    ///
1994    /// Top-level `try` on `Err` (call_depth == 0) surfaces as a
1995    /// real runtime error — there's no enclosing fn to return
1996    /// from, and the roadmap explicitly rejects the idea of
1997    /// swallowing it silently.
1998    fn eval_try(&mut self, inner: &Expr, line: u32) -> Result<Value, BopError> {
1999        let value = self.eval_expr(inner)?;
2000        // An earlier `try` in `inner` might have already fired —
2001        // e.g. `try try foo()` — in which case we just keep
2002        // propagating without poking at the unwound value.
2003        if self.pending_try_return.is_some() {
2004            return Ok(Value::None);
2005        }
2006        match &value {
2007            Value::EnumVariant(ev) if ev.variant() == "Ok" => {
2008                // Extract the single payload for `Ok(v)`, or fall
2009                // back to `none` for `Ok` used as a unit variant.
2010                match ev.payload() {
2011                    EnumPayload::Tuple(items) if items.len() == 1 => Ok(items[0].clone()),
2012                    EnumPayload::Unit => Ok(Value::None),
2013                    EnumPayload::Tuple(items) => Err(error(
2014                        line,
2015                        format!(
2016                            "try: Ok variant must carry exactly one value, got {}",
2017                            items.len()
2018                        ),
2019                    )),
2020                    EnumPayload::Struct(_) => Err(error(
2021                        line,
2022                        "try: Ok variant must carry a single positional value, not named fields",
2023                    )),
2024                }
2025            }
2026            Value::EnumVariant(ev) if ev.variant() == "Err" => {
2027                if self.call_depth == 0 {
2028                    return Err(error_with_hint(
2029                        line,
2030                        "try encountered Err at top-level",
2031                        "Wrap the calling code in a fn, or use `match` to handle both arms explicitly.",
2032                    ));
2033                }
2034                self.pending_try_return = Some(value);
2035                // Sentinel error whose only job is to unwind up
2036                // to the nearest `call_bop_fn`, which converts
2037                // it back into a `Signal::Return`. The `is_try_return`
2038                // flag on `BopError` is the only thing that
2039                // matters — line is kept for debug clarity;
2040                // message is unused.
2041                Err(BopError::try_return_signal(line))
2042            }
2043            other => Err(error(
2044                line,
2045                format!(
2046                    "try expected a Result-shaped value (Ok/Err variant), got {}",
2047                    other.type_name()
2048                ),
2049            )),
2050        }
2051    }
2052
2053    fn eval_match(
2054        &mut self,
2055        scrutinee: &Expr,
2056        arms: &[MatchArm],
2057        line: u32,
2058    ) -> Result<Value, BopError> {
2059        let value = self.eval_expr(scrutinee)?;
2060        for arm in arms {
2061            let mut bindings: Vec<(String, Value)> = Vec::new();
2062            // Resolve patterns' type references through the
2063            // walker's current scope. Capturing the three scope
2064            // tables as immutable slices / refs keeps the
2065            // closure free of the mutable-self borrow we need
2066            // right after for `push_scope` + binding injection.
2067            let scopes = &self.scopes;
2068            let type_bindings = &self.type_bindings;
2069            let module_aliases = &self.module_aliases;
2070            let resolver = |ns: Option<&str>, tn: &str| -> Option<String> {
2071                resolve_type_in(scopes, type_bindings, module_aliases, ns, tn)
2072            };
2073            if !pattern_matches(&arm.pattern, &value, &mut bindings, &resolver) {
2074                continue;
2075            }
2076            // Pattern matched — open a fresh scope, bind every
2077            // captured name, evaluate the guard (in that scope).
2078            self.push_scope();
2079            for (name, v) in bindings {
2080                self.define(name, v);
2081            }
2082            let guard_ok = match &arm.guard {
2083                Some(g) => self.eval_expr(g)?.is_truthy(),
2084                None => true,
2085            };
2086            if !guard_ok {
2087                self.pop_scope();
2088                continue;
2089            }
2090            let result = self.eval_expr(&arm.body);
2091            self.pop_scope();
2092            return result;
2093        }
2094        Err(error(
2095            line,
2096            "No match arm matched the scrutinee",
2097        ))
2098    }
2099
2100    // ─── Binary operations ─────────────────────────────────────────
2101
2102    fn binary_op(
2103        &self,
2104        left: &Value,
2105        op: &BinOp,
2106        right: &Value,
2107        line: u32,
2108    ) -> Result<Value, BopError> {
2109        match op {
2110            BinOp::Add => ops::add(left, right, line),
2111            BinOp::Sub => ops::sub(left, right, line),
2112            BinOp::Mul => ops::mul(left, right, line),
2113            BinOp::Div => ops::div(left, right, line),
2114            BinOp::Mod => ops::rem(left, right, line),
2115            BinOp::Eq => Ok(ops::eq(left, right)),
2116            BinOp::NotEq => Ok(ops::not_eq(left, right)),
2117            BinOp::Lt => ops::lt(left, right, line),
2118            BinOp::Gt => ops::gt(left, right, line),
2119            BinOp::LtEq => ops::lt_eq(left, right, line),
2120            BinOp::GtEq => ops::gt_eq(left, right, line),
2121            BinOp::And | BinOp::Or => unreachable!("handled in eval_expr"),
2122        }
2123    }
2124
2125    // ─── Function calls ────────────────────────────────────────────
2126
2127    /// Collapse the current scope stack into a flat list of
2128    /// `(name, value)` pairs — the snapshot used as a lambda's
2129    /// captures. Inner scopes shadow outer ones, so the resulting
2130    /// list is deduplicated by name with the innermost binding
2131    /// winning.
2132    fn snapshot_captures(&self) -> Vec<(String, Value)> {
2133        let mut flat = BTreeMap::new();
2134        for scope in &self.scopes {
2135            for (k, v) in scope {
2136                flat.insert(k.clone(), v.clone());
2137            }
2138        }
2139        flat.into_iter().collect()
2140    }
2141
2142    /// Call a value directly. Non-`Value::Fn` payloads are an
2143    /// explicit "not callable" error — this is the safety net for
2144    /// `let x = 5; x(1)` and friends.
2145    ///
2146    /// `name_hint` is the Ident text when the callee was a bare
2147    /// name, used only for error messages. Non-Ident callees pass
2148    /// `None`.
2149    fn call_value(
2150        &mut self,
2151        callee: Value,
2152        args: Vec<Value>,
2153        line: u32,
2154        name_hint: Option<&str>,
2155    ) -> Result<Value, BopError> {
2156        match &callee {
2157            Value::Fn(f) => {
2158                let f = Rc::clone(f);
2159                drop(callee);
2160                self.call_bop_fn(&f, args, line)
2161            }
2162            other => match name_hint {
2163                Some(n) => Err(error(
2164                    line,
2165                    format!(
2166                        "`{}` is a {}, not a function",
2167                        n,
2168                        other.type_name()
2169                    ),
2170                )),
2171                None => Err(error(
2172                    line,
2173                    crate::error_messages::cant_call_a(other.type_name()),
2174                )),
2175            },
2176        }
2177    }
2178
2179    /// Shared call path for every `Value::Fn` — the body of both
2180    /// `let f = fn(...) {...}; f(x)` and the reified version of
2181    /// `fn name(...) {...}; name(x)` come through here.
2182    fn call_bop_fn(
2183        &mut self,
2184        func: &Rc<BopFn>,
2185        args: Vec<Value>,
2186        line: u32,
2187    ) -> Result<Value, BopError> {
2188        let body = match &func.body {
2189            FnBody::Ast(stmts) => stmts,
2190            FnBody::Compiled(_) => {
2191                return Err(error(
2192                    line,
2193                    "This function was compiled for another engine and can't be run in the tree-walker",
2194                ));
2195            }
2196        };
2197        if args.len() != func.params.len() {
2198            let name = func.self_name.as_deref().unwrap_or("fn");
2199            return Err(error(
2200                line,
2201                format!(
2202                    "`{}` expects {} argument{}, but got {}",
2203                    name,
2204                    func.params.len(),
2205                    if func.params.len() == 1 { "" } else { "s" },
2206                    args.len()
2207                ),
2208            ));
2209        }
2210
2211        if self.call_depth >= MAX_CALL_DEPTH {
2212            return Err(error_with_hint(
2213                line,
2214                "Too many nested function calls (possible infinite recursion)",
2215                "Check that your recursive function has a base case that stops calling itself.",
2216            ));
2217        }
2218
2219        // A function call gets a fresh scope stack: no outer
2220        // locals leak in. Captures and parameters seed the new
2221        // scope; self-reference via `self_name` lets recursive
2222        // lambdas see themselves.
2223        //
2224        // `type_bindings` is NOT reset — types and module
2225        // aliases declared at module scope need to be visible
2226        // inside nested fn bodies for construction + pattern
2227        // matching to work. We push a fresh frame so any
2228        // type decl inside the fn body still ends up scoped to
2229        // that fn and vanishes on return.
2230        self.call_depth += 1;
2231        let saved_scopes = core::mem::replace(&mut self.scopes, vec![BTreeMap::new()]);
2232        self.type_bindings.push(BTreeMap::new());
2233
2234        // Captures go in first so parameters shadow them on
2235        // collision (matches the lexical snapshot semantics).
2236        for (name, value) in &func.captures {
2237            self.define(name.clone(), value.clone());
2238        }
2239        if let Some(self_name) = &func.self_name {
2240            // Make the reified `fn name` value visible inside its
2241            // own body so `fn fib(n) { return fib(n-1) ... }`
2242            // works without a special "named fn" path.
2243            self.define(self_name.clone(), Value::Fn(Rc::clone(func)));
2244        }
2245        for (param, arg) in func.params.iter().zip(args) {
2246            self.define(param.clone(), arg);
2247        }
2248
2249        let result = self.exec_block(body);
2250        self.scopes = saved_scopes;
2251        self.type_bindings.pop();
2252        self.call_depth -= 1;
2253
2254        // `try` unwinds as a sentinel `BopError`; the call
2255        // boundary trades that error for the stashed value and
2256        // treats it as a normal return. Any other error
2257        // propagates as usual. Always clears
2258        // `pending_try_return` so a leftover can't contaminate
2259        // a later call.
2260        match result {
2261            Ok(sig) => match sig {
2262                Signal::Return(val) => Ok(val),
2263                Signal::Break => Err(error(line, "break used outside of a loop")),
2264                Signal::Continue => Err(error(line, "continue used outside of a loop")),
2265                Signal::None => Ok(Value::None),
2266            },
2267            Err(err) => {
2268                // Check the dedicated `is_try_return` flag
2269                // instead of inspecting the message — a flag
2270                // can't collide with a user-authored error
2271                // that happens to spell the same bytes.
2272                if err.is_try_return {
2273                    if let Some(val) = self.pending_try_return.take() {
2274                        return Ok(val);
2275                    }
2276                }
2277                Err(err)
2278            }
2279        }
2280    }
2281
2282    /// Implement the `try_call(f)` builtin.
2283    ///
2284    /// Takes a zero-arg callable `f` and invokes it. On a clean
2285    /// return, yields `Result::Ok(value)`. On a **non-fatal**
2286    /// `BopError`, yields `Result::Err(RuntimeError { message,
2287    /// line })` — both values are constructed directly by
2288    /// `bop::builtins::make_try_call_ok` / `_err`, so they
2289    /// work even in programs that never declared `Result` or
2290    /// `RuntimeError` themselves.
2291    ///
2292    /// Fatal errors (resource-limit violations — see
2293    /// `BopError::is_fatal`) bypass the wrap and propagate
2294    /// unchanged, preserving the sandbox invariant.
2295    fn builtin_try_call(
2296        &mut self,
2297        args: Vec<Value>,
2298        line: u32,
2299    ) -> Result<Value, BopError> {
2300        if args.len() != 1 {
2301            return Err(error(
2302                line,
2303                format!(
2304                    "`try_call` expects 1 argument, but got {}",
2305                    args.len()
2306                ),
2307            ));
2308        }
2309        let callable = args.into_iter().next().unwrap();
2310        let func = match &callable {
2311            Value::Fn(f) => Rc::clone(f),
2312            other => {
2313                return Err(error(
2314                    line,
2315                    format!(
2316                        "`try_call` expects a function, got {}",
2317                        other.type_name()
2318                    ),
2319                ));
2320            }
2321        };
2322        drop(callable);
2323        match self.call_bop_fn(&func, Vec::new(), line) {
2324            Ok(value) => Ok(builtins::make_try_call_ok(value)),
2325            Err(err) => {
2326                if err.is_fatal {
2327                    Err(err)
2328                } else {
2329                    Ok(builtins::make_try_call_err(&err))
2330                }
2331            }
2332        }
2333    }
2334
2335    fn call_function(
2336        &mut self,
2337        name: &str,
2338        args: Vec<Value>,
2339        line: u32,
2340    ) -> Result<Value, BopError> {
2341        // 1. Global builtins. The short list: anything variadic
2342        // (`print`), a collection constructor (`range`), session-
2343        // stateful (`rand`), or inherently takes a callable
2344        // (`try_call`). Every other global builtin that used to
2345        // live here is now a method on the receiver's type —
2346        // see `methods::common_method` and `methods::numeric_method`.
2347        match name {
2348            "range" => return builtins::builtin_range(&args, line, &mut self.rand_state),
2349            "rand" => return builtins::builtin_rand(&args, line, &mut self.rand_state),
2350            "print" => {
2351                let message = args
2352                    .iter()
2353                    .map(|a| format!("{}", a))
2354                    .collect::<Vec<_>>()
2355                    .join(" ");
2356                self.host.on_print(&message);
2357                return Ok(Value::None);
2358            }
2359            "try_call" => return self.builtin_try_call(args, line),
2360            "panic" => return builtins::builtin_panic(&args, line),
2361            _ => {}
2362        }
2363
2364        // 2. Host-provided builtins
2365        if let Some(result) = self.host.call(name, &args, line) {
2366            return result;
2367        }
2368
2369        // 3. User-defined functions — synthesise a transient
2370        // `BopFn` and delegate to the shared call path. This keeps
2371        // the behaviour of `fn name` declarations and `Value::Fn`
2372        // values identical, including self-reference semantics.
2373        let func = self.functions.get(name).cloned().ok_or_else(|| {
2374            // Preference order: "did you mean" suggestion first
2375            // (most specific to the user's typo), then the
2376            // host's generic function hint (embedder-specific
2377            // tips like "available host functions: …"), then a
2378            // bare error.
2379            if let Some(hint) = self.callable_candidates_hint(name) {
2380                error_with_hint(
2381                    line,
2382                    crate::error_messages::function_not_found(name),
2383                    hint,
2384                )
2385            } else {
2386                let host_hint = self.host.function_hint();
2387                if host_hint.is_empty() {
2388                    error(line, crate::error_messages::function_not_found(name))
2389                } else {
2390                    error_with_hint(
2391                        line,
2392                        crate::error_messages::function_not_found(name),
2393                        host_hint,
2394                    )
2395                }
2396            }
2397        })?;
2398
2399        let bop_fn = Rc::new(BopFn {
2400            params: func.params,
2401            captures: Vec::new(),
2402            body: FnBody::Ast(func.body),
2403            self_name: Some(name.to_string()),
2404        });
2405        self.call_bop_fn(&bop_fn, args, line)
2406    }
2407
2408    // ─── Methods ───────────────────────────────────────────────────
2409
2410    /// Full method dispatch: user methods win, then common,
2411    /// then type-specific (via [`Self::call_method`]). This is
2412    /// what call sites outside the `MethodCall` arm (for-loop
2413    /// iterator protocol, future trait-like uses) want — the
2414    /// normal `MethodCall` path inlines the same logic.
2415    fn call_method_full(
2416        &mut self,
2417        obj: &Value,
2418        method: &str,
2419        args: Vec<Value>,
2420        line: u32,
2421    ) -> Result<Value, BopError> {
2422        // User-method dispatch first — matches MethodCall's
2423        // priority so `fn MyType.iter(self)` wins over any
2424        // built-in method of the same name.
2425        let type_key: Option<(String, String)> = match obj {
2426            Value::Struct(s) => Some((
2427                s.module_path().to_string(),
2428                s.type_name().to_string(),
2429            )),
2430            Value::EnumVariant(e) => Some((
2431                e.module_path().to_string(),
2432                e.type_name().to_string(),
2433            )),
2434            _ => None,
2435        };
2436        if let Some(key) = type_key {
2437            let user = self
2438                .methods
2439                .get(&key)
2440                .and_then(|ms| ms.get(method))
2441                .cloned();
2442            if let Some(m) = user {
2443                if m.params.len() != args.len() + 1 {
2444                    return Err(error(
2445                        line,
2446                        format!(
2447                            "`{}.{}` expects {} argument{} (including `self`), but got {}",
2448                            key.1,
2449                            method,
2450                            m.params.len(),
2451                            if m.params.len() == 1 { "" } else { "s" },
2452                            args.len() + 1
2453                        ),
2454                    ));
2455                }
2456                let mut full_args = Vec::with_capacity(args.len() + 1);
2457                full_args.push(obj.clone());
2458                full_args.extend(args);
2459                let bop_fn = Rc::new(BopFn {
2460                    params: m.params,
2461                    captures: Vec::new(),
2462                    body: FnBody::Ast(m.body),
2463                    self_name: None,
2464                });
2465                return self.call_bop_fn(&bop_fn, full_args, line);
2466            }
2467        }
2468        // Fall through to the built-in dispatch.
2469        let (ret, _mutated) = self.call_method(obj, method, &args, line)?;
2470        Ok(ret)
2471    }
2472
2473    fn call_method(
2474        &self,
2475        obj: &Value,
2476        method: &str,
2477        args: &[Value],
2478        line: u32,
2479    ) -> Result<(Value, Option<Value>), BopError> {
2480        // `type` / `to_str` / `inspect` work on every value.
2481        // Try the shared dispatcher first so we don't have to
2482        // duplicate those three names across every type-
2483        // specific method table.
2484        if let Some(result) = methods::common_method(obj, method, args, line)? {
2485            return Ok(result);
2486        }
2487        // Built-in Result combinators (`is_ok`, `is_err`,
2488        // `unwrap`, `expect`, `unwrap_or`). Callable-taking
2489        // variants (`map`, `map_err`, `and_then`) are dispatched
2490        // one level up in the MethodCall arm so they can invoke
2491        // the user callable via `call_value`.
2492        if methods::is_builtin_result(obj) {
2493            if let Some(v) = methods::result_method(obj, method, args, line)? {
2494                return Ok((v, None));
2495            }
2496        }
2497        match obj {
2498            Value::Array(arr) => methods::array_method(arr, method, args, line),
2499            Value::Str(s) => methods::string_method(s, method, args, line),
2500            Value::Dict(entries) => methods::dict_method(entries, method, args, line),
2501            Value::Int(_) | Value::Number(_) => {
2502                methods::numeric_method(obj, method, args, line)
2503            }
2504            Value::Bool(_) => methods::bool_method(obj, method, args, line),
2505            Value::Iter(_) => methods::iter_method(obj, method, args, line),
2506            _ => Err(error(
2507                line,
2508                crate::error_messages::no_such_method(obj.type_name(), method),
2509            )),
2510        }
2511    }
2512
2513    /// Handle `r.map(f)`, `r.map_err(f)`, `r.and_then(f)` for a
2514    /// built-in `Result` receiver. Factored out of the
2515    /// `MethodCall` arm so the evaluator / VM / AOT can keep the
2516    /// same shape.
2517    fn call_result_callable_method(
2518        &mut self,
2519        receiver: &Value,
2520        kind: methods::ResultCallableKind,
2521        method: &str,
2522        args: Vec<Value>,
2523        line: u32,
2524    ) -> Result<Value, BopError> {
2525        use crate::builtins::expect_args;
2526        use methods::{make_result_err, make_result_ok, ResultCallableKind};
2527        expect_args(method, &args, 1, line)?;
2528        let callable = args.into_iter().next().expect("expect_args ensured len = 1");
2529        let variant_info = match receiver {
2530            Value::EnumVariant(e) => e,
2531            _ => return Err(error(line, "Result method called on non-Result")),
2532        };
2533        let payload = match variant_info.payload() {
2534            crate::value::EnumPayload::Tuple(items) if items.len() == 1 => items[0].clone(),
2535            // Unreachable for the built-in shape, but keep the
2536            // error clean rather than panicking.
2537            _ => {
2538                return Err(error(
2539                    line,
2540                    format!("malformed Result::{} payload", variant_info.variant()),
2541                ));
2542            }
2543        };
2544        let is_ok = variant_info.variant() == "Ok";
2545        match kind {
2546            ResultCallableKind::Map => {
2547                if is_ok {
2548                    let new_value = self.call_value(callable, vec![payload], line, Some(method))?;
2549                    Ok(make_result_ok(new_value))
2550                } else {
2551                    // Err passes through unchanged. Rebuild it so
2552                    // the caller sees the same type identity —
2553                    // matches the pure-Bop combinator's behaviour.
2554                    Ok(make_result_err(payload))
2555                }
2556            }
2557            ResultCallableKind::MapErr => {
2558                if !is_ok {
2559                    let new_value = self.call_value(callable, vec![payload], line, Some(method))?;
2560                    Ok(make_result_err(new_value))
2561                } else {
2562                    Ok(make_result_ok(payload))
2563                }
2564            }
2565            ResultCallableKind::AndThen => {
2566                if is_ok {
2567                    // `f` is expected to return a Result; we
2568                    // surface whatever it returns verbatim. A
2569                    // match on the returned shape at the call
2570                    // site catches misuse.
2571                    self.call_value(callable, vec![payload], line, Some(method))
2572                } else {
2573                    Ok(make_result_err(payload))
2574                }
2575            }
2576        }
2577    }
2578}
2579
2580// ─── Pattern matching ──────────────────────────────────────────────
2581//
2582// `pattern_matches` returns true if `value` fits `pattern`, filling
2583// `bindings` with every `Binding` name along the way. On a partial
2584// match it's the caller's responsibility to discard the bindings
2585// — `eval_match` does this by only consuming them after the match
2586// + guard both succeed.
2587
2588/// Destructured view of an `Iter::Next(v) | Iter::Done` result.
2589/// Returned by [`unwrap_iter_result`] so the for-loop can bind
2590/// the payload cleanly without repeating the pattern-match.
2591enum IterStep {
2592    Next(Value),
2593    Done,
2594}
2595
2596/// Inspect a value returned from an iterator's `.next()` method.
2597/// Returns `Some(step)` when the value is a well-formed
2598/// `Iter::Next(v)` / `Iter::Done` (built-in or not), or `None`
2599/// when it doesn't match so the caller can raise a clear error.
2600fn unwrap_iter_result(v: &Value) -> Option<IterStep> {
2601    let e = match v {
2602        Value::EnumVariant(e) => e,
2603        _ => return None,
2604    };
2605    if e.type_name() != "Iter" {
2606        return None;
2607    }
2608    match (e.variant(), e.payload()) {
2609        ("Next", crate::value::EnumPayload::Tuple(items)) if items.len() == 1 => {
2610            Some(IterStep::Next(items[0].clone()))
2611        }
2612        ("Done", crate::value::EnumPayload::Unit) => Some(IterStep::Done),
2613        _ => None,
2614    }
2615}
2616
2617/// Seed a fresh evaluator's type tables with the engine-wide
2618/// builtin types (`Result`, `RuntimeError`). The shapes come from
2619/// `crate::builtins` so walker / VM / AOT can never drift out of
2620/// sync — they all read the same source.
2621///
2622/// Returns:
2623/// - struct_defs keyed by `(module_path, type_name)` with the
2624///   builtin registered under `<builtin>`;
2625/// - enum_defs keyed the same way;
2626/// - a bare-name → module_path map seeded with entries for every
2627///   builtin, so the program's outermost scope can resolve
2628///   bare `Result::Ok(...)` and `RuntimeError { ... }` without
2629///   an explicit `use`.
2630fn seed_builtin_types() -> (
2631    BTreeMap<(String, String), Vec<String>>,
2632    BTreeMap<(String, String), Vec<VariantDecl>>,
2633    BTreeMap<String, String>,
2634) {
2635    use crate::value::BUILTIN_MODULE_PATH;
2636    let mut struct_defs: BTreeMap<(String, String), Vec<String>> = BTreeMap::new();
2637    let mut enum_defs: BTreeMap<(String, String), Vec<VariantDecl>> = BTreeMap::new();
2638    let mut type_bindings: BTreeMap<String, String> = BTreeMap::new();
2639    struct_defs.insert(
2640        (String::from(BUILTIN_MODULE_PATH), String::from("RuntimeError")),
2641        crate::builtins::builtin_runtime_error_fields(),
2642    );
2643    type_bindings.insert(
2644        String::from("RuntimeError"),
2645        String::from(BUILTIN_MODULE_PATH),
2646    );
2647    enum_defs.insert(
2648        (String::from(BUILTIN_MODULE_PATH), String::from("Result")),
2649        crate::builtins::builtin_result_variants(),
2650    );
2651    type_bindings.insert(
2652        String::from("Result"),
2653        String::from(BUILTIN_MODULE_PATH),
2654    );
2655    enum_defs.insert(
2656        (String::from(BUILTIN_MODULE_PATH), String::from("Iter")),
2657        crate::builtins::builtin_iter_variants(),
2658    );
2659    type_bindings.insert(
2660        String::from("Iter"),
2661        String::from(BUILTIN_MODULE_PATH),
2662    );
2663    (struct_defs, enum_defs, type_bindings)
2664}
2665
2666/// True when two enum declarations describe the same runtime
2667/// shape. Strictly looser than `PartialEq` on `Vec<VariantDecl>`:
2668/// tuple variants compare by *arity* only (their payload field
2669/// names are positional stubs with no runtime meaning), while
2670/// struct variants still require matching field names. Same for
2671/// the outer variant ordering — positional.
2672///
2673/// Used by the "redeclare-same-shape" rules so a user program
2674/// that declares `enum Result { Ok(v), Err(e) }` is compatible
2675/// with the engine's builtin `enum Result { Ok(value), Err(error) }`.
2676fn variants_equivalent(a: &[VariantDecl], b: &[VariantDecl]) -> bool {
2677    if a.len() != b.len() {
2678        return false;
2679    }
2680    for (va, vb) in a.iter().zip(b.iter()) {
2681        if va.name != vb.name {
2682            return false;
2683        }
2684        match (&va.kind, &vb.kind) {
2685            (VariantKind::Unit, VariantKind::Unit) => {}
2686            (VariantKind::Tuple(fa), VariantKind::Tuple(fb)) => {
2687                if fa.len() != fb.len() {
2688                    return false;
2689                }
2690            }
2691            (VariantKind::Struct(fa), VariantKind::Struct(fb)) => {
2692                if fa != fb {
2693                    return false;
2694                }
2695            }
2696            _ => return false,
2697        }
2698    }
2699    true
2700}
2701
2702/// Walk a pair of scope stacks to resolve a source-level type
2703/// reference — the same logic as
2704/// [`Evaluator::resolve_type_ref`], but free-standing so the
2705/// pattern matcher can be called with a borrow of the
2706/// evaluator's tables without needing the evaluator itself.
2707/// Returns `None` if the name isn't in scope.
2708///
2709/// `module_aliases` is the persistent map of aliased `use`
2710/// modules so namespaced references (`m.Color`) resolve even
2711/// inside function bodies, where `scopes` no longer contains
2712/// `m` (the function call stack is reset per-call).
2713pub fn resolve_type_in(
2714    value_scopes: &[BTreeMap<String, Value>],
2715    type_scopes: &[BTreeMap<String, String>],
2716    module_aliases: &BTreeMap<String, Rc<crate::value::BopModule>>,
2717    namespace: Option<&str>,
2718    type_name: &str,
2719) -> Option<String> {
2720    if let Some(ns) = namespace {
2721        // First: look the alias up in value scopes (catches
2722        // locally-bound modules, if any). Then fall back to
2723        // the evaluator-level alias map so function bodies can
2724        // reach module-level aliases even when their own value
2725        // scope is empty.
2726        for scope in value_scopes.iter().rev() {
2727            if let Some(Value::Module(m)) = scope.get(ns) {
2728                if m.types.iter().any(|t| t == type_name) {
2729                    return Some(m.path.clone());
2730                }
2731                return None;
2732            }
2733        }
2734        if let Some(m) = module_aliases.get(ns) {
2735            if m.types.iter().any(|t| t == type_name) {
2736                return Some(m.path.clone());
2737            }
2738        }
2739        return None;
2740    }
2741    for scope in type_scopes.iter().rev() {
2742        if let Some(mp) = scope.get(type_name) {
2743            return Some(mp.clone());
2744        }
2745    }
2746    None
2747}
2748
2749/// Resolver that turns a source-level type reference — optional
2750/// namespace plus bare name (`m.Color` vs plain `Color`) — into
2751/// the declaring module's path. Pattern matching threads this
2752/// through so a pattern like `Color::Red` only matches values
2753/// whose module identity agrees with the current scope's binding
2754/// of `Color`.
2755///
2756/// Returning `None` means "no type with this name is visible in
2757/// the calling scope" — the matcher treats that as a mismatch
2758/// rather than a fallback, to avoid silently matching a
2759/// same-named type from a different module.
2760pub type TypeResolveFn<'a> = &'a dyn Fn(Option<&str>, &str) -> Option<String>;
2761
2762/// Attempt to match `pattern` against `value`. On success, appends
2763/// any captured `(name, Value)` bindings to `bindings` and returns
2764/// `true`; on failure, returns `false` and leaves `bindings` in an
2765/// undefined state — it's the caller's responsibility to discard it.
2766///
2767/// `resolver` is consulted whenever a pattern names a user type
2768/// (`Pattern::Struct` or `Pattern::EnumVariant`) so the matcher
2769/// can compare the value's full identity `(module_path,
2770/// type_name)` against the source-level reference.
2771///
2772/// Exported so other engines (the bytecode VM, AOT transpiler)
2773/// can run the exact same structural matcher as the tree-walker
2774/// without re-implementing the rules.
2775pub fn pattern_matches(
2776    pattern: &Pattern,
2777    value: &Value,
2778    bindings: &mut Vec<(String, Value)>,
2779    resolver: TypeResolveFn<'_>,
2780) -> bool {
2781    match pattern {
2782        Pattern::Wildcard => true,
2783        Pattern::Binding(name) => {
2784            bindings.push((name.clone(), value.clone()));
2785            true
2786        }
2787        Pattern::Literal(lit) => match (lit, value) {
2788            (LiteralPattern::Int(a), Value::Int(b)) => a == b,
2789            (LiteralPattern::Number(a), Value::Number(b)) => a == b,
2790            // Cross-type numeric literal patterns — same rule
2791            // as `values_equal`: `match 1 { 1.0 => ... }` and
2792            // `match 1.0 { 1 => ... }` both match.
2793            (LiteralPattern::Int(a), Value::Number(b)) => (*a as f64) == *b,
2794            (LiteralPattern::Number(a), Value::Int(b)) => *a == (*b as f64),
2795            (LiteralPattern::Str(a), Value::Str(b)) => a.as_str() == b.as_str(),
2796            (LiteralPattern::Bool(a), Value::Bool(b)) => a == b,
2797            (LiteralPattern::None, Value::None) => true,
2798            _ => false,
2799        },
2800        Pattern::EnumVariant {
2801            namespace,
2802            type_name,
2803            variant,
2804            payload,
2805        } => {
2806            let ev = match value {
2807                Value::EnumVariant(e) => e,
2808                _ => return false,
2809            };
2810            // Full identity check: resolve the pattern's type
2811            // reference to a module path, then compare against
2812            // the value's own module path.
2813            let expected_mp = match resolver(namespace.as_deref(), type_name) {
2814                Some(mp) => mp,
2815                None => return false,
2816            };
2817            if ev.module_path() != expected_mp.as_str()
2818                || ev.type_name() != type_name.as_str()
2819                || ev.variant() != variant.as_str()
2820            {
2821                return false;
2822            }
2823            match (payload, ev.payload()) {
2824                (VariantPatternPayload::Unit, crate::value::EnumPayload::Unit) => true,
2825                (
2826                    VariantPatternPayload::Tuple(pats),
2827                    crate::value::EnumPayload::Tuple(items),
2828                ) => {
2829                    if pats.len() != items.len() {
2830                        return false;
2831                    }
2832                    for (p, v) in pats.iter().zip(items.iter()) {
2833                        if !pattern_matches(p, v, bindings, resolver) {
2834                            return false;
2835                        }
2836                    }
2837                    true
2838                }
2839                (
2840                    VariantPatternPayload::Struct { fields, rest },
2841                    crate::value::EnumPayload::Struct(entries),
2842                ) => {
2843                    match_struct_fields(fields, *rest, entries, bindings, resolver)
2844                }
2845                _ => false,
2846            }
2847        }
2848        Pattern::Struct {
2849            namespace,
2850            type_name,
2851            fields,
2852            rest,
2853        } => {
2854            let st = match value {
2855                Value::Struct(s) => s,
2856                _ => return false,
2857            };
2858            let expected_mp = match resolver(namespace.as_deref(), type_name) {
2859                Some(mp) => mp,
2860                None => return false,
2861            };
2862            if st.module_path() != expected_mp.as_str()
2863                || st.type_name() != type_name.as_str()
2864            {
2865                return false;
2866            }
2867            match_struct_fields(fields, *rest, st.fields(), bindings, resolver)
2868        }
2869        Pattern::Array { elements, rest } => {
2870            let items = match value {
2871                Value::Array(arr) => arr,
2872                _ => return false,
2873            };
2874            match rest {
2875                None => {
2876                    if elements.len() != items.len() {
2877                        return false;
2878                    }
2879                    for (p, v) in elements.iter().zip(items.iter()) {
2880                        if !pattern_matches(p, v, bindings, resolver) {
2881                            return false;
2882                        }
2883                    }
2884                    true
2885                }
2886                Some(rest_kind) => {
2887                    if items.len() < elements.len() {
2888                        return false;
2889                    }
2890                    for (i, p) in elements.iter().enumerate() {
2891                        if !pattern_matches(p, &items[i], bindings, resolver) {
2892                            return false;
2893                        }
2894                    }
2895                    if let ArrayRest::Named(name) = rest_kind {
2896                        let tail: Vec<Value> =
2897                            items[elements.len()..].iter().cloned().collect();
2898                        bindings.push((name.clone(), Value::new_array(tail)));
2899                    }
2900                    true
2901                }
2902            }
2903        }
2904        Pattern::Or(alts) => {
2905            for alt in alts {
2906                let mut attempt: Vec<(String, Value)> = Vec::new();
2907                if pattern_matches(alt, value, &mut attempt, resolver) {
2908                    bindings.extend(attempt);
2909                    return true;
2910                }
2911            }
2912            false
2913        }
2914    }
2915}
2916
2917fn match_struct_fields(
2918    fields: &[(String, Pattern)],
2919    rest: bool,
2920    entries: &[(String, Value)],
2921    bindings: &mut Vec<(String, Value)>,
2922    resolver: TypeResolveFn<'_>,
2923) -> bool {
2924    // Every declared field-pattern must find its value in the
2925    // struct. `rest` just relaxes the requirement that *every*
2926    // value's key must appear in the pattern — non-rest patterns
2927    // may still leave fields unmatched, which is fine: the walker
2928    // matches named-field lookup semantics, not whole-shape
2929    // destructuring.
2930    let _ = rest;
2931    for (fname, pat) in fields {
2932        let value = match entries.iter().find(|(k, _)| k == fname) {
2933            Some((_, v)) => v,
2934            None => return false,
2935        };
2936        if !pattern_matches(pat, value, bindings, resolver) {
2937            return false;
2938        }
2939    }
2940    true
2941}
2942
2943// ─── REPL session ──────────────────────────────────────────────────
2944//
2945// `Evaluator` itself is a one-shot: `Evaluator::new(host, limits)` +
2946// `eval.run(stmts)` + drop. That's the right shape for running a
2947// complete program from top to bottom. REPL workloads need the
2948// opposite: fresh *input* evaluated against *accumulated* state.
2949//
2950// `ReplSession` owns every Evaluator field that should survive
2951// across inputs — scopes, functions, user types, method tables,
2952// import caches, module aliases, rng state. The ephemeral
2953// per-run fields (host, step counter, call depth, the try-
2954// return sentinel slot, transient warnings) stay on the
2955// temporary `Evaluator` that the session spins up for each
2956// `eval` call.
2957//
2958// The session model also handles REPL-specific niceties:
2959//
2960//   - Bare expression statements at the end of an input yield
2961//     their value back to the caller, so the REPL can echo it.
2962//     `let x = 5` returns None; `x + 1` returns Some(Value).
2963//   - Scope depth is normalised back to the root on each
2964//     `eval` — if an earlier input errored mid-block, we
2965//     don't leave half-pushed scopes lying around for the next
2966//     input to trip over.
2967
2968/// Persistent state for an interactive REPL session. Build
2969/// once with [`Self::new`], then call [`Self::eval`] for each
2970/// fresh line / block the user types. State carried across
2971/// calls: every `let` / `const` / `fn` / `struct` / `enum` /
2972/// method declaration, every `use`'d module's bindings and
2973/// aliases, the global rng seed, and the import cache.
2974///
2975/// A `ReplSession` is not a sandboxing boundary — embedders
2976/// that want a fresh identity between inputs should construct
2977/// a new session. Resource limits (steps, memory) are supplied
2978/// per `eval` call since they reset each time.
2979pub struct ReplSession {
2980    scopes: Vec<BTreeMap<String, Value>>,
2981    functions: BTreeMap<String, FnDef>,
2982    current_module: String,
2983    struct_defs: BTreeMap<(String, String), Vec<String>>,
2984    enum_defs: BTreeMap<(String, String), Vec<VariantDecl>>,
2985    methods: BTreeMap<(String, String), BTreeMap<String, FnDef>>,
2986    type_bindings: Vec<BTreeMap<String, String>>,
2987    module_aliases: BTreeMap<String, Rc<crate::value::BopModule>>,
2988    imports: ImportCache,
2989    imported_here: alloc_import::collections::BTreeSet<String>,
2990    rand_state: u64,
2991}
2992
2993impl Default for ReplSession {
2994    fn default() -> Self {
2995        Self::new()
2996    }
2997}
2998
2999impl ReplSession {
3000    /// Build a fresh session. Type tables are pre-seeded with
3001    /// the engine builtins (`Result`, `RuntimeError`) so
3002    /// `Result::Ok(...)` resolves without an explicit `use`.
3003    pub fn new() -> Self {
3004        let (struct_defs, enum_defs, builtin_bindings) = seed_builtin_types();
3005        Self {
3006            scopes: vec![BTreeMap::new()],
3007            functions: BTreeMap::new(),
3008            current_module: String::from(crate::value::ROOT_MODULE_PATH),
3009            struct_defs,
3010            enum_defs,
3011            methods: BTreeMap::new(),
3012            type_bindings: vec![builtin_bindings],
3013            module_aliases: BTreeMap::new(),
3014            imports: Rc::new(RefCell::new(
3015                alloc_import::collections::BTreeMap::new(),
3016            )),
3017            imported_here: alloc_import::collections::BTreeSet::new(),
3018            rand_state: 0,
3019        }
3020    }
3021
3022    /// Look up a binding by name. Convenience for tests and
3023    /// embedders that want to peek at the session's state
3024    /// between `eval` calls — e.g. checking that `let x = 5`
3025    /// actually stuck.
3026    pub fn get(&self, name: &str) -> Option<Value> {
3027        for scope in self.scopes.iter().rev() {
3028            if let Some(v) = scope.get(name) {
3029                return Some(v.clone());
3030            }
3031        }
3032        None
3033    }
3034
3035    /// Every currently-bound name in the root scope, sorted.
3036    /// Handy for REPL introspection commands (`:vars`) and
3037    /// for tab-completers that want to stay honest about
3038    /// what's actually in scope rather than guessing from
3039    /// observed tokens.
3040    pub fn binding_names(&self) -> Vec<String> {
3041        let mut names: Vec<String> = self
3042            .scopes
3043            .first()
3044            .map(|s| s.keys().cloned().collect())
3045            .unwrap_or_default();
3046        for name in self.functions.keys() {
3047            names.push(name.clone());
3048        }
3049        names.sort();
3050        names.dedup();
3051        names
3052    }
3053
3054    /// Parse `source` and run its statements against this
3055    /// session's accumulated state.
3056    ///
3057    /// If the last statement is a bare expression
3058    /// (`ExprStmt`), the session evaluates it and returns its
3059    /// value as `Ok(Some(v))` so the REPL can echo the
3060    /// result. Every other shape — `let x = ...`, `fn foo`,
3061    /// `struct`, `use`, loops — returns `Ok(None)`.
3062    ///
3063    /// Partial failure semantics: if an earlier statement
3064    /// errors, the session's state reflects whatever ran
3065    /// before the failure. That matches what a user would
3066    /// expect from an interactive prompt.
3067    pub fn eval<H: BopHost>(
3068        &mut self,
3069        source: &str,
3070        host: &mut H,
3071        limits: &BopLimits,
3072    ) -> Result<Option<Value>, BopError> {
3073        let stmts = crate::parse(source)?;
3074        self.run_stmts(&stmts, host, limits)
3075    }
3076
3077    /// Like [`Self::eval`] but takes an already-parsed AST.
3078    /// Useful when the caller has already run a
3079    /// `parse_with_warnings` pass and wants to surface
3080    /// warnings before executing.
3081    pub fn run_stmts<H: BopHost>(
3082        &mut self,
3083        stmts: &[Stmt],
3084        host: &mut H,
3085        limits: &BopLimits,
3086    ) -> Result<Option<Value>, BopError> {
3087        // If the last stmt is a bare expression we strip it
3088        // off, run the rest, and then evaluate it as an
3089        // expression so we can return its value. Anything
3090        // else runs as a normal statement block.
3091        let (tail_expr, body) = match stmts.last().map(|s| &s.kind) {
3092            Some(StmtKind::ExprStmt(_)) => {
3093                let (last, rest) = stmts.split_last().unwrap();
3094                let expr = match &last.kind {
3095                    StmtKind::ExprStmt(e) => e.clone(),
3096                    _ => unreachable!(),
3097                };
3098                (Some(expr), rest)
3099            }
3100            _ => (None, stmts),
3101        };
3102
3103        // Spin up a temporary Evaluator around the session's
3104        // state. We hand off each field by `mem::take` /
3105        // `clone` of the Rc so the Evaluator owns working
3106        // copies; at the end we unconditionally write the
3107        // (possibly-mutated) state back so partial failures
3108        // still persist whatever changes happened before the
3109        // error.
3110        let mut eval = self.take_evaluator(host, limits.clone());
3111        let result: Result<Option<Value>, BopError> = (|| {
3112            let sig = eval.exec_block(body)?;
3113            match sig {
3114                Signal::Break => return Err(error(0, "break used outside of a loop")),
3115                Signal::Continue => {
3116                    return Err(error(0, "continue used outside of a loop"));
3117                }
3118                _ => {}
3119            }
3120            match tail_expr {
3121                Some(expr) => Ok(Some(eval.eval_expr(&expr)?)),
3122                None => Ok(None),
3123            }
3124        })();
3125        // Surface any warnings accumulated on this run
3126        // (currently: glob-import shadowing). Stderr delivery
3127        // matches `Evaluator::run`; embedders that want
3128        // structured access can call `eval` via a custom host
3129        // and take warnings out through that path.
3130        #[cfg(not(feature = "no_std"))]
3131        {
3132            for w in &eval.runtime_warnings {
3133                eprintln!("warning: {}", w.message);
3134            }
3135        }
3136        self.put_evaluator(eval);
3137        result
3138    }
3139
3140    /// Internal: move the session's state into a fresh
3141    /// Evaluator. `host` and `limits` are the ephemeral
3142    /// per-run bits.
3143    fn take_evaluator<'h, H: BopHost>(
3144        &mut self,
3145        host: &'h mut H,
3146        limits: BopLimits,
3147    ) -> Evaluator<'h, H> {
3148        Evaluator {
3149            scopes: core::mem::take(&mut self.scopes),
3150            functions: core::mem::take(&mut self.functions),
3151            current_module: core::mem::take(&mut self.current_module),
3152            struct_defs: core::mem::take(&mut self.struct_defs),
3153            enum_defs: core::mem::take(&mut self.enum_defs),
3154            methods: core::mem::take(&mut self.methods),
3155            type_bindings: core::mem::take(&mut self.type_bindings),
3156            module_aliases: core::mem::take(&mut self.module_aliases),
3157            imports: Rc::clone(&self.imports),
3158            imported_here: core::mem::take(&mut self.imported_here),
3159            rand_state: self.rand_state,
3160            host,
3161            steps: 0,
3162            call_depth: 0,
3163            limits,
3164            pending_try_return: None,
3165            runtime_warnings: Vec::new(),
3166        }
3167    }
3168
3169    /// Internal: move the Evaluator's state back into the
3170    /// session. Also normalises scope depth back to the root
3171    /// so a mid-block error doesn't leave leftover pushed
3172    /// scopes hanging around for the next input.
3173    fn put_evaluator<'h, H: BopHost>(&mut self, eval: Evaluator<'h, H>) {
3174        let mut scopes = eval.scopes;
3175        if scopes.len() > 1 {
3176            scopes.truncate(1);
3177        }
3178        if scopes.is_empty() {
3179            scopes.push(BTreeMap::new());
3180        }
3181        let mut type_bindings = eval.type_bindings;
3182        if type_bindings.len() > 1 {
3183            type_bindings.truncate(1);
3184        }
3185        if type_bindings.is_empty() {
3186            // Re-seed so builtins stay visible.
3187            let (_, _, builtins) = seed_builtin_types();
3188            type_bindings.push(builtins);
3189        }
3190        self.scopes = scopes;
3191        self.functions = eval.functions;
3192        self.current_module = eval.current_module;
3193        self.struct_defs = eval.struct_defs;
3194        self.enum_defs = eval.enum_defs;
3195        self.methods = eval.methods;
3196        self.type_bindings = type_bindings;
3197        self.module_aliases = eval.module_aliases;
3198        self.imported_here = eval.imported_here;
3199        self.rand_state = eval.rand_state;
3200        // `imports` is shared via `Rc` — nothing to do; the
3201        // cache the Evaluator wrote into is the same one the
3202        // session holds.
3203    }
3204}