Skip to main content

submilli_engine/codegen/
mod.rs

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