Skip to main content

submilli_engine/codegen/
mod.rs

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