Skip to main content

submilli_engine/codegen/
mod.rs

1//! Codegen — translates a typechecked Typed AST into a Wasm module.
2//!
3//! Phase entry points require a captured and desugared tree satisfying
4//! [`TypedAst`]'s phase input contract, and run on a
5//! [`crate::compiler_limits::COMPILER_STACK_BYTES`] stack. They validate borrowed
6//! dependency declarations before cloning or traversing their type payloads.
7
8pub mod analysis;
9mod argument_defaults;
10pub mod bigint_pool;
11pub mod bounds;
12pub mod box_types;
13mod call_arguments;
14pub mod cast_check;
15mod cast_diagnostics;
16pub mod classes;
17mod closure_coercions;
18pub(crate) use closure_coercions::ADAPTER_ORIGINAL_FIELD;
19pub mod closures;
20pub mod dependency_usage;
21pub mod dwarf;
22mod field_guards;
23mod field_lookup;
24pub mod field_name_strings;
25pub mod field_names;
26pub mod function_adapters;
27pub mod function_emitter;
28pub mod imported_classes;
29pub mod init_guard;
30pub mod intrinsics;
31#[cfg(test)]
32mod invariant_tests;
33mod nullish;
34pub mod recursive_validators;
35mod runtime_descriptors;
36mod runtime_values;
37pub mod string_pool;
38pub mod symbol_table;
39mod this_binding;
40pub mod throw;
41pub mod user_subtypes;
42mod vtable_walk;
43
44use crate::compiler_error::{CompilerFailure, CompilerStage};
45use crate::tree_height;
46use std::borrow::Cow;
47use std::collections::{BTreeMap, BTreeSet};
48
49use wasm_encoder::{
50    AbstractHeapType, CodeSection, ConstExpr, CustomSection, DataCountSection, DataSection,
51    EntityType, ExportKind as WasmExportKind, ExportSection, FunctionSection, GlobalSection,
52    GlobalType, HeapType, Ieee64, ImportSection, Instruction, Module, RefType, StartSection,
53    TagKind, TagType, TypeSection, ValType,
54};
55
56use crate::{
57    LineIndex, MangledName, PackageDeclaration, Param, StmtId, Type, TypeKind, TypedAst, ValueKind,
58    ValueSymbol,
59};
60use analysis::CodegenAnalysis;
61use bigint_pool::BigIntPool;
62use function_emitter::FunctionEmitter;
63use function_emitter::stmt::emit_statement;
64use string_pool::StringPool;
65
66pub(crate) fn internal_failure(message: impl Into<String>) -> CompilerFailure {
67    CompilerFailure::Internal {
68        stage: CompilerStage::Codegen,
69        span: None,
70        message: message.into(),
71    }
72}
73
74/// Whether a package, or a symbol mangled under one, is implemented by the Rust
75/// host rather than compiled guest code. A mangled name starts with its package.
76pub(crate) fn is_host_package(name: &str) -> bool {
77    name.starts_with("submilli:")
78}
79
80/// A host package's `void` function returns no Wasm value, so each call site
81/// pushes `undefined` itself; a guest `void` function returns `undefined`.
82/// `owner` is a package name, or a mangled symbol, which begins with one.
83fn is_resultless(owner: &str, ret: &Type) -> bool {
84    ret.is_void() && is_host_package(owner)
85}
86
87/// Allocates the next index in a WebAssembly index space. Like arena IDs,
88/// `u32::MAX` itself is never allocated.
89pub(crate) fn next_index(counter: &mut u32) -> Result<u32, CompilerFailure> {
90    let index = *counter;
91    *counter = index.checked_add(1).ok_or_else(index_space_exhausted)?;
92    Ok(index)
93}
94
95/// Encodes a count or position that WebAssembly represents as `u32`.
96pub(crate) fn wasm_u32(value: usize) -> Result<u32, CompilerFailure> {
97    u32::try_from(value).map_err(|_| index_space_exhausted())
98}
99
100/// Local index of parameter `position` in a body whose local 0 holds the
101/// receiver or closure environment.
102pub(crate) fn parameter_local(position: usize) -> Result<u32, CompilerFailure> {
103    wasm_u32(position)?
104        .checked_add(1)
105        .ok_or_else(index_space_exhausted)
106}
107
108/// Function indices of the depth-guarded bodies behind a vtable's universal
109/// `equals`, `toJson` and `hash` slots.
110#[derive(Clone, Copy, Debug, Default)]
111pub struct GuardedBodies {
112    pub equals: u32,
113    pub to_json: u32,
114    pub hash: u32,
115}
116
117fn index_space_exhausted() -> CompilerFailure {
118    CompilerFailure::Limit {
119        stage: CompilerStage::Codegen,
120        span: None,
121        message: "the module exceeds a WebAssembly index or size limit".into(),
122        help: vec!["split the program into smaller modules".into()],
123    }
124}
125
126/// How JavaScript prints the static-dispatch binding `name`. The capitalized
127/// ones (`Number`, `Map`, `Temporal.Instant`) are constructors, which print as
128/// native functions; `console` is the one plain object, printed by its tag.
129fn static_value_tag(name: &str) -> String {
130    if name.starts_with(|c: char| c.is_ascii_uppercase()) {
131        format!("function {name}() {{ [native code] }}")
132    } else {
133        format!("[object {name}]")
134    }
135}
136
137/// The `Dispatch::Static` interface `ty` names in `defs`, if any. Constructor
138/// and namespace receiver bindings (`console`, `Map`, `Temporal.Instant`) are
139/// typed by these; every call site drops the receiver, so the bindings are
140/// inert — they lower to a typed null and import no global.
141fn static_interface_of<'a>(
142    defs: &'a PackageDeclaration,
143    ty: &Type,
144) -> Option<&'a crate::TypeSymbol> {
145    fn find<'a>(
146        types: &'a std::collections::BTreeMap<String, crate::TypeSymbol>,
147        namespaces: &'a std::collections::BTreeMap<String, crate::NamespaceSymbol>,
148        mangled: &crate::MangledName,
149    ) -> Option<&'a crate::TypeSymbol> {
150        types
151            .values()
152            .find(|t| t.mangled_name == *mangled)
153            .or_else(|| {
154                namespaces
155                    .values()
156                    .find_map(|ns| find(&ns.types, &ns.namespaces, mangled))
157            })
158    }
159    let Type::InterfaceRef { mangled, .. } = ty.peel() else {
160        return None;
161    };
162    find(&defs.types, &defs.namespaces, mangled).filter(|ts| {
163        matches!(
164            &ts.kind,
165            TypeKind::Interface {
166                dispatch: crate::Dispatch::Static,
167                ..
168            }
169        )
170    })
171}
172
173#[allow(clippy::too_many_arguments)]
174fn import_value_symbol(
175    name: &str,
176    value: &ValueSymbol,
177    defs: &PackageDeclaration,
178    intrinsics: &intrinsics::IntrinsicTypeIndices,
179    next_type_idx: &mut u32,
180    next_func_idx: &mut u32,
181    next_global_idx: &mut u32,
182    types: &mut wasm_encoder::TypeSection,
183    import_section: &mut wasm_encoder::ImportSection,
184    symbols: &mut SymbolTable,
185) -> Result<(), crate::compiler_error::CompilerFailure> {
186    use wasm_encoder::{EntityType, GlobalType, ValType};
187    match &value.kind {
188        ValueKind::Function { params, ret, .. } => {
189            let sig_idx = *next_type_idx;
190            crate::codegen::next_index(next_type_idx)?;
191            // `submilli:json` is the last host package on the raw ABI: its host
192            // fns take/return `$rawString` (codegen re-wraps at the boundary).
193            // Every other host package speaks the real `$string`/`$Array` ABI,
194            // so it goes through the normal `value_type` path, same as user modules.
195            let raw_string_abi = defs.package_name.as_str() == crate::runtime::JSON_MODULE_NAME;
196            let (mut param_types, result_types): (Vec<ValType>, Vec<ValType>) = if raw_string_abi {
197                json_host_signature(name, params, symbols, intrinsics)?
198            } else {
199                (
200                    params
201                        .iter()
202                        .map(|p| symbols.value_type(&p.ty))
203                        .collect::<Result<_, _>>()?,
204                    if is_resultless(&defs.package_name, ret) {
205                        vec![]
206                    } else {
207                        symbols.wasm_result(ret)?
208                    },
209                )
210            };
211            if defs.runtime_generics.contains(&value.mangled_name) {
212                symbols
213                    .runtime_generic_functions
214                    .insert(value.mangled_name.clone());
215                param_types.push(runtime_descriptors::environment_type(symbols)?);
216            }
217            if is_host_package(&defs.package_name) {
218                symbols.record_host_parameter_defaults(
219                    *next_func_idx,
220                    params.iter().map(|param| param.default.clone()).collect(),
221                );
222            }
223            if result_types.is_empty() {
224                symbols.resultless_functions.insert(*next_func_idx);
225            }
226            types.ty().function(param_types, result_types);
227            import_section.import(
228                defs.package_name.as_str(),
229                value.mangled_name.as_str(),
230                EntityType::Function(sig_idx),
231            );
232            if let Some(metadata) = call_arguments::metadata(params.iter().map(|param| {
233                (
234                    param
235                        .default
236                        .as_ref()
237                        .or(param.optional.then_some(&crate::DefaultValue::Undefined)),
238                    param.rest,
239                )
240            })) {
241                symbols
242                    .function_argument_metadata
243                    .insert(value.mangled_name.clone(), metadata);
244            }
245            symbols.record_imported_fn(
246                value.mangled_name.clone(),
247                *next_func_idx,
248                params.iter().map(|p| p.ty.clone()).collect(),
249                ret.clone(),
250                raw_string_abi,
251            );
252            crate::codegen::next_index(next_func_idx)?;
253        }
254        ValueKind::Let { ty, .. } | ValueKind::Const { ty, .. } => {
255            // Static-interface receiver bindings are inert (see
256            // `static_interface_of`): record the dispatch so the emitter lowers
257            // references to a typed null, and import nothing.
258            if let Some(ts) = static_interface_of(defs, ty) {
259                symbols.record_iface_dispatch(ts.mangled_name.clone(), crate::Dispatch::Static);
260                symbols.record_static_value_tag(
261                    ts.mangled_name.clone(),
262                    static_value_tag(&value.name),
263                );
264                return Ok(());
265            }
266            // Every exported value-global is Wasm-mutable across the board — a
267            // codegen-compiled package needs its `_start` to initialize them, and
268            // the prelude/host packages match that so the import type lines up.
269            // Const immutability is typechecker-enforced, not Wasm-enforced.
270            let global_ty = GlobalType {
271                val_type: global_val_type(ty, symbols)?,
272                mutable: true,
273                shared: false,
274            };
275            import_section.import(
276                defs.package_name.as_str(),
277                value.mangled_name.as_str(),
278                EntityType::Global(global_ty),
279            );
280            symbols.record_typed_global(value.mangled_name.clone(), *next_global_idx, ty.clone());
281            crate::codegen::next_index(next_global_idx)?;
282        }
283    };
284    Ok(())
285}
286
287/// The physical Wasm signature a Direct/Static-dispatch interface method is
288/// called through, as its user-arg slots and result (the receiver is excluded).
289///
290/// Two declarations can describe one wrapper. A host-implemented method was
291/// imported by the value-symbol loop from the declaration sitting beside its
292/// Rust impl; the interface's `MethodSig` is the *language*-level view of the
293/// same method, and the two can lower differently — Temporal's `with(fields)`
294/// declares an interface-typed bag (`(ref null $Object)`) where the host takes
295/// a structural object (`(ref $ObjectShape)`). The imported signature is the one
296/// the call actually has to satisfy, so it wins.
297///
298/// `has_receiver` is false for `Dispatch::Static`, whose value symbols carry no
299/// receiver param; every other value symbol declares the receiver as param 0
300/// (`declare_method`'s contract). The user-arg count is the only cross-check
301/// available: the receiver's *own* slot can't serve as one, because the two
302/// import paths lower it differently — this loop pushes `receiver_wasm`
303/// (`(ref $Object)`), while `import_value_symbol` runs the declared receiver
304/// through `value_type`, which for an `InterfaceRef` yields the nullable
305/// `(ref null $Object)`. A declaration that dropped the receiver while keeping
306/// the count would slip through, so keep param 0 the receiver.
307fn declared_wrapper_abi(
308    symbols: &SymbolTable,
309    mangled: &crate::MangledName,
310    sig: &crate::MethodSig,
311    has_receiver: bool,
312) -> Result<symbol_table::MethodSlotAbi, crate::compiler_error::CompilerFailure> {
313    let receiver_offset = usize::from(has_receiver);
314    if let Some(imported) = symbols.top_level_fn(mangled)
315        && sig.params.len().checked_add(receiver_offset) == Some(imported.params.len())
316    {
317        return Ok(symbol_table::MethodSlotAbi {
318            params: imported
319                .params
320                .iter()
321                .skip(receiver_offset)
322                .map(|ty| symbols.value_type(ty))
323                .collect::<Result<_, _>>()?,
324            ret: if symbols.resultless_functions.contains(&imported.wasm_idx) {
325                None
326            } else {
327                symbols.wasm_result(&imported.ret)?.first().copied()
328            },
329        });
330    }
331    Ok(symbol_table::MethodSlotAbi {
332        params: sig
333            .params
334            .iter()
335            .map(|p| symbols.value_type(&p.ty))
336            .collect::<Result<_, _>>()?,
337        ret: if is_resultless(mangled.as_str(), &sig.ret) {
338            None
339        } else {
340            symbols.wasm_result(&sig.ret)?.first().copied()
341        },
342    })
343}
344
345#[allow(clippy::too_many_arguments)]
346fn import_json_host_function(
347    name: &str,
348    intrinsics: &intrinsics::IntrinsicTypeIndices,
349    next_type_idx: &mut u32,
350    next_func_idx: &mut u32,
351    types: &mut wasm_encoder::TypeSection,
352    import_section: &mut wasm_encoder::ImportSection,
353    symbols: &mut SymbolTable,
354) -> Result<(), crate::compiler_error::CompilerFailure> {
355    let mangled = crate::mangle::host(crate::runtime::JSON_MODULE_NAME, name);
356    if symbols.func_idx(&mangled).is_some() {
357        return Ok(());
358    }
359
360    let sig_idx = *next_type_idx;
361    crate::codegen::next_index(next_type_idx)?;
362    let (param_types, result_types) = json_host_signature(name, &[], symbols, intrinsics)?;
363    types.ty().function(param_types, result_types);
364    import_section.import(
365        crate::runtime::JSON_MODULE_NAME,
366        mangled.as_str(),
367        EntityType::Function(sig_idx),
368    );
369    symbols.record_func(mangled, *next_func_idx);
370    crate::codegen::next_index(next_func_idx)?;
371
372    Ok(())
373}
374
375pub use symbol_table::SymbolTable;
376
377#[derive(Clone, Debug, PartialEq, Eq)]
378pub struct CodegenError {
379    message: &'static str,
380    span: crate::Span,
381}
382
383impl std::fmt::Display for CodegenError {
384    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385        write!(out, "internal code generation error: {}", self.message)
386    }
387}
388
389impl std::error::Error for CodegenError {}
390
391impl From<CodegenError> for CompilerFailure {
392    fn from(error: CodegenError) -> Self {
393        Self::Internal {
394            stage: CompilerStage::Codegen,
395            span: Some(error.span),
396            message: error.message.to_owned(),
397        }
398    }
399}
400
401impl CodegenError {
402    pub fn diagnostic(self) -> crate::Diagnostic {
403        crate::Diagnostic {
404            severity: crate::Severity::Error,
405            span: self.span,
406            message: self.to_string(),
407            help: vec!["report this compiler error with the source program".into()],
408            notes: Vec::new(),
409        }
410    }
411}
412
413pub struct CodegenCtx<'a> {
414    pub ta: &'a TypedAst,
415    pub strings: &'a StringPool,
416    pub bigints: &'a BigIntPool,
417    pub symbols: &'a SymbolTable,
418    pub source: &'a str,
419    pub line_index: &'a LineIndex,
420    /// The module being compiled; codegen-generated nodes (closure envs, adapter
421    /// params, runtime validators) anchor their placeholder spans to it.
422    pub file: crate::FileId,
423    /// Runtime-validator bodies for expanding recursive aliases and data interfaces.
424    pub validator_bodies: &'a recursive_validators::ValidatorBodies,
425    pub type_info: &'a crate::TypeInfoTable,
426    pub package_string_global_idx: Option<u32>,
427    failure: std::cell::Cell<Option<CompilerFailure>>,
428    /// Inline structural-test steps the current runtime check may still
429    /// emit; see [`crate::compiler_limits::MAX_INLINE_VALIDATOR_STEPS`].
430    validator_steps_left: std::cell::Cell<u64>,
431    /// The type whose runtime check is being emitted, named when the check
432    /// exhausts `validator_steps_left`.
433    validator_root: std::cell::Cell<Option<Type>>,
434    /// Whether the check being emitted is a function of its own.
435    check_is_standalone: std::cell::Cell<bool>,
436}
437
438impl CodegenCtx<'_> {
439    /// Emission accumulates bytes locally; an internal failure discards the whole
440    /// module at the codegen boundary, never returning partial Wasm to a caller.
441    fn require<T>(&self, value: Option<T>, message: &'static str) -> Option<T> {
442        if value.is_none() {
443            self.fail(message);
444        }
445        value
446    }
447
448    /// Latches a typed failure for an emitter that cannot yet return one; the
449    /// codegen boundary discards the module just as for [`Self::require`], so a
450    /// caller may return early or keep emitting after a failure.
451    fn latch<T>(&self, result: Result<T, CompilerFailure>) -> Option<T> {
452        match result {
453            Ok(value) => Some(value),
454            Err(failure) => {
455                self.record_failure(failure);
456                None
457            }
458        }
459    }
460
461    fn check_failure(&self) -> Result<(), CompilerFailure> {
462        match self.failure.take() {
463            Some(error) => Err(error),
464            None => Ok(()),
465        }
466    }
467
468    fn fail(&self, message: &'static str) {
469        self.record_failure(
470            CodegenError {
471                message,
472                span: crate::Span::at(self.file),
473            }
474            .into(),
475        );
476    }
477
478    /// Charges one step of structural-test emission to the current check, and
479    /// stops before the function it is emitted into exceeds the Wasm engine's
480    /// limits on locals and body size (see [`Self::check_function_limits`]).
481    /// The check's own failures are reported at the last source-locating span
482    /// the emitter mapped, which the test is emitted for.
483    fn charge_validator_step(&self, emitter: &mut FunctionEmitter) -> Result<(), CompilerFailure> {
484        self.charge_validator_steps(emitter, 1)
485    }
486
487    /// [`charge_validator_step`](Self::charge_validator_step) for `steps` at
488    /// once, for emission whose size varies, such as a path naming long fields.
489    fn charge_validator_steps(
490        &self,
491        emitter: &mut FunctionEmitter,
492        steps: u64,
493    ) -> Result<(), CompilerFailure> {
494        use crate::compiler_limits::MAX_INLINE_VALIDATOR_STEPS;
495        self.check_function_limits(emitter)?;
496        let Some(left) = self.validator_steps_left.get().checked_sub(steps) else {
497            return Err(self.validator_limit(
498                emitter.last_source_span(),
499                format!("exceeds the compiler limit of {MAX_INLINE_VALIDATOR_STEPS} steps"),
500                "check against a smaller type, or restructure the interfaces so members do not repeat the same nested interface",
501            ));
502        };
503        self.validator_steps_left.set(left);
504        Ok(())
505    }
506
507    /// Stops a runtime check once the function it is emitted into has more
508    /// locals, or a larger body, than the Wasm engine accepts, so runaway
509    /// checks stop early; [`Self::check_finished_function`] measures every
510    /// function once it is built.
511    ///
512    /// A limit crossed before the check emitted anything is the preceding
513    /// code's: it is reported as the function's failure at the source that
514    /// crossed it. Otherwise the check is named, at the last source-locating
515    /// span.
516    fn check_function_limits(&self, emitter: &mut FunctionEmitter) -> Result<(), CompilerFailure> {
517        let Some(limit) = FunctionLimit::exceeded(emitter.body_size(), emitter.local_count())
518        else {
519            return Ok(());
520        };
521        // A check charges a step before emitting anything, so before its
522        // first step its allowance is untouched.
523        let check_has_emitted_code =
524            self.validator_steps_left.get() < crate::compiler_limits::MAX_INLINE_VALIDATOR_STEPS;
525        if !check_has_emitted_code {
526            let span = match limit {
527                FunctionLimit::BodyBytes => emitter.body_limit_span(),
528                FunctionLimit::Locals => emitter.locals_limit_span(),
529            };
530            return Err(limit.failure(span));
531        }
532        let fix = if self.check_is_standalone.get() {
533            "check against a smaller type"
534        } else {
535            "check against a smaller type, or move some of the function's checks into another function"
536        };
537        Err(self.validator_limit(emitter.last_source_span(), limit.check_exceeds(), fix))
538    }
539
540    /// Records a limit failure for a built function the Wasm engine would
541    /// reject (see [`FunctionLimit`]), reported at the source that crossed the
542    /// limit.
543    fn check_finished_function(&self, function: FinishedFunction) {
544        let Some(limit) = FunctionLimit::exceeded(function.body_bytes, function.locals) else {
545            return;
546        };
547        let span = match limit {
548            FunctionLimit::BodyBytes => function.body_limit_span,
549            FunctionLimit::Locals => function.locals_limit_span,
550        };
551        self.record_failure(limit.failure(span));
552    }
553
554    /// A runtime-check limit, naming the check being emitted.
555    fn validator_limit(
556        &self,
557        span: Option<crate::Span>,
558        exceeded: String,
559        fix: &str,
560    ) -> CompilerFailure {
561        let root = self.validator_root.take();
562        let subject = match &root {
563            Some(ty) => format!("the runtime check of `{}`", abbreviated(ty)),
564            None => "a runtime type check".to_string(),
565        };
566        self.validator_root.set(root);
567        CompilerFailure::Limit {
568            stage: crate::compiler_error::CompilerStage::Codegen,
569            span,
570            message: format!("{subject} {exceeded}"),
571            help: vec![
572                "a runtime check of a value against an interface includes the checks of every member, so interfaces whose members reference other interfaces several times multiply its size".into(),
573                fix.into(),
574            ],
575        }
576    }
577
578    /// Runs `emit` as the runtime check of `ty` with a fresh step allowance,
579    /// unless it is part of an enclosing check, whose name and allowance it
580    /// shares.
581    fn checking<T>(&self, ty: &Type, emit: impl FnOnce() -> T) -> T {
582        if let Some(outer) = self.validator_root.take() {
583            self.validator_root.set(Some(outer));
584            return emit();
585        }
586        self.validator_steps_left
587            .set(crate::compiler_limits::MAX_INLINE_VALIDATOR_STEPS);
588        self.validator_root.set(Some(ty.clone()));
589        let result = emit();
590        self.validator_root.set(None);
591        result
592    }
593
594    /// [`Self::checking`] for a check emitted as a function of its own, which
595    /// shares that function with no other code.
596    fn checking_standalone<T>(&self, ty: &Type, emit: impl FnOnce() -> T) -> T {
597        self.check_is_standalone.set(true);
598        let result = self.checking(ty, emit);
599        self.check_is_standalone.set(false);
600        result
601    }
602
603    /// Keeps the first failure; later ones are consequences of it.
604    fn record_failure(&self, failure: CompilerFailure) {
605        let first = self.failure.take().unwrap_or(failure);
606        self.failure.set(Some(first));
607    }
608}
609
610/// A per-function limit of the Wasm engine.
611#[derive(Clone, Copy)]
612enum FunctionLimit {
613    /// A body of more than [`crate::compiler_limits::MAX_FUNCTION_BODY_BYTES`] bytes.
614    BodyBytes,
615    /// More than [`crate::compiler_limits::MAX_FUNCTION_LOCALS`] locals,
616    /// parameters included.
617    Locals,
618}
619
620impl FunctionLimit {
621    /// The limit a function of `body_bytes` and `locals` exceeds. Locals come
622    /// first: their declarations are part of the body, so enough of them take
623    /// it over the byte limit before any code does.
624    fn exceeded(body_bytes: usize, locals: u32) -> Option<Self> {
625        if locals > crate::compiler_limits::MAX_FUNCTION_LOCALS {
626            Some(Self::Locals)
627        } else if !Self::body_fits(body_bytes) {
628            Some(Self::BodyBytes)
629        } else {
630            None
631        }
632    }
633
634    /// Whether a function body of `bytes` is within the engine's limit.
635    fn body_fits(bytes: usize) -> bool {
636        bytes <= crate::compiler_limits::MAX_FUNCTION_BODY_BYTES
637    }
638
639    /// A function over this limit, reported at `span`, the source that
640    /// crossed it.
641    fn failure(self, span: Option<crate::Span>) -> CompilerFailure {
642        CompilerFailure::Limit {
643            stage: crate::compiler_error::CompilerStage::Codegen,
644            span,
645            message: format!("the function this code compiles into {}", self.function_exceeds()),
646            help: vec![
647                "split the code into smaller functions; top-level statements compile into one function, and runtime type checks of large types are emitted inline in the function that contains them".into(),
648            ],
649        }
650    }
651
652    /// What a function over this limit does, following "the function …".
653    fn function_exceeds(self) -> String {
654        use crate::compiler_limits::{MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_LOCALS};
655        match self {
656            Self::BodyBytes => {
657                format!("is larger than the {MAX_FUNCTION_BODY_BYTES} bytes a function may have")
658            }
659            Self::Locals => {
660                format!("needs more than the {MAX_FUNCTION_LOCALS} locals a function may have")
661            }
662        }
663    }
664
665    /// What a runtime check taking its function over this limit does,
666    /// following "the runtime check of …".
667    fn check_exceeds(self) -> String {
668        use crate::compiler_limits::{MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_LOCALS};
669        match self {
670            Self::BodyBytes => format!(
671                "makes its function larger than the {MAX_FUNCTION_BODY_BYTES} bytes a function may have"
672            ),
673            Self::Locals => {
674                format!("needs more than the {MAX_FUNCTION_LOCALS} locals a function may have")
675            }
676        }
677    }
678}
679
680/// A built function's size, and the sources that took it past each limit.
681struct FinishedFunction {
682    body_bytes: usize,
683    body_limit_span: Option<crate::Span>,
684    locals: u32,
685    locals_limit_span: Option<crate::Span>,
686}
687
688/// `ty` as a diagnostic names it, cut short: a type near the size limit
689/// renders as hundreds of kilobytes.
690fn abbreviated(ty: &Type) -> String {
691    const MAX_CHARS: usize = 120;
692    let text = ty.to_string();
693    if text.chars().count() <= MAX_CHARS {
694        return text;
695    }
696    let mut short: String = text.chars().take(MAX_CHARS).collect();
697    short.push('…');
698    short
699}
700
701pub fn codegen(
702    source: &str,
703    filename: &str,
704    file: crate::FileId,
705    ta: &TypedAst,
706    dependencies: &[&PackageDeclaration],
707) -> Result<Vec<u8>, CompilerFailure> {
708    Ok(codegen_with_type_info(source, filename, file, ta, dependencies)?.wasm)
709}
710
711/// Like [`codegen_with_type_info`], but names the module after `owning_package` instead of
712/// [`TypedAst::package_name`].
713///
714/// The two differ for a package's test file. It compiles as a script, so its entry point must
715/// stay mangled as `main` for the runner to find it, but the code is the package's and a gated
716/// call from it must be attributed to the package — not to `main`, which `secrets.get` refuses
717/// outright.
718pub fn codegen_owned_by(
719    owning_package: &str,
720    source: &str,
721    filename: &str,
722    file: crate::FileId,
723    ta: &TypedAst,
724    dependencies: &[&PackageDeclaration],
725) -> Result<GeneratedModule, CompilerFailure> {
726    codegen_inner(
727        source,
728        filename,
729        file,
730        ta,
731        dependencies,
732        owning_package,
733        None,
734    )
735}
736
737#[derive(Clone, Debug, PartialEq)]
738pub struct GeneratedModule {
739    pub wasm: Vec<u8>,
740    pub type_info: crate::TypeInfoTable,
741    pub runtime_functions: BTreeMap<crate::MangledName, crate::RuntimeFunction>,
742    pub runtime_globals: BTreeMap<crate::MangledName, Type>,
743    pub closure_caches: BTreeSet<crate::MangledName>,
744}
745
746pub fn codegen_with_type_info(
747    source: &str,
748    filename: &str,
749    file: crate::FileId,
750    ta: &TypedAst,
751    dependencies: &[&PackageDeclaration],
752) -> Result<GeneratedModule, CompilerFailure> {
753    codegen_inner(
754        source,
755        filename,
756        file,
757        ta,
758        dependencies,
759        &ta.package_name,
760        None,
761    )
762}
763
764pub fn codegen_package_with_type_info(
765    sources: &crate::Sources,
766    file: crate::FileId,
767    ta: &TypedAst,
768    dependencies: &[&PackageDeclaration],
769) -> Result<GeneratedModule, CompilerFailure> {
770    let source = sources.get(file).ok_or_else(|| {
771        crate::source::SourceError::UnknownFile { file }
772            .into_compiler_failure(CompilerStage::Codegen)
773    })?;
774    codegen_inner(
775        source.text(),
776        source.path.as_str(),
777        file,
778        ta,
779        dependencies,
780        &ta.package_name,
781        Some(sources),
782    )
783}
784
785fn codegen_inner(
786    source: &str,
787    filename: &str,
788    file: crate::FileId,
789    ta: &TypedAst,
790    dependencies: &[&PackageDeclaration],
791    owning_package: &str,
792    package_sources: Option<&crate::Sources>,
793) -> Result<GeneratedModule, CompilerFailure> {
794    tree_height::check_typed(ta, CompilerStage::Codegen)?;
795    crate::typechecker::infer::check_declaration_types(dependencies.iter().copied())
796        .map_err(|failure| failure.with_stage(CompilerStage::Codegen))?;
797    u32::try_from(source.len()).map_err(|_| CompilerFailure::Limit {
798        stage: CompilerStage::Codegen,
799        span: None,
800        message: "source exceeds the 32-bit source-offset limit".into(),
801        help: vec!["split the source into smaller modules".into()],
802    })?;
803    let line_index = LineIndex::new(source)
804        .map_err(|error| error.into_compiler_failure(CompilerStage::Codegen))?;
805    let debug_sources = dwarf::DebugSources {
806        file,
807        filename,
808        index: &line_index,
809        package: package_sources.map(|sources| dwarf::PackageSources {
810            sources,
811            name: owning_package,
812        }),
813    };
814    for id in ta
815        .expr_ids()
816        .map_err(|error| error.into_compiler_failure(CompilerStage::Codegen))?
817    {
818        let expr = ta
819            .try_expr(id)
820            .map_err(|error| error.into_compiler_failure(CompilerStage::Codegen))?;
821        debug_sources.location(expr.span)?;
822    }
823    for id in ta
824        .stmt_ids()
825        .map_err(|error| error.into_compiler_failure(CompilerStage::Codegen))?
826    {
827        let stmt = ta
828            .try_stmt(id)
829            .map_err(|error| error.into_compiler_failure(CompilerStage::Codegen))?;
830        debug_sources.location(stmt.span)?;
831    }
832    let source_main_return_ty = ta
833        .functions
834        .iter()
835        .find(|function| function.name.name == "main")
836        .map(|function| function.return_type.clone());
837    let lowered = runtime_values::lower(ta, dependencies)?;
838    let ta = &lowered;
839    let lowered_dependencies: Vec<_> = dependencies
840        .iter()
841        .map(|defs| runtime_values::lower_declaration(defs))
842        .collect::<Result<_, _>>()?;
843    let dependencies: Vec<_> = lowered_dependencies.iter().collect();
844    let dependencies = dependencies.as_slice();
845    let analysis = CodegenAnalysis::collect(ta, dependencies)?;
846    let pool = analysis.string_pool;
847    let bigint_pool = analysis.bigint_pool;
848    let dependency_usage = analysis.dependency_usage.finish(dependencies);
849
850    let mut module = Module::new();
851    let mut types = TypeSection::new();
852    let mut import_section = ImportSection::new();
853    let mut symbols = SymbolTable::default();
854    let mut next_type_idx: u32 = 0;
855    let mut next_func_idx: u32 = 0;
856
857    // Intrinsic types hard-coded locally in every consumer module; WasmGC canonicalization
858    // unifies them across modules at instantiation. Instance-interface types (e.g. $TextEncoder)
859    // stay in their owning module — consumers see them as (ref null $Object) via InterfaceRef.
860    let intrinsics = intrinsics::declare_intrinsic_types(&mut types);
861    symbols.set_intrinsic_type_indices(intrinsics);
862    next_type_idx = next_type_idx
863        .checked_add(intrinsics::INTRINSIC_TYPE_COUNT)
864        .ok_or_else(|| crate::codegen::internal_failure("Wasm index count overflow"))?;
865    field_names::declare_optional_name_type(
866        &mut types,
867        &mut symbols,
868        &mut next_type_idx,
869        intrinsics,
870    )?;
871
872    // The exception tag — host-owned (created per store, linker-defined under
873    // `submilli:prelude`) and imported unconditionally: every module can
874    // throw via bounds/cast/assert even when the source never mentions `Error`.
875    // There is exactly one tag, by design (only `Error`s are throwable), so it's
876    // fixed codegen plumbing, not a declaration entry. The
877    // engine matches tag imports by exact canonical type identity, which holds
878    // because the payload is an intrinsic every module declares byte-identically.
879    // Sole tag import -> tag index 0.
880    let error_tag_func_type_idx = next_type_idx;
881    types.ty().function(
882        [ValType::Ref(RefType {
883            nullable: false,
884            heap_type: HeapType::Concrete(intrinsics.error),
885        })],
886        Vec::<ValType>::new(),
887    );
888    crate::codegen::next_index(&mut next_type_idx)?;
889    import_section.import(
890        crate::runtime::prelude::MODULE_NAME,
891        crate::mangle::prelude("__error_tag").as_str(),
892        EntityType::Tag(TagType {
893            kind: TagKind::Exception,
894            func_type_idx: error_tag_func_type_idx,
895        }),
896    );
897    symbols.set_error_tag_idx(0);
898
899    // Recursive cast and narrowed-field targets compile to validator functions;
900    // discover them up front so their internal object shapes contribute field-name
901    // globals even when no literal of that shape appears in the program.
902    let dependency_types = dependency_usage.dependency_types(dependencies);
903    let validator_bodies =
904        recursive_validators::ValidatorBodies::collect(ta, dependency_types.iter());
905    let recursive_validators =
906        recursive_validators::discover(ta, dependency_types.iter(), &validator_bodies)?;
907    let mut all_shapes = ta.shapes.clone();
908    all_shapes.extend(recursive_validators.extra_shapes.iter().cloned());
909
910    let dependency_shapes = dependency_usage.dependency_shapes(dependencies);
911    let user_emitted_types: Vec<Type> =
912        user_subtypes::collect_object_shapes(dependency_shapes.iter().copied(), &all_shapes);
913    let (type_info, type_info_index) = crate::TypeInfoTable::collect_indexed(
914        ta.package_name.clone(),
915        &all_shapes,
916        &user_emitted_types,
917    );
918    let needs_host_object_to_json =
919        user_subtypes::needs_host_object_to_json_adapter(&user_emitted_types);
920
921    // Closure types emitted before boxes: value_type(Type::Function) needs the closure type index.
922    // Env structs come after boxes since their fields may reference (ref $box_T).
923    let closure_metas = analysis.closure_metas;
924    let adapter_metas = analysis.adapter_metas;
925    let mut mentioned_closure_sigs = analysis.mentioned_closure_sigs;
926    // Validator discovery can require predicate closures even when source
927    // expressions never construct or call a closure of this signature.
928    if !recursive_validators.descriptor_types.is_empty() {
929        mentioned_closure_sigs.push(field_guards::signature());
930    }
931    let dependency_values = dependency_usage.dependency_values(dependencies);
932    let hof_dependency_sigs = closures::collect_from_dependencies(
933        dependency_values.iter().map(|value| value.symbol),
934        dependency_shapes.iter().copied(),
935        dependency_types.iter(),
936        &dependency_usage,
937    )?;
938    let class_member_sigs = closures::class_member_sigs(ta, dependencies)?;
939    // The closures this module builds, then the three collectors for shapes it
940    // only names — see the `closures` module doc for why each is needed.
941    closures::emit_func_and_struct_types(
942        closure_metas
943            .iter()
944            .map(|m| closures::classify(&m.signature))
945            .chain(
946                adapter_metas
947                    .iter()
948                    .map(|a| closures::classify(&a.signature)),
949            )
950            .collect::<Result<Vec<_>, _>>()?
951            .into_iter()
952            .chain(hof_dependency_sigs.iter().copied())
953            .chain(class_member_sigs.iter().copied())
954            .chain(mentioned_closure_sigs.iter().copied()),
955        &mut types,
956        &mut symbols,
957        &mut next_type_idx,
958    )?;
959
960    // Box types emitted after user subtypes and closure types so their field refs resolve.
961    let boxed_value_types = box_types::collect(ta, &symbols)?;
962    box_types::emit(
963        &boxed_value_types,
964        &mut types,
965        &mut symbols,
966        &mut next_type_idx,
967    );
968
969    // Env structs must come after box_types::emit.
970    closures::emit_env_types(&closure_metas, &mut types, &mut symbols, &mut next_type_idx)?;
971
972    // Wasm uses separate index spaces for functions and globals.
973    let mut next_global_idx: u32 = 0;
974
975    // Reconstruct imported classes (their rec groups + function imports + vtable
976    // globals) before the local class plan, so a local subclass can subtype an
977    // imported parent and lay its fields out after the parent's (SUB-488).
978    symbols.record_rewritable_members(classes::RewritableMembers::of(ta)?);
979    let imported_class_layouts = imported_classes::reconstruct(
980        dependencies,
981        ta,
982        &dependency_usage,
983        intrinsics,
984        file,
985        &mut types,
986        &mut next_type_idx,
987        &mut import_section,
988        &mut next_func_idx,
989        &mut next_global_idx,
990        &mut symbols,
991    )?;
992
993    // Class rec group last among types so class field slots can resolve string /
994    // array / closure / box field types via `value_type`.
995    let mut class_plan = classes::ClassPlan::collect(ta, &imported_class_layouts)?;
996    class_plan.reserve_and_emit_types(&mut types, &mut next_type_idx, &mut symbols, intrinsics)?;
997    for value in &dependency_values {
998        let defs = value.package;
999        // Virtual-package functions (`@mcp/<server>`, `@tools`) aren't per-function
1000        // Wasm imports: every call lowers to the package's single host fn (see
1001        // `emit_virtual_call`), so skip emitting imports for them. Their structural
1002        // shapes are still collected via `defs.shapes`.
1003        if defs.virtual_dispatch.is_some() {
1004            continue;
1005        }
1006        import_value_symbol(
1007            value.export_name.as_ref(),
1008            value.symbol,
1009            defs,
1010            &intrinsics,
1011            &mut next_type_idx,
1012            &mut next_func_idx,
1013            &mut next_global_idx,
1014            &mut types,
1015            &mut import_section,
1016            &mut symbols,
1017        )?;
1018    }
1019    function_adapters::import_shared_closure_caches(
1020        &adapter_metas,
1021        dependencies,
1022        &mut import_section,
1023        &mut symbols,
1024        &mut next_global_idx,
1025    )?;
1026    let exported_closure_caches = function_adapters::exported_closure_caches(ta);
1027
1028    // VTable globals are not in PackageDeclaration — there is no language-level Type for $VTable.
1029    let vtable_ref = ValType::Ref(RefType {
1030        nullable: false,
1031        heap_type: HeapType::Concrete(intrinsics.vtable),
1032    });
1033    // The object-identity and boxed-primitive vtables are host-owned; the rest
1034    // are still prelude-built. The mangled lookup key stays `prelude(name)`
1035    // either way, so downstream consumers are unaffected by the source module.
1036    for (module, name) in [
1037        (crate::runtime::prelude::MODULE_NAME, "string_vtable"),
1038        (crate::runtime::prelude::MODULE_NAME, "boxed_number_vtable"),
1039        (crate::runtime::prelude::MODULE_NAME, "boxed_boolean_vtable"),
1040        (crate::runtime::prelude::MODULE_NAME, "array_vtable"),
1041        (crate::runtime::prelude::MODULE_NAME, "closure_vtable"),
1042        (crate::runtime::prelude::MODULE_NAME, "object_vtable"),
1043    ] {
1044        let mangled = crate::mangle::prelude(name);
1045        import_section.import(
1046            module,
1047            mangled.as_str(),
1048            EntityType::Global(GlobalType {
1049                val_type: vtable_ref,
1050                mutable: false,
1051                shared: false,
1052            }),
1053        );
1054        symbols.record_global(crate::mangle::prelude(name), next_global_idx);
1055        crate::codegen::next_index(&mut next_global_idx)?;
1056    }
1057    // The store's single `undefined`, read with `global.get` rather than a host call.
1058    let undefined_value = crate::mangle::prelude(crate::runtime::prelude::undefined::GLOBAL_NAME);
1059    import_section.import(
1060        crate::runtime::prelude::MODULE_NAME,
1061        undefined_value.as_str(),
1062        EntityType::Global(GlobalType {
1063            val_type: ValType::Ref(RefType {
1064                nullable: false,
1065                heap_type: HeapType::Concrete(intrinsics.undefined),
1066            }),
1067            mutable: false,
1068            shared: false,
1069        }),
1070    );
1071    symbols.record_global(undefined_value, next_global_idx);
1072    crate::codegen::next_index(&mut next_global_idx)?;
1073    if dependency_usage.uses_bigint() {
1074        let mangled = crate::mangle::prelude("bigint_vtable");
1075        import_section.import(
1076            crate::runtime::prelude::MODULE_NAME,
1077            mangled.as_str(),
1078            EntityType::Global(GlobalType {
1079                val_type: vtable_ref,
1080                mutable: false,
1081                shared: false,
1082            }),
1083        );
1084        symbols.record_global(crate::mangle::prelude("bigint_vtable"), next_global_idx);
1085        crate::codegen::next_index(&mut next_global_idx)?;
1086    }
1087
1088    // Bigint host calls use `(ref $rawBigInt) + i32 sign`, which is not expressible
1089    // in the language-level Type/Param model. Import only the calls this module emits.
1090    let raw_bigint_val_type = ValType::Ref(RefType {
1091        nullable: false,
1092        heap_type: HeapType::Concrete(intrinsics.raw_bigint),
1093    });
1094    let mut bigint_binop_sig_idx = None;
1095    let mut import_bigint_host = |name: &str, sig_idx: u32| -> Result<(), CompilerFailure> {
1096        let mangled = crate::mangle::host(crate::runtime::BIGINT_MODULE_NAME, name);
1097        import_section.import(
1098            crate::runtime::BIGINT_MODULE_NAME,
1099            mangled.as_str(),
1100            EntityType::Function(sig_idx),
1101        );
1102        symbols.record_func(mangled, next_func_idx);
1103        crate::codegen::next_index(&mut next_func_idx)?;
1104        Ok(())
1105    };
1106    for name in ["add", "sub", "mul", "div", "mod", "pow"] {
1107        if dependency_usage.is_host_value_used(crate::runtime::BIGINT_MODULE_NAME, name) {
1108            let sig_idx = if let Some(sig_idx) = bigint_binop_sig_idx {
1109                sig_idx
1110            } else {
1111                let sig_idx = next_type_idx;
1112                crate::codegen::next_index(&mut next_type_idx)?;
1113                types.ty().function(
1114                    [
1115                        ValType::I32,
1116                        raw_bigint_val_type,
1117                        ValType::I32,
1118                        raw_bigint_val_type,
1119                    ],
1120                    [ValType::I32, raw_bigint_val_type],
1121                );
1122                bigint_binop_sig_idx = Some(sig_idx);
1123                sig_idx
1124            };
1125            import_bigint_host(name, sig_idx)?;
1126        }
1127    }
1128    if dependency_usage.is_host_value_used(crate::runtime::BIGINT_MODULE_NAME, "cmp") {
1129        let sig_idx = {
1130            let sig_idx = next_type_idx;
1131            crate::codegen::next_index(&mut next_type_idx)?;
1132            types.ty().function(
1133                [
1134                    ValType::I32,
1135                    raw_bigint_val_type,
1136                    ValType::I32,
1137                    raw_bigint_val_type,
1138                ],
1139                [ValType::I32],
1140            );
1141            sig_idx
1142        };
1143        import_bigint_host("cmp", sig_idx)?;
1144    }
1145    if dependency_usage.is_host_value_used(crate::runtime::BIGINT_MODULE_NAME, "neg") {
1146        let sig_idx = {
1147            let sig_idx = next_type_idx;
1148            crate::codegen::next_index(&mut next_type_idx)?;
1149            types.ty().function(
1150                [ValType::I32, raw_bigint_val_type],
1151                [ValType::I32, raw_bigint_val_type],
1152            );
1153            sig_idx
1154        };
1155        import_bigint_host("neg", sig_idx)?;
1156    }
1157    if dependency_usage.is_host_value_used(crate::runtime::BIGINT_MODULE_NAME, "fromNumber") {
1158        let sig_idx = {
1159            let sig_idx = next_type_idx;
1160            crate::codegen::next_index(&mut next_type_idx)?;
1161            types
1162                .ty()
1163                .function([ValType::F64], [ValType::I32, raw_bigint_val_type]);
1164            sig_idx
1165        };
1166        import_bigint_host("fromNumber", sig_idx)?;
1167    }
1168    if dependency_usage.is_host_value_used(crate::runtime::BIGINT_MODULE_NAME, "fromString") {
1169        let raw_string_val_type = ValType::Ref(RefType {
1170            nullable: false,
1171            heap_type: HeapType::Concrete(intrinsics.raw_string),
1172        });
1173        let sig_idx = {
1174            let sig_idx = next_type_idx;
1175            crate::codegen::next_index(&mut next_type_idx)?;
1176            types
1177                .ty()
1178                .function([raw_string_val_type], [ValType::I32, raw_bigint_val_type]);
1179            sig_idx
1180        };
1181        import_bigint_host("fromString", sig_idx)?;
1182    }
1183    if dependency_usage.is_host_value_used(crate::runtime::BIGINT_MODULE_NAME, "toString") {
1184        let raw_string_val_type = ValType::Ref(RefType {
1185            nullable: false,
1186            heap_type: HeapType::Concrete(intrinsics.raw_string),
1187        });
1188        let sig_idx = {
1189            let sig_idx = next_type_idx;
1190            crate::codegen::next_index(&mut next_type_idx)?;
1191            types
1192                .ty()
1193                .function([ValType::I32, raw_bigint_val_type], [raw_string_val_type]);
1194            sig_idx
1195        };
1196        import_bigint_host("toString", sig_idx)?;
1197    }
1198    if dependency_usage.is_host_value_used(crate::runtime::NUMBER_MODULE_NAME, "fromBigInt") {
1199        let sig_idx = {
1200            let sig_idx = next_type_idx;
1201            crate::codegen::next_index(&mut next_type_idx)?;
1202            types
1203                .ty()
1204                .function([ValType::I32, raw_bigint_val_type], [ValType::F64]);
1205            sig_idx
1206        };
1207        let from_bigint = crate::mangle::host(crate::runtime::NUMBER_MODULE_NAME, "fromBigInt");
1208        import_section.import(
1209            crate::runtime::NUMBER_MODULE_NAME,
1210            from_bigint.as_str(),
1211            EntityType::Function(sig_idx),
1212        );
1213        symbols.record_func(from_bigint, next_func_idx);
1214        crate::codegen::next_index(&mut next_func_idx)?;
1215    }
1216
1217    let intrinsic_string_ref = ValType::Ref(RefType {
1218        nullable: false,
1219        heap_type: HeapType::Concrete(intrinsics.string),
1220    });
1221    let intrinsic_object_ref = ValType::Ref(RefType {
1222        nullable: false,
1223        heap_type: HeapType::Concrete(intrinsics.object),
1224    });
1225    for dependency_type in &dependency_types {
1226        let defs = dependency_type.package;
1227        let iface_name = dependency_type.name;
1228        let ty_sym = dependency_type.symbol;
1229        let TypeKind::Interface {
1230            dispatch,
1231            methods,
1232            properties,
1233            ..
1234        } = &ty_sym.kind
1235        else {
1236            continue;
1237        };
1238        if *dispatch == crate::Dispatch::VTable {
1239            continue;
1240        }
1241        // Record dispatch so emit_method_call can branch without re-walking dependencies.
1242        symbols.record_iface_dispatch(ty_sym.mangled_name.clone(), *dispatch);
1243        let intrinsic_bigint_ref = ValType::Ref(RefType {
1244            nullable: false,
1245            heap_type: HeapType::Concrete(intrinsics.bigint),
1246        });
1247        let receiver_wasm: Option<ValType> = match dispatch {
1248            crate::Dispatch::Static => None,
1249            crate::Dispatch::Direct => Some(match iface_name {
1250                "Number" => ValType::F64,
1251                "Boolean" => ValType::I32,
1252                "String" => intrinsic_string_ref,
1253                "BigInt" => intrinsic_bigint_ref,
1254                _ => intrinsic_object_ref,
1255            }),
1256            crate::Dispatch::VTable => {
1257                return Err(crate::codegen::internal_failure(
1258                    "invalid compiler emission dispatch",
1259                ));
1260            }
1261        };
1262        for (method_name, sig) in methods {
1263            if !dependency_usage.is_interface_member_used(ty_sym, method_name) {
1264                continue;
1265            }
1266            // TODO(reified generics): `Response#json` is recognised by the typechecker
1267            // and lowered to `JSON.parse(body)`; it has no host export, so skip its
1268            // import (declaring it would import an unresolvable symbol and fail link).
1269            // Remove once reified generics let the shim host a real per-call validator
1270            // (SUB-363).
1271            if defs.package_name.as_str() == crate::stdlib::http::MODULE_NAME
1272                && iface_name == "Response"
1273                && method_name == "json"
1274            {
1275                continue;
1276            }
1277            let mangled = crate::mangle::extend(&ty_sym.mangled_name, method_name);
1278            // Recorded above the host-implemented `continue` below, not after it:
1279            // the methods that take that branch are precisely the ones whose call
1280            // sites have no other record of the erased slots.
1281            let abi = declared_wrapper_abi(&symbols, &mangled, sig, receiver_wasm.is_some())?;
1282            symbols.record_iface_method_abi(mangled.clone(), abi.clone());
1283            // Routing invariant: a method whose dispatch key resolved in the
1284            // earlier value-symbol loop is host-implemented (every prelude
1285            // method is — its value symbol is declared beside the Rust impl in
1286            // `runtime::prelude`); only interface members without one
1287            // (stdlib shim exports like `File#close`) import here.
1288            // The receiver, when the method has one, is a Wasm parameter
1289            // without a default.
1290            let host_defaults = is_host_package(&defs.package_name).then(|| {
1291                std::iter::repeat_n(None, usize::from(receiver_wasm.is_some()))
1292                    .chain(sig.params.iter().map(|param| param.default.clone()))
1293                    .collect::<Vec<_>>()
1294            });
1295            if let Some(func_idx) = symbols.func_idx(&mangled) {
1296                if let Some(defaults) = host_defaults {
1297                    symbols.record_host_parameter_defaults(func_idx, defaults);
1298                }
1299                continue;
1300            }
1301            let mut params: Vec<ValType> = Vec::new();
1302            if let Some(recv) = receiver_wasm {
1303                params.push(recv);
1304            }
1305            params.extend(abi.params.iter().copied());
1306            if let Some(defaults) = host_defaults {
1307                symbols.record_host_parameter_defaults(next_func_idx, defaults);
1308            }
1309            let results: Vec<ValType> = abi.ret.into_iter().collect();
1310            let sig_idx = next_type_idx;
1311            crate::codegen::next_index(&mut next_type_idx)?;
1312            types.ty().function(params, results);
1313            import_section.import(
1314                defs.package_name.as_str(),
1315                mangled.as_str(),
1316                EntityType::Function(sig_idx),
1317            );
1318            symbols.record_func(mangled, next_func_idx);
1319            crate::codegen::next_index(&mut next_func_idx)?;
1320        }
1321        for (prop_name, sig) in properties {
1322            // An intrinsic member is emitted inline by codegen (`String#length`
1323            // → payload `struct.get` + `array.len`) — there is no getter to
1324            // import; record it so the emitter picks its inline lowering.
1325            if sig.intrinsic {
1326                symbols.record_intrinsic_member(crate::mangle::extend(
1327                    &ty_sym.mangled_name,
1328                    prop_name,
1329                ));
1330                continue;
1331            }
1332            if !dependency_usage.is_interface_member_used(ty_sym, prop_name) {
1333                continue;
1334            }
1335            let mangled = crate::mangle::extend(&ty_sym.mangled_name, prop_name);
1336            // Same routing invariant as the method loop above, for property
1337            // getters (`Set#size`) and static constants (`Number.EPSILON`) —
1338            // resolved as a func or a global respectively.
1339            if symbols.func_idx(&mangled).is_some() || symbols.global_idx(&mangled).is_some() {
1340                continue;
1341            }
1342            let Some(recv) = receiver_wasm else {
1343                // Static-interface property (`Number.EPSILON`): there is
1344                // no receiver to dispatch on, so it imports as a constant
1345                // global rather than a getter function. Mutable to match the
1346                // prelude's value-global exports (const-ness is typecheck-enforced).
1347                import_section.import(
1348                    defs.package_name.as_str(),
1349                    mangled.as_str(),
1350                    EntityType::Global(wasm_encoder::GlobalType {
1351                        val_type: symbols.value_type(&sig.ty)?,
1352                        mutable: true,
1353                        shared: false,
1354                    }),
1355                );
1356                symbols.record_typed_global(mangled, next_global_idx, sig.ty.clone());
1357                crate::codegen::next_index(&mut next_global_idx)?;
1358                continue;
1359            };
1360            let params = vec![recv];
1361            let results = symbols.wasm_result(&sig.ty)?;
1362            let sig_idx = next_type_idx;
1363            crate::codegen::next_index(&mut next_type_idx)?;
1364            types.ty().function(params, results);
1365            import_section.import(
1366                defs.package_name.as_str(),
1367                mangled.as_str(),
1368                EntityType::Function(sig_idx),
1369            );
1370            symbols.record_func(mangled, next_func_idx);
1371            crate::codegen::next_index(&mut next_func_idx)?;
1372        }
1373    }
1374    if needs_host_object_to_json {
1375        import_json_host_function(
1376            "stringifyTypedObject",
1377            &intrinsics,
1378            &mut next_type_idx,
1379            &mut next_func_idx,
1380            &mut types,
1381            &mut import_section,
1382            &mut symbols,
1383        )?;
1384    }
1385    import_json_host_function(
1386        "parse",
1387        &intrinsics,
1388        &mut next_type_idx,
1389        &mut next_func_idx,
1390        &mut types,
1391        &mut import_section,
1392        &mut symbols,
1393    )?;
1394
1395    // _start is always emitted even when empty, for uniform module shape.
1396    let start_type_idx = next_type_idx;
1397    crate::codegen::next_index(&mut next_type_idx)?;
1398    types
1399        .ty()
1400        .function(Vec::<ValType>::new(), Vec::<ValType>::new());
1401    let start_func_idx = next_func_idx;
1402    crate::codegen::next_index(&mut next_func_idx)?;
1403
1404    // Allocate function indices before emitting bodies, enabling forward references.
1405    let mut user_funcs: Vec<UserFunc> = Vec::new();
1406    let mut user_func_type_idx: BTreeMap<MangledName, u32> = BTreeMap::new();
1407    for f in &ta.functions {
1408        let sig_idx = next_type_idx;
1409        crate::codegen::next_index(&mut next_type_idx)?;
1410        let mut params: Vec<_> = f
1411            .params
1412            .iter()
1413            .map(|p| symbols.value_type(&p.ty))
1414            .collect::<Result<_, _>>()?;
1415        if !f.generics.is_empty() {
1416            symbols
1417                .runtime_generic_functions
1418                .insert(f.mangled_name.clone());
1419            params.push(runtime_descriptors::environment_type(&symbols)?);
1420        }
1421        types
1422            .ty()
1423            .function(params, symbols.wasm_result(&f.return_type)?);
1424        let func_idx = next_func_idx;
1425        crate::codegen::next_index(&mut next_func_idx)?;
1426        let param_types: Vec<Type> = f.params.iter().map(|p| p.ty.clone()).collect();
1427        if let Some(metadata) = call_arguments::typed_metadata(&f.params) {
1428            symbols
1429                .function_argument_metadata
1430                .insert(f.mangled_name.clone(), metadata);
1431        }
1432        symbols.record_local_fn(
1433            f.mangled_name.clone(),
1434            func_idx,
1435            param_types,
1436            f.return_type.clone(),
1437        )?;
1438        user_func_type_idx.insert(f.mangled_name.clone(), sig_idx);
1439        user_funcs.push(UserFunc {
1440            name: f.name.name.clone(),
1441            decl_span: f.name.span,
1442            type_idx: sig_idx,
1443            func_idx,
1444            body: f.body,
1445            return_type: f.return_type.clone(),
1446            params: f.params.clone(),
1447            generics: f.generics.clone(),
1448        });
1449    }
1450
1451    // Coercions can construct closures even when this module has no literals.
1452    let needs_closure_coercions = !mentioned_closure_sigs.is_empty();
1453    let closure_methods = if closure_metas.is_empty()
1454        && adapter_metas.is_empty()
1455        && class_member_sigs.is_empty()
1456        && !needs_closure_coercions
1457    {
1458        None
1459    } else {
1460        Some(closures::allocate_methods(&mut next_func_idx)?)
1461    };
1462
1463    for meta in &closure_metas {
1464        let func_idx = next_func_idx;
1465        crate::codegen::next_index(&mut next_func_idx)?;
1466        symbols.record_closure_func_idx(meta.expr_id, func_idx);
1467    }
1468
1469    for meta in &adapter_metas {
1470        let func_idx = next_func_idx;
1471        crate::codegen::next_index(&mut next_func_idx)?;
1472        symbols.record_adapter_func_idx(meta.mangled.clone(), func_idx);
1473    }
1474
1475    let closure_coercion_targets = if closure_methods.is_some() {
1476        closure_coercions::allocate(&mut symbols, &mut next_func_idx)?
1477    } else {
1478        Vec::new()
1479    };
1480
1481    // Allocated after user functions; vtable globals reference these via ref.func.
1482    let mut user_subtypes_alloc: Vec<user_subtypes::UserSubtype> = Vec::new();
1483    for ty in &user_emitted_types {
1484        if let Type::Object { .. } = ty {
1485            user_subtypes_alloc.push(user_subtypes::allocate_methods(ty, &mut next_func_idx)?);
1486        }
1487    }
1488
1489    // Class method stubs + per-class getter/setter (vtable/header globals ref.func these).
1490    class_plan.allocate_funcs(&mut next_func_idx, &mut symbols)?;
1491    let instance_field_guards = field_guards::allocate(ta, &mut symbols, &mut next_func_idx)?;
1492    let type_descriptors = runtime_descriptors::allocate(
1493        ta,
1494        &recursive_validators.descriptor_types,
1495        &mut symbols,
1496        &mut next_func_idx,
1497    )?;
1498
1499    let field_lookup_signature = field_lookup::allocate(
1500        &mut types,
1501        &mut symbols,
1502        &mut next_type_idx,
1503        &mut next_func_idx,
1504    )?;
1505
1506    // Recursive runtime validators. Allocate signatures and indices, then
1507    // register each back-edge key so structural checks can call its plan.
1508    let object_ref_null = ValType::Ref(RefType {
1509        nullable: true,
1510        heap_type: HeapType::Concrete(intrinsics.object),
1511    });
1512    let raw_array_ref = ValType::Ref(RefType {
1513        nullable: false,
1514        heap_type: HeapType::Concrete(intrinsics.raw_array),
1515    });
1516    let raw_index_array_ref = ValType::Ref(RefType {
1517        nullable: false,
1518        heap_type: HeapType::Concrete(intrinsics.raw_index_array),
1519    });
1520    let mut runtime_validator_sigs: Vec<u32> = Vec::with_capacity(recursive_validators.plans.len());
1521    for plan in &recursive_validators.plans {
1522        let sig_idx = next_type_idx;
1523        crate::codegen::next_index(&mut next_type_idx)?;
1524        types.ty().function(
1525            [
1526                object_ref_null,
1527                raw_array_ref,
1528                raw_index_array_ref,
1529                ValType::I32,
1530                runtime_descriptors::environment_type(&symbols)?,
1531            ],
1532            [ValType::I32],
1533        );
1534        let func_idx = next_func_idx;
1535        crate::codegen::next_index(&mut next_func_idx)?;
1536        symbols.record_runtime_validator(plan.key.clone(), func_idx);
1537        runtime_validator_sigs.push(sig_idx);
1538    }
1539
1540    // `main`'s result is encoded to its output `$string` entirely in wasm by the
1541    // `__main_output` shim (`() -> (ref null $string)`): scalars render via `toString`
1542    // (a `string` passes through verbatim — no JSON quoting), and structured
1543    // returns route through the same `toJson` machinery as `JSON.stringify`. Codegen
1544    // — which has the typed return type — is the single source of truth for the
1545    // encoding; the host reads the shim's `$string` verbatim. Undefined produces
1546    // no output; `never`/`error` never produce a value to encode.
1547    let main_func = ta.functions.iter().find(|f| f.name.name == "main");
1548    let main_return_ty = main_func.map(|f| f.return_type.clone());
1549    let main_output_shim = match main_return_ty.as_ref().map(Type::peel) {
1550        None | Some(Type::Never | Type::Error) => None,
1551        Some(_) => {
1552            let sig_idx = next_type_idx;
1553            types.ty().function(
1554                Vec::<ValType>::new(),
1555                [ValType::Ref(RefType {
1556                    nullable: true,
1557                    heap_type: HeapType::Concrete(intrinsics.string),
1558                })],
1559            );
1560            // Last consumer of the counter — nothing is emitted after this shim.
1561            let func_idx = next_func_idx;
1562            Some((sig_idx, func_idx))
1563        }
1564    };
1565
1566    let pkg_string_global_idx = if needs_host_object_to_json {
1567        let idx = next_global_idx;
1568        crate::codegen::next_index(&mut next_global_idx)?;
1569        Some(idx)
1570    } else {
1571        None
1572    };
1573
1574    let main_mangled = main_func.map(|f| f.mangled_name.clone());
1575    let main_func_idx = main_mangled
1576        .as_ref()
1577        .map(|mangled| {
1578            symbols.func_idx(mangled).ok_or_else(|| {
1579                crate::codegen::internal_failure("`main` recorded during user-function pre-pass")
1580            })
1581        })
1582        .transpose()?;
1583
1584    module.section(&types);
1585    module.section(&import_section);
1586
1587    let mut functions = FunctionSection::new();
1588    functions.function(start_type_idx);
1589    for f in &user_funcs {
1590        functions.function(f.type_idx);
1591    }
1592    if closure_methods.is_some() {
1593        closures::emit_method_function_entries(&mut functions, intrinsics);
1594    }
1595    for meta in &closure_metas {
1596        let sig_idx = symbols
1597            .closure_func_type_idx(closures::classify(&meta.signature)?)
1598            .ok_or_else(|| {
1599                crate::codegen::internal_failure(
1600                    "closures::emit_func_and_struct_types registered the funcref type",
1601                )
1602            })?;
1603        functions.function(sig_idx);
1604    }
1605    for meta in &adapter_metas {
1606        let sig_idx = symbols
1607            .closure_func_type_idx(closures::classify(&meta.signature)?)
1608            .ok_or_else(|| {
1609                crate::codegen::internal_failure(
1610                    "closures::emit_func_and_struct_types registered the adapter signature",
1611                )
1612            })?;
1613        functions.function(sig_idx);
1614    }
1615    closure_coercions::emit_entries(&closure_coercion_targets, &mut functions, &symbols)?;
1616    user_subtypes::emit_method_function_entries(&mut functions, &user_subtypes_alloc, intrinsics);
1617    class_plan.emit_function_entries(&mut functions, &symbols, intrinsics)?;
1618    for _ in instance_field_guards
1619        .iter()
1620        .map(|_| ())
1621        .chain(type_descriptors.iter().map(|_| ()))
1622    {
1623        functions.function(
1624            symbols
1625                .closure_func_type_idx(field_guards::signature())
1626                .ok_or_else(|| crate::codegen::internal_failure("guard signature registered"))?,
1627        );
1628    }
1629    functions.function(field_lookup_signature);
1630    for &sig_idx in &runtime_validator_sigs {
1631        functions.function(sig_idx);
1632    }
1633    if let Some((sig_idx, _)) = main_output_shim {
1634        functions.function(sig_idx);
1635    }
1636    module.section(&functions);
1637
1638    // Consumer-local globals are mutable so _start can initialize them; const immutability is typechecker-enforced.
1639    let mut globals = GlobalSection::new();
1640    let mut globals_count: u32 = 0;
1641    if pkg_string_global_idx.is_some() {
1642        globals.global(
1643            GlobalType {
1644                val_type: ValType::Ref(RefType {
1645                    nullable: true,
1646                    heap_type: HeapType::Concrete(intrinsics.string),
1647                }),
1648                mutable: true,
1649                shared: false,
1650            },
1651            &ConstExpr::ref_null(HeapType::Concrete(intrinsics.string)),
1652        );
1653        globals_count = globals_count
1654            .checked_add(1)
1655            .ok_or_else(|| crate::codegen::internal_failure("Wasm index count overflow"))?;
1656    }
1657    for g in &ta.globals {
1658        let val_type = global_val_type(&g.ty, &symbols)?;
1659        globals.global(
1660            GlobalType {
1661                val_type,
1662                mutable: true,
1663                shared: false,
1664            },
1665            &default_const_expr(val_type)?,
1666        );
1667        symbols.record_typed_global(g.mangled_name.clone(), next_global_idx, g.ty.clone());
1668        crate::codegen::next_index(&mut next_global_idx)?;
1669        globals_count = globals_count
1670            .checked_add(1)
1671            .ok_or_else(|| crate::codegen::internal_failure("Wasm index count overflow"))?;
1672    }
1673    let init_flags_count =
1674        init_guard::allocate_flags(ta, &mut globals, &mut symbols, &mut next_global_idx)?;
1675    globals_count = globals_count
1676        .checked_add(init_flags_count)
1677        .ok_or_else(|| crate::codegen::internal_failure("Wasm index count overflow"))?;
1678    let vtable_globals_count = wasm_u32(user_subtypes_alloc.len())?;
1679    user_subtypes::emit_vtable_globals(
1680        &mut globals,
1681        &mut user_subtypes_alloc,
1682        &mut symbols,
1683        &mut next_global_idx,
1684        intrinsics,
1685    )?;
1686    let mut field_names_shapes = field_names::collect(&user_emitted_types);
1687    // Classes carry a field-names array global too (header slot 1); add each
1688    // class's declaration-order field-name list, deduping against object shapes.
1689    for class_fields in class_plan.field_name_lists() {
1690        if !field_names_shapes.contains(&class_fields) {
1691            field_names_shapes.push(class_fields);
1692        }
1693    }
1694    let string_vtable_global_idx =
1695        symbols.prelude_global_idx("string_vtable").ok_or_else(|| {
1696            crate::codegen::internal_failure(
1697                "string_vtable imported from prelude in the hard-coded import bootstrap",
1698            )
1699        })?;
1700    let field_names_globals_count = wasm_u32(field_names_shapes.len())?;
1701    field_names::emit(
1702        &field_names_shapes,
1703        &mut globals,
1704        &mut symbols,
1705        &mut next_global_idx,
1706        intrinsics,
1707        string_vtable_global_idx,
1708    )?;
1709    // Names that need a per-name `$string` global even when no object literal
1710    // of that shape appears in this module — field access and name-based
1711    // dispatch read the global by name. Object-literal field names come from
1712    // `user_emitted_types`; the cases below do not.
1713    let mut extra_field_names = analysis.extra_field_names;
1714    for dependency_type in &dependency_types {
1715        let ty_sym = dependency_type.symbol;
1716        if let TypeKind::Interface {
1717            methods,
1718            properties,
1719            dispatch: crate::Dispatch::VTable,
1720            ..
1721        } = &ty_sym.kind
1722        {
1723            for method_name in methods.keys() {
1724                if dependency_usage.is_interface_member_used(ty_sym, method_name) {
1725                    extra_field_names.push(method_name.clone());
1726                }
1727            }
1728            for property_name in properties.keys() {
1729                if dependency_usage.is_interface_member_used(ty_sym, property_name) {
1730                    extra_field_names.push(property_name.clone());
1731                }
1732            }
1733        } else if let TypeKind::Class { accessors, .. } = &ty_sym.kind {
1734            // An imported class's accessor getter/setter names + the property name
1735            // are scanned at runtime by the dynamic property path, so the consumer
1736            // needs their `$string` globals too.
1737            for acc in accessors {
1738                extra_field_names.push(acc.name().to_string());
1739                match acc {
1740                    crate::AccessorSig::Getter { name, .. } => {
1741                        extra_field_names.push(crate::codegen::classes::accessor_getter_name(name));
1742                    }
1743                    crate::AccessorSig::Setter { name, .. } => {
1744                        extra_field_names.push(crate::codegen::classes::accessor_setter_name(name));
1745                    }
1746                }
1747            }
1748        }
1749    }
1750    // Class field names need per-name `$string` globals for the getter's `ref.eq`/
1751    // `string_eq` comparisons.
1752    for class_fields in class_plan.field_name_lists() {
1753        extra_field_names.extend(class_fields.into_iter().map(|field| field.name));
1754    }
1755    // Accessor property names are scanned at runtime by the dynamic property path
1756    // even though they back no data slot, so they need their own `$string` globals.
1757    extra_field_names.extend(class_plan.accessor_property_names());
1758    let field_name_strings = field_name_strings::collect(&user_emitted_types, &extra_field_names);
1759    let field_name_strings_count = wasm_u32(field_name_strings.len())?;
1760    field_name_strings::emit(
1761        &field_name_strings,
1762        &mut globals,
1763        &mut symbols,
1764        &mut next_global_idx,
1765        intrinsics,
1766        string_vtable_global_idx,
1767    )?;
1768    // Per-class header singletons.
1769    class_plan.emit_globals(&mut globals, &mut symbols, &mut next_global_idx, intrinsics)?;
1770    let mut closure_methods_for_emit = closure_methods;
1771    let closure_vtable_count = if let Some(methods) = closure_methods_for_emit.as_mut() {
1772        closures::emit_vtable_global(
1773            &mut globals,
1774            methods,
1775            &mut symbols,
1776            &mut next_global_idx,
1777            intrinsics,
1778        )?;
1779        1
1780    } else {
1781        0
1782    };
1783    let adapter_closure_globals_count = function_adapters::allocate_closure_globals(
1784        &adapter_metas,
1785        &exported_closure_caches,
1786        &mut globals,
1787        &mut symbols,
1788        &mut next_global_idx,
1789    )?;
1790    let descriptor_globals_count =
1791        runtime_descriptors::allocate_globals(&mut globals, &mut symbols, &mut next_global_idx)?;
1792    if [
1793        adapter_closure_globals_count,
1794        descriptor_globals_count,
1795        globals_count,
1796        vtable_globals_count,
1797        field_names_globals_count,
1798        field_name_strings_count,
1799        closure_vtable_count,
1800    ]
1801    .iter()
1802    .any(|&count| count > 0)
1803    {
1804        module.section(&globals);
1805    }
1806
1807    let mut exports = ExportSection::new();
1808    if let Some(main_func_idx) = main_func_idx {
1809        exports.export("main", WasmExportKind::Func, main_func_idx);
1810    }
1811    if let Some((_, func_idx)) = main_output_shim {
1812        exports.export("__main_output", WasmExportKind::Func, func_idx);
1813    }
1814    for entry in &ta.exports {
1815        match entry.kind {
1816            crate::ExportKind::Function => {
1817                let func_idx = symbols
1818                    .top_level_fn(&entry.target)
1819                    .map(|f| f.wasm_idx)
1820                    .ok_or_else(|| {
1821                        crate::codegen::internal_failure(
1822                            "checked package export function target exists",
1823                        )
1824                    })?;
1825                exports.export(entry.public_name.as_str(), WasmExportKind::Func, func_idx);
1826            }
1827            crate::ExportKind::Global => {
1828                let global_idx = symbols.global_idx(&entry.target).ok_or_else(|| {
1829                    crate::codegen::internal_failure("checked package export global target exists")
1830                })?;
1831                exports.export(
1832                    entry.public_name.as_str(),
1833                    WasmExportKind::Global,
1834                    global_idx,
1835                );
1836            }
1837            crate::ExportKind::Type => {}
1838        }
1839    }
1840    // Export each exported class's entry points (constructor, ctor-init, method
1841    // bodies) so another package can construct, dispatch, and `extends` it. A
1842    // class export is an `ExportKind::Type` entry whose target is the class's
1843    // mangled name (SUB-488).
1844    // Hidden implementations also export their runtime entry points. Their
1845    // declarations stay in compiler-only metadata, outside the source API.
1846    let exported_class_mangles: BTreeSet<&MangledName> = ta
1847        .types
1848        .iter()
1849        .filter_map(|decl| match decl {
1850            crate::TypedTypeDecl::Class(class) => Some(&class.mangled_name),
1851            _ => None,
1852        })
1853        .collect();
1854    for (name, func_idx) in
1855        class_plan.exported_funcs(&symbols, |m| exported_class_mangles.contains(m))?
1856    {
1857        exports.export(name.as_str(), WasmExportKind::Func, func_idx);
1858    }
1859    for (name, global_idx) in
1860        class_plan.exported_vtable_globals(&symbols, |m| exported_class_mangles.contains(m))?
1861    {
1862        exports.export(name.as_str(), WasmExportKind::Global, global_idx);
1863    }
1864    for (public_name, function) in &exported_closure_caches {
1865        let global_idx = symbols
1866            .adapter_closure_global_idx(function)
1867            .ok_or_else(|| crate::codegen::internal_failure("exported closure cache allocated"))?;
1868        exports.export(
1869            crate::mangle::closure_cache(public_name).as_str(),
1870            WasmExportKind::Global,
1871            global_idx,
1872        );
1873    }
1874    module.section(&exports);
1875
1876    module.section(&StartSection {
1877        function_index: start_func_idx,
1878    });
1879
1880    // Wasm requires declarative element coverage for any function referenced by ref.func outside element segments.
1881    let mut declared = user_subtypes::declared_method_funcs(&user_subtypes_alloc);
1882    if let Some(methods) = closure_methods_for_emit {
1883        declared.extend(closures::declared_funcs(methods));
1884    }
1885    for meta in &closure_metas {
1886        let idx = symbols
1887            .closure_func_idx(meta.expr_id)
1888            .ok_or_else(|| crate::codegen::internal_failure("closure func index allocated"))?;
1889        declared.push(idx);
1890    }
1891    for meta in &adapter_metas {
1892        let idx = symbols
1893            .adapter_func_idx(&meta.mangled)
1894            .ok_or_else(|| crate::codegen::internal_failure("adapter func index allocated"))?;
1895        declared.push(idx);
1896    }
1897    declared.extend(
1898        closure_coercion_targets
1899            .iter()
1900            .map(|&sig| {
1901                symbols
1902                    .closure_coercion(sig)
1903                    .ok_or_else(|| crate::codegen::internal_failure("coercion allocated"))
1904            })
1905            .collect::<Result<Vec<_>, _>>()?,
1906    );
1907    declared.extend(class_plan.declared_funcs(&symbols));
1908    declared.extend(instance_field_guards.iter().map(|guard| guard.function));
1909    declared.extend(type_descriptors.iter().map(|(_, function)| *function));
1910    if !declared.is_empty() {
1911        let mut elements = wasm_encoder::ElementSection::new();
1912        elements.declared(wasm_encoder::Elements::Functions(Cow::Owned(declared)));
1913        module.section(&elements);
1914    }
1915
1916    // DataCount must precede Code per Wasm spec.
1917    let total_data_segments = pool
1918        .strings
1919        .len()
1920        .checked_add(bigint_pool.literals.len())
1921        .ok_or_else(|| internal_failure("data segment count overflow"))?;
1922    if total_data_segments > 0 {
1923        module.section(&DataCountSection {
1924            count: wasm_u32(total_data_segments)?,
1925        });
1926    }
1927
1928    let ctx = CodegenCtx {
1929        ta,
1930        strings: &pool,
1931        bigints: &bigint_pool,
1932        symbols: &symbols,
1933        source,
1934        line_index: &line_index,
1935        file,
1936        validator_bodies: &validator_bodies,
1937        type_info: &type_info,
1938        package_string_global_idx: pkg_string_global_idx,
1939        failure: std::cell::Cell::new(None),
1940        validator_steps_left: std::cell::Cell::new(
1941            crate::compiler_limits::MAX_INLINE_VALIDATOR_STEPS,
1942        ),
1943        validator_root: std::cell::Cell::new(None),
1944        check_is_standalone: std::cell::Cell::new(false),
1945    };
1946
1947    let mut code = CodeSection::new();
1948    let mut start_emitter = FunctionEmitter::new(&ctx, &[])?;
1949    if let Some(pkg_string_global_idx) = pkg_string_global_idx {
1950        emit_package_string_init(
1951            &mut start_emitter,
1952            ta.package_name.as_str(),
1953            pkg_string_global_idx,
1954            intrinsics.raw_string,
1955            intrinsics.string,
1956            string_vtable_global_idx,
1957        )?;
1958    }
1959    for (index, &stmt_id) in ctx.ta.top_level_statements.iter().enumerate() {
1960        init_guard::emit_mark_classes_declared(&mut start_emitter, &ctx, index);
1961        emit_statement(&mut start_emitter, &ctx, stmt_id)?;
1962        ctx.check_failure()?;
1963    }
1964    init_guard::emit_mark_classes_declared(
1965        &mut start_emitter,
1966        &ctx,
1967        ctx.ta.top_level_statements.len(),
1968    );
1969    let mut debug_functions = dwarf::DebugFunctions::default();
1970    debug_functions.write(
1971        &mut code,
1972        start_emitter.build_with_lines()?,
1973        TOP_LEVEL_FRAME_NAME.to_string(),
1974        crate::Span::at(ctx.file),
1975    )?;
1976
1977    for func in &user_funcs {
1978        let built = function_emitter::emit_function(
1979            &func.generics,
1980            &ctx,
1981            &func.params,
1982            func.body,
1983            &func.return_type,
1984        )?;
1985        ctx.check_failure()?;
1986        debug_functions.write(&mut code, built, func.name.clone(), func.decl_span)?;
1987    }
1988
1989    if let Some(methods) = closure_methods_for_emit {
1990        closures::emit_method_bodies(&mut code, methods, &symbols)?;
1991    }
1992
1993    for meta in &closure_metas {
1994        let built = function_emitter::emit_closure_function(&ctx, meta)?;
1995        ctx.check_failure()?;
1996        let (name, decl_span) = closure_frame(ta, meta)?;
1997        debug_functions.write(&mut code, built, name, decl_span)?;
1998    }
1999
2000    function_adapters::emit_bodies(&adapter_metas, &mut code, &ctx)?;
2001    closure_coercions::emit_bodies(&closure_coercion_targets, &mut code, &ctx)?;
2002
2003    user_subtypes::emit_method_bodies(
2004        &mut code,
2005        &user_subtypes_alloc,
2006        &symbols,
2007        &type_info,
2008        &type_info_index,
2009        pkg_string_global_idx,
2010    )?;
2011
2012    class_plan.emit_bodies(&mut code, &mut debug_functions, &ctx)?;
2013    // A failure recorded by the bodies above comes before those of the checks
2014    // emitted as functions of their own below.
2015    ctx.check_failure()?;
2016    for guard in &instance_field_guards {
2017        code.function(&field_guards::body(&ctx, guard)?);
2018    }
2019    for (ty, _) in &type_descriptors {
2020        code.function(&runtime_descriptors::body(&ctx, ty)?);
2021    }
2022
2023    code.function(&field_lookup::body(&ctx)?);
2024    for (validator_id, plan) in recursive_validators.plans.iter().enumerate() {
2025        let validator = ctx.checking_standalone(&plan.key, || {
2026            cast_check::emit_runtime_validator_body(
2027                &ctx,
2028                &plan.key,
2029                &plan.body,
2030                plan.rejects_polymorphic_edge,
2031                i32::try_from(validator_id)
2032                    .map_err(|_| internal_failure("validator id exceeds i32"))?,
2033            )
2034        })?;
2035        code.function(&validator);
2036    }
2037
2038    if let (Some(_), Some(main_func_idx), Some(main_return_ty)) =
2039        (main_output_shim, main_func_idx, main_return_ty.as_ref())
2040    {
2041        code.function(&function_emitter::json::emit_main_output_shim(
2042            &ctx,
2043            main_func_idx,
2044            main_return_ty,
2045            source_main_return_ty.as_ref().unwrap_or(main_return_ty),
2046        )?);
2047    }
2048
2049    ctx.check_failure()?;
2050    module.section(&code);
2051
2052    // DWARF sections appear after Code, before Data — LLVM/Emscripten convention.
2053    let vec_count_size = leb128_u32_size(code.len()) as u64;
2054    let code_content_size = vec_count_size
2055        .checked_add(u64::try_from(code.byte_len()).map_err(|_| index_space_exhausted())?)
2056        .ok_or_else(index_space_exhausted)?;
2057
2058    let funcs = debug_functions.into_debug_info(vec_count_size)?;
2059    for (sect_name, bytes) in
2060        dwarf::build_dwarf(&funcs, code_content_size, filename, &debug_sources)?
2061    {
2062        module.section(&CustomSection {
2063            name: Cow::Borrowed(sect_name),
2064            data: Cow::Owned(bytes),
2065        });
2066    }
2067
2068    // The owning package, for the runtime to read back off the innermost wasm frame. This is
2069    // the sole source of caller identity for capability checks, so it ships on every module —
2070    // a module without it resolves to no principal and its gated calls are refused.
2071    {
2072        let mut names = wasm_encoder::NameSection::new();
2073        names.module(owning_package);
2074        module.section(&names);
2075    }
2076
2077    if total_data_segments > 0 {
2078        let mut data = DataSection::new();
2079        for i in 0..pool.strings.len() {
2080            data.passive(pool.utf16_le_bytes(i)?);
2081        }
2082        // Bigint segments indexed from pool.strings.len() at each array.new_data site.
2083        for i in 0..bigint_pool.literals.len() {
2084            data.passive(bigint_pool.le_bytes(i)?);
2085        }
2086        module.section(&data);
2087    }
2088
2089    Ok(GeneratedModule {
2090        wasm: module.finish(),
2091        type_info,
2092        runtime_functions: runtime_values::signatures(ta),
2093        runtime_globals: runtime_values::global_types(ta),
2094        closure_caches: exported_closure_caches.into_keys().collect(),
2095    })
2096}
2097
2098/// Backtrace names for functions without a source name: the start function,
2099/// which runs a module's top-level statements, and an unnamed closure. Angle
2100/// brackets keep them apart from any source identifier.
2101const TOP_LEVEL_FRAME_NAME: &str = "<top level>";
2102const ANONYMOUS_FRAME_NAME: &str = "<anonymous>";
2103
2104/// The backtrace name and declaration span of a closure: its own name when it is
2105/// a named function expression or a nested function declaration, otherwise
2106/// [`ANONYMOUS_FRAME_NAME`] at the closure expression.
2107fn closure_frame(
2108    ta: &TypedAst,
2109    meta: &closures::ClosureMeta,
2110) -> Result<(String, crate::Span), CompilerFailure> {
2111    let name = meta
2112        .self_name
2113        .as_ref()
2114        .or_else(|| ta.nested_function_names.get(&meta.expr_id));
2115    if let Some(name) = name {
2116        return Ok((name.name.clone(), name.span));
2117    }
2118    let span = ta.try_expr(meta.expr_id).map_err(arena_failure)?.span;
2119    Ok((ANONYMOUS_FRAME_NAME.to_string(), span))
2120}
2121
2122struct UserFunc {
2123    name: String,
2124    decl_span: crate::Span,
2125    type_idx: u32,
2126    #[allow(dead_code)]
2127    func_idx: u32,
2128    body: StmtId,
2129    return_type: Type,
2130    params: Vec<crate::TypedParam>,
2131    generics: Vec<String>,
2132}
2133
2134/// The encoded size of `v` as unsigned LEB128: a function body's local count,
2135/// or the Code section's function count, which `CodeSection::byte_len`
2136/// excludes and DWARF offsets add back.
2137fn leb128_u32_size(mut v: u32) -> usize {
2138    let mut size = 0;
2139    loop {
2140        size += 1;
2141        v >>= 7;
2142        if v == 0 {
2143            return size;
2144        }
2145    }
2146}
2147
2148fn json_host_signature(
2149    name: &str,
2150    params: &[Param],
2151    symbols: &SymbolTable,
2152    intrinsics: &intrinsics::IntrinsicTypeIndices,
2153) -> Result<
2154    (Vec<wasm_encoder::ValType>, Vec<wasm_encoder::ValType>),
2155    crate::compiler_error::CompilerFailure,
2156> {
2157    use wasm_encoder::{HeapType, RefType, ValType};
2158
2159    let raw_string_ref = ValType::Ref(RefType {
2160        nullable: false,
2161        heap_type: HeapType::Concrete(intrinsics.raw_string),
2162    });
2163    Ok(match name {
2164        // json.parse returns the language's `unknown` slot.
2165        "parse" => (
2166            vec![raw_string_ref],
2167            vec![ValType::Ref(RefType {
2168                nullable: true,
2169                heap_type: HeapType::Concrete(intrinsics.object),
2170            })],
2171        ),
2172        "stringify" => (vec![raw_string_ref], vec![raw_string_ref]),
2173        "stringifyTypedObject" => (
2174            vec![
2175                raw_string_ref,
2176                ValType::I32,
2177                ValType::Ref(RefType {
2178                    nullable: false,
2179                    heap_type: HeapType::Abstract {
2180                        shared: false,
2181                        ty: AbstractHeapType::Struct,
2182                    },
2183                }),
2184            ],
2185            vec![raw_string_ref],
2186        ),
2187        "stringifyPrettyNumber" => (vec![raw_string_ref, ValType::F64], vec![raw_string_ref]),
2188        "stringifyPrettyString" => (vec![raw_string_ref, raw_string_ref], vec![raw_string_ref]),
2189        _ => (
2190            params
2191                .iter()
2192                .map(|p| symbols.host_value_type(&p.ty))
2193                .collect::<Result<_, _>>()?,
2194            vec![],
2195        ),
2196    })
2197}
2198
2199/// Reference-typed globals are nullable so the `ref.null` initializer validates;
2200/// `_start` fills the slot before user code runs and reads reattach `ref.as_non_null`.
2201///
2202/// Every consumer that *imports* such a global must declare it the same way:
2203/// the engine's import check is invariant on a mutable global's content type,
2204/// so `(mut (ref null $string))` and `(mut (ref $string))` do not link.
2205fn global_val_type(
2206    ty: &Type,
2207    im: &SymbolTable,
2208) -> Result<ValType, crate::compiler_error::CompilerFailure> {
2209    Ok(match im.value_type(ty)? {
2210        ValType::Ref(r) => ValType::Ref(RefType {
2211            nullable: true,
2212            ..r
2213        }),
2214        other => other,
2215    })
2216}
2217
2218/// Zero/null const-expr placeholder — value is overwritten by _start before any user code runs.
2219fn default_const_expr(val_type: ValType) -> Result<ConstExpr, CompilerFailure> {
2220    Ok(match val_type {
2221        ValType::F64 => ConstExpr::f64_const(Ieee64::from(0.0_f64)),
2222        ValType::I32 => ConstExpr::i32_const(0),
2223        ValType::Ref(RefType { heap_type, .. }) => ConstExpr::ref_null(heap_type),
2224        other => {
2225            return Err(crate::codegen::internal_failure(format!(
2226                "unsupported default constant type {other:?}"
2227            )));
2228        }
2229    })
2230}
2231
2232fn emit_package_string_init(
2233    emitter: &mut FunctionEmitter<'_>,
2234    package_name: &str,
2235    pkg_string_global_idx: u32,
2236    raw_string_type_idx: u32,
2237    string_type_idx: u32,
2238    string_vtable_global_idx: u32,
2239) -> Result<(), CompilerFailure> {
2240    emitter.instruction(Instruction::GlobalGet(string_vtable_global_idx));
2241    for code_unit in package_name.encode_utf16() {
2242        emitter.instruction(Instruction::I32Const(code_unit as i32));
2243    }
2244    emitter.instruction(Instruction::ArrayNewFixed {
2245        array_type_index: raw_string_type_idx,
2246        array_size: wasm_u32(package_name.encode_utf16().count())?,
2247    });
2248    emitter.instruction(Instruction::I64Const(0));
2249    emitter.instruction(Instruction::StructNew(string_type_idx));
2250    emitter.instruction(Instruction::GlobalSet(pkg_string_global_idx));
2251    Ok(())
2252}
2253
2254fn arena_failure(error: crate::arena::ArenaError) -> CompilerFailure {
2255    error.into_compiler_failure(CompilerStage::Codegen)
2256}
2257
2258#[cfg(test)]
2259pub(crate) mod tests {
2260    use super::{SymbolTable, ValueSymbol, codegen, codegen_with_type_info};
2261    use crate::runtime::prelude;
2262    use crate::{
2263        Asi, ModulePath, NamespaceSymbol, ObjectField, PackageDeclaration, Sources, Token,
2264        TokenKind, Type, TypedAst, capture, check, desugar, infer, infer_package, lower_patterns,
2265        parse,
2266    };
2267    use wasmparser::{Parser, Payload};
2268
2269    #[test]
2270    fn missing_record_import_returns_an_internal_error_without_wasm() {
2271        let source = "function main(): number | undefined { const d: Record<string, number> = {}; const key: string = 'x'; return d[key]; }";
2272        let ta = type_check(source);
2273        let (prelude_defs, host_defs, internal_defs) =
2274            prelude::cached_runtime_package_declarations();
2275        let mut dependencies: Vec<_> = prelude_defs
2276            .iter()
2277            .chain(host_defs.iter())
2278            .chain(internal_defs.iter())
2279            .cloned()
2280            .collect();
2281        for declaration in &mut dependencies {
2282            declaration.values.retain(|_, value| {
2283                !value
2284                    .mangled_name
2285                    .as_str()
2286                    .ends_with("ObjectConstructor##getField")
2287            });
2288        }
2289        let refs: Vec<_> = dependencies.iter().collect();
2290        let error = codegen_with_type_info(source, "record.ts", crate::FileId(0), &ta, &refs)
2291            .expect_err("missing internal import must not return a module");
2292        assert!(
2293            error.to_string().contains("dynamic read imported"),
2294            "{error}"
2295        );
2296        super::tests::compile("function main(): number { return 42; }");
2297    }
2298
2299    fn type_check(source: &str) -> TypedAst {
2300        type_check_with_packages(source, &[])
2301    }
2302
2303    fn type_check_with_packages(source: &str, packages: &[&PackageDeclaration]) -> TypedAst {
2304        let mut asi = Asi::new(source, crate::FileId(0));
2305        let mut tokens: Vec<Token> = Vec::new();
2306        loop {
2307            let tok = asi.next_token();
2308            let is_eof = matches!(tok.kind, TokenKind::Eof);
2309            tokens.push(tok);
2310            if is_eof {
2311                break;
2312            }
2313        }
2314        let lex_diags = asi.into_diagnostics();
2315        assert!(
2316            lex_diags.is_empty(),
2317            "unexpected lexer diags: {lex_diags:?}"
2318        );
2319        let (ast, parse_diags) = parse(source, tokens, crate::FileId(0));
2320        assert!(
2321            parse_diags.is_empty(),
2322            "unexpected parser diags: {parse_diags:?}"
2323        );
2324        let packages = runtime_packages(packages);
2325        let (mut ta, mut diags) = infer(source, "main", &ast, &packages);
2326        diags.extend(check(&ta).unwrap());
2327        ta = capture(ta).unwrap();
2328        ta = desugar(ta, crate::FileId(0)).unwrap();
2329        assert!(diags.is_empty(), "unexpected typecheck diags: {diags:?}");
2330        ta
2331    }
2332
2333    fn runtime_packages<'a>(packages: &[&'a PackageDeclaration]) -> Vec<&'a PackageDeclaration> {
2334        let (prelude_defs, host_defs, _) = prelude::cached_runtime_package_declarations();
2335        let mut out = Vec::with_capacity(prelude_defs.len() + host_defs.len() + packages.len());
2336        out.extend(prelude_defs.iter());
2337        out.extend(host_defs.iter());
2338        out.extend(packages.iter().copied());
2339        out
2340    }
2341
2342    fn infer_with_runtime_packages(source: &str, ast: &crate::Ast) -> Vec<crate::Diagnostic> {
2343        let packages = runtime_packages(&[]);
2344        let (_, diags) = infer(source, "main", ast, &packages);
2345        diags
2346    }
2347
2348    fn parse_module(
2349        sources: &mut Sources,
2350        module: &str,
2351        source: &str,
2352    ) -> (ModulePath, crate::FileId, crate::Ast) {
2353        let file = sources.add(module.to_string(), source).unwrap();
2354        let mut asi = Asi::new(source, file);
2355        let mut tokens: Vec<Token> = Vec::new();
2356        loop {
2357            let tok = asi.next_token();
2358            let is_eof = matches!(tok.kind, TokenKind::Eof);
2359            tokens.push(tok);
2360            if is_eof {
2361                break;
2362            }
2363        }
2364        let lex_diags = asi.into_diagnostics();
2365        assert!(
2366            lex_diags.is_empty(),
2367            "unexpected lexer diags: {lex_diags:?}"
2368        );
2369        let (mut ast, parse_diags) = parse(source, tokens, file);
2370        assert!(
2371            parse_diags.is_empty(),
2372            "unexpected parser diags: {parse_diags:?}"
2373        );
2374        ast = lower_patterns(ast).unwrap();
2375        (ModulePath::from(module), file, ast)
2376    }
2377
2378    pub(crate) fn compile_package_modules(
2379        package_name: &str,
2380        modules: &[(&str, &str)],
2381        packages: &[&PackageDeclaration],
2382    ) -> (Vec<u8>, PackageDeclaration, crate::TypeInfoTable) {
2383        let mut sources = Sources::new();
2384        let owned_modules: Vec<_> = modules
2385            .iter()
2386            .map(|(module, source)| parse_module(&mut sources, module, source))
2387            .collect();
2388        let module_refs: Vec<_> = owned_modules
2389            .iter()
2390            .map(|(module, file, ast)| (module.clone(), *file, ast))
2391            .collect();
2392        let stdlib_defs = crate::runtime::stdlib_package_declarations();
2393        let mut external_packages: std::collections::BTreeMap<String, PackageDeclaration> =
2394            stdlib_defs
2395                .into_iter()
2396                .map(|defs| (defs.package_name.clone(), defs))
2397                .collect();
2398        let (prelude_defs, host_defs, _) = prelude::cached_runtime_package_declarations();
2399        for defs in prelude_defs {
2400            external_packages.insert(defs.package_name.clone(), defs.clone());
2401        }
2402        for defs in host_defs {
2403            external_packages.insert(defs.package_name.clone(), defs.clone());
2404        }
2405        for defs in packages {
2406            external_packages.insert(defs.package_name.clone(), (*defs).clone());
2407        }
2408        let (mut ta, mut package, diags) = infer_package(
2409            package_name,
2410            ModulePath::from("lib"),
2411            module_refs,
2412            &sources,
2413            external_packages,
2414            std::collections::BTreeMap::new(),
2415        );
2416        assert!(diags.is_empty(), "unexpected package diags: {diags:?}");
2417        ta = capture(ta).unwrap();
2418        let root_file = owned_modules
2419            .iter()
2420            .find(|(module, _, _)| module.as_str() == "lib")
2421            .map(|(_, file, _)| *file)
2422            .expect("test package has lib module");
2423        ta = desugar(ta, root_file).unwrap();
2424
2425        let (prelude_defs, host_defs, internal_defs) =
2426            prelude::cached_runtime_package_declarations();
2427        let stdlib_defs = crate::runtime::stdlib_package_declarations();
2428        let mut dependencies: Vec<&PackageDeclaration> = prelude_defs.iter().collect();
2429        dependencies.extend(host_defs.iter());
2430        dependencies.extend(internal_defs.iter());
2431        dependencies.extend(stdlib_defs.iter());
2432        dependencies.extend_from_slice(packages);
2433        let generated =
2434            super::codegen_package_with_type_info(&sources, root_file, &ta, &dependencies)
2435                .expect("code generation");
2436        package.runtime_functions = generated.runtime_functions;
2437        package.runtime_globals = generated.runtime_globals;
2438        package.closure_caches = generated.closure_caches;
2439        (generated.wasm, package, generated.type_info)
2440    }
2441
2442    pub(crate) fn compile(source: &str) -> Vec<u8> {
2443        crate::compile::compile_script(source, "script.subm", crate::FileId(0), &[], &[])
2444            .expect("test-only compile expects no diagnostics")
2445            .wasm
2446    }
2447
2448    #[test]
2449    fn compiled_type_info_includes_stringify_object_shapes() {
2450        let source = r#"
2451function main(): string {
2452  const original = "{\"id\":1,\"name\":\"alice\"}";
2453  const u = JSON.parse(original) as { id: number, name: string };
2454  return JSON.stringify(u);
2455}
2456"#;
2457        let compiled =
2458            crate::compile::compile_script(source, "script.subm", crate::FileId(0), &[], &[])
2459                .expect("script should compile");
2460        let ty = Type::Object {
2461            index: None,
2462            fields: std::collections::BTreeMap::from([
2463                ("id".to_string(), ObjectField::required(Type::Number)),
2464                ("name".to_string(), ObjectField::required(Type::String)),
2465            ]),
2466        };
2467
2468        let id = compiled
2469            .type_info
2470            .object_type_id(&ty)
2471            .expect("compiled TypeInfo should contain the object stringify target");
2472        let info = compiled.type_info.get(id).expect("TypeInfo id is valid");
2473        assert!(matches!(
2474            &info.kind,
2475            crate::TypeInfoKind::Object { fields } if fields.len() == 2
2476        ));
2477
2478        let ta = type_check(source);
2479        let emitted = super::user_subtypes::collect_object_shapes(std::iter::empty(), &ta.shapes);
2480        assert!(
2481            emitted
2482                .iter()
2483                .all(|ty| compiled.type_info.object_type_id(ty).is_some()),
2484            "every emitted object subtype should have TypeInfo"
2485        );
2486    }
2487
2488    fn build_symbol_table(source: &str) -> SymbolTable {
2489        let _ta = type_check(source);
2490        let prelude_defs = prelude::prelude_package_declaration();
2491        let dependencies: [&crate::PackageDeclaration; 1] = [&prelude_defs];
2492        let mut map = SymbolTable::default();
2493        let mut next_type_idx: u32 = 0;
2494        let mut next_func_idx: u32 = 0;
2495        let mut next_global_idx: u32 = 0;
2496        map.set_intrinsic_type_indices(super::intrinsics::IntrinsicTypeIndices {
2497            raw_string: 0,
2498            vtable: 1,
2499            object: 2,
2500            undefined: 51,
2501            string: 3,
2502            boxed_number: 4,
2503            boxed_boolean: 5,
2504            field_names: 6,
2505            object_fields: 7,
2506            object_shape: 8,
2507            to_string_fn: 9,
2508            to_json_fn: 10,
2509            equals_fn: 11,
2510            hash_fn: 12,
2511            field_getter: 13,
2512            field_setter: 14,
2513            raw_array: 15,
2514            array: 16,
2515            raw_uint8_array: 17,
2516            uint8_array: 18,
2517            closure: 19,
2518            class_vtable: 20,
2519            error_vtable: 21,
2520            error: 22,
2521            raw_bigint: 23,
2522            bigint: 24,
2523            regex_capture_array: 25,
2524            regex_match: 26,
2525            regex: 27,
2526            regex_match_box: 28,
2527            temporal_instant: 29,
2528            temporal_duration: 30,
2529            temporal_zdt: 31,
2530            raw_index_array: 32,
2531            map: 33,
2532            set: 34,
2533            url: 35,
2534            fs_stat: 41,
2535            fs_peek: 42,
2536            fs_dir_entry: 43,
2537            fs_info: 44,
2538            fs_file_writer: 45,
2539            http_response: 46,
2540            http_download_result: 47,
2541            session_entry: 48,
2542            session_page: 49,
2543            fs_mount_info: 50,
2544
2545            temporal_plain_date: 36,
2546            temporal_plain_time: 37,
2547            temporal_plain_date_time: 38,
2548            temporal_plain_year_month: 39,
2549            temporal_plain_month_day: 40,
2550        });
2551        next_type_idx += super::intrinsics::INTRINSIC_TYPE_COUNT;
2552        let _ = next_type_idx;
2553        for defs in &dependencies {
2554            for value in defs.values.values() {
2555                record(&mut map, value, &mut next_func_idx, &mut next_global_idx);
2556            }
2557            walk_test_namespaces(
2558                &defs.namespaces,
2559                &mut map,
2560                &mut next_func_idx,
2561                &mut next_global_idx,
2562            );
2563        }
2564        map
2565    }
2566
2567    fn record(
2568        map: &mut SymbolTable,
2569        value: &ValueSymbol,
2570        next_func_idx: &mut u32,
2571        next_global_idx: &mut u32,
2572    ) {
2573        match &value.kind {
2574            crate::ValueKind::Function { .. } => {
2575                map.record_func(value.mangled_name.clone(), *next_func_idx);
2576                *next_func_idx += 1;
2577            }
2578            crate::ValueKind::Let { .. } | crate::ValueKind::Const { .. } => {
2579                map.record_global(value.mangled_name.clone(), *next_global_idx);
2580                *next_global_idx += 1;
2581            }
2582        }
2583    }
2584
2585    fn walk_test_namespaces(
2586        map_in: &std::collections::BTreeMap<String, NamespaceSymbol>,
2587        map: &mut SymbolTable,
2588        next_func_idx: &mut u32,
2589        next_global_idx: &mut u32,
2590    ) {
2591        for ns in map_in.values() {
2592            for value in ns.values.values() {
2593                record(map, value, next_func_idx, next_global_idx);
2594            }
2595            walk_test_namespaces(&ns.namespaces, map, next_func_idx, next_global_idx);
2596        }
2597    }
2598
2599    fn function_count(bytes: &[u8]) -> usize {
2600        for payload in Parser::new(0).parse_all(bytes) {
2601            if let Payload::FunctionSection(reader) = payload.expect("payload") {
2602                return reader.count() as usize;
2603            }
2604        }
2605        0
2606    }
2607
2608    fn imports(bytes: &[u8]) -> Vec<(String, String)> {
2609        let mut out = Vec::new();
2610        for payload in Parser::new(0).parse_all(bytes) {
2611            if let Payload::ImportSection(reader) = payload.expect("payload") {
2612                for entry in reader {
2613                    match entry.expect("import") {
2614                        wasmparser::Imports::Single(_, imp) => {
2615                            out.push((imp.module.to_string(), imp.name.to_string()));
2616                        }
2617                        other => panic!("unexpected import form {other:?}"),
2618                    }
2619                }
2620            }
2621        }
2622        out
2623    }
2624
2625    fn global_count(bytes: &[u8]) -> usize {
2626        for payload in Parser::new(0).parse_all(bytes) {
2627            if let Payload::GlobalSection(reader) = payload.expect("payload") {
2628                return reader.count() as usize;
2629            }
2630        }
2631        0
2632    }
2633
2634    fn global_decls(bytes: &[u8]) -> Vec<(bool, &'static str)> {
2635        let mut out = Vec::new();
2636        for payload in Parser::new(0).parse_all(bytes) {
2637            if let Payload::GlobalSection(reader) = payload.expect("payload") {
2638                for g in reader {
2639                    let g = g.expect("global");
2640                    let kind = match g.ty.content_type {
2641                        wasmparser::ValType::F64 => "f64",
2642                        wasmparser::ValType::I32 => "i32",
2643                        wasmparser::ValType::Ref(_) => "ref",
2644                        _ => "other",
2645                    };
2646                    out.push((g.ty.mutable, kind));
2647                }
2648            }
2649        }
2650        out
2651    }
2652
2653    fn start_function_idx(bytes: &[u8]) -> Option<u32> {
2654        for payload in Parser::new(0).parse_all(bytes) {
2655            if let Payload::StartSection { func, .. } = payload.expect("payload") {
2656                return Some(func);
2657            }
2658        }
2659        None
2660    }
2661
2662    fn data_count(bytes: &[u8]) -> Option<u32> {
2663        for payload in Parser::new(0).parse_all(bytes) {
2664            if let Payload::DataCountSection { count, .. } = payload.expect("payload") {
2665                return Some(count);
2666            }
2667        }
2668        None
2669    }
2670
2671    fn data_segments(bytes: &[u8]) -> Vec<Vec<u8>> {
2672        let mut out = Vec::new();
2673        for payload in Parser::new(0).parse_all(bytes) {
2674            if let Payload::DataSection(reader) = payload.expect("payload") {
2675                for d in reader {
2676                    let d = d.expect("data segment");
2677                    out.push(d.data.to_vec());
2678                }
2679            }
2680        }
2681        out
2682    }
2683
2684    /// The `name` section's module subsection — the identity the runtime attributes a gated
2685    /// call to. Read through the engine rather than by hand, so these tests fail if the
2686    /// accessor and the emitter ever disagree.
2687    fn module_name(bytes: &[u8]) -> Option<String> {
2688        let engine = crate::runtime::RuntimeConfig::default()
2689            .engine()
2690            .expect("engine");
2691        let module = wasmtime::Module::new(&engine, bytes).expect("module validates");
2692        module.name().map(str::to_string)
2693    }
2694
2695    #[test]
2696    fn package_module_is_named_for_its_package() {
2697        let (bytes, _decl, _ti) = compile_package_modules(
2698            "test:pkg",
2699            &[(
2700                "lib",
2701                "/** Identity.\n * @param x Value to return.\n * @returns `x`. */\nexport function noop(x: number): number { return x; }",
2702            )],
2703            &[],
2704        );
2705        assert_eq!(module_name(&bytes).as_deref(), Some("test:pkg"));
2706    }
2707
2708    #[test]
2709    fn script_module_is_named_main() {
2710        let bytes = compile("function main(): number { return 1; }");
2711        assert_eq!(module_name(&bytes).as_deref(), Some("main"));
2712    }
2713
2714    /// A package's test file compiles as a script — its entry point stays mangled as `main` so
2715    /// the runner can find it — but the code is the package's, so a gated call from it must
2716    /// attribute to the package. Without this, `secrets.get` in a package's own tests is
2717    /// refused outright by the unconditional deny of `secrets.get` to `main`.
2718    #[test]
2719    fn test_file_module_is_named_for_its_owning_package() {
2720        let source = "function main(): number { return 1; }";
2721        let compiled = crate::compile::compile_script_owned_by(
2722            "test:pkg",
2723            source,
2724            "tests/thing.test.ts",
2725            crate::FileId(0),
2726            &[],
2727            &[],
2728        )
2729        .expect("compiles");
2730        assert_eq!(module_name(&compiled.wasm).as_deref(), Some("test:pkg"));
2731
2732        assert!(
2733            main_export_func_idx(&compiled.wasm).is_some(),
2734            "entry point must stay exported as `main` — the module name is identity, not mangling"
2735        );
2736    }
2737
2738    /// Records every attribution the policy engine is asked about and allows all of them, so
2739    /// the only thing that can refuse `main` is the unconditional `secrets.get` rule.
2740    /// A test-local deny would mask which mechanism actually held.
2741    struct RecordingAllowAll {
2742        seen: std::sync::Mutex<Vec<(String, String)>>,
2743    }
2744
2745    impl crate::runtime::SecurityCheck for RecordingAllowAll {
2746        fn check(
2747            &self,
2748            caller: &str,
2749            capability: &str,
2750            _context: &serde_json::Value,
2751        ) -> crate::runtime::CheckOutcome {
2752            self.seen
2753                .lock()
2754                .expect("mutex")
2755                .push((caller.to_string(), capability.to_string()));
2756            crate::runtime::CheckOutcome::Allow { rule: None }
2757        }
2758    }
2759
2760    struct FixedSecret;
2761
2762    impl crate::runtime::SecretProvider for FixedSecret {
2763        fn get<'a>(
2764            &'a self,
2765            _name: &'a str,
2766        ) -> std::pin::Pin<
2767            Box<dyn std::future::Future<Output = Result<Option<String>, String>> + Send + 'a>,
2768        > {
2769            Box::pin(async { Ok(Some("SECRET-VALUE".to_string())) })
2770        }
2771    }
2772
2773    /// The escalation reported in PR #4, and the isolation guarantee in
2774    /// `docs/semantic-security.md` that it breaks: a caller gets its own permission set and
2775    /// inherits nothing from the frame above it.
2776    ///
2777    /// `JSON.stringify` of a caller-supplied value dispatches that value's own `toJson`. That
2778    /// method is `main`'s code, but it runs while the package sits on top of the caller stack,
2779    /// so its gated calls are attributed to the package — and `main` reads a secret it is
2780    /// unconditionally denied.
2781    #[tokio::test]
2782    async fn main_authored_code_keeps_main_identity_inside_a_package_call() {
2783        let (lib_bytes, lib_decl, lib_type_info) = compile_package_modules(
2784            "test:vuln",
2785            &[(
2786                "lib",
2787                r#"
2788                /**
2789                 * Pass-through JSON encoder.
2790                 * @param value Value to encode.
2791                 * @returns `value` as JSON.
2792                 */
2793                export function passthrough(value: unknown): string | undefined {
2794                    return JSON.stringify(value);
2795                }
2796                "#,
2797            )],
2798            &[],
2799        );
2800
2801        let recording = std::sync::Arc::new(RecordingAllowAll {
2802            seen: std::sync::Mutex::new(Vec::new()),
2803        });
2804        let cfg = crate::runtime::RuntimeConfig::default();
2805        let engine = cfg.engine().expect("engine");
2806        let mut data =
2807            crate::runtime::StoreData::with_vfs(crate::runtime::Vfs::tempdir().expect("tempdir"));
2808        data.install_type_info(lib_type_info);
2809        data.security_check = recording.clone();
2810        data.secret_provider = std::sync::Arc::new(FixedSecret);
2811        let mut store = cfg.store(&engine, data).expect("store");
2812        let mut linker = wasmtime::Linker::<crate::runtime::StoreData>::new(&engine);
2813        crate::runtime::install_runtime_async(&mut linker, &mut store)
2814            .await
2815            .expect("install runtime");
2816        let lib_module = wasmtime::Module::new(&engine, &lib_bytes).expect("library module");
2817        let lib_inst = linker
2818            .instantiate_async(&mut store, &lib_module)
2819            .await
2820            .expect("instantiate library");
2821        linker
2822            .instance(&mut store, "test:vuln", lib_inst)
2823            .expect("register library instance");
2824        let public_name = crate::mangle::package_symbol("test:vuln", "passthrough");
2825        let func = lib_inst
2826            .get_func(&mut store, public_name.as_str())
2827            .expect("library public export");
2828        linker
2829            .define(&mut store, "test:vuln", "passthrough", func)
2830            .expect("plain package import alias");
2831
2832        // Control: main reading the secret directly is refused ahead of any policy.
2833        let direct = crate::compile::compile_script(
2834            r#"
2835            import { get } from "submilli:secrets";
2836            function main(): void { const _ = get("TOKEN"); }
2837            "#,
2838            "direct.subm",
2839            crate::FileId(0),
2840            &[&lib_decl],
2841            &[],
2842        )
2843        .expect("direct consumer compiles");
2844        store.data_mut().install_type_info(direct.type_info.clone());
2845        let direct_module = wasmtime::Module::new(&engine, &direct.wasm).expect("direct module");
2846        let direct_inst = linker
2847            .instantiate_async(&mut store, &direct_module)
2848            .await
2849            .expect("instantiate direct consumer");
2850        let err = crate::runtime::dispatch_main_async(&mut store, &direct_inst)
2851            .await
2852            .expect_err("direct read is denied");
2853        assert!(
2854            err.to_string().contains("permission denied"),
2855            "expected denial, got: {err}",
2856        );
2857
2858        // Escalation: the same read, reached through the package's stringify.
2859        let escalated = crate::compile::compile_script(
2860            r#"
2861            import { passthrough } from "test:vuln";
2862            import { get } from "submilli:secrets";
2863
2864            class Exfil {
2865                stolen: string;
2866                constructor() { this.stolen = "none"; }
2867                toJson(): string {
2868                    const token = get("TOKEN");
2869                    if (token !== undefined) { this.stolen = token; }
2870                    return "\"ok\"";
2871                }
2872            }
2873
2874            function main(): string {
2875                const payload = new Exfil();
2876                const _ = passthrough(payload);
2877                return payload.stolen;
2878            }
2879            "#,
2880            "escalated.subm",
2881            crate::FileId(0),
2882            &[&lib_decl],
2883            &[],
2884        )
2885        .expect("escalating consumer compiles");
2886        store
2887            .data_mut()
2888            .install_type_info(escalated.type_info.clone());
2889        let escalated_module =
2890            wasmtime::Module::new(&engine, &escalated.wasm).expect("escalated module");
2891        let escalated_inst = linker
2892            .instantiate_async(&mut store, &escalated_module)
2893            .await
2894            .expect("instantiate escalating consumer");
2895        let result = crate::runtime::dispatch_main_async(&mut store, &escalated_inst).await;
2896        let seen = recording.seen.lock().expect("mutex").clone();
2897
2898        // The sharp assertion: nothing `main` authored may reach the policy engine wearing the
2899        // package's name. Under the bug this holds `("test:vuln", "secrets.get")`.
2900        assert!(
2901            !seen.iter().any(|(caller, _)| caller == "test:vuln"),
2902            "main-authored code was attributed to the package it was handed to: {seen:?}",
2903        );
2904        assert!(
2905            result.is_err(),
2906            "main must not obtain a secret it is unconditionally denied; got {result:?}",
2907        );
2908    }
2909
2910    /// Links `lib_bytes` under `package`, runs `consumer_src` as the script, and returns every
2911    /// (caller, capability) pair the policy engine was asked about, plus whether `main` ran to
2912    /// completion. A denial is a legitimate outcome — for the escalation shapes it is the
2913    /// point — so the caller decides which it expects.
2914    async fn attributions_for(
2915        package: &str,
2916        lib_bytes: &[u8],
2917        lib_decl: &PackageDeclaration,
2918        lib_type_info: crate::TypeInfoTable,
2919        consumer_src: &str,
2920    ) -> (Vec<(String, String)>, bool) {
2921        let recording = std::sync::Arc::new(RecordingAllowAll {
2922            seen: std::sync::Mutex::new(Vec::new()),
2923        });
2924        let cfg = crate::runtime::RuntimeConfig::default();
2925        let engine = cfg.engine().expect("engine");
2926        let mut data =
2927            crate::runtime::StoreData::with_vfs(crate::runtime::Vfs::tempdir().expect("tempdir"));
2928        data.install_type_info(lib_type_info);
2929        data.security_check = recording.clone();
2930        data.secret_provider = std::sync::Arc::new(FixedSecret);
2931        let mut store = cfg.store(&engine, data).expect("store");
2932        let mut linker = wasmtime::Linker::<crate::runtime::StoreData>::new(&engine);
2933        crate::runtime::install_runtime_async(&mut linker, &mut store)
2934            .await
2935            .expect("install runtime");
2936        let lib_module = wasmtime::Module::new(&engine, lib_bytes).expect("library module");
2937        let lib_inst = linker
2938            .instantiate_async(&mut store, &lib_module)
2939            .await
2940            .expect("instantiate library");
2941        linker
2942            .instance(&mut store, package, lib_inst)
2943            .expect("register library instance");
2944
2945        let consumer = crate::compile::compile_script(
2946            consumer_src,
2947            "consumer.subm",
2948            crate::FileId(0),
2949            &[lib_decl],
2950            &[],
2951        )
2952        .expect("consumer compiles");
2953        store
2954            .data_mut()
2955            .install_type_info(consumer.type_info.clone());
2956        let consumer_module = wasmtime::Module::new(&engine, &consumer.wasm).expect("module");
2957        let inst = linker
2958            .instantiate_async(&mut store, &consumer_module)
2959            .await
2960            .expect("instantiate consumer");
2961        let ran_ok = crate::runtime::dispatch_main_async(&mut store, &inst)
2962            .await
2963            .is_ok();
2964        (recording.seen.lock().expect("mutex").clone(), ran_ok)
2965    }
2966
2967    /// The escalation's route is `JSON.stringify` reaching a caller-supplied `toJson`, and it
2968    /// does not need the value handed over directly — an element inside a collection the
2969    /// package serializes dispatches the same way.
2970    #[tokio::test]
2971    async fn main_authored_tojson_inside_an_array_keeps_main_identity() {
2972        let (lib_bytes, lib_decl, lib_type_info) = compile_package_modules(
2973            "test:arr",
2974            &[(
2975                "lib",
2976                r#"
2977                /**
2978                 * Pass-through JSON encoder for a collection.
2979                 * @param values Values to encode.
2980                 * @returns `values` as JSON.
2981                 */
2982                export function passthroughAll(values: unknown[]): string {
2983                    return JSON.stringify(values);
2984                }
2985                "#,
2986            )],
2987            &[],
2988        );
2989        let (seen, ran_ok) = attributions_for(
2990            "test:arr",
2991            &lib_bytes,
2992            &lib_decl,
2993            lib_type_info,
2994            r#"
2995            import { passthroughAll } from "test:arr";
2996            import { get } from "submilli:secrets";
2997
2998            class Exfil {
2999                stolen: string;
3000                constructor() { this.stolen = "none"; }
3001                toJson(): string {
3002                    const t = get("TOKEN");
3003                    if (t !== undefined) { this.stolen = t; }
3004                    return "\"ok\"";
3005                }
3006            }
3007
3008            function main(): string {
3009                const payload = new Exfil();
3010                const _ = passthroughAll([payload]);
3011                return payload.stolen;
3012            }
3013            "#,
3014        )
3015        .await;
3016        assert!(
3017            !seen.iter().any(|(caller, _)| caller == "test:arr"),
3018            "an element's toJson is main's code wherever the package serializes it: {seen:?}",
3019        );
3020        assert!(
3021            !ran_ok,
3022            "identified as main, the read must be refused rather than completing",
3023        );
3024    }
3025
3026    /// The report names exported class members as a second route with the same root cause:
3027    /// constructors and methods carry no identity wrapper, so their gated calls were
3028    /// attributed to whoever called them. The practical consequence was that a
3029    /// credential-resolving API had to be a plain exported function — as a class method it
3030    /// would be attributed to `main` and refused.
3031    #[tokio::test]
3032    async fn a_packages_exported_class_method_is_attributed_to_the_package() {
3033        let (lib_bytes, lib_decl, lib_type_info) = compile_package_modules(
3034            "test:cls",
3035            &[(
3036                "lib",
3037                r#"
3038                import { get } from "submilli:secrets";
3039
3040                /** Resolves a credential from inside a class method. */
3041                export class Client {
3042                    token: string;
3043                    constructor() { this.token = "none"; }
3044
3045                    /** Reads the token.
3046                     * @returns The credential, or "none" when it is absent.
3047                     */
3048                    load(): string {
3049                        const t = get("TOKEN");
3050                        return t === undefined ? "none" : t;
3051                    }
3052                }
3053                "#,
3054            )],
3055            &[],
3056        );
3057        let (seen, ran_ok) = attributions_for(
3058            "test:cls",
3059            &lib_bytes,
3060            &lib_decl,
3061            lib_type_info,
3062            r#"
3063            import { Client } from "test:cls";
3064            function main(): string {
3065                const c = new Client();
3066                return c.load();
3067            }
3068            "#,
3069        )
3070        .await;
3071        assert!(ran_ok, "the package is permitted, so main must complete");
3072        assert_eq!(
3073            seen,
3074            vec![("test:cls".to_string(), "secrets.get".to_string())],
3075            "a class method's gated call belongs to the package that defines it",
3076        );
3077    }
3078
3079    /// A package's module-level initializers run in its own `_start`, so they were already
3080    /// correct while a frame was pushed around instantiation. Under frame-derived identity
3081    /// they are correct by construction — that code genuinely executes in the package's own
3082    /// module. Asserted rather than assumed.
3083    #[tokio::test]
3084    async fn a_packages_initializer_is_attributed_to_the_package() {
3085        let (lib_bytes, lib_decl, lib_type_info) = compile_package_modules(
3086            "test:init",
3087            &[(
3088                "lib",
3089                r#"
3090                import { get } from "submilli:secrets";
3091
3092                const TOKEN: string | undefined = get("TOKEN");
3093
3094                /**
3095                 * Reports whether the module-level read succeeded.
3096                 * @returns Whether the read succeeded.
3097                 */
3098                export function loaded(): boolean {
3099                    return TOKEN !== undefined;
3100                }
3101                "#,
3102            )],
3103            &[],
3104        );
3105        let (seen, ran_ok) = attributions_for(
3106            "test:init",
3107            &lib_bytes,
3108            &lib_decl,
3109            lib_type_info,
3110            r#"
3111            import { loaded } from "test:init";
3112            function main(): boolean { return loaded(); }
3113            "#,
3114        )
3115        .await;
3116        assert!(ran_ok, "the package is permitted, so main must complete");
3117        assert_eq!(
3118            seen,
3119            vec![("test:init".to_string(), "secrets.get".to_string())],
3120            "a package initializer's gated call belongs to the package",
3121        );
3122    }
3123
3124    fn custom_section(bytes: &[u8], name: &str) -> Option<Vec<u8>> {
3125        for payload in Parser::new(0).parse_all(bytes) {
3126            if let Payload::CustomSection(reader) = payload.expect("payload")
3127                && reader.name() == name
3128            {
3129                return Some(reader.data().to_vec());
3130            }
3131        }
3132        None
3133    }
3134
3135    fn export_names(bytes: &[u8]) -> Vec<String> {
3136        let mut names = Vec::new();
3137        for payload in Parser::new(0).parse_all(bytes) {
3138            if let Payload::ExportSection(reader) = payload.expect("payload") {
3139                for export in reader {
3140                    names.push(export.expect("export").name.to_string());
3141                }
3142            }
3143        }
3144        names
3145    }
3146
3147    fn export_entries(bytes: &[u8]) -> Vec<(String, wasmparser::ExternalKind, u32)> {
3148        let mut exports = Vec::new();
3149        for payload in Parser::new(0).parse_all(bytes) {
3150            if let Payload::ExportSection(reader) = payload.expect("payload") {
3151                for export in reader {
3152                    let export = export.expect("export");
3153                    exports.push((export.name.to_string(), export.kind, export.index));
3154                }
3155            }
3156        }
3157        exports
3158    }
3159
3160    fn imported_func_count(bytes: &[u8]) -> u32 {
3161        let mut count = 0;
3162        for payload in Parser::new(0).parse_all(bytes) {
3163            if let Payload::ImportSection(reader) = payload.expect("payload") {
3164                for entry in reader {
3165                    if let wasmparser::Imports::Single(_, imp) = entry.expect("import")
3166                        && matches!(imp.ty, wasmparser::TypeRef::Func(_))
3167                    {
3168                        count += 1;
3169                    }
3170                }
3171            }
3172        }
3173        count
3174    }
3175
3176    /// Function-space index of the imported func whose linker field is `field`.
3177    fn imported_func_idx(bytes: &[u8], field: &str) -> u32 {
3178        let mut count = 0;
3179        for payload in Parser::new(0).parse_all(bytes) {
3180            if let Payload::ImportSection(reader) = payload.expect("payload") {
3181                for entry in reader {
3182                    if let wasmparser::Imports::Single(_, imp) = entry.expect("import")
3183                        && matches!(imp.ty, wasmparser::TypeRef::Func(_))
3184                    {
3185                        if imp.name == field {
3186                            return count;
3187                        }
3188                        count += 1;
3189                    }
3190                }
3191            }
3192        }
3193        panic!("no func import named {field}");
3194    }
3195
3196    fn body_call_targets(bytes: &[u8], body_idx: usize) -> Vec<u32> {
3197        let mut bodies_seen = 0;
3198        let mut targets = Vec::new();
3199        for payload in Parser::new(0).parse_all(bytes) {
3200            if let Payload::CodeSectionEntry(body) = payload.expect("payload") {
3201                if bodies_seen == body_idx {
3202                    let mut reader = body
3203                        .get_operators_reader()
3204                        .expect("body operators readable");
3205                    while !reader.eof() {
3206                        if let wasmparser::Operator::Call { function_index } =
3207                            reader.read().expect("operator")
3208                        {
3209                            targets.push(function_index);
3210                        }
3211                    }
3212                    return targets;
3213                }
3214                bodies_seen += 1;
3215            }
3216        }
3217        targets
3218    }
3219
3220    fn main_body_call_targets(bytes: &[u8]) -> Vec<u32> {
3221        let main_idx = main_export_func_idx(bytes).expect("main exported");
3222        let body_idx = (main_idx - imported_func_count(bytes)) as usize;
3223        body_call_targets(bytes, body_idx)
3224    }
3225
3226    fn main_export_func_idx(bytes: &[u8]) -> Option<u32> {
3227        for payload in Parser::new(0).parse_all(bytes) {
3228            if let Payload::ExportSection(reader) = payload.expect("payload") {
3229                for export in reader {
3230                    let export = export.expect("export");
3231                    if export.name == "main"
3232                        && matches!(export.kind, wasmparser::ExternalKind::Func)
3233                    {
3234                        return Some(export.index);
3235                    }
3236                }
3237            }
3238        }
3239        None
3240    }
3241
3242    fn instantiate_against_prelude(consumer_bytes: &[u8]) {
3243        let _ = link_consumer(consumer_bytes);
3244    }
3245
3246    fn link_consumer(
3247        consumer_bytes: &[u8],
3248    ) -> (
3249        wasmtime::Store<crate::runtime::StoreData>,
3250        wasmtime::Instance,
3251    ) {
3252        let cfg = crate::runtime::RuntimeConfig::default();
3253        link_consumer_with(&cfg, consumer_bytes)
3254    }
3255
3256    fn link_consumer_with(
3257        cfg: &crate::runtime::RuntimeConfig,
3258        consumer_bytes: &[u8],
3259    ) -> (
3260        wasmtime::Store<crate::runtime::StoreData>,
3261        wasmtime::Instance,
3262    ) {
3263        let engine = cfg.engine().expect("engine builds with default config");
3264        let mut store = cfg
3265            .store(
3266                &engine,
3267                crate::runtime::StoreData::with_tempdir()
3268                    .expect("tempdir allocates for codegen test VFS"),
3269            )
3270            .expect("store builds with default config");
3271        let consumer_module =
3272            wasmtime::Module::new(&engine, consumer_bytes).expect("consumer module");
3273        let mut linker = wasmtime::Linker::<crate::runtime::StoreData>::new(&engine);
3274        // Async install/instantiate (the linker holds async http/fs host fns);
3275        // driven to completion here since no IO happens during setup. `main` is a
3276        // pure-compute func, so the tests still call it synchronously.
3277        let consumer_inst = pollster::block_on(async {
3278            crate::runtime::install_runtime_async(&mut linker, &mut store)
3279                .await
3280                .expect("runtime installs the prelude + host functions");
3281            linker
3282                .instantiate_async(&mut store, &consumer_module)
3283                .await
3284                .expect("consumer instantiates against the runtime")
3285        });
3286        (store, consumer_inst)
3287    }
3288
3289    fn run_main_f64(source: &str) -> f64 {
3290        run_main_number(&compile(source))
3291    }
3292
3293    #[test]
3294    fn undefined_backend_distinguishes_nullish_values_and_absence() {
3295        assert_eq!(
3296            run_main_f64(
3297                r#"
3298            function read(value: { a?: number } | null | undefined): number | undefined {
3299                return value?.a;
3300            }
3301            function main(): number {
3302                const absent: { a?: number } = {};
3303                const present: { a?: number } = { a: undefined };
3304                assert(absent.a === undefined, "missing read");
3305                assert(!("a" in absent) && "a" in present, "presence");
3306                assert(read(null) === undefined && read(undefined) === undefined, "chain");
3307                assert((read(undefined) ?? 7) === 7, "coalesce");
3308                const value: number | undefined = undefined;
3309                assert(!value && typeof value === "undefined", "truthiness and tag");
3310                assert(typeof null === "object" && typeof 1n === "bigint", "other tags");
3311                return 1;
3312            }
3313        "#
3314            ),
3315            1.0
3316        );
3317    }
3318
3319    #[test]
3320    fn undefined_backend_preserves_void_callback_completion() {
3321        assert_eq!(
3322            run_main_f64(
3323                r#"
3324            function empty(): void {}
3325            function invoke(callback: () => void): unknown { return callback(); }
3326            function main(): number {
3327                const callback: () => void = () => 42;
3328                const result: unknown = invoke(callback);
3329                assert(result === 42, "void callback preserves value");
3330                const completion: unknown = empty();
3331                assert(completion === undefined, "fallthrough completion");
3332                assert((void 42) === undefined, "void expression");
3333                return 1;
3334            }
3335        "#
3336            ),
3337            1.0
3338        );
3339    }
3340
3341    #[test]
3342    fn undefined_backend_optional_tuple_casts_preserve_length() {
3343        assert_eq!(
3344            run_main_f64(
3345                r#"
3346            function check(value: unknown): [number, string?] {
3347                return value as [number, string?];
3348            }
3349            function main(): number {
3350                const short = check([1]);
3351                const entries: unknown[] = [1, undefined];
3352                const explicit = check(entries);
3353                assert(short.length === 1 && short[1] === undefined, "omitted tuple slot");
3354                assert(explicit.length === 2 && explicit[1] === undefined, "present tuple slot");
3355                assert(JSON.stringify(short) === "[1]", "short tuple JSON");
3356                return 1;
3357            }
3358        "#
3359            ),
3360            1.0
3361        );
3362    }
3363
3364    #[test]
3365    fn undefined_backend_native_defaults_and_constructor_properties() {
3366        assert_eq!(
3367            run_main_f64(
3368                r#"
3369            class Value {
3370                constructor(public value: number = 17) {}
3371            }
3372            function main(): number {
3373                assert(new Value(undefined).value === 17, "parameter property default");
3374                assert("abc".slice(1, undefined) === "bc", "native numeric default");
3375                assert("a".padStart(2, undefined) === " a", "native string default");
3376                return 1;
3377            }
3378        "#
3379            ),
3380            1.0
3381        );
3382    }
3383
3384    fn run_main_number(bytes: &[u8]) -> f64 {
3385        let (mut store, inst) = link_consumer(bytes);
3386        let main = inst.get_func(&mut store, "main").expect("main export");
3387        let mut results = [wasmtime::Val::F64(0)];
3388        pollster::block_on(main.call_async(&mut store, &[], &mut results))
3389            .expect("main does not trap");
3390        match &results[0] {
3391            wasmtime::Val::F64(bits) => f64::from_bits(*bits),
3392            wasmtime::Val::AnyRef(Some(value)) => {
3393                let boxed = value
3394                    .as_struct(&mut store)
3395                    .expect("read result")
3396                    .expect("boxed number");
3397                boxed
3398                    .field(&mut store, 1)
3399                    .expect("number payload")
3400                    .f64()
3401                    .expect("f64 payload")
3402            }
3403            other => panic!("expected numeric result, got {other:?}"),
3404        }
3405    }
3406
3407    fn run_main_i32(source: &str) -> i32 {
3408        let bytes = compile(source);
3409        let (mut store, inst) = link_consumer(&bytes);
3410        let main = inst
3411            .get_typed_func::<(), i32>(&mut store, "main")
3412            .expect("main is exported as `() -> i32`");
3413        pollster::block_on(main.call_async(&mut store, ())).expect("main does not trap")
3414    }
3415
3416    fn run_main_expecting_error(source: &str) -> String {
3417        let bytes = compile(source);
3418        let (mut store, inst) = link_consumer(&bytes);
3419        let main = inst.get_func(&mut store, "main").expect("main export");
3420        let result = pollster::block_on(main.call_async(
3421            &mut store,
3422            &[],
3423            &mut [wasmtime::Val::AnyRef(None)],
3424        ));
3425        // Same uncaught-exception reshaping `dispatch_main_async` applies, so a
3426        // throw renders with its stashed backtrace like a trap does.
3427        let err = result.expect_err("expected main() to trap or throw");
3428        let err = crate::runtime::exec::uncaught_error(&mut store, err);
3429        let (sources, file) = crate::Sources::single("script.subm", source).unwrap();
3430        crate::render_backtrace(&err, &sources, file, crate::BacktraceMode::Full)
3431            .expect("backtrace empty — was wasm_backtrace_details enabled?")
3432    }
3433
3434    fn dump_dwarf_dies(bytes: &[u8]) -> String {
3435        use gimli::LittleEndian;
3436        use gimli::read::{AttributeValue, DebugAbbrev, DebugInfo, DebugStr};
3437        use std::fmt::Write;
3438
3439        let info_bytes = custom_section(bytes, ".debug_info").expect(".debug_info");
3440        let abbrev_bytes = custom_section(bytes, ".debug_abbrev").expect(".debug_abbrev");
3441        let str_bytes = custom_section(bytes, ".debug_str").expect(".debug_str");
3442        let debug_info = DebugInfo::new(&info_bytes, LittleEndian);
3443        let debug_abbrev = DebugAbbrev::new(&abbrev_bytes, LittleEndian);
3444        let debug_str = DebugStr::new(&str_bytes, LittleEndian);
3445
3446        let mut out = String::new();
3447        let mut units = debug_info.units();
3448        while let Some(header) = units.next().expect("unit") {
3449            let abbreviations = header.abbreviations(&debug_abbrev).expect("abbreviations");
3450            let mut entries = header.entries(&abbreviations);
3451            while let Some(entry) = entries.next_dfs().expect("entry") {
3452                let tag = entry.tag().static_string().unwrap_or("DW_TAG_<unknown>");
3453                writeln!(out, "<{:#x}> {}", entry.offset().0, tag).unwrap();
3454                for attr in entry.attrs() {
3455                    let name = attr.name().static_string().unwrap_or("DW_AT_<unknown>");
3456                    let value_repr = match attr.value() {
3457                        AttributeValue::Addr(a) => format!("Addr({a:#x})"),
3458                        AttributeValue::Udata(u) => format!("Udata({u})"),
3459                        AttributeValue::DebugStrRef(off) => {
3460                            let s = debug_str.get_str(off).expect("str");
3461                            format!("Str({:?})", std::str::from_utf8(s.slice()).unwrap_or("?"))
3462                        }
3463                        AttributeValue::Language(lang) => format!(
3464                            "Language({})",
3465                            lang.static_string().unwrap_or("DW_LANG_<unknown>")
3466                        ),
3467                        other => format!("{other:?}"),
3468                    };
3469                    writeln!(out, "  {name} = {value_repr}").unwrap();
3470                }
3471            }
3472        }
3473        out
3474    }
3475
3476    fn dump_dwarf_lines(bytes: &[u8]) -> String {
3477        use gimli::LittleEndian;
3478        use gimli::read::{DebugAbbrev, DebugInfo, DebugLine};
3479        use std::fmt::Write;
3480
3481        let info_bytes = custom_section(bytes, ".debug_info").expect(".debug_info");
3482        let abbrev_bytes = custom_section(bytes, ".debug_abbrev").expect(".debug_abbrev");
3483        let line_bytes = custom_section(bytes, ".debug_line").expect(".debug_line");
3484        let debug_info = DebugInfo::new(&info_bytes, LittleEndian);
3485        let debug_abbrev = DebugAbbrev::new(&abbrev_bytes, LittleEndian);
3486        let debug_line = DebugLine::new(&line_bytes, LittleEndian);
3487
3488        let mut out = String::new();
3489        let header = debug_info
3490            .units()
3491            .next()
3492            .expect("units")
3493            .expect("at least one unit");
3494        let _ = header.abbreviations(&debug_abbrev).expect("abbrevs");
3495        let program = debug_line
3496            .program(gimli::DebugLineOffset(0), header.address_size(), None, None)
3497            .expect("line program");
3498        let (program, sequences) = program.sequences().expect("sequences");
3499        for seq in sequences {
3500            writeln!(out, "sequence start={:#x} end={:#x}", seq.start, seq.end,).unwrap();
3501            let cloned = program.clone();
3502            let mut state = cloned.resume_from(&seq);
3503            while let Some((header, row)) = state.next_row().expect("next_row") {
3504                let _ = header;
3505                let line = row.line().map_or(0, std::num::NonZero::get);
3506                writeln!(
3507                    out,
3508                    "  addr={:#x} file={} line={} col={:?} stmt={}{}",
3509                    row.address(),
3510                    row.file_index(),
3511                    line,
3512                    row.column(),
3513                    row.is_stmt(),
3514                    if row.end_sequence() {
3515                        " end_sequence"
3516                    } else {
3517                        ""
3518                    },
3519                )
3520                .unwrap();
3521            }
3522        }
3523        out
3524    }
3525
3526    #[test]
3527    fn empty_main_links_with_prelude() {
3528        instantiate_against_prelude(&compile("function main(): void { }"));
3529    }
3530
3531    #[test]
3532    fn main_with_number_return_links_with_prelude() {
3533        instantiate_against_prelude(&compile("function main(): number { return 0; }"));
3534    }
3535
3536    #[test]
3537    fn main_with_boolean_return_links_with_prelude() {
3538        instantiate_against_prelude(&compile("function main(): boolean { return true; }"));
3539    }
3540
3541    #[test]
3542    fn string_runtime_imported_from_prelude() {
3543        let bytes = compile("function main(): void { }");
3544        // User functions, module start, output shim, and shared field lookup.
3545        assert_eq!(function_count(&bytes), 4);
3546        let stable_imports: Vec<_> = imports(&bytes)
3547            .into_iter()
3548            .filter(|(_, name)| !is_sub421_temporal_getter_import(name))
3549            .collect();
3550        assert!(
3551            stable_imports.contains(&(
3552                crate::runtime::prelude::MODULE_NAME.to_string(),
3553                crate::mangle::prelude("__error_tag").to_string(),
3554            )),
3555            "host tag import should use canonical name"
3556        );
3557        assert!(
3558            stable_imports.contains(&(
3559                crate::runtime::prelude::MODULE_NAME.to_string(),
3560                crate::mangle::prelude("string_vtable").to_string(),
3561            )),
3562            "host-owned string_vtable import should use canonical name"
3563        );
3564        assert!(
3565            !stable_imports
3566                .iter()
3567                .any(|(module, _)| module == "submilli:crypto"),
3568            "unused stdlib imports should be tree-shaken"
3569        );
3570        assert!(
3571            !stable_imports
3572                .iter()
3573                .any(|(module, name)| module == "submilli:prelude" && name == "string_vtable"),
3574            "prelude imports must not use short names"
3575        );
3576        assert_eq!(data_count(&bytes), None);
3577        assert!(data_segments(&bytes).is_empty());
3578    }
3579
3580    fn is_sub421_temporal_getter_import(name: &str) -> bool {
3581        matches!(
3582            name,
3583            "Temporal#PlainDate#monthCode"
3584                | "Temporal#PlainDate#dayOfYear"
3585                | "Temporal#PlainDate#weekOfYear"
3586                | "Temporal#PlainDate#yearOfWeek"
3587                | "Temporal#PlainDate#daysInWeek"
3588                | "Temporal#PlainDate#daysInMonth"
3589                | "Temporal#PlainDate#daysInYear"
3590                | "Temporal#PlainDate#monthsInYear"
3591                | "Temporal#PlainDate#inLeapYear"
3592                | "Temporal#PlainTime#millisecond"
3593                | "Temporal#PlainTime#microsecond"
3594                | "Temporal#PlainDateTime#dayOfWeek"
3595                | "Temporal#PlainDateTime#monthCode"
3596                | "Temporal#PlainDateTime#dayOfYear"
3597                | "Temporal#PlainDateTime#weekOfYear"
3598                | "Temporal#PlainDateTime#yearOfWeek"
3599                | "Temporal#PlainDateTime#daysInWeek"
3600                | "Temporal#PlainDateTime#daysInMonth"
3601                | "Temporal#PlainDateTime#daysInYear"
3602                | "Temporal#PlainDateTime#monthsInYear"
3603                | "Temporal#PlainDateTime#inLeapYear"
3604                | "Temporal#PlainDateTime#millisecond"
3605                | "Temporal#PlainDateTime#microsecond"
3606                | "Temporal#PlainYearMonth#monthCode"
3607                | "Temporal#PlainYearMonth#daysInMonth"
3608                | "Temporal#PlainYearMonth#daysInYear"
3609                | "Temporal#PlainYearMonth#monthsInYear"
3610                | "Temporal#PlainYearMonth#inLeapYear"
3611                | "Temporal#PlainMonthDay#monthCode"
3612                | "Temporal#ZonedDateTime#monthCode"
3613                | "Temporal#ZonedDateTime#dayOfYear"
3614                | "Temporal#ZonedDateTime#weekOfYear"
3615                | "Temporal#ZonedDateTime#yearOfWeek"
3616                | "Temporal#ZonedDateTime#daysInWeek"
3617                | "Temporal#ZonedDateTime#daysInMonth"
3618                | "Temporal#ZonedDateTime#daysInYear"
3619                | "Temporal#ZonedDateTime#monthsInYear"
3620                | "Temporal#ZonedDateTime#inLeapYear"
3621                | "Temporal#ZonedDateTime#millisecond"
3622                | "Temporal#ZonedDateTime#microsecond"
3623                | "Temporal#ZonedDateTime#nanosecond"
3624        )
3625    }
3626
3627    #[test]
3628    fn main_function_index_follows_imports() {
3629        let bytes = compile("function main(): void { }");
3630        assert_eq!(
3631            main_export_func_idx(&bytes),
3632            Some(imported_func_count(&bytes) + 1),
3633        );
3634    }
3635
3636    #[test]
3637    fn main_still_exported_uniquely() {
3638        let bytes = compile("function main(): void { }");
3639        assert_eq!(
3640            export_names(&bytes),
3641            vec!["main".to_string(), "__main_output".to_string()]
3642        );
3643    }
3644
3645    #[test]
3646    fn symbol_table_resolves_prelude_function_indices() {
3647        let map = build_symbol_table("function main(): void { }");
3648        // Alphabetical: isFinite, isNaN, string_cmp, string_concat, string_eq, string_length.
3649        assert_eq!(map.prelude_func_idx("isFinite"), Some(0));
3650        assert_eq!(map.prelude_func_idx("isNaN"), Some(1));
3651        assert_eq!(map.prelude_func_idx("string_cmp"), Some(2));
3652        assert_eq!(map.prelude_func_idx("string_concat"), Some(3));
3653        assert_eq!(map.prelude_func_idx("string_eq"), Some(4));
3654        assert_eq!(map.prelude_func_idx("string_length"), Some(5));
3655        assert_eq!(map.prelude_func_idx("missing"), None);
3656    }
3657
3658    #[test]
3659    fn symbol_table_resolves_prelude_string_type_index() {
3660        let map = build_symbol_table("function main(): void { }");
3661        assert_eq!(map.string_type_idx(), Some(3));
3662        assert_eq!(map.raw_string_type_idx(), Some(0));
3663    }
3664
3665    #[test]
3666    fn module_with_one_string_adds_one_data_segment() {
3667        let bytes = compile(r#"let x: string = "hello"; function main(): void { }"#);
3668        instantiate_against_prelude(&bytes);
3669        assert_eq!(data_count(&bytes), Some(1));
3670        let segments = data_segments(&bytes);
3671        assert_eq!(segments.len(), 1);
3672        assert_eq!(segments[0], b"h\0e\0l\0l\0o\0".to_vec());
3673    }
3674
3675    #[test]
3676    fn module_with_two_distinct_strings_has_two_segments() {
3677        let bytes = compile(
3678            r#"let x: string = "hello"; let y: string = "world"; function main(): void { }"#,
3679        );
3680        instantiate_against_prelude(&bytes);
3681        assert_eq!(data_count(&bytes), Some(2));
3682        let segments = data_segments(&bytes);
3683        assert_eq!(segments.len(), 2);
3684        assert_eq!(segments[0], b"h\0e\0l\0l\0o\0".to_vec());
3685        assert_eq!(segments[1], b"w\0o\0r\0l\0d\0".to_vec());
3686    }
3687
3688    #[test]
3689    fn module_with_dedup_strings_has_one_segment() {
3690        let bytes = compile(
3691            r#"let x: string = "hello"; let y: string = "hello"; function main(): void { }"#,
3692        );
3693        instantiate_against_prelude(&bytes);
3694        assert_eq!(data_count(&bytes), Some(1));
3695        assert_eq!(data_segments(&bytes).len(), 1);
3696    }
3697
3698    #[test]
3699    fn module_with_strings_keeps_runtime_imports() {
3700        let bytes = compile(r#"let x: string = "hi"; function main(): void { }"#);
3701        // User functions, module start, output shim, and shared field lookup.
3702        assert_eq!(function_count(&bytes), 4);
3703        let imp = imports(&bytes);
3704        assert!(imp.contains(&(
3705            crate::runtime::prelude::MODULE_NAME.to_string(),
3706            crate::mangle::prelude("string_vtable").to_string(),
3707        )));
3708        assert!(
3709            !imp.iter()
3710                .any(|(module, _)| module == crate::runtime::BIGINT_MODULE_NAME),
3711            "unused bigint imports should be tree-shaken",
3712        );
3713    }
3714
3715    #[test]
3716    fn cross_module_string_type_canonicalization_proves_out() {
3717        let bytes = compile(r#"let x: string = "hello"; function main(): void { }"#);
3718        instantiate_against_prelude(&bytes);
3719    }
3720
3721    #[test]
3722    fn helper_emits_function_body_but_is_not_exported() {
3723        let bytes = compile("function main(): void { } function helper(): void { }");
3724        instantiate_against_prelude(&bytes);
3725        // User functions, module start, output shim, and shared field lookup.
3726        assert_eq!(function_count(&bytes), 5);
3727        assert_eq!(
3728            export_names(&bytes),
3729            vec!["main".to_string(), "__main_output".to_string()]
3730        );
3731    }
3732
3733    #[test]
3734    fn typed_exports_emit_function_and_global_but_skip_types() {
3735        let bytes = compile(
3736            r#"
3737            export interface Public { value: number; }
3738            export const answer: number = 42;
3739            export function api(): number { return answer; }
3740            function main(): void { }
3741            "#,
3742        );
3743        instantiate_against_prelude(&bytes);
3744        let exports = export_entries(&bytes);
3745        assert!(
3746            exports.iter().any(|(name, kind, _)| {
3747                name == "main#api" && matches!(kind, wasmparser::ExternalKind::Func)
3748            }),
3749            "expected exported function entry, got {exports:?}",
3750        );
3751        assert!(
3752            exports.iter().any(|(name, kind, _)| {
3753                name == "main#answer" && matches!(kind, wasmparser::ExternalKind::Global)
3754            }),
3755            "expected exported global entry, got {exports:?}",
3756        );
3757        assert!(
3758            !exports.iter().any(|(name, _, _)| name == "main#Public"),
3759            "type-only exports must not become Wasm exports: {exports:?}",
3760        );
3761    }
3762
3763    #[test]
3764    fn package_reexport_aliases_share_one_wrapper_index() {
3765        let (bytes, package, _) = compile_package_modules(
3766            "test:lib",
3767            &[
3768                (
3769                    "lib",
3770                    r#"export { inner as first, inner as second } from "./util";"#,
3771                ),
3772                (
3773                    "util",
3774                    "/** Inner function.\n * @returns One. */\nexport function inner(): number { return 1; }",
3775                ),
3776            ],
3777            &[],
3778        );
3779        assert_eq!(
3780            package.values.keys().collect::<Vec<_>>(),
3781            vec![&"first".to_string(), &"second".to_string()],
3782        );
3783        let exports = export_entries(&bytes);
3784        let first = exports
3785            .iter()
3786            .find(|(name, kind, _)| {
3787                name == "test:lib#first" && matches!(kind, wasmparser::ExternalKind::Func)
3788            })
3789            .expect("first function export")
3790            .2;
3791        let second = exports
3792            .iter()
3793            .find(|(name, kind, _)| {
3794                name == "test:lib#second" && matches!(kind, wasmparser::ExternalKind::Func)
3795            })
3796            .expect("second function export")
3797            .2;
3798        assert_eq!(first, second, "aliases for one target share one wrapper");
3799        assert!(
3800            main_export_func_idx(&bytes).is_none(),
3801            "libraries have no main"
3802        );
3803    }
3804
3805    struct RecordingSecurity {
3806        seen: std::sync::Mutex<Vec<(String, String)>>,
3807    }
3808
3809    impl crate::runtime::SecurityCheck for RecordingSecurity {
3810        fn check(
3811            &self,
3812            caller: &str,
3813            capability: &str,
3814            _context: &serde_json::Value,
3815        ) -> crate::runtime::CheckOutcome {
3816            self.seen
3817                .lock()
3818                .expect("recording security mutex")
3819                .push((caller.to_string(), capability.to_string()));
3820            if capability == "test.denied" {
3821                crate::runtime::CheckOutcome::Deny {
3822                    rule: None,
3823                    reason: "blocked by test".to_string(),
3824                }
3825            } else {
3826                crate::runtime::CheckOutcome::Allow { rule: None }
3827            }
3828        }
3829    }
3830
3831    #[tokio::test]
3832    async fn a_package_api_attributes_to_the_package_on_both_outcomes() {
3833        let (lib_bytes, lib_decl, lib_type_info) = compile_package_modules(
3834            "test:lib",
3835            &[(
3836                "lib",
3837                r#"
3838                import security from "submilli:security";
3839                /**
3840                 * Allowed operation.
3841                 * @capability test.allowed {}
3842                 */
3843                export function allowed(): void {
3844                    security.check("test.allowed", {});
3845                }
3846                /**
3847                 * Denied operation.
3848                 * @capability test.denied {}
3849                 */
3850                export function denied(): void {
3851                    security.check("test.denied", {});
3852                }
3853                "#,
3854            )],
3855            &[],
3856        );
3857        let recording = std::sync::Arc::new(RecordingSecurity {
3858            seen: std::sync::Mutex::new(Vec::new()),
3859        });
3860        let cfg = crate::runtime::RuntimeConfig::default();
3861        let engine = cfg.engine().expect("engine");
3862        let mut data =
3863            crate::runtime::StoreData::with_vfs(crate::runtime::Vfs::tempdir().expect("tempdir"));
3864        data.install_type_info(lib_type_info);
3865        data.security_check = recording.clone();
3866        let mut store = cfg.store(&engine, data).expect("store");
3867        let mut linker = wasmtime::Linker::<crate::runtime::StoreData>::new(&engine);
3868        crate::runtime::install_runtime_async(&mut linker, &mut store)
3869            .await
3870            .expect("install runtime");
3871        let lib_module = wasmtime::Module::new(&engine, &lib_bytes).expect("library module");
3872        let lib_inst = linker
3873            .instantiate_async(&mut store, &lib_module)
3874            .await
3875            .expect("instantiate library");
3876        linker
3877            .instance(&mut store, "test:lib", lib_inst)
3878            .expect("register library instance");
3879        for name in ["allowed", "denied"] {
3880            let public_name = crate::mangle::package_symbol("test:lib", name);
3881            let func = lib_inst
3882                .get_func(&mut store, public_name.as_str())
3883                .expect("library public export");
3884            linker
3885                .define(&mut store, "test:lib", name, func)
3886                .expect("plain package import alias");
3887        }
3888
3889        let allowed = crate::compile::compile_script(
3890            r#"import { allowed } from "test:lib"; function main(): void { allowed(); }"#,
3891            "consumer.subm",
3892            crate::FileId(0),
3893            &[&lib_decl],
3894            &[],
3895        )
3896        .expect("allowed consumer compiles");
3897        store
3898            .data_mut()
3899            .install_type_info(allowed.type_info.clone());
3900        let allowed_module = wasmtime::Module::new(&engine, &allowed.wasm).expect("allowed module");
3901        let allowed_inst = linker
3902            .instantiate_async(&mut store, &allowed_module)
3903            .await
3904            .expect("instantiate allowed consumer");
3905        crate::runtime::dispatch_main_async(&mut store, &allowed_inst)
3906            .await
3907            .expect("allowed consumer runs");
3908
3909        assert_eq!(
3910            recording
3911                .seen
3912                .lock()
3913                .expect("recording security mutex")
3914                .as_slice(),
3915            &[("main".to_string(), "test.allowed".to_string())],
3916        );
3917
3918        let denied = crate::compile::compile_script(
3919            r#"import { denied } from "test:lib"; function main(): void { denied(); }"#,
3920            "consumer.subm",
3921            crate::FileId(0),
3922            &[&lib_decl],
3923            &[],
3924        )
3925        .expect("denied consumer compiles");
3926        store.data_mut().install_type_info(denied.type_info.clone());
3927        let denied_module = wasmtime::Module::new(&engine, &denied.wasm).expect("denied module");
3928        let denied_inst = linker
3929            .instantiate_async(&mut store, &denied_module)
3930            .await
3931            .expect("instantiate denied consumer");
3932        let err = crate::runtime::dispatch_main_async(&mut store, &denied_inst)
3933            .await
3934            .expect_err("denied security check throws");
3935        assert!(
3936            err.to_string().contains("permission denied"),
3937            "unexpected error: {err}",
3938        );
3939        assert!(
3940            err.to_string().contains("caller=main"),
3941            "unexpected error: {err}",
3942        );
3943    }
3944
3945    /// A denial caught mid-flight, rather than one that ends the program. Identity is read
3946    /// per call from the running frame, so nothing can persist across the catch — this pins
3947    /// that: `main`'s next call after catching a package's denial is still `main`'s, and is
3948    /// still refused the secret.
3949    #[tokio::test]
3950    async fn a_caught_throw_from_a_package_export_leaves_main_as_the_caller() {
3951        let (lib_bytes, lib_decl, lib_type_info) = compile_package_modules(
3952            "test:lib",
3953            &[(
3954                "lib",
3955                r#"
3956                /** Always throws, so the caller can catch and continue. */
3957                export function boom(): void {
3958                    throw new Error("boom");
3959                }
3960                "#,
3961            )],
3962            &[],
3963        );
3964        let cfg = crate::runtime::RuntimeConfig::default();
3965        let engine = cfg.engine().expect("engine");
3966        let mut data =
3967            crate::runtime::StoreData::with_vfs(crate::runtime::Vfs::tempdir().expect("tempdir"));
3968        data.install_type_info(lib_type_info);
3969        let mut store = cfg.store_async(&engine, data).expect("store");
3970        let mut linker = wasmtime::Linker::<crate::runtime::StoreData>::new(&engine);
3971        crate::runtime::install_runtime_async(&mut linker, &mut store)
3972            .await
3973            .expect("install runtime");
3974        let lib_module = wasmtime::Module::new(&engine, &lib_bytes).expect("library module");
3975        let lib_inst = linker
3976            .instantiate_async(&mut store, &lib_module)
3977            .await
3978            .expect("instantiate library");
3979        linker
3980            .instance(&mut store, "test:lib", lib_inst)
3981            .expect("register library instance");
3982        let public_name = crate::mangle::package_symbol("test:lib", "boom");
3983        let func = lib_inst
3984            .get_func(&mut store, public_name.as_str())
3985            .expect("library public export");
3986        linker
3987            .define(&mut store, "test:lib", "boom", func)
3988            .expect("plain package import alias");
3989
3990        let consumer = crate::compile::compile_script(
3991            r#"
3992            import { boom } from "test:lib";
3993            import { get } from "submilli:secrets";
3994
3995            function main(): void {
3996                try {
3997                    boom();
3998                } catch (e: Error) {
3999                    // The export's frame must come off even though it threw.
4000                }
4001                const _ = get("TOKEN");
4002            }
4003            "#,
4004            "consumer.subm",
4005            crate::FileId(0),
4006            &[&lib_decl],
4007            &[],
4008        )
4009        .expect("consumer compiles");
4010        store
4011            .data_mut()
4012            .install_type_info(consumer.type_info.clone());
4013        let consumer_module =
4014            wasmtime::Module::new(&engine, &consumer.wasm).expect("consumer module");
4015        let consumer_inst = linker
4016            .instantiate_async(&mut store, &consumer_module)
4017            .await
4018            .expect("instantiate consumer");
4019
4020        let err = crate::runtime::dispatch_main_async(&mut store, &consumer_inst)
4021            .await
4022            .expect_err("secrets.get from main is refused");
4023
4024        assert!(
4025            err.to_string().contains("caller=main"),
4026            "a leaked frame would attribute this to test:lib: {err}",
4027        );
4028    }
4029
4030    /// Records every gated call and optionally refuses one named caller, so a
4031    /// test can pin *who* a call was attributed to rather than only whether it
4032    /// was allowed.
4033    struct RecordingPerCaller {
4034        seen: std::sync::Mutex<Vec<(String, String)>>,
4035        deny_caller: Option<&'static str>,
4036    }
4037
4038    impl crate::runtime::SecurityCheck for RecordingPerCaller {
4039        fn check(
4040            &self,
4041            caller: &str,
4042            capability: &str,
4043            _context: &serde_json::Value,
4044        ) -> crate::runtime::CheckOutcome {
4045            self.seen
4046                .lock()
4047                .expect("recording security mutex")
4048                .push((caller.to_string(), capability.to_string()));
4049            if self.deny_caller == Some(caller) {
4050                crate::runtime::CheckOutcome::Deny {
4051                    rule: None,
4052                    reason: "blocked by test".to_string(),
4053                }
4054            } else {
4055                crate::runtime::CheckOutcome::Allow { rule: None }
4056            }
4057        }
4058    }
4059
4060    /// A package whose module-level initializer makes a gated call: the probe
4061    /// runs inside the Wasm start function, during instantiation.
4062    const PACKAGE_WITH_GATED_INITIALIZER: &str = r#"
4063        import { exists } from "submilli:fs";
4064        const _probe: boolean = exists("/init-probe");
4065        /** No-op export; the initializer is what this package is for. */
4066        export function noop(): void {}
4067        "#;
4068
4069    async fn install_package_with_gated_initializer(
4070        recording: std::sync::Arc<RecordingPerCaller>,
4071    ) -> (
4072        wasmtime::Store<crate::runtime::StoreData>,
4073        wasmtime::Result<()>,
4074    ) {
4075        let (lib_bytes, lib_decl, lib_type_info) =
4076            compile_package_modules("test:lib", &[("lib", PACKAGE_WITH_GATED_INITIALIZER)], &[]);
4077        let cfg = crate::runtime::RuntimeConfig::default();
4078        let engine = cfg.engine().expect("engine");
4079        let mut data =
4080            crate::runtime::StoreData::with_vfs(crate::runtime::Vfs::tempdir().expect("tempdir"));
4081        data.security_check = recording;
4082        let mut store = cfg.store(&engine, data).expect("store");
4083        let mut linker = wasmtime::Linker::<crate::runtime::StoreData>::new(&engine);
4084        crate::runtime::install_runtime_async(&mut linker, &mut store)
4085            .await
4086            .expect("install runtime");
4087        let lib_module = wasmtime::Module::new(&engine, &lib_bytes).expect("library module");
4088        let outcome = crate::runtime::install_package_modules_async(
4089            &mut linker,
4090            &mut store,
4091            &[crate::runtime::LinkedPackageModule {
4092                module: &lib_module,
4093                declaration: &lib_decl,
4094                type_info: &lib_type_info,
4095            }],
4096        )
4097        .await;
4098        (store, outcome)
4099    }
4100
4101    #[tokio::test]
4102    async fn package_initializers_are_attributed_to_the_package() {
4103        let recording = std::sync::Arc::new(RecordingPerCaller {
4104            seen: std::sync::Mutex::new(Vec::new()),
4105            deny_caller: None,
4106        });
4107        let (_store, outcome) = install_package_with_gated_initializer(recording.clone()).await;
4108        outcome.expect("package instantiates");
4109
4110        assert_eq!(
4111            recording
4112                .seen
4113                .lock()
4114                .expect("recording security mutex")
4115                .as_slice(),
4116            &[("test:lib".to_string(), "fs.stat".to_string())],
4117        );
4118    }
4119
4120    /// The documented behaviour break: package init used to run as `main`, so a
4121    /// package whose initializer needs a capability granted to `main` but not to
4122    /// the package now fails to instantiate.
4123    #[tokio::test]
4124    async fn package_initializer_denied_to_the_package_fails_and_restores_the_stack() {
4125        let recording = std::sync::Arc::new(RecordingPerCaller {
4126            seen: std::sync::Mutex::new(Vec::new()),
4127            deny_caller: Some("test:lib"),
4128        });
4129        let (_store, outcome) = install_package_with_gated_initializer(recording.clone()).await;
4130        let err = outcome.expect_err("denied initializer traps");
4131
4132        // The initializer runs in the start function, so the denial escapes
4133        // instantiation as the engine's opaque `ThrownException`. An operator
4134        // reading only "wasm exception thrown" cannot tell which package or
4135        // capability was refused, which is the whole diagnosis.
4136        let message = err.to_string();
4137        assert!(
4138            message.contains("package `test:lib` failed to initialize"),
4139            "the failure must name the package: {message}",
4140        );
4141        assert!(
4142            message.contains("fs.stat") && message.contains("caller=test:lib"),
4143            "the failure must name the capability and caller: {message}",
4144        );
4145
4146        assert_eq!(
4147            recording
4148                .seen
4149                .lock()
4150                .expect("recording security mutex")
4151                .as_slice(),
4152            &[("test:lib".to_string(), "fs.stat".to_string())],
4153        );
4154    }
4155
4156    fn fake_globals_definitions() -> crate::PackageDeclaration {
4157        use crate::{PackageDeclaration, Span, Type, ValueKind, ValueSymbol};
4158        let mut defs = PackageDeclaration::with_package("test:globals");
4159        defs.values.insert(
4160            "counter".to_string(),
4161            ValueSymbol {
4162                name: "counter".to_string(),
4163                mangled_name: crate::mangle::package_symbol("test:globals", "counter"),
4164                declaration_span: Span::at(crate::FileId(0)),
4165                kind: ValueKind::Let {
4166                    ty: Type::Number,
4167                    doc: None,
4168                },
4169            },
4170        );
4171        defs.values.insert(
4172            "max_iterations".to_string(),
4173            ValueSymbol {
4174                name: "max_iterations".to_string(),
4175                mangled_name: crate::mangle::package_symbol("test:globals", "max_iterations"),
4176                declaration_span: Span::at(crate::FileId(0)),
4177                kind: ValueKind::Const {
4178                    ty: Type::Number,
4179                    doc: None,
4180                },
4181            },
4182        );
4183        defs
4184    }
4185
4186    fn compile_with_package_imports<'a>(
4187        source: &str,
4188        packages: &'a [&'a crate::PackageDeclaration],
4189    ) -> Vec<u8> {
4190        let ta = type_check_with_packages(source, packages);
4191        let (prelude_defs, host_defs, internal_defs) =
4192            prelude::cached_runtime_package_declarations();
4193        let mut dependencies: Vec<&crate::PackageDeclaration> = prelude_defs.iter().collect();
4194        dependencies.extend(host_defs.iter());
4195        dependencies.extend(internal_defs.iter());
4196        dependencies.extend_from_slice(packages);
4197        codegen(source, "script.subm", crate::FileId(0), &ta, &dependencies)
4198            .expect("code generation")
4199    }
4200
4201    fn imports_with_kind(bytes: &[u8]) -> Vec<(String, String, &'static str, bool)> {
4202        let mut out = Vec::new();
4203        for payload in Parser::new(0).parse_all(bytes) {
4204            if let Payload::ImportSection(reader) = payload.expect("payload") {
4205                for entry in reader {
4206                    match entry.expect("import") {
4207                        wasmparser::Imports::Single(_, imp) => {
4208                            let (kind, mutable) = match imp.ty {
4209                                wasmparser::TypeRef::Func(_) => ("func", false),
4210                                wasmparser::TypeRef::Global(g) => ("global", g.mutable),
4211                                _ => ("other", false),
4212                            };
4213                            out.push((imp.module.to_string(), imp.name.to_string(), kind, mutable));
4214                        }
4215                        other => panic!("unexpected import form {other:?}"),
4216                    }
4217                }
4218            }
4219        }
4220        out
4221    }
4222
4223    #[test]
4224    fn let_and_const_become_global_imports() {
4225        let defs = fake_globals_definitions();
4226        let bytes = compile_with_package_imports(
4227            r#"import { counter, max_iterations } from "test:globals";
4228function main(): number { return counter + max_iterations; }"#,
4229            &[&defs],
4230        );
4231        let imp = imports_with_kind(&bytes);
4232        let counter_name = crate::mangle::package_symbol("test:globals", "counter");
4233        let counter = imp
4234            .iter()
4235            .find(|(_, n, _, _)| n == counter_name.as_str())
4236            .expect("counter import");
4237        assert_eq!(counter.0, "test:globals");
4238        assert_eq!(counter.2, "global");
4239        assert!(counter.3, "let imports as mutable global");
4240
4241        let max_iterations_name = crate::mangle::package_symbol("test:globals", "max_iterations");
4242        let max_it = imp
4243            .iter()
4244            .find(|(_, n, _, _)| n == max_iterations_name.as_str())
4245            .expect("max_iterations import");
4246        assert_eq!(max_it.0, "test:globals");
4247        assert_eq!(max_it.2, "global");
4248        // A codegen-compiled package exports every top-level global mutable (so
4249        // `_start` can initialize it), so a cross-package `const` import must be
4250        // mutable too — const immutability is typechecker-enforced, not Wasm.
4251        assert!(
4252            max_it.3,
4253            "const imports as mutable global to match the exporter"
4254        );
4255    }
4256
4257    #[test]
4258    fn ported_methods_route_to_prelude_host_not_wasm_wrapper() {
4259        let src = "function main(): void { \
4260                   [1, 2].forEach((x: number) => { }); \
4261                   const r: string = \"ab\".repeat(2); \
4262                   assert(r.length === 4); \
4263                   const s = new Set<number>(); \
4264                   s.add(1); \
4265                   s.delete(1); \
4266                   s.add(2); \
4267                   assert(s.size === 1 && s.has(2)); \
4268                   const f: string = (3.14).toFixed(1); \
4269                   const b: string = true.toString(); \
4270                   assert(Number.isInteger(4) && Number.parseInt(\"10\", 10) === 10); \
4271                   assert(Number.EPSILON > 0); \
4272                   assert(Math.PI > 3 && Math.abs(-2) === 2 && Math.imul(2, 3) === 6 && Math.min(3, 1, 2) === 1 && Math.random() >= 0); \
4273                   const u = Uint8Array.fromHex(\"01ff\"); \
4274                   u.fill(7); \
4275                   assert(u.length === 2 && u.toHex() === \"0707\"); \
4276                   const enc = new TextEncoder().encode(\"hi\"); \
4277                   const dec: string = new TextDecoder().decode(enc); \
4278                   assert(dec === \"hi\"); \
4279                   const pd = Temporal.PlainDate.from(\"2024-03-09\"); \
4280                   const pd2 = pd.add({ days: 1 }).subtract({ days: 1 }).with({ day: 10 }); \
4281                   const pd3 = pd.toPlainYearMonth().toPlainDate({ day: 9 }); \
4282                   const pd4 = pd.toPlainMonthDay().toPlainDate({ year: 2024 }); \
4283                   const pdtFromDate = pd.toPlainDateTime(); \
4284                   const zFromDate = pd.toZonedDateTime(\"UTC\"); \
4285                   assert(pd.until(pd2).days === 1 && pd2.since(pd).days === 1); \
4286                   assert(pd.year === 2024 && pd.toString() === \"2024-03-09\" && pd.toJSON() === \"2024-03-09\" && pd2.day === 10 && pd3.day === 9 && pd4.year === 2024 && pdtFromDate.hour === 0 && zFromDate.year === 2024); \
4287                   const pt = Temporal.PlainTime.from(\"15:30:45.123456789\"); \
4288                   const pt2 = pt.add({ hours: 1 }).with({ minute: 0 }); \
4289                   assert(pt.until(pt2).hours === 1 && pt.hour === 15 && pt.nanosecond === 123456789 && pt2.minute === 0); \
4290                   const pdt = Temporal.PlainDateTime.from(\"2024-03-09T15:30:45\"); \
4291                   const pdt2 = pdt.subtract({ hours: 1 }).with({ second: 0 }); \
4292                   const zFromDateTime = pdt.toZonedDateTime(\"UTC\"); \
4293                   assert(pdt.since(pdt2).hours === 1 && pdt.toPlainDate().day === 9 && pdt.toPlainTime().hour === 15 && pdt.toPlainYearMonth().month === 3 && pdt.toPlainMonthDay().day === 9); \
4294                   assert(pdt.month === 3 && pdt2.second === 0 && zFromDateTime.month === 3 && Temporal.PlainDateTime.compare(pdt, pdt) === 0); \
4295                   const ym = Temporal.PlainYearMonth.from(\"2024-03\"); \
4296                   const ym2 = ym.add({ months: 1 }).with({ month: 5 }); \
4297                   assert(ym.until(ym2).months === 2 && ym.monthCode === \"M03\" && ym2.month === 5); \
4298                   const md = Temporal.PlainMonthDay.from(\"03-09\"); \
4299                   const md2 = md.with({ day: 10 }); \
4300                   assert(md.day === 9 && md2.day === 10); \
4301                   const i0 = Temporal.Instant.from(\"2024-03-09T15:30:45.123456789Z\"); \
4302                   const i1 = Temporal.Instant.fromEpochMilliseconds(i0.epochMilliseconds); \
4303                   const i2 = Temporal.Instant.fromEpochNanoseconds(i0.epochNanoseconds); \
4304                   const i3 = i0.add({ hours: 1 }).subtract({ hours: 1 }).round({ smallestUnit: \"second\" }); \
4305                   const id0 = i0.until(i1); const id1 = i1.since(i0); \
4306                   const iz = i0.toZonedDateTimeISO(\"UTC\"); \
4307                   assert(Temporal.Instant.compare(i0, i2) === 0 && i0.equals(i2) && i0.toString().length > 0 && i0.toJSON().length > 0 && i3.epochMilliseconds > 0 && id0.seconds === id1.seconds && iz.year === 2024); \
4308                   const dur0 = new Temporal.Duration({ hours: 1 }); const dur1 = Temporal.Duration.from(\"PT1H\"); \
4309                   assert(Temporal.Duration.compare(dur0, dur1) === 0 && Temporal.ZonedDateTime.compare(iz, iz) === 0); \
4310                   const ni = Temporal.Now.instant(); const ntz = Temporal.Now.timeZoneId(); const nz = Temporal.Now.zonedDateTimeISO(\"UTC\"); \
4311                   assert(ni.epochMilliseconds > 0 && ntz.length > 0 && nz.timeZoneId === \"UTC\"); \
4312                   const re: RegExp = new RegExp(\"a\", \"g\"); \
4313                   assert(re.test(\"a\") && re.global && re.source === \"a\" && re.flags === \"g\" && re.lastIndex >= 0); \
4314                   const rmatch: RegExpMatch | null = re.exec(\"a\"); \
4315                   assert(rmatch !== null); \
4316                   if (rmatch !== null) { const gg = rmatch.groups; const ng = rmatch.namedGroups; \
4317                     assert(rmatch.match === \"a\" && rmatch.index === 0 && rmatch.input === \"a\" && gg.length >= 0 && ng.size >= 0); } \
4318                   const sm: RegExpMatch | null = \"a\".match(re); \
4319                   const marr = \"aa\".matchAll(re); \
4320                   const rep: string = \"a\".replace(\"a\", \"b\"); \
4321                   const repall: string = \"aa\".replaceAll(\"a\", \"b\"); \
4322                   const sp = \"a,b\".split(\",\"); \
4323                   assert(sm === sm && \"a\".search(re) >= -1 && marr.length >= 0 && rep.length >= 0 && repall.length >= 0 && sp.length === 2); \
4324                   assert(isNaN(NaN) && !isFinite(Infinity)); \
4325                   const oo = { a: 1 }; \
4326                   assert(Object.keys(oo).length === 1 && Object.values(oo).length === 1); \
4327                   assert(Object.entries(oo).length === 1 && Object.hasOwn(oo, \"a\") && Object.is(1, 1)); }";
4328        let ta = type_check(src);
4329        let (prelude_defs, host_defs, internal_defs) =
4330            prelude::cached_runtime_package_declarations();
4331        let mut dependencies: Vec<&crate::PackageDeclaration> = prelude_defs.iter().collect();
4332        dependencies.extend(host_defs.iter());
4333        dependencies.extend(internal_defs.iter());
4334        let bytes = codegen(src, "script.subm", crate::FileId(0), &ta, &dependencies)
4335            .expect("code generation");
4336        let imp = imports_with_kind(&bytes);
4337
4338        // Used ported methods route through the Rust prelude-host package under
4339        // their dispatch key; the dead Wasm prelude wrappers are left unimported.
4340        // `Set#size` is a property getter, so checking it routes proves the host
4341        // port owns the whole Set surface — nothing falls back to the Wasm prelude.
4342        for (iface, method) in [
4343            ("String", "repeat"),
4344            ("Array", "forEach"),
4345            ("SetConstructor", "new"),
4346            ("Set", "add"),
4347            ("Set", "has"),
4348            ("Set", "delete"),
4349            ("Set", "size"),
4350            ("Number", "toFixed"),
4351            ("Boolean", "toString"),
4352            ("NumberConstructor", "isInteger"),
4353            ("NumberConstructor", "parseInt"),
4354            // Static constant `Number.EPSILON` imports its global from prelude-host too.
4355            ("NumberConstructor", "EPSILON"),
4356            // Uint8Array: an instance method, a property getter, a static.
4357            ("Uint8Array", "fill"),
4358            ("Uint8Array", "length"),
4359            ("Uint8Array", "toHex"),
4360            ("Uint8ArrayConstructor", "fromHex"),
4361            // TextEncoder/TextDecoder: stateless instance methods + their `new` ctors.
4362            ("TextEncoder", "encode"),
4363            ("TextDecoder", "decode"),
4364            ("TextEncoderConstructor", "new"),
4365            ("TextDecoderConstructor", "new"),
4366            // RegExp: construction, instance methods, property getters.
4367            ("RegExpConstructor", "new"),
4368            ("RegExp", "test"),
4369            ("RegExp", "exec"),
4370            ("RegExp", "source"),
4371            ("RegExp", "flags"),
4372            ("RegExp", "lastIndex"),
4373            ("RegExp", "global"),
4374            // RegExpMatch accessors, including the Array/Map-building ones.
4375            ("RegExpMatch", "match"),
4376            ("RegExpMatch", "index"),
4377            ("RegExpMatch", "input"),
4378            ("RegExpMatch", "groups"),
4379            ("RegExpMatch", "namedGroups"),
4380            // String regex-arm methods (subsumes the SUB-605 string arm).
4381            ("String", "match"),
4382            ("String", "search"),
4383            ("String", "matchAll"),
4384            ("String", "replace"),
4385            ("String", "replaceAll"),
4386            ("String", "split"),
4387            // ObjectConstructor statics.
4388            ("ObjectConstructor", "keys"),
4389            ("ObjectConstructor", "values"),
4390            ("ObjectConstructor", "entries"),
4391            ("ObjectConstructor", "hasOwn"),
4392            ("ObjectConstructor", "is"),
4393        ] {
4394            let key = crate::mangle::extend(&crate::mangle::prelude(iface), method);
4395            assert!(
4396                imp.iter()
4397                    .any(|(m, n, _, _)| m == crate::runtime::prelude::MODULE_NAME
4398                        && n == key.as_str()),
4399                "{iface}#{method} should import from the prelude host module"
4400            );
4401        }
4402
4403        for name in ["PI", "abs", "imul", "min", "random"] {
4404            let key = crate::runtime::prelude::math::math_key(name);
4405            assert!(
4406                imp.iter()
4407                    .any(|(m, n, _, _)| m == crate::runtime::prelude::MODULE_NAME
4408                        && n == key.as_str()),
4409                "Math#{name} should import from the prelude host module"
4410            );
4411        }
4412
4413        // Temporal methods route to Rust prelude-host functions under their
4414        // prelude dispatch keys. Used calls must not import the dead Wasm wrappers.
4415        for (iface, method) in [
4416            ("InstantConstructor", "from"),
4417            ("InstantConstructor", "fromEpochMilliseconds"),
4418            ("InstantConstructor", "fromEpochNanoseconds"),
4419            ("InstantConstructor", "compare"),
4420            ("Instant", "epochMilliseconds"),
4421            ("Instant", "epochNanoseconds"),
4422            ("Instant", "add"),
4423            ("Instant", "subtract"),
4424            ("Instant", "until"),
4425            ("Instant", "since"),
4426            ("Instant", "round"),
4427            ("Instant", "equals"),
4428            ("Instant", "toString"),
4429            ("Instant", "toJSON"),
4430            ("Instant", "toZonedDateTimeISO"),
4431            ("Now", "instant"),
4432            ("Now", "timeZoneId"),
4433            ("Now", "zonedDateTimeISO"),
4434            ("DurationConstructor", "new"),
4435            ("DurationConstructor", "from"),
4436            ("DurationConstructor", "compare"),
4437            ("Duration", "years"),
4438            ("Duration", "months"),
4439            ("Duration", "weeks"),
4440            ("Duration", "days"),
4441            ("Duration", "hours"),
4442            ("Duration", "minutes"),
4443            ("Duration", "seconds"),
4444            ("Duration", "milliseconds"),
4445            ("Duration", "microseconds"),
4446            ("Duration", "nanoseconds"),
4447            ("Duration", "sign"),
4448            ("Duration", "blank"),
4449            ("Duration", "add"),
4450            ("Duration", "subtract"),
4451            ("Duration", "negated"),
4452            ("Duration", "abs"),
4453            ("Duration", "with"),
4454            ("Duration", "round"),
4455            ("Duration", "total"),
4456            ("Duration", "toString"),
4457            ("Duration", "toJSON"),
4458            ("ZonedDateTimeConstructor", "from"),
4459            ("ZonedDateTimeConstructor", "compare"),
4460            ("ZonedDateTime", "timeZoneId"),
4461            ("ZonedDateTime", "offset"),
4462            ("ZonedDateTime", "offsetNanoseconds"),
4463            ("ZonedDateTime", "epochMilliseconds"),
4464            ("ZonedDateTime", "epochNanoseconds"),
4465            ("ZonedDateTime", "hoursInDay"),
4466            ("ZonedDateTime", "year"),
4467            ("ZonedDateTime", "monthCode"),
4468            ("ZonedDateTime", "dayOfWeek"),
4469            ("ZonedDateTime", "inLeapYear"),
4470            ("ZonedDateTime", "nanosecond"),
4471            ("ZonedDateTime", "add"),
4472            ("ZonedDateTime", "subtract"),
4473            ("ZonedDateTime", "until"),
4474            ("ZonedDateTime", "since"),
4475            ("ZonedDateTime", "with"),
4476            ("ZonedDateTime", "withTimeZone"),
4477            ("ZonedDateTime", "round"),
4478            ("ZonedDateTime", "startOfDay"),
4479            ("ZonedDateTime", "toInstant"),
4480            ("ZonedDateTime", "toPlainDate"),
4481            ("ZonedDateTime", "toPlainTime"),
4482            ("ZonedDateTime", "toPlainDateTime"),
4483            ("ZonedDateTime", "equals"),
4484            ("ZonedDateTime", "toString"),
4485            ("ZonedDateTime", "toJSON"),
4486            ("PlainDateConstructor", "from"),
4487            ("PlainDate", "year"),
4488            ("PlainDate", "toString"),
4489            ("PlainDate", "toJSON"),
4490            ("PlainDate", "add"),
4491            ("PlainDate", "subtract"),
4492            ("PlainDate", "until"),
4493            ("PlainDate", "since"),
4494            ("PlainDate", "with"),
4495            ("PlainDate", "toPlainYearMonth"),
4496            ("PlainDate", "toPlainMonthDay"),
4497            ("PlainDate", "toPlainDateTime"),
4498            ("PlainDate", "toZonedDateTime"),
4499            ("PlainTimeConstructor", "from"),
4500            ("PlainTime", "hour"),
4501            ("PlainTime", "nanosecond"),
4502            ("PlainTime", "add"),
4503            ("PlainTime", "until"),
4504            ("PlainTime", "with"),
4505            ("PlainDateTimeConstructor", "from"),
4506            ("PlainDateTime", "month"),
4507            ("PlainDateTime", "subtract"),
4508            ("PlainDateTime", "since"),
4509            ("PlainDateTime", "with"),
4510            ("PlainDateTime", "toPlainDate"),
4511            ("PlainDateTime", "toPlainTime"),
4512            ("PlainDateTime", "toPlainYearMonth"),
4513            ("PlainDateTime", "toPlainMonthDay"),
4514            ("PlainDateTime", "toZonedDateTime"),
4515            ("PlainDateTimeConstructor", "compare"),
4516            ("PlainYearMonthConstructor", "from"),
4517            ("PlainYearMonth", "monthCode"),
4518            ("PlainYearMonth", "add"),
4519            ("PlainYearMonth", "until"),
4520            ("PlainYearMonth", "with"),
4521            ("PlainYearMonth", "toPlainDate"),
4522            ("PlainMonthDayConstructor", "from"),
4523            ("PlainMonthDay", "day"),
4524            ("PlainMonthDay", "with"),
4525            ("PlainMonthDay", "toPlainDate"),
4526        ] {
4527            let key = crate::mangle::extend(
4528                &crate::mangle::extend(&crate::mangle::prelude("Temporal"), iface),
4529                method,
4530            );
4531            assert!(
4532                imp.iter()
4533                    .any(|(m, n, _, _)| m == crate::runtime::prelude::MODULE_NAME
4534                        && n == key.as_str()),
4535                "Temporal#{iface}#{method} should import from the prelude host module"
4536            );
4537        }
4538
4539        // The boxed-primitive vtables are host-owned now, imported from prelude-host.
4540        for name in ["boxed_number_vtable", "boxed_boolean_vtable"] {
4541            let key = crate::mangle::prelude(name);
4542            assert!(
4543                imp.iter()
4544                    .any(|(m, n, _, _)| m == crate::runtime::prelude::MODULE_NAME
4545                        && n == key.as_str()),
4546                "{name} should import from the prelude host module"
4547            );
4548        }
4549
4550        // `globalThis` numeric symbols also route to prelude-host. `NaN` is
4551        // lowered as an immediate in this fixture, so no import is emitted for it.
4552        for name in ["isNaN", "isFinite", "Infinity"] {
4553            let key = crate::mangle::prelude(name);
4554            assert!(
4555                imp.iter()
4556                    .any(|(m, n, _, _)| m == crate::runtime::prelude::MODULE_NAME
4557                        && n == key.as_str()),
4558                "globalThis {name} should import from the prelude host module"
4559            );
4560        }
4561        let unused = crate::mangle::extend(&crate::mangle::prelude("String"), "charAt");
4562        assert!(
4563            !imp.iter().any(|(_, n, _, _)| n == unused.as_str()),
4564            "unused ported methods should be tree-shaken",
4565        );
4566    }
4567
4568    #[test]
4569    fn global_imports_assigned_indices_starting_from_zero() {
4570        let defs = fake_globals_definitions();
4571        let ta = type_check("function main(): void { }");
4572        let prelude_defs = prelude::prelude_package_declaration();
4573        let prelude_host_defs = crate::runtime::prelude::package_declaration();
4574        let dependencies: [&crate::PackageDeclaration; 3] =
4575            [&prelude_defs, &prelude_host_defs, &defs];
4576        let _ = codegen(
4577            "function main(): void { }",
4578            "script.subm",
4579            crate::FileId(0),
4580            &ta,
4581            &dependencies,
4582        );
4583
4584        let mut map = SymbolTable::default();
4585        let mut next_global_idx: u32 = 0;
4586        for defs in [&prelude_defs, &defs] {
4587            for value in defs.values.values() {
4588                let ty = match &value.kind {
4589                    crate::ValueKind::Let { ty, .. } | crate::ValueKind::Const { ty, .. } => {
4590                        Some(ty)
4591                    }
4592                    _ => None,
4593                };
4594                if let Some(ty) = ty {
4595                    if matches!(ty, crate::Type::InterfaceRef { .. }) {
4596                        continue;
4597                    }
4598                    map.record_global(value.mangled_name.clone(), next_global_idx);
4599                    next_global_idx += 1;
4600                }
4601            }
4602        }
4603        assert_eq!(map.prelude_global_idx("Infinity"), Some(0));
4604        assert_eq!(map.prelude_global_idx("NaN"), Some(1));
4605        assert_eq!(map.prelude_global_idx("string_comma"), Some(2));
4606        assert_eq!(map.prelude_global_idx("string_false"), Some(3));
4607        assert_eq!(map.prelude_global_idx("string_null"), Some(4));
4608        assert_eq!(map.prelude_global_idx("string_object_function"), Some(5));
4609        assert_eq!(map.prelude_global_idx("string_object_object"), Some(6));
4610        assert_eq!(map.prelude_global_idx("string_true"), Some(7));
4611        assert_eq!(
4612            map.global_idx(&crate::mangle::package_symbol("test:globals", "counter")),
4613            Some(8),
4614        );
4615        assert_eq!(
4616            map.global_idx(&crate::mangle::package_symbol(
4617                "test:globals",
4618                "max_iterations"
4619            )),
4620            Some(9),
4621        );
4622        assert_eq!(
4623            map.global_idx(&crate::mangle::package_symbol("test:globals", "missing")),
4624            None,
4625        );
4626    }
4627
4628    #[test]
4629    fn module_with_global_imports_validates() {
4630        let defs = fake_globals_definitions();
4631        let bytes = compile_with_package_imports("function main(): void { }", &[&defs]);
4632        wasmparser::Validator::new()
4633            .validate_all(&bytes)
4634            .expect("module with global imports validates");
4635    }
4636
4637    #[test]
4638    fn no_top_level_globals_still_emits_empty_start() {
4639        let bytes = compile("function main(): void { }");
4640        assert_eq!(
4641            start_function_idx(&bytes),
4642            Some(imported_func_count(&bytes))
4643        );
4644        // User functions, module start, output shim, and shared field lookup.
4645        assert_eq!(function_count(&bytes), 4);
4646    }
4647
4648    #[test]
4649    fn top_level_string_let_declares_one_mutable_ref_global() {
4650        let bytes = compile(r#"let x: string = "hi"; function main(): void { }"#);
4651        let baseline = compile("function main(): void { }");
4652        assert_eq!(global_count(&bytes), global_count(&baseline) + 1);
4653        assert!(
4654            global_decls(&bytes).contains(&(true, "ref")),
4655            "top-level string let should declare a mutable ref global",
4656        );
4657    }
4658
4659    #[test]
4660    fn top_level_const_also_declared_mutable_at_wasm_level() {
4661        let bytes = compile(r#"const x: string = "hi"; function main(): void { }"#);
4662        let baseline = compile("function main(): void { }");
4663        assert_eq!(global_count(&bytes), global_count(&baseline) + 1);
4664        assert!(
4665            global_decls(&bytes).contains(&(true, "ref")),
4666            "top-level string const should declare a mutable ref global",
4667        );
4668    }
4669
4670    #[test]
4671    fn top_level_let_emits_start_section_pointing_at_first_local_func() {
4672        let bytes = compile(r#"let x: string = "hi"; function main(): void { }"#);
4673        let first_local_func = imported_func_count(&bytes);
4674        assert_eq!(start_function_idx(&bytes), Some(first_local_func));
4675        assert_eq!(main_export_func_idx(&bytes), Some(first_local_func + 1));
4676    }
4677
4678    #[test]
4679    fn top_level_string_let_validates_and_links_with_prelude() {
4680        let bytes = compile(r#"let x: string = "hello"; function main(): void { }"#);
4681        instantiate_against_prelude(&bytes);
4682    }
4683
4684    #[test]
4685    fn arithmetic_precedence_returns_seven() {
4686        assert_eq!(
4687            run_main_f64("function main(): number { return 1 + 2 * 3; }"),
4688            7.0
4689        );
4690    }
4691
4692    #[test]
4693    fn arithmetic_subtraction_and_division() {
4694        assert_eq!(
4695            run_main_f64("function main(): number { return 10 - 4 / 2; }"),
4696            8.0
4697        );
4698    }
4699
4700    #[test]
4701    fn modulo_truncates_toward_zero() {
4702        // JS semantics: lhs sign wins.
4703        assert_eq!(
4704            run_main_f64("function main(): number { return 10 % 3; }"),
4705            1.0
4706        );
4707        assert_eq!(
4708            run_main_f64("function main(): number { return -10 % 3; }"),
4709            -1.0
4710        );
4711        assert_eq!(
4712            run_main_f64("function main(): number { return 10 % -3; }"),
4713            1.0
4714        );
4715    }
4716
4717    #[test]
4718    fn division_by_zero_yields_infinity() {
4719        // f64.div doesn't trap on 1/0 per IEEE 754.
4720        let r = run_main_f64("function main(): number { return 1 / 0; }");
4721        assert!(r.is_infinite() && r.is_sign_positive());
4722    }
4723
4724    #[test]
4725    fn zero_div_zero_yields_nan() {
4726        let r = run_main_f64("function main(): number { return 0 / 0; }");
4727        assert!(r.is_nan());
4728    }
4729
4730    #[test]
4731    fn unary_neg_negates_grouped_addition() {
4732        assert_eq!(
4733            run_main_f64("function main(): number { return -(2 + 3); }"),
4734            -5.0
4735        );
4736    }
4737
4738    #[test]
4739    fn unary_pos_is_identity() {
4740        assert_eq!(run_main_f64("function main(): number { return +5; }"), 5.0);
4741    }
4742
4743    #[test]
4744    fn nested_arithmetic_round_trip() {
4745        assert_eq!(
4746            run_main_f64("function main(): number { return (1 + 2) * (4 - 1); }"),
4747            9.0
4748        );
4749    }
4750
4751    #[test]
4752    fn top_level_let_referenced_from_main_body() {
4753        assert_eq!(
4754            run_main_f64("let x: number = 7; function main(): number { return x + 1; }"),
4755            8.0
4756        );
4757    }
4758
4759    #[test]
4760    fn while_counter_loop_sums_three_iterations() {
4761        let src = "function main(): number {\n  let i = 0;\n  let n = 0;\n  while (i < 3) {\n    n = n + i;\n    i = i + 1;\n  }\n  return n;\n}";
4762        assert_eq!(run_main_f64(src), 3.0);
4763    }
4764
4765    #[test]
4766    fn while_zero_iterations_skips_body() {
4767        let src = "function main(): number {\n  let i = 0;\n  while (i < 0) {\n    i = i + 1;\n  }\n  return i;\n}";
4768        assert_eq!(run_main_f64(src), 0.0);
4769    }
4770
4771    #[test]
4772    fn while_with_nested_if_counts_evens() {
4773        let src = "function main(): number {\n  let i = 0;\n  let evens = 0;\n  while (i < 5) {\n    if (i % 2 === 0) {\n      evens = evens + 1;\n    }\n    i = i + 1;\n  }\n  return evens;\n}";
4774        assert_eq!(run_main_f64(src), 3.0);
4775    }
4776
4777    #[test]
4778    fn nested_while_loops_multiply_iterations() {
4779        let src = "function main(): number {\n  let total = 0;\n  let i = 0;\n  while (i < 3) {\n    let j = 0;\n    while (j < 4) {\n      total = total + 1;\n      j = j + 1;\n    }\n    i = i + 1;\n  }\n  return total;\n}";
4780        assert_eq!(run_main_f64(src), 12.0);
4781    }
4782
4783    #[test]
4784    fn while_drives_top_level_global() {
4785        let src = "let g = 0; function main(): number {\n  let i = 0;\n  while (i < 4) {\n    g = g + 1;\n    i = i + 1;\n  }\n  return g;\n}";
4786        assert_eq!(run_main_f64(src), 4.0);
4787    }
4788
4789    #[test]
4790    fn if_true_takes_then_branch() {
4791        assert_eq!(
4792            run_main_f64("function main(): number { if (true) { return 1; } return 2; }"),
4793            1.0
4794        );
4795    }
4796
4797    #[test]
4798    fn if_false_skips_then_branch() {
4799        assert_eq!(
4800            run_main_f64("function main(): number { if (false) { return 1; } return 2; }"),
4801            2.0
4802        );
4803    }
4804
4805    #[test]
4806    fn if_else_picks_branch() {
4807        assert_eq!(
4808            run_main_f64("function main(): number { if (false) { return 1; } else { return 2; } }"),
4809            2.0
4810        );
4811        assert_eq!(
4812            run_main_f64("function main(): number { if (true) { return 1; } else { return 2; } }"),
4813            1.0
4814        );
4815    }
4816
4817    #[test]
4818    fn if_combines_with_assignment() {
4819        assert_eq!(
4820            run_main_f64(
4821                "function main(): number { let x: number = 0; if (1 < 2) { x = 1; } else { x = 2; } return x; }"
4822            ),
4823            1.0
4824        );
4825    }
4826
4827    #[test]
4828    fn else_if_chain_terminal_else() {
4829        let src = "function main(): number { let x: number = 3; if (x === 1) { return 10; } else if (x === 2) { return 20; } else { return 30; } }";
4830        assert_eq!(run_main_f64(src), 30.0);
4831    }
4832
4833    #[test]
4834    fn else_if_chain_middle_branch() {
4835        let src = "function main(): number { let x: number = 2; if (x === 1) { return 10; } else if (x === 2) { return 20; } else { return 30; } }";
4836        assert_eq!(run_main_f64(src), 20.0);
4837    }
4838
4839    #[test]
4840    fn nested_if_inside_if() {
4841        let src = "function main(): number { let x: number = 3; if (x > 0) { if (x > 2) { return 99; } return 11; } return 0; }";
4842        assert_eq!(run_main_f64(src), 99.0);
4843    }
4844
4845    #[test]
4846    fn function_local_let_round_trips_via_local() {
4847        assert_eq!(
4848            run_main_f64("function main(): number { let x: number = 1; return x; }"),
4849            1.0
4850        );
4851    }
4852
4853    #[test]
4854    fn function_local_assignment_overwrites_local() {
4855        assert_eq!(
4856            run_main_f64("function main(): number { let x: number = 1; x = 2; return x; }"),
4857            2.0
4858        );
4859    }
4860
4861    #[test]
4862    fn function_local_let_inferred_from_initializer() {
4863        assert_eq!(
4864            run_main_f64("function main(): number { let x = 5; return x + 1; }"),
4865            6.0
4866        );
4867    }
4868
4869    #[test]
4870    fn function_local_const_round_trips() {
4871        assert_eq!(
4872            run_main_f64("function main(): number { const c = 5; return c; }"),
4873            5.0
4874        );
4875    }
4876
4877    #[test]
4878    fn assignment_to_top_level_let_is_global_set() {
4879        assert_eq!(
4880            run_main_f64("let g = 1; function main(): number { g = 2; return g; }"),
4881            2.0
4882        );
4883    }
4884
4885    #[test]
4886    fn assignment_uses_self_reference_on_rhs() {
4887        assert_eq!(
4888            run_main_f64("function main(): number { let i = 0; i = i + 1; i = i + 1; return i; }"),
4889            2.0
4890        );
4891    }
4892
4893    #[test]
4894    fn const_reassignment_is_compile_error() {
4895        let mut asi = Asi::new(
4896            "function main(): number { const c = 5; c = 6; return c; }",
4897            crate::FileId(0),
4898        );
4899        let mut tokens: Vec<Token> = Vec::new();
4900        loop {
4901            let tok = asi.next_token();
4902            let is_eof = matches!(tok.kind, TokenKind::Eof);
4903            tokens.push(tok);
4904            if is_eof {
4905                break;
4906            }
4907        }
4908        let (ast, _) = parse(
4909            "function main(): number { const c = 5; c = 6; return c; }",
4910            tokens,
4911            crate::FileId(0),
4912        );
4913        let diags = infer_with_runtime_packages(
4914            "function main(): number { const c = 5; c = 6; return c; }",
4915            &ast,
4916        );
4917        assert!(
4918            diags
4919                .iter()
4920                .any(|d| d.message == "cannot assign to const binding `c`"),
4921            "expected const-reassignment diagnostic, got {diags:?}",
4922        );
4923    }
4924
4925    #[test]
4926    fn assignment_type_mismatch_is_compile_error() {
4927        let src = "function main(): number { let x: number = 1; x = 2; return x; }";
4928        let _ = compile(src);
4929
4930        let bad = r#"function main(): number { let x: number = 1; x = "hi"; return x; }"#;
4931        let mut asi = Asi::new(bad, crate::FileId(0));
4932        let mut tokens: Vec<Token> = Vec::new();
4933        loop {
4934            let tok = asi.next_token();
4935            let is_eof = matches!(tok.kind, TokenKind::Eof);
4936            tokens.push(tok);
4937            if is_eof {
4938                break;
4939            }
4940        }
4941        let (ast, _) = parse(bad, tokens, crate::FileId(0));
4942        let diags = infer_with_runtime_packages(bad, &ast);
4943        assert!(
4944            diags
4945                .iter()
4946                .any(|d| d.message.contains("expected `number`")),
4947            "expected type-mismatch diagnostic, got {diags:?}",
4948        );
4949    }
4950
4951    #[test]
4952    fn assignment_to_function_is_compile_error() {
4953        let bad = "function helper(): void { } function main(): void { helper = 1; }";
4954        let mut asi = Asi::new(bad, crate::FileId(0));
4955        let mut tokens: Vec<Token> = Vec::new();
4956        loop {
4957            let tok = asi.next_token();
4958            let is_eof = matches!(tok.kind, TokenKind::Eof);
4959            tokens.push(tok);
4960            if is_eof {
4961                break;
4962            }
4963        }
4964        let (ast, _) = parse(bad, tokens, crate::FileId(0));
4965        let diags = infer_with_runtime_packages(bad, &ast);
4966        assert!(
4967            diags
4968                .iter()
4969                .any(|d| d.message == "cannot assign to function `helper`"),
4970            "expected function-reassignment diagnostic, got {diags:?}",
4971        );
4972    }
4973
4974    #[test]
4975    fn boolean_literal_round_trips() {
4976        assert_eq!(run_main_i32("function main(): boolean { return true; }"), 1);
4977        assert_eq!(
4978            run_main_i32("function main(): boolean { return false; }"),
4979            0
4980        );
4981    }
4982
4983    #[test]
4984    fn numeric_comparisons() {
4985        assert_eq!(
4986            run_main_i32("function main(): boolean { return 1 < 2; }"),
4987            1
4988        );
4989        assert_eq!(
4990            run_main_i32("function main(): boolean { return 2 < 1; }"),
4991            0
4992        );
4993        assert_eq!(
4994            run_main_i32("function main(): boolean { return 2 > 1; }"),
4995            1
4996        );
4997        assert_eq!(
4998            run_main_i32("function main(): boolean { return 2 <= 2; }"),
4999            1
5000        );
5001        assert_eq!(
5002            run_main_i32("function main(): boolean { return 2 >= 3; }"),
5003            0
5004        );
5005    }
5006
5007    #[test]
5008    fn numeric_equality() {
5009        assert_eq!(
5010            run_main_i32("function main(): boolean { return 1 === 1; }"),
5011            1
5012        );
5013        assert_eq!(
5014            run_main_i32("function main(): boolean { const two: number = 2; return 1 !== two; }"),
5015            1
5016        );
5017        assert_eq!(
5018            run_main_i32("function main(): boolean { return 1 == 1; }"),
5019            1
5020        );
5021        assert_eq!(
5022            run_main_i32("function main(): boolean { return 1 != 1; }"),
5023            0
5024        );
5025    }
5026
5027    #[test]
5028    fn boolean_equality() {
5029        assert_eq!(
5030            run_main_i32("function main(): boolean { return true === true; }"),
5031            1
5032        );
5033        assert_eq!(
5034            run_main_i32(
5035                "function no(): boolean { return false; } function main(): boolean { return true !== no(); }"
5036            ),
5037            1
5038        );
5039    }
5040
5041    #[test]
5042    fn string_equality_via_runtime() {
5043        assert_eq!(
5044            run_main_i32(r#"function main(): boolean { return ("a" + "b") === "ab"; }"#),
5045            1
5046        );
5047        assert_eq!(
5048            run_main_i32(
5049                r#"function main(): boolean { const ac: string = "ac"; return "ab" !== ac; }"#
5050            ),
5051            1
5052        );
5053        assert_eq!(
5054            run_main_i32(
5055                r#"function main(): boolean { const ac: string = "ac"; return "ab" === ac; }"#
5056            ),
5057            0
5058        );
5059    }
5060
5061    #[test]
5062    fn unary_not() {
5063        assert_eq!(
5064            run_main_i32("function main(): boolean { return !true; }"),
5065            0
5066        );
5067        assert_eq!(
5068            run_main_i32("function main(): boolean { return !false; }"),
5069            1
5070        );
5071        assert_eq!(
5072            run_main_i32("function main(): boolean { return !!true; }"),
5073            1
5074        );
5075    }
5076
5077    #[test]
5078    fn logical_and_or_results() {
5079        assert_eq!(
5080            run_main_i32("function main(): boolean { return true && true; }"),
5081            1
5082        );
5083        assert_eq!(
5084            run_main_i32("function main(): boolean { return true && false; }"),
5085            0
5086        );
5087        assert_eq!(
5088            run_main_i32("function main(): boolean { return false || true; }"),
5089            1
5090        );
5091        assert_eq!(
5092            run_main_i32("function main(): boolean { return false || false; }"),
5093            0
5094        );
5095    }
5096
5097    #[test]
5098    fn logical_and_short_circuits_structurally() {
5099        let bytes = compile("function main(): boolean { return false && true; }");
5100        instantiate_against_prelude(&bytes);
5101        let main_idx = main_export_func_idx(&bytes).expect("main exported");
5102        let body_idx = (main_idx - imported_func_count(&bytes)) as usize;
5103        let mut bodies_seen = 0;
5104        let mut saw_if = false;
5105        for payload in Parser::new(0).parse_all(&bytes) {
5106            if let Payload::CodeSectionEntry(body) = payload.expect("payload") {
5107                if bodies_seen == body_idx {
5108                    let mut reader = body
5109                        .get_operators_reader()
5110                        .expect("body operators readable");
5111                    while !reader.eof() {
5112                        if matches!(
5113                            reader.read().expect("operator"),
5114                            wasmparser::Operator::If { .. }
5115                        ) {
5116                            saw_if = true;
5117                            break;
5118                        }
5119                    }
5120                    break;
5121                }
5122                bodies_seen += 1;
5123            }
5124        }
5125        assert!(saw_if, "logical && should compile to a branchy `if` block");
5126    }
5127
5128    #[test]
5129    fn string_concat_compiles_and_links() {
5130        let bytes = compile(r#"function main(): string { return "a" + "b"; }"#);
5131        instantiate_against_prelude(&bytes);
5132
5133        let segments = data_segments(&bytes);
5134        assert_eq!(segments.len(), 2);
5135        assert_eq!(segments[0], b"a\0".to_vec());
5136        assert_eq!(segments[1], b"b\0".to_vec());
5137
5138        let concat_idx = imported_func_idx(&bytes, "submilli:prelude#string_concat");
5139        let calls = main_body_call_targets(&bytes);
5140        assert_eq!(
5141            calls,
5142            vec![concat_idx],
5143            "main should call string_concat exactly once",
5144        );
5145    }
5146
5147    #[test]
5148    fn string_concat_with_mixed_length_literals() {
5149        let bytes = compile(r#"function main(): string { return "Hello, " + "world"; }"#);
5150        instantiate_against_prelude(&bytes);
5151        assert_eq!(data_segments(&bytes).len(), 2);
5152        let concat_idx = imported_func_idx(&bytes, "submilli:prelude#string_concat");
5153        assert_eq!(main_body_call_targets(&bytes), vec![concat_idx]);
5154    }
5155
5156    #[test]
5157    fn string_concat_chained_emits_two_calls() {
5158        let bytes = compile(r#"function main(): string { return "a" + "b" + "c"; }"#);
5159        instantiate_against_prelude(&bytes);
5160        let concat_idx = imported_func_idx(&bytes, "submilli:prelude#string_concat");
5161        assert_eq!(main_body_call_targets(&bytes), vec![concat_idx, concat_idx]);
5162    }
5163
5164    #[test]
5165    fn string_concat_uses_top_level_global() {
5166        let bytes = compile(
5167            r#"let greeting: string = "Hello, "; function main(): string { return greeting + "world"; }"#,
5168        );
5169        instantiate_against_prelude(&bytes);
5170        let concat_idx = imported_func_idx(&bytes, "submilli:prelude#string_concat");
5171        assert_eq!(main_body_call_targets(&bytes), vec![concat_idx]);
5172    }
5173
5174    #[test]
5175    fn user_global_idx_resolves_for_consumer_locals() {
5176        let mut map = SymbolTable::default();
5177        map.record_global(crate::mangle::package_symbol("main", "x"), 0);
5178        map.record_global(crate::mangle::package_symbol("main", "y"), 1);
5179        assert_eq!(
5180            map.global_idx(&crate::mangle::package_symbol("main", "x")),
5181            Some(0)
5182        );
5183        assert_eq!(
5184            map.global_idx(&crate::mangle::package_symbol("main", "y")),
5185            Some(1)
5186        );
5187        assert_eq!(
5188            map.global_idx(&crate::mangle::package_symbol("main", "missing")),
5189            None
5190        );
5191    }
5192
5193    #[test]
5194    fn dwarf_sections_emitted_and_validate() {
5195        let bytes = compile("function main(): void { }");
5196        assert!(!custom_section(&bytes, ".debug_info").unwrap().is_empty());
5197        assert!(!custom_section(&bytes, ".debug_abbrev").unwrap().is_empty());
5198        assert!(!custom_section(&bytes, ".debug_str").unwrap().is_empty());
5199        instantiate_against_prelude(&bytes);
5200    }
5201
5202    #[test]
5203    fn dwarf_subprogram_carries_main_name() {
5204        let bytes = compile("function main(): void { }");
5205        let strs = custom_section(&bytes, ".debug_str").unwrap();
5206        assert!(
5207            strs.windows(4).any(|w| w == b"main"),
5208            "expected `main` in .debug_str, got {strs:?}",
5209        );
5210    }
5211
5212    #[test]
5213    fn dwarf_names_the_start_function_as_the_top_level() {
5214        let bytes = compile("function main(): void { }");
5215        let strs = custom_section(&bytes, ".debug_str").unwrap();
5216        assert!(
5217            strs.windows(11).any(|w| w == b"<top level>"),
5218            "expected `<top level>` in .debug_str, got {strs:?}",
5219        );
5220        assert!(
5221            !strs.windows(6).any(|w| w == b"_start"),
5222            "_start should not appear in .debug_str",
5223        );
5224    }
5225
5226    #[test]
5227    fn dwarf_die_snapshot() {
5228        let bytes = compile("function main(): void { }");
5229        let dump = dump_dwarf_dies(&bytes);
5230        insta::assert_snapshot!(dump);
5231    }
5232
5233    #[test]
5234    fn forward_reference_call_resolves() {
5235        assert_eq!(
5236            run_main_f64(
5237                "function main(): number { return helper(); } \
5238                 function helper(): number { return 42; }",
5239            ),
5240            42.0,
5241        );
5242    }
5243
5244    #[test]
5245    fn function_with_args_passes_through() {
5246        assert_eq!(
5247            run_main_f64(
5248                "function double(n: number): number { return n * 2; } \
5249                 function main(): number { return double(21); }",
5250            ),
5251            42.0,
5252        );
5253    }
5254
5255    #[test]
5256    fn function_with_multiple_args_in_order() {
5257        assert_eq!(
5258            run_main_f64(
5259                "function sub(a: number, b: number): number { return a - b; } \
5260                 function main(): number { return sub(10, 3); }",
5261            ),
5262            7.0,
5263        );
5264    }
5265
5266    #[test]
5267    fn nested_call_evaluates_inner_first() {
5268        assert_eq!(
5269            run_main_f64(
5270                "function inner(n: number): number { return n + 1; } \
5271                 function outer(n: number): number { return n * 10; } \
5272                 function main(): number { return outer(inner(5)); }",
5273            ),
5274            60.0,
5275        );
5276    }
5277
5278    #[test]
5279    fn recursion_works_via_self_call() {
5280        assert_eq!(
5281            run_main_f64(
5282                "function fact(n: number): number { \
5283                    if (n <= 1) { return 1; } else { return n * fact(n - 1); } \
5284                 } \
5285                 function main(): number { return fact(5); }",
5286            ),
5287            120.0,
5288        );
5289    }
5290
5291    #[test]
5292    fn mvp_target_greeting_self_check() {
5293        let src = "\
5294function greet(name: string): string {\n\
5295  return \"Hello, \" + name;\n\
5296}\n\
5297function main(): boolean {\n\
5298  return greet(\"world\") === \"Hello, world\";\n\
5299}\n";
5300        assert_eq!(run_main_i32(src), 1);
5301    }
5302
5303    #[test]
5304    fn top_level_let_initializer_calls_user_function() {
5305        let src = "\
5306function compute(): number { return 7; }\n\
5307let cached: number = compute();\n\
5308function main(): number { return cached + 1; }\n";
5309        assert_eq!(run_main_f64(src), 8.0);
5310    }
5311
5312    #[test]
5313    fn dwarf_subprograms_for_each_user_function() {
5314        let bytes = compile("function main(): void { } function helper(): void { }");
5315        let dump = dump_dwarf_dies(&bytes);
5316        insta::assert_snapshot!(dump);
5317    }
5318
5319    #[test]
5320    fn dwarf_line_section_emitted() {
5321        let bytes = compile("function main(): void { }");
5322        assert!(
5323            !custom_section(&bytes, ".debug_line").unwrap().is_empty(),
5324            ".debug_line should be emitted",
5325        );
5326        instantiate_against_prelude(&bytes);
5327    }
5328
5329    #[test]
5330    fn dwarf_line_program_snapshot() {
5331        let src = "\
5332function main(): number {
5333  let x: number = 1;
5334  let y: number = 2;
5335  return x + y;
5336}
5337";
5338        let bytes = compile(src);
5339        let dump = dump_dwarf_lines(&bytes);
5340        insta::assert_snapshot!(dump);
5341    }
5342
5343    #[test]
5344    fn dwarf_line_program_covers_helper() {
5345        let src = "\
5346function main(): number {
5347  return helper();
5348}
5349function helper(): number {
5350  return 42;
5351}
5352";
5353        let bytes = compile(src);
5354        let dump = dump_dwarf_lines(&bytes);
5355        insta::assert_snapshot!(dump);
5356    }
5357
5358    #[test]
5359    fn assert_true_is_no_op() {
5360        assert_eq!(
5361            run_main_f64("function main(): number { assert(true, \"ok\"); return 1; }",),
5362            1.0,
5363        );
5364    }
5365
5366    #[test]
5367    fn assert_typechecks_with_string_message() {
5368        run_main_f64(
5369            "function main(): number { let m: string = \"ok\"; assert(true, m); return 0; }",
5370        );
5371    }
5372
5373    #[test]
5374    fn assert_evaluates_msg_for_side_effects() {
5375        assert_eq!(
5376            run_main_f64("function main(): number { assert(true, \"a\" + \"b\"); return 1; }",),
5377            1.0,
5378        );
5379    }
5380
5381    #[test]
5382    fn user_function_named_assert_is_rejected() {
5383        let src = "function assert(): void { } function main(): void { }";
5384        let mut asi = Asi::new(src, crate::FileId(0));
5385        let mut tokens: Vec<Token> = Vec::new();
5386        loop {
5387            let tok = asi.next_token();
5388            let is_eof = matches!(tok.kind, TokenKind::Eof);
5389            tokens.push(tok);
5390            if is_eof {
5391                break;
5392            }
5393        }
5394        let (ast, _) = parse(src, tokens, crate::FileId(0));
5395        let diags = infer_with_runtime_packages(src, &ast);
5396        assert!(
5397            diags.iter().any(|d| d.message
5398                == "`assert` is a reserved compiler intrinsic and cannot be redeclared"),
5399            "expected reserved-intrinsic diagnostic, got {diags:?}",
5400        );
5401    }
5402
5403    #[test]
5404    fn assert_false_throws_with_backtrace() {
5405        let dump = run_main_expecting_error("function main(): void { assert(false, \"oops\"); }");
5406        insta::assert_snapshot!(dump);
5407    }
5408
5409    #[test]
5410    fn multi_frame_backtrace() {
5411        let src = "\
5412function inner(): void { assert(false, \"x\"); }
5413function main(): void { inner(); }
5414";
5415        let dump = run_main_expecting_error(src);
5416        insta::assert_snapshot!(dump);
5417    }
5418
5419    #[test]
5420    fn three_frame_backtrace_labels_caller_in_middle() {
5421        let src = "\
5422function deepest(): void { assert(false, \"x\"); }
5423function middle(): void { deepest(); }
5424function main(): void { middle(); }
5425";
5426        let dump = run_main_expecting_error(src);
5427        insta::assert_snapshot!(dump);
5428    }
5429
5430    #[test]
5431    fn closure_method_accessor_and_constructor_frames_are_named() {
5432        let src = "\
5433class Refused { constructor() { assert(false, \"x\"); } }
5434class Holder {
5435  get refused(): Refused { return new Refused(); }
5436  read(): Refused { return this.refused; }
5437}
5438function main(): void {
5439  const named = function outer(): void {
5440    [1].forEach((n: number): void => { new Holder().read(); });
5441  };
5442  named();
5443}
5444";
5445        let dump = run_main_expecting_error(src);
5446        insta::assert_snapshot!(dump);
5447    }
5448
5449    #[test]
5450    fn implicit_constructor_frame_points_at_its_own_class() {
5451        // `Derived`'s init has no rows of its own before it calls `Base`'s; the
5452        // engine looks lines up by address alone, so without a row at its start
5453        // the frame would show the line of the function written before it.
5454        let src = "\
5455class Base {
5456  constructor(n: number) { assert(n > 5, \"small\"); }
5457  describe(): string { return \"unrelated\"; }
5458}
5459class Derived extends Base {}
5460function main(): void { new Derived(1); }
5461";
5462        let dump = run_main_expecting_error(src);
5463        assert!(
5464            dump.contains("  at new Derived (script.subm:5:7)  [caller]\n"),
5465            "{dump}"
5466        );
5467    }
5468
5469    #[test]
5470    fn a_method_named_like_an_accessor_is_not_framed_as_one() {
5471        let src = "\
5472class C { \"get x\"(): void { assert(false, \"x\"); } }
5473function main(): void { new C()[\"get x\"](); }
5474";
5475        let dump = run_main_expecting_error(src);
5476        assert!(dump.contains("  at C.get x (script.subm:1:"), "{dump}");
5477    }
5478
5479    #[test]
5480    fn a_method_key_with_control_characters_compiles_and_frames_escaped() {
5481        let src = "\
5482class C { \"a\\u0000\\nb\"(): void { assert(false, \"x\"); } }
5483function main(): void { new C()[\"a\\u0000\\nb\"](); }
5484";
5485        let dump = run_main_expecting_error(src);
5486        assert!(
5487            dump.contains("  at C.a\\u{0}\\nb (script.subm:1:"),
5488            "{dump}"
5489        );
5490    }
5491
5492    #[test]
5493    fn failed_cast_backtrace_points_at_the_cast() {
5494        let src = "\
5495interface P { a: number }
5496function g(n: number, p: P): number { return n; }
5497function main(): void {
5498  const r = g(1,
5499    JSON.parse(\"{}\") as P);
5500}
5501";
5502        let dump = run_main_expecting_error(src);
5503        insta::assert_snapshot!(dump);
5504    }
5505
5506    #[test]
5507    fn code_after_a_cast_keeps_its_backtrace_location() {
5508        let src = "\
5509interface P { a: number }
5510function g(n: number, p: P): number { assert(false, \"x\"); return n; }
5511function main(): void {
5512  const r = g(1, JSON.parse(\"{\\\"a\\\": 1}\") as P);
5513}
5514";
5515        let dump = run_main_expecting_error(src);
5516        insta::assert_snapshot!(dump);
5517    }
5518
5519    #[test]
5520    fn render_backtrace_returns_none_for_non_trap_error() {
5521        #[derive(Debug)]
5522        struct Plain;
5523        impl std::fmt::Display for Plain {
5524            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5525                f.write_str("plain error")
5526            }
5527        }
5528        impl std::error::Error for Plain {}
5529        let err: wasmtime::Error = wasmtime::Error::new(Plain);
5530        let (sources, file) = crate::Sources::single("script.subm", "").unwrap();
5531        assert!(
5532            crate::render_backtrace(&err, &sources, file, crate::BacktraceMode::Full).is_none()
5533        );
5534    }
5535
5536    fn run_main_with_cfg_expecting_trap(
5537        cfg: &crate::runtime::RuntimeConfig,
5538        source: &str,
5539    ) -> String {
5540        let bytes = compile(source);
5541        let (mut store, inst) = link_consumer_with(cfg, &bytes);
5542        let main = inst
5543            .get_func(&mut store, "main")
5544            .expect("main signature is `() -> void` in expecting-trap fixtures");
5545        let err = pollster::block_on(main.call_async(
5546            &mut store,
5547            &[],
5548            &mut [wasmtime::Val::AnyRef(None)],
5549        ))
5550        .expect_err("expected main() to trap");
5551        let (sources, file) = crate::Sources::single("script.subm", source).unwrap();
5552        crate::render_backtrace(&err, &sources, file, crate::BacktraceMode::Full)
5553            .expect("backtrace empty — was wasm_backtrace_details enabled?")
5554    }
5555
5556    #[test]
5557    fn raw_trap_renders_an_error_header() {
5558        let cfg = crate::runtime::RuntimeConfig {
5559            max_wasm_stack: 64 * 1024,
5560            ..Default::default()
5561        };
5562        let dump = run_main_with_cfg_expecting_trap(
5563            &cfg,
5564            "function rec(): void { rec(); } function main(): void { rec(); }",
5565        );
5566        assert!(
5567            dump.starts_with("error: call stack exhausted\n"),
5568            "expected an `error:` header naming the trap, got:\n{dump}",
5569        );
5570    }
5571
5572    #[test]
5573    fn fuel_trap_header_does_not_leak_the_engine() {
5574        // The engine's own text here is `all fuel consumed by WebAssembly`, which
5575        // names a host mechanism rather than anything the program can act on.
5576        let cfg = crate::runtime::RuntimeConfig {
5577            fuel: 1024,
5578            ..Default::default()
5579        };
5580        let dump =
5581            run_main_with_cfg_expecting_trap(&cfg, "function main(): void { while (true) { } }");
5582        assert!(
5583            dump.starts_with("error: fuel exhausted\n"),
5584            "expected a curated `error:` header, got:\n{dump}",
5585        );
5586        assert!(
5587            !dump.contains("WebAssembly"),
5588            "engine wording leaked into the header:\n{dump}",
5589        );
5590    }
5591
5592    #[test]
5593    fn fuel_exhaustion_traps_with_label() {
5594        let cfg = crate::runtime::RuntimeConfig {
5595            fuel: 1024,
5596            ..Default::default()
5597        };
5598        let dump =
5599            run_main_with_cfg_expecting_trap(&cfg, "function main(): void { while (true) { } }");
5600        assert!(
5601            dump.contains("[fuel exhausted]"),
5602            "expected fuel-exhausted label, got:\n{dump}",
5603        );
5604    }
5605
5606    #[test]
5607    fn stack_overflow_traps_with_label() {
5608        let cfg = crate::runtime::RuntimeConfig {
5609            max_wasm_stack: 64 * 1024,
5610            ..Default::default()
5611        };
5612        let dump = run_main_with_cfg_expecting_trap(
5613            &cfg,
5614            "function rec(): void { rec(); } \
5615             function main(): void { rec(); }",
5616        );
5617        assert!(
5618            dump.contains("[stack overflow]"),
5619            "expected stack-overflow label, got:\n{dump}",
5620        );
5621    }
5622
5623    fn run_main_capturing_console(source: &str) -> String {
5624        use std::sync::{Arc, Mutex};
5625        struct Shared(Arc<Mutex<Vec<u8>>>);
5626        impl std::io::Write for Shared {
5627            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
5628                self.0.lock().unwrap().write(buf)
5629            }
5630            fn flush(&mut self) -> std::io::Result<()> {
5631                Ok(())
5632            }
5633        }
5634        let buf = Arc::new(Mutex::new(Vec::new()));
5635        let mut data = crate::runtime::StoreData::with_vfs(
5636            crate::runtime::Vfs::tempdir().expect("tempdir for codegen test VFS"),
5637        );
5638        data.console = Box::new(Shared(Arc::clone(&buf)));
5639        let cfg = crate::runtime::RuntimeConfig::default();
5640        let bytes = compile(source);
5641        let engine = cfg.engine().expect("engine");
5642        let mut store = cfg.store(&engine, data).expect("store");
5643        let consumer_module = wasmtime::Module::new(&engine, &bytes).expect("consumer module");
5644        let mut linker = wasmtime::Linker::<crate::runtime::StoreData>::new(&engine);
5645        let inst = pollster::block_on(async {
5646            crate::runtime::install_runtime_async(&mut linker, &mut store)
5647                .await
5648                .expect("runtime");
5649            linker
5650                .instantiate_async(&mut store, &consumer_module)
5651                .await
5652                .expect("consumer instantiates")
5653        });
5654        let main = inst.get_func(&mut store, "main").expect("main () -> void");
5655        pollster::block_on(main.call_async(&mut store, &[], &mut [wasmtime::Val::AnyRef(None)]))
5656            .expect("main does not trap");
5657        let captured = buf.lock().unwrap().clone();
5658        String::from_utf8(captured).expect("utf8")
5659    }
5660
5661    #[test]
5662    fn console_log_writes_message_with_newline() {
5663        let out = run_main_capturing_console("function main(): void { console.log(\"hi\"); }");
5664        assert_eq!(out, "hi\n");
5665    }
5666
5667    #[test]
5668    fn console_log_uses_string_concat() {
5669        let out = run_main_capturing_console(
5670            "function main(): void { console.log(\"Hello, \" + \"world\"); }",
5671        );
5672        assert_eq!(out, "Hello, world\n");
5673    }
5674
5675    #[test]
5676    fn console_log_joins_multiple_primitive_args() {
5677        let out =
5678            run_main_capturing_console("function main(): void { console.log(\"a\", 1, true); }");
5679        assert_eq!(out, "a 1 true\n");
5680    }
5681
5682    #[test]
5683    fn console_log_coerces_array_and_object_args() {
5684        let out =
5685            run_main_capturing_console("function main(): void { console.log([1, 2], { x: 1 }); }");
5686        assert_eq!(out, "1,2 [object Object]\n");
5687    }
5688
5689    #[test]
5690    fn console_log_accepts_single_non_string_arg() {
5691        let out = run_main_capturing_console(
5692            "function main(): void { const items = [1, 2, 3]; console.log(items.length); }",
5693        );
5694        assert_eq!(out, "3\n");
5695    }
5696
5697    #[test]
5698    fn console_log_multiple_calls_appends() {
5699        let out = run_main_capturing_console(
5700            "function main(): void { console.log(\"a\"); console.log(\"b\"); }",
5701        );
5702        assert_eq!(out, "a\nb\n");
5703    }
5704
5705    #[test]
5706    fn console_log_inside_function_call() {
5707        let out = run_main_capturing_console(
5708            "function shout(s: string): void { console.log(s); } \
5709             function main(): void { shout(\"hello\"); }",
5710        );
5711        assert_eq!(out, "hello\n");
5712    }
5713
5714    #[test]
5715    fn user_function_named_console_is_rejected() {
5716        let src = "function console(): void { } function main(): void { }";
5717        let mut asi = Asi::new(src, crate::FileId(0));
5718        let mut tokens: Vec<Token> = Vec::new();
5719        loop {
5720            let tok = asi.next_token();
5721            let is_eof = matches!(tok.kind, TokenKind::Eof);
5722            tokens.push(tok);
5723            if is_eof {
5724                break;
5725            }
5726        }
5727        let (ast, _) = parse(src, tokens, crate::FileId(0));
5728        let diags = infer_with_runtime_packages(src, &ast);
5729        assert!(
5730            diags
5731                .iter()
5732                .any(|d| d.message.contains("duplicate declaration of `console`")),
5733            "expected duplicate-declaration diagnostic, got {diags:?}",
5734        );
5735    }
5736
5737    #[test]
5738    fn object_literal_round_trip() {
5739        let result =
5740            run_main_f64("function main(): number { let p = { x: 1, y: 2 }; return p.x + p.y; }");
5741        assert_eq!(result, 3.0);
5742    }
5743
5744    #[test]
5745    fn object_literal_field_order_independent() {
5746        let result =
5747            run_main_f64("function main(): number { let p = { y: 10, x: 5 }; return p.y; }");
5748        assert_eq!(result, 10.0);
5749    }
5750
5751    #[test]
5752    fn object_with_string_field_round_trip() {
5753        let bytes = compile(
5754            r#"function main(): string { let p = { name: "hi", age: 30 }; return p.name; }"#,
5755        );
5756        let cfg = crate::runtime::RuntimeConfig::default();
5757        let result = pollster::block_on(cfg.run(&bytes)).expect("run");
5758        assert_eq!(result.value.as_deref(), Some("hi"));
5759    }
5760
5761    #[test]
5762    fn object_passed_through_function_signature() {
5763        let result = run_main_f64(
5764            "function take(p: { v: number }): number { return p.v; } function main(): number { return take({ v: 7 }); }",
5765        );
5766        assert_eq!(result, 7.0);
5767    }
5768
5769    #[test]
5770    fn array_of_numbers_round_trip() {
5771        let result = run_main_f64("function main(): number { let a = [1, 2, 3]; return a[1]; }");
5772        assert_eq!(result, 2.0);
5773    }
5774
5775    #[test]
5776    fn array_of_numbers_annotated() {
5777        let result =
5778            run_main_f64("function main(): number { let a: number[] = [42]; return a[0]; }");
5779        assert_eq!(result, 42.0);
5780    }
5781
5782    #[test]
5783    fn array_of_strings_no_boxing() {
5784        let bytes = compile(r#"function main(): string { let a = ["a", "b", "c"]; return a[2]; }"#);
5785        let cfg = crate::runtime::RuntimeConfig::default();
5786        let result = pollster::block_on(cfg.run(&bytes)).expect("run");
5787        assert_eq!(result.value.as_deref(), Some("c"));
5788    }
5789
5790    #[test]
5791    fn array_of_objects_field_access() {
5792        let result = run_main_f64(
5793            "function main(): number { let xs: { v: number }[] = [{ v: 7 }]; return xs[0].v; }",
5794        );
5795        assert_eq!(result, 7.0);
5796    }
5797
5798    #[test]
5799    fn array_index_out_of_bounds_throws_catchable() {
5800        // An OOB index used to map to a raw `array.get` that traps uncatchably;
5801        // it now throws a catchable `Error`, so user `try/catch` can recover.
5802        let bytes = compile(
5803            r#"function main(): string {
5804                let a = [1, 2, 3];
5805                try {
5806                    let _ = a[10];
5807                    return "no throw";
5808                } catch (e: Error) {
5809                    return e.message;
5810                }
5811            }"#,
5812        );
5813        let cfg = crate::runtime::RuntimeConfig::default();
5814        let result =
5815            pollster::block_on(cfg.run(&bytes)).expect("OOB throw is catchable, not a trap");
5816        assert_eq!(result.value.as_deref(), Some("index out of range"));
5817    }
5818
5819    #[test]
5820    fn array_index_out_of_bounds_uncaught_is_not_a_trap() {
5821        let bytes = compile("function main(): number { let a = [1, 2, 3]; return a[10]; }");
5822        let cfg = crate::runtime::RuntimeConfig::default();
5823        let err = pollster::block_on(cfg.run(&bytes)).expect_err("uncaught OOB throw still aborts");
5824        assert!(
5825            err.downcast_ref::<wasmtime::Trap>().is_none(),
5826            "OOB should surface as a thrown exception, not a Wasm trap: {err:#}",
5827        );
5828    }
5829
5830    // Test helpers flip `boxed: true` manually to exercise boxed codegen paths without needing closure end-to-end.
5831    use crate::codegen::function_emitter::stmt::emit_statement;
5832    use crate::{ClosureBody, ExprId, StmtId, TypedExprKind, TypedStmtKind};
5833    use wasm_encoder::HeapType;
5834    use wasm_encoder::Ieee64;
5835    use wasm_encoder::Instruction;
5836    use wasm_encoder::{
5837        CodeSection, ConstExpr, ExportKind, ExportSection, Function, FunctionSection,
5838        GlobalSection, ImportSection, Module, RefType, TypeSection, ValType,
5839    };
5840
5841    fn boxed_typed_ast(source: &str) -> TypedAst {
5842        let mut asi = Asi::new(source, crate::FileId(0));
5843        let mut tokens: Vec<Token> = Vec::new();
5844        loop {
5845            let tok = asi.next_token();
5846            let is_eof = matches!(tok.kind, TokenKind::Eof);
5847            tokens.push(tok);
5848            if is_eof {
5849                break;
5850            }
5851        }
5852        let lex_diags = asi.into_diagnostics();
5853        assert!(lex_diags.is_empty(), "{lex_diags:?}");
5854        let (ast, parse_diags) = parse(source, tokens, crate::FileId(0));
5855        assert!(parse_diags.is_empty(), "{parse_diags:?}");
5856        let packages = runtime_packages(&[]);
5857        let (mut ta, infer_diags) = infer(source, "main", &ast, &packages);
5858        assert!(infer_diags.is_empty(), "{infer_diags:?}");
5859        let chk = check(&ta).unwrap();
5860        assert!(chk.is_empty(), "{chk:?}");
5861        ta = capture(ta).unwrap();
5862        ta = desugar(ta, crate::FileId(0)).unwrap();
5863        ta
5864    }
5865
5866    fn box_named(ta: &mut TypedAst, names: &[&str]) {
5867        let func_meta: Vec<(usize, Vec<String>, StmtId)> = ta
5868            .functions
5869            .iter()
5870            .enumerate()
5871            .map(|(i, f)| {
5872                let names = f.params.iter().map(|p| p.name.name.clone()).collect();
5873                (i, names, f.body)
5874            })
5875            .collect();
5876        for (fi, param_names, body) in func_meta {
5877            for (pi, pname) in param_names.iter().enumerate() {
5878                if names.contains(&pname.as_str()) {
5879                    ta.functions[fi].params[pi].boxed = true;
5880                }
5881            }
5882            box_walk_stmt(ta, body, names);
5883        }
5884        let stmt_ids: Vec<StmtId> = ta.top_level_statements.clone();
5885        for sid in stmt_ids {
5886            box_walk_stmt(ta, sid, names);
5887        }
5888    }
5889
5890    fn box_walk_stmt(ta: &mut TypedAst, id: StmtId, names: &[&str]) {
5891        let kind = ta.try_stmt(id).unwrap().kind.clone();
5892        match kind {
5893            TypedStmtKind::Block(stmts) => {
5894                for s in stmts {
5895                    box_walk_stmt(ta, s, names);
5896                }
5897            }
5898            TypedStmtKind::Let { name, value, .. } => {
5899                if names.iter().any(|n| *n == name.name)
5900                    && let TypedStmtKind::Let { boxed, .. } = &mut ta.try_stmt_mut(id).unwrap().kind
5901                {
5902                    *boxed = true;
5903                }
5904                box_walk_expr(ta, value, names);
5905            }
5906            TypedStmtKind::Const { value, .. } => box_walk_expr(ta, value, names),
5907            TypedStmtKind::If {
5908                condition,
5909                then_block,
5910                else_block,
5911            } => {
5912                box_walk_expr(ta, condition, names);
5913                box_walk_stmt(ta, then_block, names);
5914                if let Some(eb) = else_block {
5915                    box_walk_stmt(ta, eb, names);
5916                }
5917            }
5918            TypedStmtKind::While { condition, body } => {
5919                box_walk_expr(ta, condition, names);
5920                box_walk_stmt(ta, body, names);
5921            }
5922            TypedStmtKind::For {
5923                init,
5924                condition,
5925                update,
5926                body,
5927            } => {
5928                if let Some(i) = init {
5929                    box_walk_stmt(ta, i, names);
5930                }
5931                if let Some(c) = condition {
5932                    box_walk_expr(ta, c, names);
5933                }
5934                if let Some(u) = update {
5935                    box_walk_stmt(ta, u, names);
5936                }
5937                box_walk_stmt(ta, body, names);
5938            }
5939            TypedStmtKind::ForOf { iter, body, .. } => {
5940                box_walk_expr(ta, iter, names);
5941                box_walk_stmt(ta, body, names);
5942            }
5943            TypedStmtKind::DoWhile { body, condition } => {
5944                box_walk_stmt(ta, body, names);
5945                box_walk_expr(ta, condition, names);
5946            }
5947            TypedStmtKind::Switch {
5948                discriminant,
5949                cases,
5950                default,
5951                ..
5952            } => {
5953                box_walk_expr(ta, discriminant, names);
5954                for comparison in cases
5955                    .iter()
5956                    .flat_map(crate::TypedSwitchCase::label_comparisons)
5957                {
5958                    box_walk_expr(ta, comparison, names);
5959                }
5960                for case in cases {
5961                    box_walk_stmt(ta, case.body, names);
5962                }
5963                if let Some(d) = default {
5964                    box_walk_stmt(ta, d, names);
5965                }
5966            }
5967            TypedStmtKind::Break | TypedStmtKind::Continue | TypedStmtKind::ReboxLocal { .. } => {}
5968            TypedStmtKind::Return(value) => {
5969                if let Some(v) = value {
5970                    box_walk_expr(ta, v, names);
5971                }
5972            }
5973            TypedStmtKind::Expr(e) => box_walk_expr(ta, e, names),
5974            TypedStmtKind::AssignLocal { ident, value, .. } => {
5975                if names.iter().any(|n| *n == ident.name)
5976                    && let TypedStmtKind::AssignLocal { boxed, .. } =
5977                        &mut ta.try_stmt_mut(id).unwrap().kind
5978                {
5979                    *boxed = true;
5980                }
5981                box_walk_expr(ta, value, names);
5982            }
5983            TypedStmtKind::AssignGlobal { value, .. } => {
5984                box_walk_expr(ta, value, names);
5985            }
5986            TypedStmtKind::AssignField {
5987                receiver, value, ..
5988            } => {
5989                box_walk_expr(ta, receiver, names);
5990                box_walk_expr(ta, value, names);
5991            }
5992            TypedStmtKind::AssignIndex {
5993                receiver,
5994                index,
5995                value,
5996                ..
5997            } => {
5998                box_walk_expr(ta, receiver, names);
5999                box_walk_expr(ta, index, names);
6000                box_walk_expr(ta, value, names);
6001            }
6002            TypedStmtKind::NarrowRegion { source, body, .. } => {
6003                box_walk_expr(ta, source, names);
6004                box_walk_stmt(ta, body, names);
6005            }
6006            TypedStmtKind::Throw { value } => box_walk_expr(ta, value, names),
6007            TypedStmtKind::Try {
6008                body,
6009                catches,
6010                finally,
6011            } => {
6012                box_walk_stmt(ta, body, names);
6013                for (i, c) in catches.iter().enumerate() {
6014                    if names.iter().any(|n| *n == c.binding.name)
6015                        && let TypedStmtKind::Try { catches: cs, .. } =
6016                            &mut ta.try_stmt_mut(id).unwrap().kind
6017                    {
6018                        cs[i].boxed = true;
6019                    }
6020                    box_walk_stmt(ta, c.body, names);
6021                }
6022                if let Some(f) = finally {
6023                    box_walk_stmt(ta, f, names);
6024                }
6025            }
6026        }
6027    }
6028
6029    fn box_walk_expr(ta: &mut TypedAst, id: ExprId, names: &[&str]) {
6030        let kind = ta.try_expr(id).unwrap().kind.clone();
6031        match kind {
6032            TypedExprKind::LocalRef { ident, .. } => {
6033                if names.iter().any(|n| *n == ident.name)
6034                    && let TypedExprKind::LocalRef { boxed, .. } =
6035                        &mut ta.try_expr_mut(id).unwrap().kind
6036                {
6037                    *boxed = true;
6038                }
6039            }
6040            TypedExprKind::LocalNarrowRef { .. } => {}
6041            TypedExprKind::Closure { params, body, .. } => {
6042                for (i, p) in params.iter().enumerate() {
6043                    if names.iter().any(|n| *n == p.name.name)
6044                        && let TypedExprKind::Closure { params, .. } =
6045                            &mut ta.try_expr_mut(id).unwrap().kind
6046                    {
6047                        params[i].boxed = true;
6048                    }
6049                }
6050                match body {
6051                    ClosureBody::Expr(e) => box_walk_expr(ta, e, names),
6052                    ClosureBody::Block(b) => box_walk_stmt(ta, b, names),
6053                }
6054            }
6055            TypedExprKind::Binary { lhs, rhs, .. } => {
6056                box_walk_expr(ta, lhs, names);
6057                box_walk_expr(ta, rhs, names);
6058            }
6059            TypedExprKind::Unary { operand, .. } => box_walk_expr(ta, operand, names),
6060            TypedExprKind::TypeofTag { value, .. } | TypedExprKind::InstanceOf { value, .. } => {
6061                box_walk_expr(ta, value, names);
6062            }
6063            TypedExprKind::Call { args, .. }
6064            | TypedExprKind::VirtualCall { args, .. }
6065            | TypedExprKind::SuperCtorCall { args, .. }
6066            | TypedExprKind::SuperMethodCall { args, .. } => {
6067                for a in args {
6068                    box_walk_expr(ta, a, names);
6069                }
6070            }
6071            TypedExprKind::CallClosure { callee, args } => {
6072                box_walk_expr(ta, callee, names);
6073                for a in args {
6074                    box_walk_expr(ta, a, names);
6075                }
6076            }
6077            TypedExprKind::GenericCall { args, .. } => {
6078                for a in args {
6079                    box_walk_expr(ta, a.expr, names);
6080                }
6081            }
6082            TypedExprKind::MethodCall { receiver, args, .. } => {
6083                box_walk_expr(ta, receiver, names);
6084                for a in args {
6085                    box_walk_expr(ta, a, names);
6086                }
6087            }
6088            TypedExprKind::GenericMethodCall { receiver, args, .. } => {
6089                box_walk_expr(ta, receiver, names);
6090                for a in args {
6091                    box_walk_expr(ta, a.expr, names);
6092                }
6093            }
6094            TypedExprKind::IntrinsicCall { args, .. } => {
6095                for a in args {
6096                    box_walk_expr(ta, a, names);
6097                }
6098            }
6099            TypedExprKind::ObjectLiteral { members, .. } => {
6100                for member in members {
6101                    for expression in member.expressions() {
6102                        box_walk_expr(ta, expression, names);
6103                    }
6104                }
6105            }
6106            TypedExprKind::ArrayLiteral { elements, .. } => {
6107                for e in elements {
6108                    box_walk_expr(ta, e.expr_id(), names);
6109                }
6110            }
6111            TypedExprKind::TupleLiteral { elements, .. } => {
6112                for e in elements {
6113                    box_walk_expr(ta, e, names);
6114                }
6115            }
6116            TypedExprKind::FieldAccess { receiver, .. }
6117            | TypedExprKind::InterfacePropertyAccess { receiver, .. } => {
6118                box_walk_expr(ta, receiver, names);
6119            }
6120            TypedExprKind::IndexAccess { receiver, index } => {
6121                box_walk_expr(ta, receiver, names);
6122                box_walk_expr(ta, index, names);
6123            }
6124            TypedExprKind::Narrowed { source, inner, .. } => {
6125                box_walk_expr(ta, source, names);
6126                box_walk_expr(ta, inner, names);
6127            }
6128            TypedExprKind::Ternary { cond, then_, else_ } => {
6129                box_walk_expr(ta, cond, names);
6130                box_walk_expr(ta, then_, names);
6131                box_walk_expr(ta, else_, names);
6132            }
6133            TypedExprKind::NullishCoalesce { lhs, rhs } => {
6134                box_walk_expr(ta, lhs, names);
6135                box_walk_expr(ta, rhs, names);
6136            }
6137            TypedExprKind::OptionalChain { base, parts } => {
6138                box_walk_expr(ta, base, names);
6139                let to_walk: Vec<ExprId> = parts
6140                    .iter()
6141                    .flat_map(|p| match p {
6142                        crate::TypedChainPart::Index { idx, .. } => vec![*idx],
6143                        crate::TypedChainPart::Call { args, .. }
6144                        | crate::TypedChainPart::MethodCall { args, .. } => args.clone(),
6145                        crate::TypedChainPart::Field { .. }
6146                        | crate::TypedChainPart::InterfaceProperty { .. }
6147                        | crate::TypedChainPart::NonNull { .. } => Vec::new(),
6148                    })
6149                    .collect();
6150                for id in to_walk {
6151                    box_walk_expr(ta, id, names);
6152                }
6153            }
6154            TypedExprKind::PostfixUnary { target, .. } => match target {
6155                crate::PostfixTarget::Local { ident, .. } => {
6156                    if names.iter().any(|n| *n == ident.name)
6157                        && let TypedExprKind::PostfixUnary {
6158                            target: crate::PostfixTarget::Local { boxed, .. },
6159                            ..
6160                        } = &mut ta.try_expr_mut(id).unwrap().kind
6161                    {
6162                        *boxed = true;
6163                    }
6164                }
6165                crate::PostfixTarget::Global { .. } => {}
6166                crate::PostfixTarget::Field { receiver, .. } => {
6167                    box_walk_expr(ta, receiver, names);
6168                }
6169                crate::PostfixTarget::Index {
6170                    receiver, index, ..
6171                } => {
6172                    box_walk_expr(ta, receiver, names);
6173                    box_walk_expr(ta, index, names);
6174                }
6175            },
6176            TypedExprKind::NonNullAssert { value } | TypedExprKind::Cast { value, .. } => {
6177                box_walk_expr(ta, value, names);
6178            }
6179            TypedExprKind::EffectThen { effect, result } => {
6180                box_walk_expr(ta, effect, names);
6181                box_walk_expr(ta, result, names);
6182            }
6183            TypedExprKind::Sequence { stmts, result } => {
6184                for stmt in stmts {
6185                    box_walk_stmt(ta, stmt, names);
6186                }
6187                box_walk_expr(ta, result, names);
6188            }
6189            TypedExprKind::Number(_)
6190            | TypedExprKind::BigInt(_)
6191            | TypedExprKind::String(_)
6192            | TypedExprKind::Boolean(_)
6193            | TypedExprKind::Null
6194            | TypedExprKind::Undefined
6195            | TypedExprKind::This
6196            | TypedExprKind::Regex { .. }
6197            | TypedExprKind::GlobalRef { .. }
6198            | TypedExprKind::FunctionRef { .. }
6199            | TypedExprKind::NumberEnumMember { .. }
6200            | TypedExprKind::StringEnumMember { .. } => {}
6201        }
6202    }
6203
6204    fn compile_with_boxed(source: &str, names: &[&str]) -> Vec<u8> {
6205        let mut ta = boxed_typed_ast(source);
6206        box_named(&mut ta, names);
6207
6208        let (prelude_defs, host_defs, internal_defs) =
6209            prelude::cached_runtime_package_declarations();
6210        let mut dependencies: Vec<&crate::PackageDeclaration> = prelude_defs.iter().collect();
6211        dependencies.extend(host_defs.iter());
6212        // `submilli:json` lives in the internal host packages; `main(): string`
6213        // now routes its JSON encoding through `submilli:json.stringify`.
6214        dependencies.extend(internal_defs.iter());
6215        codegen(source, "script.subm", crate::FileId(0), &ta, &dependencies)
6216            .expect("code generation")
6217    }
6218
6219    fn run_main_f64_boxed(source: &str, names: &[&str]) -> f64 {
6220        run_main_number(&compile_with_boxed(source, names))
6221    }
6222
6223    fn run_main_string_boxed(source: &str, names: &[&str]) -> String {
6224        let bytes = compile_with_boxed(source, names);
6225        let cfg = crate::runtime::RuntimeConfig::default();
6226        let result = pollster::block_on(cfg.run(&bytes)).expect("main does not trap");
6227        result.value.expect("main returns a string")
6228    }
6229
6230    #[test]
6231    fn boxed_let_number_round_trip() {
6232        let source = "function main(): number { let x: number = 1; x = x + 1; return x; }";
6233        let result = run_main_f64_boxed(source, &["x"]);
6234        assert_eq!(result, 2.0);
6235    }
6236
6237    #[test]
6238    fn boxed_let_string_round_trip() {
6239        let source =
6240            r#"function main(): string { let s: string = "hello"; s = "world"; return s; }"#;
6241        let result = run_main_string_boxed(source, &["s"]);
6242        assert_eq!(result, "world");
6243    }
6244
6245    #[test]
6246    fn boxed_let_user_object_round_trip() {
6247        let source = "
6248            function main(): number {
6249                let p: { v: number } = { v: 42 };
6250                p = { v: 99 };
6251                return p.v;
6252            }
6253        ";
6254        let result = run_main_f64_boxed(source, &["p"]);
6255        assert_eq!(result, 99.0);
6256    }
6257
6258    #[test]
6259    fn boxed_param_round_trip() {
6260        let source = "
6261            function helper(p: number): number { return p + 1; }
6262            function main(): number { return helper(5); }
6263        ";
6264        let result = run_main_f64_boxed(source, &["p"]);
6265        assert_eq!(result, 6.0);
6266    }
6267
6268    #[test]
6269    fn boxed_param_with_assignment() {
6270        let source = "
6271            function helper(p: number): number { p = p + 10; return p; }
6272            function main(): number { return helper(7); }
6273        ";
6274        let result = run_main_f64_boxed(source, &["p"]);
6275        assert_eq!(result, 17.0);
6276    }
6277
6278    #[test]
6279    fn mixed_boxed_and_unboxed_locals_in_one_function() {
6280        let source = "
6281            function main(): number {
6282                let b: number = 10;
6283                let u: number = 20;
6284                b = b + 1;
6285                u = u + 2;
6286                return b + u;
6287            }
6288        ";
6289        let result = run_main_f64_boxed(source, &["b"]);
6290        assert_eq!(result, 33.0);
6291    }
6292
6293    #[test]
6294    fn multiple_distinct_box_types_coexist() {
6295        let source = r#"
6296            function main(): string {
6297                let n: number = 1;
6298                let s: string = "x";
6299                n = n + 41;
6300                s = "answer";
6301                return s;
6302            }
6303        "#;
6304        let result = run_main_string_boxed(source, &["n", "s"]);
6305        assert_eq!(result, "answer");
6306        let mut ta = boxed_typed_ast(source);
6307        box_named(&mut ta, &["n", "s"]);
6308        let collected = super::box_types::collect(&ta, &mock_symbols_with_intrinsics()).unwrap();
6309        assert_eq!(collected.len(), 2);
6310    }
6311
6312    #[test]
6313    fn no_boxed_bindings_produces_no_box_types() {
6314        let ta = boxed_typed_ast("function main(): void { let x: number = 1; }");
6315        let collected = super::box_types::collect(&ta, &mock_symbols_with_intrinsics()).unwrap();
6316        assert!(collected.is_empty(), "{collected:?}");
6317    }
6318
6319    #[test]
6320    fn record_box_type_is_idempotent_per_value_type() {
6321        use crate::Type;
6322        use wasm_encoder::ValType;
6323        let mut symbols = SymbolTable::default();
6324        symbols.record_box_type(ValType::F64, 42);
6325        assert_eq!(symbols.box_type_idx(&Type::Number).unwrap(), Some(42));
6326        symbols.record_box_type(ValType::F64, 42);
6327        assert_eq!(symbols.box_type_idx(&Type::Number).unwrap(), Some(42));
6328    }
6329
6330    #[test]
6331    fn literal_typed_and_base_share_box_wrapper() {
6332        use crate::Type;
6333        use crate::types::LiteralF64;
6334        use wasm_encoder::ValType;
6335        let mut symbols = SymbolTable::default();
6336        symbols.record_box_type(ValType::F64, 7);
6337        assert_eq!(symbols.box_type_idx(&Type::Number).unwrap(), Some(7));
6338        assert_eq!(
6339            symbols
6340                .box_type_idx(&Type::NumberLiteral(LiteralF64(42.0)))
6341                .unwrap(),
6342            Some(7),
6343        );
6344    }
6345
6346    pub(super) fn mock_symbols_with_intrinsics() -> SymbolTable {
6347        let mut map = SymbolTable::default();
6348        map.set_intrinsic_type_indices(super::intrinsics::IntrinsicTypeIndices {
6349            raw_string: 0,
6350            vtable: 1,
6351            object: 2,
6352            undefined: 51,
6353            string: 3,
6354            boxed_number: 4,
6355            boxed_boolean: 5,
6356            field_names: 6,
6357            object_fields: 7,
6358            object_shape: 8,
6359            to_string_fn: 9,
6360            to_json_fn: 10,
6361            equals_fn: 11,
6362            hash_fn: 12,
6363            field_getter: 13,
6364            field_setter: 14,
6365            raw_array: 15,
6366            array: 16,
6367            raw_uint8_array: 17,
6368            uint8_array: 18,
6369            closure: 19,
6370            class_vtable: 20,
6371            error_vtable: 21,
6372            error: 22,
6373            raw_bigint: 23,
6374            bigint: 24,
6375            regex_capture_array: 25,
6376            regex_match: 26,
6377            regex: 27,
6378            regex_match_box: 28,
6379            temporal_instant: 29,
6380            temporal_duration: 30,
6381            temporal_zdt: 31,
6382            raw_index_array: 32,
6383            map: 33,
6384            set: 34,
6385            url: 35,
6386            fs_stat: 41,
6387            fs_peek: 42,
6388            fs_dir_entry: 43,
6389            fs_info: 44,
6390            fs_file_writer: 45,
6391            http_response: 46,
6392            http_download_result: 47,
6393            session_entry: 48,
6394            session_page: 49,
6395            fs_mount_info: 50,
6396
6397            temporal_plain_date: 36,
6398            temporal_plain_time: 37,
6399            temporal_plain_date_time: 38,
6400            temporal_plain_year_month: 39,
6401            temporal_plain_month_day: 40,
6402        });
6403        map
6404    }
6405
6406    fn compile_with_closures(source: &str) -> Vec<u8> {
6407        compile(source)
6408    }
6409
6410    #[test]
6411    fn closure_zero_captures_validates() {
6412        let source = "function main(): void { let f = (x: number) => x * 2; }";
6413        let bytes = compile_with_closures(source);
6414        instantiate_against_prelude(&bytes);
6415    }
6416
6417    #[test]
6418    fn closure_const_capture_validates() {
6419        let source = "
6420            function main(): void {
6421                const y: number = 5;
6422                let f = (x: number) => x + y;
6423            }
6424        ";
6425        let bytes = compile_with_closures(source);
6426        instantiate_against_prelude(&bytes);
6427    }
6428
6429    #[test]
6430    fn closure_let_capture_validates_with_box() {
6431        let source = "
6432            function main(): void {
6433                let y: number = 5;
6434                let f = (x: number) => x + y;
6435            }
6436        ";
6437        let bytes = compile_with_closures(source);
6438        instantiate_against_prelude(&bytes);
6439    }
6440
6441    #[test]
6442    fn closure_param_capture_validates_with_box() {
6443        let source = "
6444            function host(p: number): void {
6445                let f = (x: number) => p + x;
6446            }
6447            function main(): void { host(7); }
6448        ";
6449        let bytes = compile_with_closures(source);
6450        instantiate_against_prelude(&bytes);
6451    }
6452
6453    #[test]
6454    fn closure_block_body_with_assignment_validates() {
6455        let source = "
6456            function main(): void {
6457                let counter: number = 0;
6458                let inc = (): void => { counter = counter + 1; };
6459            }
6460        ";
6461        let bytes = compile_with_closures(source);
6462        instantiate_against_prelude(&bytes);
6463    }
6464
6465    #[test]
6466    fn two_arrows_same_signature_share_closure_struct() {
6467        let source = "
6468            function main(): void {
6469                let f = (x: number) => x + 1;
6470                let g = (y: number) => y * 2;
6471            }
6472        ";
6473        let ta = boxed_typed_ast(source);
6474        let metas = super::analysis::CodegenAnalysis::collect(&ta, &[])
6475            .unwrap()
6476            .closure_metas;
6477        assert_eq!(metas.len(), 2);
6478        assert_eq!(metas[0].signature, metas[1].signature);
6479        let bytes = compile_with_closures(source);
6480        instantiate_against_prelude(&bytes);
6481    }
6482
6483    #[test]
6484    fn nested_closure_validates() {
6485        let source = "
6486            function main(): void {
6487                let y: number = 1;
6488                let outer = (): number => {
6489                    let inner = (x: number) => x + y;
6490                    return 0;
6491                };
6492            }
6493        ";
6494        let bytes = compile_with_closures(source);
6495        instantiate_against_prelude(&bytes);
6496    }
6497
6498    #[test]
6499    fn function_typed_binding_lowers_to_closure_struct_ref() {
6500        let source = "function main(): void { let f = (x: number) => x; }";
6501        let bytes = compile_with_closures(source);
6502        instantiate_against_prelude(&bytes);
6503    }
6504
6505    #[test]
6506    fn no_closures_no_closure_types_emitted() {
6507        let ta = boxed_typed_ast("function main(): void { let x: number = 1; }");
6508        let metas = super::analysis::CodegenAnalysis::collect(&ta, &[])
6509            .unwrap()
6510            .closure_metas;
6511        assert!(metas.is_empty(), "{metas:?}");
6512    }
6513
6514    #[test]
6515    fn inline_arrow_call_returns_value() {
6516        let result = run_main_f64("function main(): number { return ((x: number) => x + 1)(5); }");
6517        assert_eq!(result, 6.0);
6518    }
6519
6520    #[test]
6521    fn let_stored_arrow_call_returns_value() {
6522        let result =
6523            run_main_f64("function main(): number { let f = (x: number) => x + 1; return f(5); }");
6524        assert_eq!(result, 6.0);
6525    }
6526
6527    #[test]
6528    fn closure_call_with_multiple_args() {
6529        let result = run_main_f64(
6530            "function main(): number { let add = (a: number, b: number) => a + b; return add(2, 3); }",
6531        );
6532        assert_eq!(result, 5.0);
6533    }
6534
6535    #[test]
6536    fn captured_let_counter_returns_three() {
6537        let result = run_main_f64(
6538            "function main(): number {
6539                let count: number = 0;
6540                let inc = (): number => { count = count + 1; return count; };
6541                inc();
6542                inc();
6543                return inc();
6544            }",
6545        );
6546        assert_eq!(result, 3.0);
6547    }
6548
6549    #[test]
6550    fn higher_order_function_calls_callback() {
6551        let result = run_main_f64(
6552            "function apply(f: (x: number) => number, x: number): number { return f(x); }
6553             function main(): number { return apply((n: number) => n + 10, 5); }",
6554        );
6555        assert_eq!(result, 15.0);
6556    }
6557
6558    #[test]
6559    fn function_as_value_let_bound_call_returns_underlying_result() {
6560        let result = run_main_f64(
6561            "function greet(x: number): number { return x + 1; }
6562             function main(): number { let f = greet; return f(2); }",
6563        );
6564        assert_eq!(result, 3.0);
6565    }
6566
6567    #[test]
6568    fn function_as_value_passed_to_hof_invokes_underlying() {
6569        let result = run_main_f64(
6570            "function greet(x: number): number { return x + 1; }
6571             function apply(f: (n: number) => number, n: number): number { return f(n); }
6572             function main(): number { return apply(greet, 5); }",
6573        );
6574        assert_eq!(result, 6.0);
6575    }
6576
6577    #[test]
6578    fn function_as_value_flows_through_multiple_bindings() {
6579        let result = run_main_f64(
6580            "function greet(x: number): number { return x + 10; }
6581             function main(): number { let f = greet; let g = f; return g(2); }",
6582        );
6583        assert_eq!(result, 12.0);
6584    }
6585
6586    #[test]
6587    fn direct_call_path_unchanged_when_function_also_used_as_value() {
6588        let result = run_main_f64(
6589            "function greet(x: number): number { return x + 1; }
6590             function apply(f: (n: number) => number, n: number): number { return f(n); }
6591             function main(): number {
6592                 let direct = greet(10);
6593                 let indirect = apply(greet, 20);
6594                 return direct + indirect;
6595             }",
6596        );
6597        assert_eq!(result, 11.0 + 21.0);
6598    }
6599
6600    #[test]
6601    fn no_function_as_value_no_adapters_emitted() {
6602        let ta = boxed_typed_ast(
6603            "function greet(x: number): number { return x + 1; }
6604             function main(): void { greet(5); }",
6605        );
6606        let metas = super::analysis::CodegenAnalysis::collect(&ta, &[])
6607            .unwrap()
6608            .adapter_metas;
6609        assert!(metas.is_empty(), "{metas:?}");
6610    }
6611
6612    #[test]
6613    fn direct_callees_in_nested_positions_are_not_collected_as_adapters() {
6614        let ta = boxed_typed_ast(
6615            "function greet(x: number): number { return x + 1; }
6616             function nested(x: number): number { return x; }
6617             function inner(): number { return 1; }
6618             function outer(x: number): number { return x; }
6619             function main(): void {
6620                 greet(5);
6621                 nested(greet(5));
6622                 outer(inner());
6623             }",
6624        );
6625        let metas = super::analysis::CodegenAnalysis::collect(&ta, &[])
6626            .unwrap()
6627            .adapter_metas;
6628        assert!(
6629            metas.is_empty(),
6630            "direct-only callsites should produce no adapters, got {metas:?}",
6631        );
6632    }
6633
6634    #[test]
6635    fn function_as_value_collects_one_adapter_per_distinct_function() {
6636        let ta = boxed_typed_ast(
6637            "function f1(x: number): number { return x + 1; }
6638             function f2(x: number): number { return x + 2; }
6639             function apply(g: (n: number) => number, n: number): number { return g(n); }
6640             function main(): void {
6641                 let a = f1;
6642                 let b = f1;
6643                 apply(f2, 0);
6644             }",
6645        );
6646        let metas = super::analysis::CodegenAnalysis::collect(&ta, &[])
6647            .unwrap()
6648            .adapter_metas;
6649        assert_eq!(metas.len(), 2, "{metas:?}");
6650        let names: Vec<&str> = metas.iter().map(|m| m.name.as_str()).collect();
6651        assert!(names.contains(&"f1"));
6652        assert!(names.contains(&"f2"));
6653    }
6654
6655    #[test]
6656    fn higher_order_function_with_capture() {
6657        let result = run_main_f64(
6658            "function apply(f: (x: number) => number, x: number): number { return f(x); }
6659             function main(): number {
6660                 let bias: number = 100;
6661                 return apply((n: number) => n + bias, 7);
6662             }",
6663        );
6664        assert_eq!(result, 107.0);
6665    }
6666
6667    #[allow(dead_code)]
6668    fn _silence_unused() {
6669        let _ = (
6670            CodeSection::new(),
6671            ConstExpr::i32_const(0),
6672            ExportKind::Func,
6673            ExportSection::new(),
6674            FunctionSection::new(),
6675            GlobalSection::new(),
6676            ImportSection::new(),
6677            Module::new(),
6678            RefType {
6679                nullable: false,
6680                heap_type: HeapType::Concrete(0),
6681            },
6682            TypeSection::new(),
6683            ValType::F64,
6684            Function::new(vec![]),
6685            Ieee64::from(0.0_f64),
6686            Instruction::Nop,
6687            emit_statement,
6688        );
6689    }
6690}