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