Skip to main content

submilli_engine/codegen/
mod.rs

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