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 global_scope_clone(&self) -> Result<HashMap<String, NanValue>, RuntimeError> {
475        let frame = self
476            .env
477            .first()
478            .ok_or_else(|| RuntimeError::Error("No global scope".to_string()))?;
479        match frame {
480            EnvFrame::Owned(scope) => Ok(scope.clone()),
481            EnvFrame::Shared(scope) => Ok((**scope).clone()),
482            EnvFrame::Slots(_) => Err(RuntimeError::Error(
483                "Invalid global scope frame: Slots".to_string(),
484            )),
485        }
486    }
487
488    /// Public lookup — returns old Value for callers not yet migrated.
489    pub fn lookup(&self, name: &str) -> Result<Value, RuntimeError> {
490        let nv = self.lookup_nv(name)?;
491        Ok(nv.to_value(&self.arena))
492    }
493
494    /// Public define — accepts old Value, converts to NanValue internally.
495    pub fn define(&mut self, name: String, val: Value) {
496        let nv = NanValue::from_value(&val, &mut self.arena);
497        if let Ok(scope) = self.last_owned_scope_mut() {
498            scope.insert(name, nv);
499        }
500    }
501
502    /// Define with NanValue directly.
503    pub(super) fn define_nv(&mut self, name: String, nv: NanValue) {
504        if let Ok(scope) = self.last_owned_scope_mut() {
505            scope.insert(name, nv);
506        }
507    }
508
509    fn alias_exposed_type_namespaces(&mut self, module_val: &Value) {
510        let Value::Namespace { members, .. } = module_val else {
511            return;
512        };
513        for (name, member) in members {
514            if matches!(member, Value::Namespace { .. }) {
515                self.define(name.clone(), member.clone());
516            }
517        }
518    }
519
520    /// O(1) slot-based variable lookup — returns NanValue (Copy, no clone).
521    pub(super) fn lookup_slot(&self, slot: u16) -> Result<NanValue, RuntimeError> {
522        let idx = self.env.len() - 1;
523        match &self.env[idx] {
524            EnvFrame::Slots(v) => Ok(v[slot as usize]),
525            _ => Err(RuntimeError::Error(
526                "Resolved lookup on non-Slots frame".to_string(),
527            )),
528        }
529    }
530
531    /// Define a value in the current Slots frame at the given slot index.
532    pub(super) fn define_slot(&mut self, slot: u16, val: NanValue) {
533        let idx = self.env.len() - 1;
534        if let EnvFrame::Slots(v) = &mut self.env[idx] {
535            v[slot as usize] = val;
536        }
537    }
538
539    pub fn define_module_path(&mut self, path: &str, val: Value) -> Result<(), RuntimeError> {
540        let alias_source = val.clone();
541        let parts: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
542        if parts.is_empty() {
543            return Err(RuntimeError::Error("Empty module path".to_string()));
544        }
545        if parts.len() == 1 {
546            self.define(parts[0].to_string(), val);
547            self.alias_exposed_type_namespaces(&alias_source);
548            return Ok(());
549        }
550
551        let head = parts[0];
552        let tail = &parts[1..];
553
554        let result = {
555            let scope = self.last_owned_scope_mut()?;
556            if let Some(existing_nv) = scope.remove(head) {
557                let existing = existing_nv.to_value(&self.arena);
558                match existing {
559                    Value::Namespace { name, mut members } => {
560                        Self::insert_namespace_path(&mut members, tail, val)?;
561                        let ns = Value::Namespace { name, members };
562                        let nv = NanValue::from_value(&ns, &mut self.arena);
563                        // Re-borrow scope after arena mutation
564                        let scope = self.last_owned_scope_mut()?;
565                        scope.insert(head.to_string(), nv);
566                        Ok(())
567                    }
568                    _ => {
569                        // Put it back
570                        let scope2 = self.last_owned_scope_mut()?;
571                        scope2.insert(head.to_string(), existing_nv);
572                        Err(RuntimeError::Error(format!(
573                            "Cannot mount module '{}': '{}' is not a namespace",
574                            parts.join("."),
575                            head
576                        )))
577                    }
578                }
579            } else {
580                let mut members = HashMap::new();
581                Self::insert_namespace_path(&mut members, tail, val)?;
582                let ns = Value::Namespace {
583                    name: head.to_string(),
584                    members,
585                };
586                let nv = NanValue::from_value(&ns, &mut self.arena);
587                let scope = self.last_owned_scope_mut()?;
588                scope.insert(head.to_string(), nv);
589                Ok(())
590            }
591        };
592
593        if result.is_ok() {
594            self.alias_exposed_type_namespaces(&alias_source);
595        }
596        result
597    }
598
599    pub(super) fn insert_namespace_path(
600        scope: &mut HashMap<String, Value>,
601        parts: &[&str],
602        val: Value,
603    ) -> Result<(), RuntimeError> {
604        if parts.len() == 1 {
605            scope.insert(parts[0].to_string(), val);
606            return Ok(());
607        }
608
609        let head = parts[0];
610        let tail = &parts[1..];
611
612        if let Some(existing) = scope.remove(head) {
613            match existing {
614                Value::Namespace { name, mut members } => {
615                    Self::insert_namespace_path(&mut members, tail, val)?;
616                    scope.insert(head.to_string(), Value::Namespace { name, members });
617                    Ok(())
618                }
619                _ => Err(RuntimeError::Error(format!(
620                    "Cannot mount module '{}': '{}' is not a namespace",
621                    parts.join("."),
622                    head
623                ))),
624            }
625        } else {
626            let mut members = HashMap::new();
627            Self::insert_namespace_path(&mut members, tail, val)?;
628            scope.insert(
629                head.to_string(),
630                Value::Namespace {
631                    name: head.to_string(),
632                    members,
633                },
634            );
635            Ok(())
636        }
637    }
638
639    pub(super) fn module_cache_key(path: &Path) -> String {
640        canonicalize_path(path).to_string_lossy().to_string()
641    }
642
643    pub(super) fn module_decl(items: &[TopLevel]) -> Option<&Module> {
644        items.iter().find_map(|item| {
645            if let TopLevel::Module(m) = item {
646                Some(m)
647            } else {
648                None
649            }
650        })
651    }
652
653    pub(super) fn exposed_set(items: &[TopLevel]) -> Option<HashSet<String>> {
654        Self::module_decl(items).and_then(|m| {
655            if m.exposes.is_empty() {
656                None
657            } else {
658                Some(m.exposes.iter().cloned().collect())
659            }
660        })
661    }
662
663    pub(super) fn cycle_display(loading: &[String], next: &str) -> String {
664        let mut chain = loading
665            .iter()
666            .map(|key| {
667                Path::new(key)
668                    .file_stem()
669                    .and_then(|s| s.to_str())
670                    .unwrap_or(key)
671                    .to_string()
672            })
673            .collect::<Vec<_>>();
674        chain.push(
675            Path::new(next)
676                .file_stem()
677                .and_then(|s| s.to_str())
678                .unwrap_or(next)
679                .to_string(),
680        );
681        chain.join(" -> ")
682    }
683
684    pub fn load_module(
685        &mut self,
686        name: &str,
687        base_dir: &str,
688        loading: &mut Vec<String>,
689        loading_set: &mut HashSet<String>,
690    ) -> Result<Value, RuntimeError> {
691        let path = find_module_file(name, base_dir).ok_or_else(|| {
692            RuntimeError::Error(format!("Module '{}' not found in '{}'", name, base_dir))
693        })?;
694        let cache_key = Self::module_cache_key(&path);
695
696        if let Some(cached) = self.module_cache.get(&cache_key) {
697            return Ok(cached.clone());
698        }
699
700        if loading_set.contains(&cache_key) {
701            return Err(RuntimeError::Error(format!(
702                "Circular import: {}",
703                Self::cycle_display(loading, &cache_key)
704            )));
705        }
706
707        loading.push(cache_key.clone());
708        loading_set.insert(cache_key.clone());
709        let result = (|| -> Result<Value, RuntimeError> {
710            let src = std::fs::read_to_string(&path).map_err(|e| {
711                RuntimeError::Error(format!("Cannot read '{}': {}", path.display(), e))
712            })?;
713            let mut items = parse_source(&src).map_err(|e| {
714                RuntimeError::Error(format!("Parse error in '{}': {}", path.display(), e))
715            })?;
716            require_module_declaration(&items, &path.to_string_lossy())
717                .map_err(RuntimeError::Error)?;
718            crate::resolver::resolve_program(&mut items);
719
720            if let Some(module) = Self::module_decl(&items) {
721                let expected = name.rsplit('.').next().unwrap_or(name);
722                if module.name != expected {
723                    return Err(RuntimeError::Error(format!(
724                        "Module name mismatch: expected '{}' (from '{}'), found '{}' in '{}'",
725                        expected,
726                        name,
727                        module.name,
728                        path.display()
729                    )));
730                }
731            }
732
733            let mut sub = Interpreter::new();
734
735            if let Some(module) = Self::module_decl(&items) {
736                for dep_name in &module.depends {
737                    let dep_ns = self.load_module(dep_name, base_dir, loading, loading_set)?;
738                    sub.define_module_path(dep_name, dep_ns)?;
739                }
740            }
741
742            for item in &items {
743                if let TopLevel::TypeDef(td) = item {
744                    sub.register_type_def(td);
745                }
746            }
747            for item in &items {
748                if let TopLevel::FnDef(fd) = item {
749                    sub.exec_fn_def(fd)?;
750                }
751            }
752            // Re-encode sub's NanValues into self's arena so that
753            // home_globals NanValue indices are valid in self.arena.
754            let sub_globals_nv = sub.global_scope_clone()?;
755            let mut reencoded_globals: HashMap<String, NanValue> =
756                HashMap::with_capacity(sub_globals_nv.len());
757            for (k, nv) in &sub_globals_nv {
758                let old_val = nv.to_value(&sub.arena);
759                let new_nv = NanValue::from_value(&old_val, &mut self.arena);
760                reencoded_globals.insert(k.clone(), new_nv);
761            }
762            let module_globals = Rc::new(reencoded_globals);
763
764            let exposed = Self::exposed_set(&items);
765            let opaque_set: HashSet<String> = Self::module_decl(&items)
766                .map(|m| m.exposes_opaque.iter().cloned().collect())
767                .unwrap_or_default();
768            let mut members = HashMap::new();
769            for item in &items {
770                match item {
771                    TopLevel::FnDef(fd) => {
772                        let include = match &exposed {
773                            Some(set) => set.contains(&fd.name),
774                            None => !fd.name.starts_with('_'),
775                        };
776                        if include {
777                            let mut val = sub.lookup(&fd.name).map_err(|_| {
778                                RuntimeError::Error(format!(
779                                    "Failed to export '{}.{}'",
780                                    name, fd.name
781                                ))
782                            })?;
783                            if let Value::Fn(function) = &mut val {
784                                Rc::make_mut(function).home_globals =
785                                    Some(Rc::clone(&module_globals));
786                            }
787                            members.insert(fd.name.clone(), val);
788                        }
789                    }
790                    TopLevel::TypeDef(TypeDef::Sum {
791                        name: type_name, ..
792                    }) => {
793                        // Skip opaque sum types — do not expose constructors.
794                        if opaque_set.contains(type_name) {
795                            continue;
796                        }
797                        let include = match &exposed {
798                            Some(set) => set.contains(type_name),
799                            None => !type_name.starts_with('_'),
800                        };
801                        if include {
802                            let val = sub.lookup(type_name).map_err(|_| {
803                                RuntimeError::Error(format!(
804                                    "Failed to export '{}.{}'",
805                                    name, type_name
806                                ))
807                            })?;
808                            members.insert(type_name.clone(), val);
809                        }
810                    }
811                    _ => {}
812                }
813            }
814
815            Ok(Value::Namespace {
816                name: name.to_string(),
817                members,
818            })
819        })();
820        loading.pop();
821        loading_set.remove(&cache_key);
822
823        match result {
824            Ok(ns) => {
825                self.module_cache.insert(cache_key, ns.clone());
826                Ok(ns)
827            }
828            Err(e) => Err(e),
829        }
830    }
831}