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