Skip to main content

submilli_engine/codegen/
mod.rs

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