Skip to main content

submilli_engine/codegen/
imported_classes.rs

1//! Cross-package class reconstruction (SUB-488).
2//!
3//! A consumer that imports a `class` from another package never sees that
4//! class's Wasm module — only its public `PackageDeclaration`. To use the class
5//! (construct it, read fields, dispatch methods, `instanceof`, or `extends` it)
6//! the consumer must rebuild the class's WasmGC types in its own module so that
7//! structural canonicalization unifies them with the producer's.
8//!
9//! Each class is emitted as its own rec group `(rec $C_vtable $C)` — matching the
10//! producer ([`super::classes`]) — so the consumer reconstructs only the classes
11//! it transitively needs, parents first. There are no cross-class type
12//! references (a class's fields live in the shared object-fields payload, not as
13//! named struct fields), so per-class groups always canonicalize. The consumer
14//! also imports each class's constructor, self-first ctor-init, and method bodies
15//! (exported by the producer in [`super::classes::ClassPlan::exported_funcs`]) so
16//! `new`, `super(...)`, and a local subclass's inherited vtable slots resolve.
17
18use std::collections::{BTreeMap, BTreeSet};
19
20use wasm_encoder::{
21    EntityType, FieldType, GlobalType, HeapType, ImportSection, RefType, StorageType, StructType,
22    SubType, TypeSection, ValType,
23};
24
25use crate::codegen::classes::{method_vtable_slot, shadows_inherited_field};
26use crate::codegen::dependency_usage::DependencyUsage;
27use crate::codegen::intrinsics::IntrinsicTypeIndices;
28use crate::codegen::symbol_table::{MethodSlotAbi, SymbolTable};
29use crate::codegen::{internal_failure, next_index, wasm_u32};
30use crate::compiler_error::CompilerFailure;
31use crate::{MangledName, Param, TypeKind, TypedAst, TypedTypeDecl};
32
33/// The resolved full layout of an imported class, handed to [`super::classes`]
34/// so a local subclass extending it lays its own fields/methods out after the
35/// inherited prefix.
36pub struct ImportedClassLayout {
37    pub private_members: BTreeSet<String>,
38    /// Full data-field names (inherited prefix then own), object-payload order.
39    pub fields: Vec<String>,
40    pub optional_fields: std::collections::BTreeSet<String>,
41    pub narrowing_checks: BTreeMap<String, crate::FieldNarrowingCheck>,
42    /// Full vtable method slots (inherited prefix then own/override), in order.
43    pub methods: Vec<ImportedSlot>,
44    /// The class's own constructor params, so a local subclass with an *implicit*
45    /// constructor extending this class inherits its parameter signature.
46    pub ctor_params: Vec<crate::TypedParam>,
47}
48
49pub struct ImportedSlot {
50    pub name: String,
51    /// The class whose body backs this slot (the declarer, or nearest ancestor
52    /// for an inherited slot) — the key under which its method func was imported.
53    pub owner: MangledName,
54    /// Declared parameter/return types of the owner's body, so a local subclass
55    /// can emit its own payload adapter for this slot.
56    pub param_tys: Vec<crate::Type>,
57    pub argument_metadata: Option<String>,
58    pub ret_ty: crate::Type,
59    pub generic: bool,
60}
61
62/// Per-class data gathered from the dependency `PackageDeclaration`s. `fields`
63/// and `methods` are `BTreeMap`s, so iterating their keys yields the canonical
64/// (sorted) own-member order the producer assigns slots in — no order metadata
65/// has to cross the package boundary.
66struct ClassInfo<'a> {
67    generics: &'a [String],
68    package: &'a str,
69    compiled_class: bool,
70    runtime_generics: &'a BTreeSet<MangledName>,
71    name: &'a str,
72    extends: Option<MangledName>,
73    fields: &'a BTreeMap<String, crate::FieldSig>,
74    narrowing_checks: &'a BTreeMap<String, crate::FieldNarrowingCheck>,
75    methods: &'a BTreeMap<String, crate::MethodSig>,
76    constructor: &'a [Param],
77    /// Accessor functions; codegen reconstructs the same synthetic getter/setter
78    /// vtable methods the producer emitted.
79    accessors: &'a [crate::AccessorSig],
80    statics: &'a BTreeMap<String, crate::MethodSig>,
81    static_visibility: &'a BTreeMap<String, crate::Visibility>,
82    static_fields: &'a BTreeMap<String, crate::FieldSig>,
83}
84
85impl<'a> ClassInfo<'a> {
86    /// Private statics are not exported by the producer, so the consumer must
87    /// not import them.
88    fn public_statics(&self) -> impl Iterator<Item = (&'a String, &'a crate::MethodSig)> {
89        self.statics.iter().filter(|(name, _)| {
90            self.static_visibility.get(*name) != Some(&crate::Visibility::Private)
91        })
92    }
93
94    fn public_static_fields(&self) -> impl Iterator<Item = (&'a String, &'a crate::FieldSig)> {
95        self.static_fields
96            .iter()
97            .filter(|(_, field)| field.visibility != crate::Visibility::Private)
98    }
99}
100
101/// The producer's vtable-method set for an imported class: real methods plus the
102/// synthetic getter/setter methods derived from its accessors, each as
103/// `(name, params, ret)`. Mirrors `codegen::classes::accessor_methods` so slot
104/// indices and signatures line up across the package boundary.
105fn vtable_methods(class: &ClassInfo<'_>) -> BTreeMap<String, (Vec<crate::Type>, crate::Type)> {
106    let mut out: BTreeMap<String, (Vec<crate::Type>, crate::Type)> = class
107        .methods
108        .iter()
109        .map(|(name, sig)| {
110            (
111                name.clone(),
112                (
113                    sig.params.iter().map(|p| p.ty.clone()).collect(),
114                    sig.ret.clone(),
115                ),
116            )
117        })
118        .collect();
119    for acc in class.accessors {
120        match acc {
121            crate::AccessorSig::Getter { name, ret_ty } => {
122                out.insert(
123                    crate::codegen::classes::accessor_getter_name(name),
124                    (Vec::new(), ret_ty.clone()),
125                );
126            }
127            crate::AccessorSig::Setter { name, param } => {
128                out.insert(
129                    crate::codegen::classes::accessor_setter_name(name),
130                    (vec![param.ty.clone()], crate::Type::Void),
131                );
132            }
133        }
134    }
135    out
136}
137
138/// The linker module a class's function bodies import from — the owning
139/// package for user classes and prelude-declared (host-implemented) classes
140/// alike: `Error`'s constructor/ctor-init are Rust host fns registered under
141/// `submilli:prelude`.
142fn host_module_for<'a>(class: &ClassInfo<'a>) -> &'a str {
143    class.package
144}
145
146/// Reconstruct every imported class reachable from this module: emit its rec
147/// group, record type/slot indices, and import its functions. Returns the
148/// resolved layout of each, keyed by mangled name, for `classes::ClassPlan`.
149#[allow(clippy::too_many_arguments)]
150pub fn reconstruct(
151    dependencies: &[&crate::PackageDeclaration],
152    ta: &TypedAst,
153    usage: &DependencyUsage,
154    intrinsics: IntrinsicTypeIndices,
155    file: crate::FileId,
156    types: &mut TypeSection,
157    next_type_idx: &mut u32,
158    imports: &mut ImportSection,
159    next_func_idx: &mut u32,
160    next_global_idx: &mut u32,
161    symbols: &mut SymbolTable,
162) -> Result<BTreeMap<MangledName, ImportedClassLayout>, crate::compiler_error::CompilerFailure> {
163    let info = collect_class_info(dependencies);
164    let order = needed_in_topo_order(&info, ta, usage)?;
165
166    let mut layouts: BTreeMap<MangledName, ImportedClassLayout> = BTreeMap::new();
167    for mangled in &order {
168        let class = class_info(&info, mangled)?;
169        let layout = reconstruct_one(
170            mangled,
171            class,
172            &info,
173            &layouts,
174            intrinsics,
175            file,
176            types,
177            next_type_idx,
178            imports,
179            next_func_idx,
180            next_global_idx,
181            symbols,
182        )?;
183        layouts.insert(mangled.clone(), layout);
184    }
185    // Statics come last — see `import_statics`.
186    for mangled in &order {
187        import_statics(
188            mangled,
189            class_info(&info, mangled)?,
190            usage,
191            types,
192            next_type_idx,
193            imports,
194            next_func_idx,
195            next_global_idx,
196            symbols,
197        )?;
198    }
199    Ok(layouts)
200}
201
202/// Import the statics this module actually uses: self-less functions and
203/// backing globals, keyed `Class#static#name`.
204///
205/// Unlike methods and constructors, these signatures are **unerased** — the
206/// producer emits statics through the ordinary function/global path, where
207/// `value_type` resolves a class to its concrete struct. So every class one
208/// names must already be reconstructed, or `value_type` takes the
209/// `(ref null $Object)` fallback and the import fails to link.
210///
211/// Two things keep that true. This runs in a pass after all reconstruction, so
212/// declaration order can't matter. And the usage gate holds the rest: resolving
213/// a static marks every class its signature names, so one that reaches here has
214/// already pulled the classes it names *from this module's dependency closure*
215/// into the reconstructed set. A class from outside that closure is marked but
216/// never reconstructed, and still takes the fallback — the residual hole is a
217/// consumer that declares fewer dependencies than the statics it uses reach.
218///
219/// Importing *unused* statics is what would break the invariant outright: a
220/// class reconstructed only as someone's ancestor carries statics nothing
221/// prepared types for, and they need not even be expressible here — a
222/// function-typed static wants a closure struct this module may never have
223/// registered.
224#[allow(clippy::too_many_arguments)]
225fn import_statics(
226    mangled: &MangledName,
227    class: &ClassInfo<'_>,
228    usage: &DependencyUsage,
229    types: &mut TypeSection,
230    next_type_idx: &mut u32,
231    imports: &mut ImportSection,
232    next_func_idx: &mut u32,
233    next_global_idx: &mut u32,
234    symbols: &mut SymbolTable,
235) -> Result<(), crate::compiler_error::CompilerFailure> {
236    for (name, sig) in class.public_statics() {
237        let key = crate::mangle::static_member(mangled, name);
238        if !usage.is_value_used(&key) {
239            continue;
240        }
241        let mut param_types: Vec<ValType> = sig
242            .params
243            .iter()
244            .map(|p| symbols.value_type(&p.ty))
245            .collect::<Result<_, _>>()?;
246        if class.runtime_generics.contains(&key) {
247            symbols.runtime_generic_functions.insert(key.clone());
248            param_types.push(super::runtime_descriptors::environment_type(symbols)?);
249        }
250        let results = symbols.wasm_result(&sig.ret)?;
251        let sig_idx = next_index(next_type_idx)?;
252        types.ty().function(param_types, results);
253        imports.import(
254            host_module_for(class),
255            key.as_str(),
256            EntityType::Function(sig_idx),
257        );
258        let idx = next_index(next_func_idx)?;
259        symbols.record_imported_fn(
260            key,
261            idx,
262            sig.params.iter().map(|p| p.ty.clone()).collect(),
263            sig.ret.clone(),
264            false,
265        );
266    }
267    for (name, field) in class.public_static_fields() {
268        let key = crate::mangle::static_member(mangled, name);
269        if !usage.is_value_used(&key) {
270            continue;
271        }
272        // Mutable like every exported value-global — the producer's `_start`
273        // initializes it; const-ness is typechecker-enforced.
274        imports.import(
275            host_module_for(class),
276            key.as_str(),
277            EntityType::Global(GlobalType {
278                val_type: super::global_val_type(&field.ty, symbols)?,
279                mutable: true,
280                shared: false,
281            }),
282        );
283        symbols.record_typed_global(key, next_index(next_global_idx)?, field.ty.clone());
284    }
285    Ok(())
286}
287
288fn collect_class_info<'a>(
289    dependencies: &'a [&'a crate::PackageDeclaration],
290) -> BTreeMap<MangledName, ClassInfo<'a>> {
291    let mut info = BTreeMap::new();
292    for defs in dependencies {
293        for sym in defs.runtime_types.values().chain(defs.types.values()) {
294            if let TypeKind::Class {
295                generics,
296                extends,
297                fields,
298                narrowing_checks,
299                methods,
300                accessors,
301                constructor,
302                statics,
303                static_visibility,
304                static_fields,
305                ..
306            } = &sym.kind
307            {
308                info.insert(
309                    sym.mangled_name.clone(),
310                    ClassInfo {
311                        generics,
312                        runtime_generics: &defs.runtime_generics,
313                        package: defs.package_name.as_str(),
314                        compiled_class: defs.runtime_types.contains_key(sym.mangled_name.as_str()),
315                        name: sym.name.as_str(),
316                        // Layout and vtables depend on the parent's name;
317                        // concrete arguments travel in the instance context.
318                        extends: extends.as_ref().map(|e| e.parent.clone()),
319                        fields,
320                        narrowing_checks,
321                        methods,
322                        constructor,
323                        accessors,
324                        statics,
325                        static_visibility,
326                        static_fields,
327                    },
328                );
329            }
330        }
331    }
332    info
333}
334
335/// Imported classes the module needs: every imported class referenced by the
336/// program plus the imported parents of local classes, transitively closed over
337/// `extends`, ordered parents-first.
338fn needed_in_topo_order(
339    info: &BTreeMap<MangledName, ClassInfo<'_>>,
340    ta: &TypedAst,
341    usage: &DependencyUsage,
342) -> Result<Vec<MangledName>, CompilerFailure> {
343    let mut roots: BTreeSet<MangledName> = BTreeSet::new();
344    for mangled in info.keys() {
345        // `is_type_reachable_by_name` also catches classes used only through
346        // their statics (never constructed, never named as a type).
347        if usage.is_type_reachable_by_name(mangled) {
348            roots.insert(mangled.clone());
349        }
350    }
351    // Imported parents of local classes (the parent itself may not be "used").
352    for decl in &ta.types {
353        if let TypedTypeDecl::Class(c) = decl
354            && let Some(parent) = &c.extends
355            && info.contains_key(parent)
356        {
357            roots.insert(parent.clone());
358        }
359    }
360
361    let mut order: Vec<MangledName> = Vec::new();
362    let mut seen: BTreeSet<&MangledName> = BTreeSet::new();
363    for root in &roots {
364        push_ancestors_first(info, root, &mut seen, &mut order)?;
365    }
366    Ok(order)
367}
368
369/// Appends `root` and its not-yet-ordered ancestors, parents first. Unlike a
370/// local class, an imported class's ancestors must all be declared: without
371/// one, the subclass would be reconstructed without its field and slot prefix
372/// and fail to link as `imported global type mismatch`, naming nothing.
373fn push_ancestors_first<'a>(
374    info: &'a BTreeMap<MangledName, ClassInfo<'_>>,
375    root: &'a MangledName,
376    seen: &mut BTreeSet<&'a MangledName>,
377    order: &mut Vec<MangledName>,
378) -> Result<(), CompilerFailure> {
379    let mut chain: Vec<&MangledName> = Vec::new();
380    let mut next = Some(root);
381    while let Some(mangled) = next {
382        if seen.contains(mangled) {
383            break;
384        }
385        let class = info.get(mangled).ok_or_else(|| {
386            internal_failure(format!(
387                "the dependency closure passed to codegen has no declaration for \
388                 `{mangled}`, an ancestor of imported class `{root}`"
389            ))
390        })?;
391        if chain.contains(&mangled) {
392            return Err(internal_failure(format!(
393                "imported class `{mangled}` inherits from itself"
394            )));
395        }
396        chain.push(mangled);
397        next = class.extends.as_ref();
398    }
399    for mangled in chain.into_iter().rev() {
400        seen.insert(mangled);
401        order.push(mangled.clone());
402    }
403    Ok(())
404}
405
406fn class_info<'i, 'a>(
407    info: &'i BTreeMap<MangledName, ClassInfo<'a>>,
408    mangled: &MangledName,
409) -> Result<&'i ClassInfo<'a>, CompilerFailure> {
410    info.get(mangled).ok_or_else(|| {
411        internal_failure(format!(
412            "imported class `{mangled}` has no reconstructed declaration"
413        ))
414    })
415}
416
417#[allow(clippy::too_many_arguments)]
418fn reconstruct_one(
419    mangled: &MangledName,
420    class: &ClassInfo<'_>,
421    info: &BTreeMap<MangledName, ClassInfo<'_>>,
422    layouts: &BTreeMap<MangledName, ImportedClassLayout>,
423    intrinsics: IntrinsicTypeIndices,
424    file: crate::FileId,
425    types: &mut TypeSection,
426    next_type_idx: &mut u32,
427    imports: &mut ImportSection,
428    next_func_idx: &mut u32,
429    next_global_idx: &mut u32,
430    symbols: &mut SymbolTable,
431) -> Result<ImportedClassLayout, crate::compiler_error::CompilerFailure> {
432    // Parents are reconstructed first (`needed_in_topo_order`).
433    let parent_layout = match &class.extends {
434        Some(parent) => Some(layouts.get(parent).ok_or_else(|| {
435            internal_failure(format!(
436                "imported class `{mangled}` was reconstructed before its parent `{parent}`"
437            ))
438        })?),
439        None => None,
440    };
441    // Full field list = inherited prefix (already reconstructed) + own *data*
442    // fields. Accessor properties are in `fields` for typing/docs but back no data
443    // slot, so they're excluded — matching the producer's payload layout.
444    let mut fields: Vec<String> = parent_layout.map(|l| l.fields.clone()).unwrap_or_default();
445    for name in class
446        .fields
447        .keys()
448        .filter(|name| !class.accessors.iter().any(|a| a.name() == name.as_str()))
449    {
450        if shadows_inherited_field(fields.iter().map(String::as_str), name) {
451            continue;
452        }
453        fields.push(name.clone());
454    }
455    let mut optional_fields = parent_layout
456        .map(|layout| layout.optional_fields.clone())
457        .unwrap_or_default();
458    for (name, field) in class.fields {
459        if field.optional {
460            optional_fields.insert(name.clone());
461        } else {
462            optional_fields.remove(name);
463        }
464    }
465    let mut narrowing_checks = parent_layout
466        .map(|layout| layout.narrowing_checks.clone())
467        .unwrap_or_default();
468    narrowing_checks.extend(class.narrowing_checks.clone());
469
470    // The producer's full vtable-method set (real methods + accessor getter/setter),
471    // sorted by name to match the producer's slot assignment.
472    let methods = vtable_methods(class);
473
474    // Full vtable slots = parent's slots, then fold in own methods (override →
475    // same slot/owner-of-body, new → appended owned by this class).
476    let mut slots: Vec<ImportedSlot> = parent_layout
477        .map(|l| {
478            l.methods
479                .iter()
480                .map(|s| ImportedSlot {
481                    name: s.name.clone(),
482                    owner: s.owner.clone(),
483                    param_tys: s.param_tys.clone(),
484                    argument_metadata: s.argument_metadata.clone(),
485                    ret_ty: s.ret_ty.clone(),
486                    generic: s.generic,
487                })
488                .collect()
489        })
490        .unwrap_or_default();
491    for (name, (param_tys, ret_ty)) in &methods {
492        let argument_metadata = match class.methods.get(name) {
493            Some(sig) => super::call_arguments::metadata(sig.params.iter().map(|param| {
494                (
495                    param
496                        .default
497                        .as_ref()
498                        .or(param.optional.then_some(&crate::DefaultValue::Undefined)),
499                    param.rest,
500                )
501            })),
502            None => None,
503        };
504        let generic = class
505            .methods
506            .get(name)
507            .is_some_and(|sig| !sig.generics.is_empty());
508        if let Some(slot) = slots.iter_mut().find(|s| &s.name == name) {
509            // Override: this class supplies the body, so the slot must describe
510            // it rather than the ancestor's.
511            slot.owner = mangled.clone();
512            slot.param_tys = param_tys.clone();
513            slot.argument_metadata = argument_metadata;
514            slot.ret_ty = ret_ty.clone();
515            slot.generic = generic;
516        } else {
517            slots.push(ImportedSlot {
518                name: name.clone(),
519                owner: mangled.clone(),
520                param_tys: param_tys.clone(),
521                argument_metadata,
522                ret_ty: ret_ty.clone(),
523                generic,
524            });
525        }
526    }
527
528    // 1. Method-signature fn-types for slots that *originate* on this class
529    //    (newly introduced). Overrides/inherited slots reuse the originating
530    //    ancestor's sig, looked up from the symbol table (parents come first).
531    for (name, (param_tys, ret)) in &methods {
532        let originates = class
533            .extends
534            .as_ref()
535            .and_then(|p| symbols.class_method_sig(p, name))
536            .is_none();
537        if !originates {
538            continue;
539        }
540        let mut params: Vec<ValType> = vec![ref_to(intrinsics.object)];
541        params.extend(
542            param_tys
543                .iter()
544                .map(|_| symbols.value_type(&crate::Type::Unknown))
545                .collect::<Result<Vec<_>, _>>()?,
546        );
547        let results = symbols.slot_wasm_result(ret)?;
548        let abi = MethodSlotAbi {
549            params: params.iter().skip(1).copied().collect(),
550            ret: results.first().copied(),
551        };
552        let sig_idx = next_index(next_type_idx)?;
553        types.ty().function(params, results);
554        symbols.record_class_method_sig(mangled.clone(), name.clone(), sig_idx);
555        symbols.record_class_method_abi(mangled.clone(), name.clone(), abi);
556    }
557    // Inherited slots: copy the originating ancestor's sig (and its erasure
558    // shape) under this class's key.
559    for slot in &slots {
560        if symbols.class_method_sig(mangled, &slot.name).is_none() {
561            let sig = match symbols.class_method_sig(&slot.owner, &slot.name) {
562                Some(sig) => sig,
563                None => ancestor_sig(info, symbols, class, &slot.name)?.ok_or_else(|| {
564                    internal_failure("an inherited method slot's signature was not reconstructed")
565                })?,
566            };
567            symbols.record_class_method_sig(mangled.clone(), slot.name.clone(), sig);
568        }
569        if symbols.class_method_abi(mangled, &slot.name).is_none()
570            && let Some(abi) = symbols
571                .class_method_abi(&slot.owner, &slot.name)
572                .cloned()
573                .or_else(|| {
574                    class
575                        .extends
576                        .as_ref()
577                        .and_then(|p| symbols.class_method_abi(p, &slot.name).cloned())
578                })
579        {
580            symbols.record_class_method_abi(mangled.clone(), slot.name.clone(), abi);
581        }
582    }
583
584    // 2. Reserve the (vtable, struct) pair and 3. emit the rec group.
585    let vtable_idx = next_index(next_type_idx)?;
586    let struct_idx = next_index(next_type_idx)?;
587    symbols.record_class_vtable_type(mangled.clone(), vtable_idx);
588    symbols.record_class_struct_type(mangled.clone(), struct_idx);
589
590    // Parents are reconstructed first, so a missing parent type is a broken
591    // order, not a root class.
592    let (vtable_super, struct_super) = match &class.extends {
593        Some(parent) => (
594            symbols.class_vtable_type_idx(parent),
595            symbols.class_struct_type_idx(parent),
596        ),
597        None => (Some(intrinsics.class_vtable), Some(intrinsics.object_shape)),
598    };
599    let (Some(vtable_super), Some(struct_super)) = (vtable_super, struct_super) else {
600        return Err(internal_failure(
601            "an imported class's parent types were not reconstructed first",
602        ));
603    };
604    symbols.record_struct_supertype(vtable_idx, vtable_super);
605    symbols.record_struct_supertype(struct_idx, struct_super);
606
607    let mut vtable_fields: Vec<FieldType> = vec![
608        fieldtype_ref(intrinsics.to_string_fn),
609        fieldtype_ref(intrinsics.to_json_fn),
610        fieldtype_ref(intrinsics.equals_fn),
611        fieldtype_ref(intrinsics.hash_fn),
612        fieldtype_ref_null(intrinsics.class_vtable),
613        FieldType {
614            element_type: StorageType::Val(ValType::I32),
615            mutable: false,
616        },
617    ];
618    for slot in &slots {
619        let sig = symbols
620            .class_method_sig(mangled, &slot.name)
621            .ok_or_else(|| internal_failure("an imported class method slot has no signature"))?;
622        vtable_fields.push(fieldtype_ref(sig));
623    }
624    let vtable = substruct(vtable_fields, Some(vtable_super));
625
626    let struct_ty = substruct(
627        vec![
628            fieldtype_ref(vtable_idx),
629            FieldType {
630                mutable: true,
631                ..fieldtype_ref(intrinsics.field_names)
632            },
633            FieldType {
634                mutable: true,
635                ..fieldtype_ref(intrinsics.object_fields)
636            },
637            FieldType {
638                element_type: StorageType::Val(ValType::Ref(RefType::ANYREF)),
639                mutable: true,
640            },
641            FieldType {
642                element_type: StorageType::Val(ValType::I64),
643                mutable: true,
644            },
645        ],
646        Some(struct_super),
647    );
648    types.ty().rec([vtable, struct_ty]);
649
650    // Import the producer's vtable-singleton global — the class's nominal
651    // identity. `instanceof` compares against it by ref.eq, and a local
652    // subclass's vtable wires it in as the parent link. For host classes
653    // (`Error`) the producer is the Rust runtime, which `linker.define`s it.
654    imports.import(
655        host_module_for(class),
656        crate::codegen::classes::vtable_global_export_name(mangled).as_str(),
657        EntityType::Global(GlobalType {
658            val_type: ref_to(vtable_idx),
659            mutable: false,
660            shared: false,
661        }),
662    );
663    symbols.record_class_vtable_global(mangled.clone(), next_index(next_global_idx)?);
664
665    // Record field payload slots and method vtable slots (universal slots +
666    // parent link precede).
667    for (i, name) in fields.iter().enumerate() {
668        symbols.record_class_field_slot(mangled.clone(), name.clone(), wasm_u32(i)?);
669    }
670    for (name, check) in &narrowing_checks {
671        symbols.record_class_field_narrowing_check(mangled.clone(), name.clone(), check.clone());
672    }
673    for (i, slot) in slots.iter().enumerate() {
674        // Native classes keep their methods in the vtable, without the mutable
675        // closure payload emitted by the compiler for guest class instances.
676        if class.compiled_class
677            && !slot.generic
678            && !slot.name.starts_with("get ")
679            && !slot.name.starts_with("set ")
680        {
681            symbols.record_class_payload_method(mangled.clone(), slot.name.clone());
682        }
683        symbols.record_class_method_slot(
684            mangled.clone(),
685            slot.name.clone(),
686            method_vtable_slot(i)?,
687        );
688    }
689
690    symbols
691        .class_type_parameters
692        .insert(mangled.clone(), class.generics.to_vec());
693    let parent_guarded = match &class.extends {
694        Some(parent) => {
695            symbols
696                .recorded_class_guard_layout(parent)?
697                .has_instance_guards
698        }
699        None => false,
700    };
701    let named_payload_len = fields
702        .len()
703        .checked_add(slots.iter().filter(|slot| !slot.generic).count())
704        .ok_or_else(|| internal_failure("an imported class payload is too large"))?;
705    symbols.record_class_guard_layout(
706        mangled.clone(),
707        class.extends.as_ref(),
708        wasm_u32(named_payload_len)?,
709        (!class.generics.is_empty() || !narrowing_checks.is_empty() || parent_guarded)
710            && class.compiled_class,
711    )?;
712
713    // 4. Import the constructor, ctor-init, and own method bodies.
714    let ctor_params: Vec<ValType> = class
715        .constructor
716        .iter()
717        .map(|p| symbols.slot_value_type(&p.ty))
718        .collect::<Result<_, _>>()?;
719    let ctor_param_tys: Vec<crate::Type> = class.constructor.iter().map(|p| p.ty.clone()).collect();
720    symbols.record_class_ctor_abi(mangled.clone(), ctor_params.clone());
721    let class_ref = crate::Type::class_ref(
722        crate::Package(class.package.to_string()),
723        class.name.to_string(),
724        mangled.clone(),
725        Vec::new(),
726    );
727
728    // Entry constructor: `(params) -> (ref $C)`.
729    let mut entry_params = ctor_params.clone();
730    if symbols
731        .recorded_class_guard_layout(mangled)?
732        .has_instance_guards
733    {
734        entry_params.push(ref_to(intrinsics.object_fields));
735    }
736    let ctor_sig = next_index(next_type_idx)?;
737    types.ty().function(entry_params, [ref_to(struct_idx)]);
738    let ctor_mangled = crate::mangle::extend(mangled, "constructor");
739    imports.import(
740        host_module_for(class),
741        ctor_mangled.as_str(),
742        EntityType::Function(ctor_sig),
743    );
744    let ctor_idx = next_index(next_func_idx)?;
745    symbols.record_imported_fn(ctor_mangled, ctor_idx, ctor_param_tys, class_ref, false);
746
747    // Self-first ctor-init: `((ref $Object), params...) -> ()`.
748    let mut init_params = vec![ref_to(intrinsics.object)];
749    init_params.extend(ctor_params);
750    let init_sig = next_index(next_type_idx)?;
751    types.ty().function(init_params, Vec::<ValType>::new());
752    let init_mangled = crate::mangle::extend(mangled, "constructor_init");
753    imports.import(
754        host_module_for(class),
755        init_mangled.as_str(),
756        EntityType::Function(init_sig),
757    );
758    let init_idx = next_index(next_func_idx)?;
759    symbols.record_class_ctor_init_func(mangled.clone(), init_idx);
760
761    // Own method bodies — reuse the slot's `((ref $Object), params...) -> ret`
762    // sig. A local subclass's vtable global `ref.func`s the inherited ones.
763    for name in methods.keys() {
764        let sig = symbols
765            .class_method_sig(mangled, name)
766            .ok_or_else(|| internal_failure("an imported class's own method has no signature"))?;
767        let method_mangled = crate::mangle::extend(mangled, name);
768        imports.import(
769            host_module_for(class),
770            method_mangled.as_str(),
771            EntityType::Function(sig),
772        );
773        let idx = next_index(next_func_idx)?;
774        symbols.record_class_method_func(mangled.clone(), name.clone(), idx);
775    }
776
777    let ctor_typed_params = class
778        .constructor
779        .iter()
780        .map(|p| crate::TypedParam {
781            name: crate::Ident {
782                name: p.name.clone(),
783                span: crate::Span::at(file),
784            },
785            ty: p.ty.clone(),
786            optional: p.optional,
787            boxed: false,
788            rest: p.rest,
789            default: p.default.clone(),
790        })
791        .collect();
792
793    let mut private_members = parent_layout
794        .map(|layout| layout.private_members.clone())
795        .unwrap_or_default();
796    for (name, field) in class.fields {
797        if field.visibility == crate::Visibility::Private {
798            private_members.insert(name.clone());
799        }
800    }
801    for accessor in class.accessors {
802        if class
803            .fields
804            .get(accessor.name())
805            .is_some_and(|field| field.visibility == crate::Visibility::Private)
806        {
807            private_members.insert(super::classes::accessor_getter_name(accessor.name()));
808            private_members.insert(super::classes::accessor_setter_name(accessor.name()));
809        }
810    }
811    Ok(ImportedClassLayout {
812        private_members,
813        fields,
814        optional_fields,
815        narrowing_checks,
816        methods: slots,
817        ctor_params: ctor_typed_params,
818    })
819}
820
821/// Walk the `extends` chain to find the class that originates `method`, returning
822/// the sig recorded under it. Used only as a fallback for inherited slots.
823fn ancestor_sig(
824    info: &BTreeMap<MangledName, ClassInfo<'_>>,
825    symbols: &SymbolTable,
826    class: &ClassInfo<'_>,
827    method: &str,
828) -> Result<Option<u32>, CompilerFailure> {
829    let mut visited: BTreeSet<&MangledName> = BTreeSet::new();
830    let mut cur = class.extends.as_ref();
831    while let Some(m) = cur {
832        if !visited.insert(m) {
833            return Err(internal_failure(format!(
834                "imported class `{m}` inherits from itself"
835            )));
836        }
837        if let Some(sig) = symbols.class_method_sig(m, method) {
838            return Ok(Some(sig));
839        }
840        cur = info.get(m).and_then(|c| c.extends.as_ref());
841    }
842    Ok(None)
843}
844
845fn ref_to(idx: u32) -> ValType {
846    ValType::Ref(RefType {
847        nullable: false,
848        heap_type: HeapType::Concrete(idx),
849    })
850}
851
852fn fieldtype_ref(idx: u32) -> FieldType {
853    FieldType {
854        element_type: StorageType::Val(ref_to(idx)),
855        mutable: false,
856    }
857}
858
859fn fieldtype_ref_null(idx: u32) -> FieldType {
860    FieldType {
861        element_type: StorageType::Val(ValType::Ref(RefType {
862            nullable: true,
863            heap_type: HeapType::Concrete(idx),
864        })),
865        mutable: false,
866    }
867}
868
869fn substruct(fields: Vec<FieldType>, supertype: Option<u32>) -> SubType {
870    SubType {
871        is_final: false,
872        supertype_idx: supertype,
873        composite_type: wasm_encoder::CompositeType {
874            inner: wasm_encoder::CompositeInnerType::Struct(StructType {
875                fields: fields.into_boxed_slice(),
876            }),
877            shared: false,
878            descriptor: None,
879            describes: None,
880        },
881    }
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887    use crate::compile::{PackageSourceModule, compile_package_checked};
888    use crate::compiler_error::CompilerFailure;
889
890    fn declaration() -> crate::PackageDeclaration {
891        let modules = [PackageSourceModule {
892            path: crate::ModulePath::from("lib"),
893            source: "export class A { x: number = 1 } export class B extends A {}",
894        }];
895        compile_package_checked("dep", crate::ModulePath::from("lib"), &modules, &[])
896            .unwrap()
897            .declaration
898    }
899
900    /// A local class extending the imported `B`, so B and its ancestors are needed.
901    fn subclass_of(parent: &MangledName) -> TypedAst {
902        let (mut ta, diagnostics) = crate::compile::typecheck_to_typed_ast(
903            "class C {} function main(): void {}",
904            crate::FileId(0),
905        );
906        assert!(diagnostics.is_empty(), "{diagnostics:?}");
907        for decl in &mut ta.types {
908            if let TypedTypeDecl::Class(class) = decl {
909                class.extends = Some(parent.clone());
910            }
911        }
912        ta
913    }
914
915    fn mangled(declaration: &crate::PackageDeclaration, name: &str) -> MangledName {
916        declaration
917            .runtime_types
918            .values()
919            .chain(declaration.types.values())
920            .find(|symbol| symbol.name == name)
921            .unwrap()
922            .mangled_name
923            .clone()
924    }
925
926    #[test]
927    fn incomplete_or_cyclic_dependency_closures_are_internal_failures() {
928        let declaration = declaration();
929        let a = mangled(&declaration, "A");
930        let b = mangled(&declaration, "B");
931        let ta = subclass_of(&b);
932        let usage = DependencyUsage::empty();
933
934        let dependencies = [&declaration];
935        let info = collect_class_info(&dependencies);
936        assert_eq!(
937            needed_in_topo_order(&info, &ta, &usage).unwrap(),
938            [a.clone(), b.clone()]
939        );
940
941        // A missing ancestor fails rather than reconstructing a subclass
942        // without its prefix, which would link as a type mismatch.
943        let mut incomplete = declaration.clone();
944        incomplete
945            .runtime_types
946            .retain(|_, symbol| symbol.mangled_name != a);
947        incomplete
948            .types
949            .retain(|_, symbol| symbol.mangled_name != a);
950        let dependencies = [&incomplete];
951        let info = collect_class_info(&dependencies);
952        assert!(matches!(
953            needed_in_topo_order(&info, &ta, &usage),
954            Err(CompilerFailure::Internal { .. })
955        ));
956
957        let mut cyclic = declaration;
958        for symbol in cyclic
959            .runtime_types
960            .values_mut()
961            .chain(cyclic.types.values_mut())
962        {
963            if symbol.mangled_name == a
964                && let TypeKind::Class { extends, .. } = &mut symbol.kind
965            {
966                *extends = Some(crate::ClassExtends {
967                    parent: b.clone(),
968                    args: Vec::new(),
969                });
970            }
971        }
972        let dependencies = [&cyclic];
973        let info = collect_class_info(&dependencies);
974        assert!(matches!(
975            needed_in_topo_order(&info, &ta, &usage),
976            Err(CompilerFailure::Internal { .. })
977        ));
978    }
979}