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