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