Skip to main content

aver/interpreter/
core.rs

1use super::*;
2
3impl Default for Interpreter {
4    fn default() -> Self {
5        Self::new()
6    }
7}
8
9impl Interpreter {
10    pub fn new() -> Self {
11        let mut arena = Arena::new();
12        let mut global: HashMap<String, NanValue> = HashMap::new();
13
14        // Register all namespaces directly as NanValue (no Value→NanValue conversion)
15        args::register_nv(&mut global, &mut arena);
16        console::register_nv(&mut global, &mut arena);
17        http::register_nv(&mut global, &mut arena);
18        http_server::register_nv(&mut global, &mut arena);
19        disk::register_nv(&mut global, &mut arena);
20        env::register_nv(&mut global, &mut arena);
21        random::register_nv(&mut global, &mut arena);
22        tcp::register_nv(&mut global, &mut arena);
23        #[cfg(feature = "terminal")]
24        terminal::register_nv(&mut global, &mut arena);
25        time::register_nv(&mut global, &mut arena);
26        bool::register_nv(&mut global, &mut arena);
27        int::register_nv(&mut global, &mut arena);
28        float::register_nv(&mut global, &mut arena);
29        string::register_nv(&mut global, &mut arena);
30        list::register_nv(&mut global, &mut arena);
31        map::register_nv(&mut global, &mut arena);
32        vector::register_nv(&mut global, &mut arena);
33        char::register_nv(&mut global, &mut arena);
34        byte::register_nv(&mut global, &mut arena);
35
36        // Result namespace
37        {
38            use std::sync::Arc as Rc;
39            let mut members: Vec<(Rc<str>, NanValue)> = Vec::new();
40            let ok_idx = arena.push_builtin("__ctor:Result.Ok");
41            members.push((Rc::from("Ok"), NanValue::new_builtin(ok_idx)));
42            let err_idx = arena.push_builtin("__ctor:Result.Err");
43            members.push((Rc::from("Err"), NanValue::new_builtin(err_idx)));
44            for (name, builtin_name) in result::extra_members() {
45                let idx = arena.push_builtin(&builtin_name);
46                members.push((Rc::from(name), NanValue::new_builtin(idx)));
47            }
48            let ns_idx = arena.push(crate::nan_value::ArenaEntry::Namespace {
49                name: Rc::from("Result"),
50                members,
51            });
52            global.insert("Result".to_string(), NanValue::new_namespace(ns_idx));
53        }
54        // Option namespace
55        {
56            use std::sync::Arc as Rc;
57            let mut members: Vec<(Rc<str>, NanValue)> = Vec::new();
58            let some_idx = arena.push_builtin("__ctor:Option.Some");
59            members.push((Rc::from("Some"), NanValue::new_builtin(some_idx)));
60            members.push((Rc::from("None"), NanValue::NONE));
61            for (name, builtin_name) in option::extra_members() {
62                let idx = arena.push_builtin(&builtin_name);
63                members.push((Rc::from(name), NanValue::new_builtin(idx)));
64            }
65            let ns_idx = arena.push(crate::nan_value::ArenaEntry::Namespace {
66                name: Rc::from("Option"),
67                members,
68            });
69            global.insert("Option".to_string(), NanValue::new_namespace(ns_idx));
70        }
71
72        let mut record_schemas = HashMap::new();
73        record_schemas.insert(
74            "HttpResponse".to_string(),
75            vec![
76                "status".to_string(),
77                "body".to_string(),
78                "headers".to_string(),
79            ],
80        );
81        record_schemas.insert(
82            "HttpRequest".to_string(),
83            vec![
84                "method".to_string(),
85                "path".to_string(),
86                "body".to_string(),
87                "headers".to_string(),
88            ],
89        );
90        record_schemas.insert(
91            "Header".to_string(),
92            vec!["name".to_string(), "value".to_string()],
93        );
94        record_schemas.insert(
95            "Tcp.Connection".to_string(),
96            vec!["id".to_string(), "host".to_string(), "port".to_string()],
97        );
98        #[cfg(feature = "terminal")]
99        record_schemas.insert(
100            "Terminal.Size".to_string(),
101            vec!["width".to_string(), "height".to_string()],
102        );
103
104        Interpreter {
105            env: vec![EnvFrame::Owned(global)],
106            env_base: 1,
107            arena,
108            module_cache: HashMap::new(),
109            mounted_module_paths: HashSet::new(),
110            record_schemas,
111            call_stack: Vec::new(),
112            active_local_slots: None,
113            memo_fns: HashSet::new(),
114            memo_cache: HashMap::new(),
115            replay_state: EffectReplayState::default(),
116            recording_sink: None,
117            verify_match_coverage: None,
118            runtime_policy: None,
119            cli_args: Vec::new(),
120            last_call_line: 0,
121        }
122    }
123
124    pub fn execution_mode(&self) -> ExecutionMode {
125        match self.replay_state.mode() {
126            EffectReplayMode::Normal => ExecutionMode::Normal,
127            EffectReplayMode::Record => ExecutionMode::Record,
128            EffectReplayMode::Replay => ExecutionMode::Replay,
129        }
130    }
131
132    pub fn set_execution_mode_normal(&mut self) {
133        self.replay_state.set_normal();
134    }
135
136    pub fn start_recording(&mut self) {
137        self.replay_state.start_recording();
138    }
139
140    pub fn start_replay(&mut self, effects: Vec<EffectRecord>, validate_args: bool) {
141        self.replay_state.start_replay(effects, validate_args);
142    }
143
144    pub fn take_recorded_effects(&mut self) -> Vec<EffectRecord> {
145        self.replay_state.take_recorded_effects()
146    }
147
148    pub fn replay_progress(&self) -> (usize, usize) {
149        self.replay_state.replay_progress()
150    }
151
152    pub fn args_diff_count(&self) -> usize {
153        self.replay_state.args_diff_count()
154    }
155
156    pub fn ensure_replay_consumed(&self) -> Result<(), RuntimeError> {
157        self.replay_state
158            .ensure_replay_consumed()
159            .map_err(|err| match err {
160                crate::replay::ReplayFailure::Unconsumed { remaining } => {
161                    RuntimeError::ReplayUnconsumed { remaining }
162                }
163                other => RuntimeError::Error(format!("invalid replay state: {:?}", other)),
164            })
165    }
166
167    pub fn configure_recording_sink(&mut self, cfg: RecordingConfig) {
168        self.recording_sink = Some(RecordingSink {
169            path: cfg.path,
170            request_id: cfg.request_id,
171            timestamp: cfg.timestamp,
172            program_file: cfg.program_file,
173            module_root: cfg.module_root,
174            entry_fn: cfg.entry_fn,
175            input: cfg.input,
176        });
177    }
178
179    pub fn recording_sink_path(&self) -> Option<std::path::PathBuf> {
180        self.recording_sink.as_ref().map(|s| s.path.clone())
181    }
182
183    pub fn persist_recording_snapshot(&self, output: RecordedOutcome) -> Result<(), RuntimeError> {
184        let Some(sink) = &self.recording_sink else {
185            return Ok(());
186        };
187
188        let recording = SessionRecording {
189            schema_version: 1,
190            request_id: sink.request_id.clone(),
191            timestamp: sink.timestamp.clone(),
192            program_file: sink.program_file.clone(),
193            module_root: sink.module_root.clone(),
194            entry_fn: sink.entry_fn.clone(),
195            input: sink.input.clone(),
196            effects: self.replay_state.recorded_effects().to_vec(),
197            output,
198        };
199
200        let json = session_recording_to_string_pretty(&recording);
201        std::fs::write(&sink.path, json).map_err(|e| {
202            RuntimeError::Error(format!(
203                "Cannot write recording '{}': {}",
204                sink.path.display(),
205                e
206            ))
207        })?;
208        Ok(())
209    }
210
211    /// Mark a set of function names as eligible for auto-memoization.
212    pub fn enable_memo(&mut self, fns: HashSet<String>) {
213        self.memo_fns = fns;
214    }
215
216    /// Register a named effect set alias.
217    /// Set the runtime policy from an `aver.toml` configuration.
218    pub fn set_runtime_policy(&mut self, config: crate::config::ProjectConfig) {
219        self.runtime_policy = Some(config);
220    }
221
222    /// Set command-line arguments available via `Args.get()`.
223    pub fn set_cli_args(&mut self, args: Vec<String>) {
224        self.cli_args = args;
225    }
226
227    /// Check whether a builtin call is permitted by the runtime policy.
228    /// Skipped in Replay mode (deterministic playback).
229    pub(super) fn check_runtime_policy(
230        &self,
231        name: &str,
232        args: &[Value],
233    ) -> Result<(), RuntimeError> {
234        if self.execution_mode() == ExecutionMode::Replay {
235            return Ok(());
236        }
237        let Some(policy) = &self.runtime_policy else {
238            return Ok(());
239        };
240
241        if name.starts_with("Http.") {
242            if let Some(Value::Str(url)) = args.first() {
243                policy
244                    .check_http_host(name, url)
245                    .map_err(RuntimeError::Error)?;
246            }
247        } else if name.starts_with("Disk.") {
248            if let Some(Value::Str(path)) = args.first() {
249                policy
250                    .check_disk_path(name, path)
251                    .map_err(RuntimeError::Error)?;
252            }
253        } else if name.starts_with("Env.")
254            && let Some(Value::Str(key)) = args.first()
255        {
256            policy
257                .check_env_key(name, key)
258                .map_err(RuntimeError::Error)?;
259        }
260
261        Ok(())
262    }
263
264    pub fn start_verify_match_coverage(&mut self, fn_name: &str) {
265        let Ok(fn_val) = self.lookup(fn_name) else {
266            self.verify_match_coverage = None;
267            return;
268        };
269        let Value::Fn(function) = fn_val else {
270            self.verify_match_coverage = None;
271            return;
272        };
273
274        let mut expected = std::collections::BTreeMap::new();
275        Self::collect_match_sites_from_fn_body(function.body.as_ref(), &mut expected);
276        if expected.is_empty() {
277            self.verify_match_coverage = None;
278            return;
279        }
280
281        self.verify_match_coverage = Some(VerifyMatchCoverageTracker {
282            target_fn: fn_name.to_string(),
283            expected_arms: expected,
284            visited_arms: HashMap::new(),
285        });
286    }
287
288    pub fn finish_verify_match_coverage(&mut self) -> Vec<VerifyMatchCoverageMiss> {
289        let Some(tracker) = self.verify_match_coverage.take() else {
290            return vec![];
291        };
292
293        let mut misses = Vec::new();
294        for ((line, arm_count), expected_total) in tracker.expected_arms {
295            let visited = tracker.visited_arms.get(&(line, arm_count));
296            let mut missing = Vec::new();
297            for arm_idx in 0..expected_total {
298                let covered = visited.is_some_and(|set| set.contains(&arm_idx));
299                if !covered {
300                    missing.push(arm_idx);
301                }
302            }
303            if !missing.is_empty() {
304                misses.push(VerifyMatchCoverageMiss {
305                    line,
306                    total_arms: expected_total,
307                    missing_arms: missing,
308                });
309            }
310        }
311        misses
312    }
313
314    pub(super) fn note_verify_match_arm(&mut self, line: usize, arm_count: usize, arm_idx: usize) {
315        let Some(tracker) = self.verify_match_coverage.as_mut() else {
316            return;
317        };
318        let Some(frame) = self.call_stack.last() else {
319            return;
320        };
321        if frame.name.as_str() != tracker.target_fn {
322            return;
323        }
324        let key = (line, arm_count);
325        if !tracker.expected_arms.contains_key(&key) {
326            return;
327        }
328        tracker.visited_arms.entry(key).or_default().insert(arm_idx);
329    }
330
331    fn collect_match_sites_from_fn_body(
332        body: &FnBody,
333        out: &mut std::collections::BTreeMap<MatchSiteKey, usize>,
334    ) {
335        for stmt in body.stmts() {
336            Self::collect_match_sites_from_stmt(stmt, out);
337        }
338    }
339
340    fn collect_match_sites_from_stmt(
341        stmt: &Stmt,
342        out: &mut std::collections::BTreeMap<MatchSiteKey, usize>,
343    ) {
344        match stmt {
345            Stmt::Binding(_, _, spanned_expr) | Stmt::Expr(spanned_expr) => {
346                Self::collect_match_sites_from_spanned(spanned_expr, out);
347            }
348        }
349    }
350
351    fn collect_match_sites_from_spanned(
352        spanned: &Spanned<Expr>,
353        out: &mut std::collections::BTreeMap<MatchSiteKey, usize>,
354    ) {
355        let line = spanned.line;
356        match &spanned.node {
357            Expr::Match { subject, arms } => {
358                out.insert((line, arms.len()), arms.len());
359                Self::collect_match_sites_from_spanned(subject, out);
360                for arm in arms {
361                    Self::collect_match_sites_from_spanned(&arm.body, out);
362                }
363            }
364            Expr::FnCall(fn_expr, args) => {
365                Self::collect_match_sites_from_spanned(fn_expr, out);
366                for arg in args {
367                    Self::collect_match_sites_from_spanned(arg, out);
368                }
369            }
370            Expr::BinOp(_, left, right) => {
371                Self::collect_match_sites_from_spanned(left, out);
372                Self::collect_match_sites_from_spanned(right, out);
373            }
374            Expr::Attr(obj, _) => {
375                Self::collect_match_sites_from_spanned(obj, out);
376            }
377            Expr::ErrorProp(inner) => {
378                Self::collect_match_sites_from_spanned(inner, out);
379            }
380            Expr::Constructor(_, maybe_arg) => {
381                if let Some(arg) = maybe_arg {
382                    Self::collect_match_sites_from_spanned(arg, out);
383                }
384            }
385            Expr::InterpolatedStr(parts) => {
386                for part in parts {
387                    if let StrPart::Parsed(expr) = part {
388                        Self::collect_match_sites_from_spanned(expr, out);
389                    }
390                }
391            }
392            Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
393                for item in items {
394                    Self::collect_match_sites_from_spanned(item, out);
395                }
396            }
397            Expr::MapLiteral(entries) => {
398                for (key, value) in entries {
399                    Self::collect_match_sites_from_spanned(key, out);
400                    Self::collect_match_sites_from_spanned(value, out);
401                }
402            }
403            Expr::RecordCreate { fields, .. } => {
404                for (_, expr) in fields {
405                    Self::collect_match_sites_from_spanned(expr, out);
406                }
407            }
408            Expr::RecordUpdate { base, updates, .. } => {
409                Self::collect_match_sites_from_spanned(base, out);
410                for (_, expr) in updates {
411                    Self::collect_match_sites_from_spanned(expr, out);
412                }
413            }
414            Expr::TailCall(boxed) => {
415                for arg in &boxed.1 {
416                    Self::collect_match_sites_from_spanned(arg, out);
417                }
418            }
419            Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) => {}
420        }
421    }
422
423    // -------------------------------------------------------------------------
424    // Environment management
425    // -------------------------------------------------------------------------
426    pub(super) fn push_env(&mut self, frame: EnvFrame) {
427        self.env.push(frame);
428    }
429
430    pub(super) fn pop_env(&mut self) {
431        if self.env.len() > 1 {
432            self.env.pop();
433        }
434    }
435
436    pub(super) fn last_owned_scope_mut(
437        &mut self,
438    ) -> Result<&mut HashMap<String, NanValue>, RuntimeError> {
439        let frame = self
440            .env
441            .last_mut()
442            .ok_or_else(|| RuntimeError::Error("No active scope".to_string()))?;
443        match frame {
444            EnvFrame::Owned(scope) => Ok(scope),
445            EnvFrame::Shared(_) | EnvFrame::Slots(_) => Err(RuntimeError::Error(
446                "Cannot define name in non-owned frame".to_string(),
447            )),
448        }
449    }
450
451    /// Primary lookup — returns NanValue (Copy, 8 bytes, no clone needed).
452    pub(super) fn lookup_nv(&self, name: &str) -> Result<NanValue, RuntimeError> {
453        let env = &self.env;
454        let mut i = env.len();
455        let base = self.env_base;
456        while i > base {
457            i -= 1;
458            match &env[i] {
459                EnvFrame::Owned(scope) => {
460                    if let Some(v) = scope.get(name) {
461                        return Ok(*v);
462                    }
463                }
464                EnvFrame::Shared(scope) => {
465                    if let Some(v) = scope.get(name) {
466                        return Ok(*v);
467                    }
468                }
469                EnvFrame::Slots(_) => {}
470            }
471        }
472        match &env[0] {
473            EnvFrame::Owned(scope) => scope.get(name).copied(),
474            EnvFrame::Shared(scope) => scope.get(name).copied(),
475            EnvFrame::Slots(_) => None,
476        }
477        .ok_or_else(|| RuntimeError::Error(format!("Undefined variable: '{}'", name)))
478    }
479
480    pub(super) fn lookup_path_nv(&self, path: &str) -> Result<NanValue, RuntimeError> {
481        let mut parts = path.split('.').filter(|part| !part.is_empty());
482        let Some(first) = parts.next() else {
483            return Err(RuntimeError::Error("Empty path lookup".to_string()));
484        };
485
486        let mut current = self.lookup_nv(first)?;
487        for part in parts {
488            if !current.is_namespace() {
489                return Err(RuntimeError::Error(format!(
490                    "Cannot resolve '{}': '{}' is not a namespace",
491                    path, part
492                )));
493            }
494            let (_, members) = self.arena.get_namespace(current.symbol_index());
495            let Some((_, next)) = members.iter().find(|(name, _)| name.as_ref() == part) else {
496                return Err(RuntimeError::Error(format!(
497                    "Undefined variable: '{}'",
498                    path
499                )));
500            };
501            current = *next;
502        }
503
504        Ok(current)
505    }
506
507    pub(super) fn global_scope_clone(&self) -> Result<HashMap<String, NanValue>, RuntimeError> {
508        let frame = self
509            .env
510            .first()
511            .ok_or_else(|| RuntimeError::Error("No global scope".to_string()))?;
512        match frame {
513            EnvFrame::Owned(scope) => Ok(scope.clone()),
514            EnvFrame::Shared(scope) => Ok((**scope).clone()),
515            EnvFrame::Slots(_) => Err(RuntimeError::Error(
516                "Invalid global scope frame: Slots".to_string(),
517            )),
518        }
519    }
520
521    /// Public lookup — returns old Value for callers not yet migrated.
522    pub fn lookup(&self, name: &str) -> Result<Value, RuntimeError> {
523        let nv = self.lookup_nv(name)?;
524        Ok(nv.to_value(&self.arena))
525    }
526
527    /// Public define — accepts old Value, converts to NanValue internally.
528    pub fn define(&mut self, name: String, val: Value) {
529        let nv = NanValue::from_value(&val, &mut self.arena);
530        if let Ok(scope) = self.last_owned_scope_mut() {
531            scope.insert(name, nv);
532        }
533    }
534
535    /// Define with NanValue directly.
536    pub(super) fn define_nv(&mut self, name: String, nv: NanValue) {
537        if let Ok(scope) = self.last_owned_scope_mut() {
538            scope.insert(name, nv);
539        }
540    }
541
542    fn alias_exposed_type_namespaces(&mut self, module_val: &Value) {
543        let Value::Namespace { members, .. } = module_val else {
544            return;
545        };
546        for (name, member) in members {
547            if matches!(member, Value::Namespace { .. }) {
548                self.define(name.clone(), member.clone());
549            }
550        }
551    }
552
553    fn current_slots_frame_index(&self) -> Option<usize> {
554        let start = self.env_base.min(self.env.len());
555        (start..self.env.len())
556            .rev()
557            .find(|&idx| matches!(self.env[idx], EnvFrame::Slots(_)))
558    }
559
560    /// O(1) slot-based variable lookup — returns NanValue (Copy, no clone).
561    pub(super) fn lookup_slot(&self, slot: u16) -> Result<NanValue, RuntimeError> {
562        let idx = self
563            .current_slots_frame_index()
564            .ok_or_else(|| RuntimeError::Error("Resolved lookup on non-Slots frame".to_string()))?;
565        match &self.env[idx] {
566            EnvFrame::Slots(v) => Ok(v[slot as usize]),
567            _ => Err(RuntimeError::Error(
568                "Resolved lookup on non-Slots frame".to_string(),
569            )),
570        }
571    }
572
573    /// Define a value in the current Slots frame at the given slot index.
574    pub(super) fn define_slot(&mut self, slot: u16, val: NanValue) {
575        let Some(idx) = self.current_slots_frame_index() else {
576            return;
577        };
578        if let EnvFrame::Slots(v) = &mut self.env[idx] {
579            v[slot as usize] = val;
580        }
581    }
582
583    pub fn define_module_path(&mut self, path: &str, val: Value) -> Result<(), RuntimeError> {
584        let alias_source = val.clone();
585        let parts: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
586        if parts.is_empty() {
587            return Err(RuntimeError::Error("Empty module path".to_string()));
588        }
589        if parts.len() == 1 {
590            self.define(parts[0].to_string(), val);
591            self.mounted_module_paths.insert(path.to_string());
592            self.alias_exposed_type_namespaces(&alias_source);
593            return Ok(());
594        }
595
596        let head = parts[0];
597        let tail = &parts[1..];
598
599        let result = {
600            let scope = self.last_owned_scope_mut()?;
601            if let Some(existing_nv) = scope.remove(head) {
602                let existing = existing_nv.to_value(&self.arena);
603                match existing {
604                    Value::Namespace { name, mut members } => {
605                        Self::insert_namespace_path(&mut members, tail, val)?;
606                        let ns = Value::Namespace { name, members };
607                        let nv = NanValue::from_value(&ns, &mut self.arena);
608                        // Re-borrow scope after arena mutation
609                        let scope = self.last_owned_scope_mut()?;
610                        scope.insert(head.to_string(), nv);
611                        Ok(())
612                    }
613                    _ => {
614                        // Put it back
615                        let scope2 = self.last_owned_scope_mut()?;
616                        scope2.insert(head.to_string(), existing_nv);
617                        Err(RuntimeError::Error(format!(
618                            "Cannot mount module '{}': '{}' is not a namespace",
619                            parts.join("."),
620                            head
621                        )))
622                    }
623                }
624            } else {
625                let mut members = HashMap::new();
626                Self::insert_namespace_path(&mut members, tail, val)?;
627                let ns = Value::Namespace {
628                    name: head.to_string(),
629                    members,
630                };
631                let nv = NanValue::from_value(&ns, &mut self.arena);
632                let scope = self.last_owned_scope_mut()?;
633                scope.insert(head.to_string(), nv);
634                Ok(())
635            }
636        };
637
638        if result.is_ok() {
639            self.mounted_module_paths.insert(path.to_string());
640            self.alias_exposed_type_namespaces(&alias_source);
641        }
642        result
643    }
644
645    pub(super) fn insert_namespace_path(
646        scope: &mut HashMap<String, Value>,
647        parts: &[&str],
648        val: Value,
649    ) -> Result<(), RuntimeError> {
650        if parts.len() == 1 {
651            scope.insert(parts[0].to_string(), val);
652            return Ok(());
653        }
654
655        let head = parts[0];
656        let tail = &parts[1..];
657
658        if let Some(existing) = scope.remove(head) {
659            match existing {
660                Value::Namespace { name, mut members } => {
661                    Self::insert_namespace_path(&mut members, tail, val)?;
662                    scope.insert(head.to_string(), Value::Namespace { name, members });
663                    Ok(())
664                }
665                _ => Err(RuntimeError::Error(format!(
666                    "Cannot mount module '{}': '{}' is not a namespace",
667                    parts.join("."),
668                    head
669                ))),
670            }
671        } else {
672            let mut members = HashMap::new();
673            Self::insert_namespace_path(&mut members, tail, val)?;
674            scope.insert(
675                head.to_string(),
676                Value::Namespace {
677                    name: head.to_string(),
678                    members,
679                },
680            );
681            Ok(())
682        }
683    }
684
685    pub(super) fn module_cache_key(path: &Path) -> String {
686        canonicalize_path(path).to_string_lossy().to_string()
687    }
688
689    pub(super) fn module_decl(items: &[TopLevel]) -> Option<&Module> {
690        items.iter().find_map(|item| {
691            if let TopLevel::Module(m) = item {
692                Some(m)
693            } else {
694                None
695            }
696        })
697    }
698
699    pub(super) fn exposed_set(items: &[TopLevel]) -> Option<HashSet<String>> {
700        Self::module_decl(items).and_then(|m| {
701            if m.exposes.is_empty() {
702                None
703            } else {
704                Some(m.exposes.iter().cloned().collect())
705            }
706        })
707    }
708
709    pub(super) fn cycle_display(loading: &[String], next: &str) -> String {
710        let mut chain = loading
711            .iter()
712            .map(|key| {
713                Path::new(key)
714                    .file_stem()
715                    .and_then(|s| s.to_str())
716                    .unwrap_or(key)
717                    .to_string()
718            })
719            .collect::<Vec<_>>();
720        chain.push(
721            Path::new(next)
722                .file_stem()
723                .and_then(|s| s.to_str())
724                .unwrap_or(next)
725                .to_string(),
726        );
727        chain.join(" -> ")
728    }
729
730    pub fn load_module(
731        &mut self,
732        name: &str,
733        base_dir: &str,
734        loading: &mut Vec<String>,
735        loading_set: &mut HashSet<String>,
736    ) -> Result<Value, RuntimeError> {
737        let path = find_module_file(name, base_dir).ok_or_else(|| {
738            RuntimeError::Error(format!("Module '{}' not found in '{}'", name, base_dir))
739        })?;
740        let cache_key = Self::module_cache_key(&path);
741
742        if let Some(cached) = self.module_cache.get(&cache_key) {
743            return Ok(cached.clone());
744        }
745
746        if loading_set.contains(&cache_key) {
747            return Err(RuntimeError::Error(format!(
748                "Circular import: {}",
749                Self::cycle_display(loading, &cache_key)
750            )));
751        }
752
753        loading.push(cache_key.clone());
754        loading_set.insert(cache_key.clone());
755        let result = (|| -> Result<Value, RuntimeError> {
756            let src = std::fs::read_to_string(&path).map_err(|e| {
757                RuntimeError::Error(format!("Cannot read '{}': {}", path.display(), e))
758            })?;
759            let mut items = parse_source(&src).map_err(|e| {
760                RuntimeError::Error(format!("Parse error in '{}': {}", path.display(), e))
761            })?;
762            require_module_declaration(&items, &path.to_string_lossy())
763                .map_err(RuntimeError::Error)?;
764            crate::resolver::resolve_program(&mut items);
765
766            for item in &items {
767                if let TopLevel::TypeDef(td) = item {
768                    self.import_type_def_runtime(td);
769                }
770            }
771
772            if let Some(module) = Self::module_decl(&items) {
773                let expected = name.rsplit('.').next().unwrap_or(name);
774                if module.name != expected {
775                    return Err(RuntimeError::Error(format!(
776                        "Module name mismatch: expected '{}' (from '{}'), found '{}' in '{}'",
777                        expected,
778                        name,
779                        module.name,
780                        path.display()
781                    )));
782                }
783            }
784
785            let mut sub = Interpreter::new();
786
787            if let Some(module) = Self::module_decl(&items) {
788                for dep_name in &module.depends {
789                    let dep_ns = self.load_module(dep_name, base_dir, loading, loading_set)?;
790                    sub.define_module_path(dep_name, dep_ns)?;
791                }
792            }
793
794            for item in &items {
795                if let TopLevel::TypeDef(td) = item {
796                    sub.register_type_def(td);
797                }
798            }
799            for item in &items {
800                if let TopLevel::FnDef(fd) = item {
801                    sub.exec_fn_def(fd)?;
802                }
803            }
804            // Re-encode sub's NanValues into self's arena so that
805            // home_globals NanValue indices are valid in self.arena.
806            let sub_globals_nv = sub.global_scope_clone()?;
807            let mut reencoded_globals: HashMap<String, NanValue> =
808                HashMap::with_capacity(sub_globals_nv.len());
809            for (k, nv) in &sub_globals_nv {
810                let old_val = nv.to_value(&sub.arena);
811                let new_nv = NanValue::from_value(&old_val, &mut self.arena);
812                reencoded_globals.insert(k.clone(), new_nv);
813            }
814            let module_globals = Rc::new(reencoded_globals);
815
816            let exposed = Self::exposed_set(&items);
817            let opaque_set: HashSet<String> = Self::module_decl(&items)
818                .map(|m| m.exposes_opaque.iter().cloned().collect())
819                .unwrap_or_default();
820            let mut members = HashMap::new();
821            for item in &items {
822                match item {
823                    TopLevel::FnDef(fd) => {
824                        let include = match &exposed {
825                            Some(set) => set.contains(&fd.name),
826                            None => !fd.name.starts_with('_'),
827                        };
828                        if include {
829                            let mut val = sub.lookup(&fd.name).map_err(|_| {
830                                RuntimeError::Error(format!(
831                                    "Failed to export '{}.{}'",
832                                    name, fd.name
833                                ))
834                            })?;
835                            if let Value::Fn(function) = &mut val {
836                                Rc::make_mut(function).home_globals =
837                                    Some(Rc::clone(&module_globals));
838                            }
839                            members.insert(fd.name.clone(), val);
840                        }
841                    }
842                    TopLevel::TypeDef(TypeDef::Sum {
843                        name: type_name, ..
844                    }) => {
845                        // Skip opaque sum types — do not expose constructors.
846                        if opaque_set.contains(type_name) {
847                            continue;
848                        }
849                        let include = match &exposed {
850                            Some(set) => set.contains(type_name),
851                            None => !type_name.starts_with('_'),
852                        };
853                        if include {
854                            let val = sub.lookup(type_name).map_err(|_| {
855                                RuntimeError::Error(format!(
856                                    "Failed to export '{}.{}'",
857                                    name, type_name
858                                ))
859                            })?;
860                            members.insert(type_name.clone(), val);
861                        }
862                    }
863                    _ => {}
864                }
865            }
866
867            Ok(Value::Namespace {
868                name: name.to_string(),
869                members,
870            })
871        })();
872        loading.pop();
873        loading_set.remove(&cache_key);
874
875        match result {
876            Ok(ns) => {
877                self.module_cache.insert(cache_key, ns.clone());
878                Ok(ns)
879            }
880            Err(e) => Err(e),
881        }
882    }
883}