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