plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


plum-wasm-codegen/src/lib.rs
3d6f280 1
// Functions/methods are named camelCase across this project (matching plum's own
3d6f280 2
// naming convention), not Rust's idiomatic snake_case — silence the resulting lint.
3d6f280 3
#![allow(non_snake_case)]
3d6f280 4
bb8ca38 5
use wasm_encoder::*;
5d8ada1 6
use std::cell::RefCell;
35af6cf 7
use std::collections::{HashMap, HashSet};
bb8ca38 8
use plum_core::ast;
5d8ada1 9
use plum_checker::types::{PlumType, TypeEnv, TypeScheme};
4fda634 10
use plum_checker::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo, EnumParams};
5d8ada1 11
5ef4579 12
/// One entry in the module's type section. Wasm's type section is a SINGLE shared
5ef4579 13
/// index space for function types AND (once wasm-gc is in play) composite
5ef4579 14
/// struct/array types — `Rec` entries occupy as many consecutive indices as they
5ef4579 15
/// have members, exactly like `CoreTypeEncoder::rec` groups multiple sub-types
5ef4579 16
/// under one recursive-group declaration.
5ef4579 17
enum TypeEntry {
5ef4579 18
    Func(FuncType),
5ef4579 19
    /// A whole `rec` group of struct/array sub-types, declared together so members
5ef4579 20
    /// can reference each other (including themselves) regardless of declaration
5ef4579 21
    /// order within the group.
5ef4579 22
    Rec(Vec<SubType>),
5ef4579 23
}
5ef4579 24
0e39618 25
/// One entry in the module's data section. Like the type section, data segments
0e39618 26
/// share ONE index space regardless of kind — a `Passive` segment's index (needed by
0e39618 27
/// `array.new_data`) is its position among ALL segments, active or passive.
0e39618 28
enum DataSegmentEntry {
0e39618 29
    Active(u32, Vec<u8>),
0e39618 30
    Passive(Vec<u8>),
0e39618 31
}
0e39618 32
bb8ca38 33
pub struct WasmModule {
5ef4579 34
    types: Vec<TypeEntry>,
5ef4579 35
    /// Running count of type-section INDICES assigned so far — NOT the same as
5ef4579 36
    /// `types.len()`, since one `TypeEntry::Rec` occupies as many indices as it has
5ef4579 37
    /// members while still being a single `Vec` element.
5ef4579 38
    next_type_idx: u32,
bb8ca38 39
    imports: Vec<(String, String, u32)>,
bb8ca38 40
    functions: Vec<(u32, Vec<u8>)>,
bb8ca38 41
    exports: Vec<(String, ExportKind, u32)>,
bb8ca38 42
    globals: Vec<(ValType, bool, Vec<u8>)>,
0e39618 43
    data_segments: Vec<DataSegmentEntry>,
db00bd9 44
    /// Function indices, in table order — the single funcref table used for
db00bd9 45
    /// closure `call_indirect` dispatch. Index into this vec IS the table index.
db00bd9 46
    table_elements: Vec<u32>,
bb8ca38 47
    pub func_import_count: u32,
bb8ca38 48
    pub func_count: u32,
bb8ca38 49
    global_count: u32,
0e39618 50
    start_function: Option<u32>,
bb8ca38 51
}
bb8ca38 52
bb8ca38 53
impl WasmModule {
bb8ca38 54
    pub fn new() -> Self {
bb8ca38 55
        Self {
bb8ca38 56
            types: Vec::new(),
5ef4579 57
            next_type_idx: 0,
bb8ca38 58
            imports: Vec::new(),
bb8ca38 59
            functions: Vec::new(),
bb8ca38 60
            exports: Vec::new(),
bb8ca38 61
            globals: Vec::new(),
bb8ca38 62
            data_segments: Vec::new(),
db00bd9 63
            table_elements: Vec::new(),
bb8ca38 64
            func_import_count: 0,
bb8ca38 65
            func_count: 0,
bb8ca38 66
            global_count: 0,
0e39618 67
            start_function: None,
bb8ca38 68
        }
bb8ca38 69
    }
bb8ca38 70
3d6f280 71
    pub fn addType(&mut self, params: &[ValType], results: &[ValType]) -> u32 {
5ef4579 72
        let idx = self.next_type_idx;
5ef4579 73
        self.types.push(TypeEntry::Func(FuncType::new(params.iter().copied(), results.iter().copied())));
5ef4579 74
        self.next_type_idx += 1;
bb8ca38 75
        idx
bb8ca38 76
    }
bb8ca38 77
5ef4579 78
    /// Declares a whole `rec` group of wasm-gc struct/array sub-types together,
5ef4579 79
    /// returning the type index assigned to each member, in order. Grouping
5ef4579 80
    /// unrelated types is harmless — the point is that MUTUALLY referencing types
5ef4579 81
    /// (e.g. an enum's supertype and its variant subtypes, or a self-referential
5ef4579 82
    /// struct field) MUST share a `rec` group to reference each other regardless of
5ef4579 83
    /// which one is declared "first".
5ef4579 84
    pub fn addGcTypes(&mut self, subtypes: Vec<SubType>) -> Vec<u32> {
5ef4579 85
        let base = self.next_type_idx;
5ef4579 86
        let count = subtypes.len() as u32;
5ef4579 87
        self.types.push(TypeEntry::Rec(subtypes));
5ef4579 88
        self.next_type_idx += count;
5ef4579 89
        (base..base + count).collect()
5ef4579 90
    }
5ef4579 91
3d6f280 92
    pub fn addImport(&mut self, module: &str, name: &str, type_idx: u32) -> u32 {
bb8ca38 93
        let idx = self.func_import_count;
bb8ca38 94
        self.imports.push((module.to_string(), name.to_string(), type_idx));
bb8ca38 95
        self.func_import_count += 1;
bb8ca38 96
        idx
bb8ca38 97
    }
bb8ca38 98
3d6f280 99
    pub fn addFunction(&mut self, type_idx: u32, body: &[u8]) -> u32 {
bb8ca38 100
        let idx = self.func_import_count + self.func_count;
bb8ca38 101
        self.functions.push((type_idx, body.to_vec()));
bb8ca38 102
        self.func_count += 1;
bb8ca38 103
        idx
bb8ca38 104
    }
bb8ca38 105
3d6f280 106
    pub fn addExport(&mut self, name: &str, kind: ExportKind, idx: u32) {
bb8ca38 107
        self.exports.push((name.to_string(), kind, idx));
bb8ca38 108
    }
bb8ca38 109
3d6f280 110
    pub fn addGlobal(&mut self, val_type: ValType, mutable: bool, init: &[u8]) -> u32 {
bb8ca38 111
        let idx = self.global_count;
bb8ca38 112
        self.globals.push((val_type, mutable, init.to_vec()));
bb8ca38 113
        self.global_count += 1;
bb8ca38 114
        idx
bb8ca38 115
    }
bb8ca38 116
3d6f280 117
    pub fn addDataSegment(&mut self, offset: u32, data: &[u8]) {
0e39618 118
        self.data_segments.push(DataSegmentEntry::Active(offset, data.to_vec()));
0e39618 119
    }
0e39618 120
0e39618 121
    /// Adds a passive segment (no implicit memory-init offset) and returns its index
0e39618 122
    /// in the shared active/passive data-segment index space — the index `array.new_data`
0e39618 123
    /// needs to reference it. Passive segments require a `DataCountSection` (see `finish`).
0e39618 124
    pub fn addPassiveDataSegment(&mut self, data: &[u8]) -> u32 {
0e39618 125
        let idx = self.data_segments.len() as u32;
0e39618 126
        self.data_segments.push(DataSegmentEntry::Passive(data.to_vec()));
0e39618 127
        idx
0e39618 128
    }
0e39618 129
0e39618 130
    /// Registers `func_idx` to run once automatically at instantiation, before any
0e39618 131
    /// export is callable — needed because `struct.new` (and therefore constructing
0e39618 132
    /// any GC singleton, like the pre-allocated payload-free enum variants) is not
0e39618 133
    /// allowed inside a `global`'s own const-expr initializer (confirmed empirically
0e39618 134
    /// in Task 1 — `wasmtimeGcConfigAllowsStructNewInGlobalConstExpr` fails), so those
0e39618 135
    /// globals are declared `mutable` with a `ref.null` initial value and populated
0e39618 136
    /// here instead.
0e39618 137
    pub fn setStartFunction(&mut self, func_idx: u32) {
0e39618 138
        self.start_function = Some(func_idx);
bb8ca38 139
    }
bb8ca38 140
db00bd9 141
    /// Registers `func_idx` as the next slot in the single funcref table used for
db00bd9 142
    /// closure `call_indirect` dispatch, returning its table index.
3d6f280 143
    pub fn addTableElement(&mut self, func_idx: u32) -> u32 {
db00bd9 144
        let table_idx = self.table_elements.len() as u32;
db00bd9 145
        self.table_elements.push(func_idx);
db00bd9 146
        table_idx
db00bd9 147
    }
db00bd9 148
bb8ca38 149
    pub fn finish(&mut self) -> Vec<u8> {
bb8ca38 150
        let mut module = wasm_encoder::Module::new();
bb8ca38 151
bb8ca38 152
        // Type section
bb8ca38 153
        let mut types = TypeSection::new();
5ef4579 154
        for entry in &self.types {
5ef4579 155
            match entry {
5ef4579 156
                TypeEntry::Func(ft) => {
5ef4579 157
                    types.ty().function(ft.params().iter().copied(), ft.results().iter().copied());
5ef4579 158
                }
5ef4579 159
                TypeEntry::Rec(subtypes) => {
5ef4579 160
                    types.ty().rec(subtypes.iter().cloned());
5ef4579 161
                }
5ef4579 162
            }
bb8ca38 163
        }
bb8ca38 164
        module.section(&types);
bb8ca38 165
bb8ca38 166
        // Import section
bb8ca38 167
        if !self.imports.is_empty() {
bb8ca38 168
            let mut imports = ImportSection::new();
bb8ca38 169
            for (module_name, name, type_idx) in &self.imports {
bb8ca38 170
                imports.import(module_name, name, EntityType::Function(*type_idx));
bb8ca38 171
            }
bb8ca38 172
            module.section(&imports);
bb8ca38 173
        }
bb8ca38 174
bb8ca38 175
        // Function section
bb8ca38 176
        if !self.functions.is_empty() {
bb8ca38 177
            let mut funcs = FunctionSection::new();
bb8ca38 178
            for (type_idx, _) in &self.functions {
bb8ca38 179
                funcs.function(*type_idx);
bb8ca38 180
            }
bb8ca38 181
            module.section(&funcs);
bb8ca38 182
        }
bb8ca38 183
db00bd9 184
        // Table section
db00bd9 185
        if !self.table_elements.is_empty() {
db00bd9 186
            let mut tables = TableSection::new();
db00bd9 187
            tables.table(TableType {
db00bd9 188
                element_type: RefType::FUNCREF,
db00bd9 189
                minimum: self.table_elements.len() as u64,
db00bd9 190
                maximum: Some(self.table_elements.len() as u64),
db00bd9 191
                table64: false,
db00bd9 192
                shared: false,
db00bd9 193
            });
db00bd9 194
            module.section(&tables);
db00bd9 195
        }
db00bd9 196
bb8ca38 197
        // Global section
bb8ca38 198
        if !self.globals.is_empty() {
bb8ca38 199
            let mut globals = GlobalSection::new();
bb8ca38 200
            for (val_type, mutable, init_expr) in &self.globals {
bb8ca38 201
                let expr = ConstExpr::raw(init_expr.iter().copied());
bb8ca38 202
                globals.global(
bb8ca38 203
                    GlobalType { val_type: *val_type, mutable: *mutable, shared: false },
bb8ca38 204
                    &expr,
bb8ca38 205
                );
bb8ca38 206
            }
bb8ca38 207
            module.section(&globals);
bb8ca38 208
        }
bb8ca38 209
bb8ca38 210
        // Export section
bb8ca38 211
        if !self.exports.is_empty() {
bb8ca38 212
            let mut exports = ExportSection::new();
bb8ca38 213
            for (name, kind, idx) in &self.exports {
bb8ca38 214
                exports.export(name, *kind, *idx);
bb8ca38 215
            }
bb8ca38 216
            module.section(&exports);
db00bd9 217
        }
db00bd9 218
0e39618 219
        // Start section
0e39618 220
        if let Some(func_idx) = self.start_function {
0e39618 221
            module.section(&StartSection { function_index: func_idx });
0e39618 222
        }
0e39618 223
db00bd9 224
        // Element section
db00bd9 225
        if !self.table_elements.is_empty() {
db00bd9 226
            let mut elements = ElementSection::new();
db00bd9 227
            let offset = ConstExpr::i32_const(0);
db00bd9 228
            elements.active(Some(0), &offset, Elements::Functions(std::borrow::Cow::Borrowed(&self.table_elements)));
db00bd9 229
            module.section(&elements);
bb8ca38 230
        }
bb8ca38 231
0e39618 232
        // DataCount section — required whenever `array.new_data`/`data.drop` reference
0e39618 233
        // a passive segment, and must appear before the code section.
0e39618 234
        let has_passive = self.data_segments.iter().any(|e| matches!(e, DataSegmentEntry::Passive(_)));
0e39618 235
        if has_passive {
0e39618 236
            module.section(&DataCountSection { count: self.data_segments.len() as u32 });
0e39618 237
        }
0e39618 238
bb8ca38 239
        // Code section
bb8ca38 240
        if !self.functions.is_empty() {
bb8ca38 241
            let mut code = CodeSection::new();
bb8ca38 242
            for (_, body_bytes) in &self.functions {
bb8ca38 243
                code.raw(body_bytes);
bb8ca38 244
            }
bb8ca38 245
            module.section(&code);
bb8ca38 246
        }
bb8ca38 247
bb8ca38 248
        // Data section
bb8ca38 249
        if !self.data_segments.is_empty() {
bb8ca38 250
            let mut data = DataSection::new();
0e39618 251
            for entry in &self.data_segments {
0e39618 252
                match entry {
0e39618 253
                    DataSegmentEntry::Active(offset, bytes) => {
0e39618 254
                        let offset_expr = ConstExpr::i32_const(*offset as i32);
0e39618 255
                        data.active(0, &offset_expr, bytes.iter().copied());
0e39618 256
                    }
0e39618 257
                    DataSegmentEntry::Passive(bytes) => {
0e39618 258
                        data.passive(bytes.iter().copied());
0e39618 259
                    }
0e39618 260
                }
bb8ca38 261
            }
bb8ca38 262
            module.section(&data);
bb8ca38 263
        }
bb8ca38 264
bb8ca38 265
        module.finish()
bb8ca38 266
    }
bb8ca38 267
}
bb8ca38 268
bb8ca38 269
impl Default for WasmModule {
bb8ca38 270
    fn default() -> Self {
bb8ca38 271
        Self::new()
bb8ca38 272
    }
bb8ca38 273
}
bb8ca38 274
bb8ca38 275
#[derive(Clone)]
bb8ca38 276
pub struct FuncSig {
bb8ca38 277
    pub params: Vec<ValType>,
bb8ca38 278
    pub ret: Option<ValType>,
bb8ca38 279
}
bb8ca38 280
4ba0db3 281
/// Everything codegen needs to know about one closure *literal* found in the program.
4ba0db3 282
/// wasm has no native closures: each literal `|v| body` becomes its own real wasm
0e39618 283
/// function (registered in the funcref table), and a closure *value* is a `ref` to
0e39618 284
/// the shared `{table_idx: i32, env: anyref}` struct (`GcTypeRegistry::closure_type_idx`).
0e39618 285
/// `env` is a `ref.cast` of this closure literal's OWN env struct type
0e39618 286
/// (`env_type_idx`), one field per captured (free) variable in `free_vars` order.
4ba0db3 287
pub struct ClosureInfo {
4ba0db3 288
    /// Reserved wasm function index for this closure's compiled body.
4ba0db3 289
    pub func_idx: u32,
0e39618 290
    /// Index of `func_idx` in the funcref table (the `i32` stored in the closure
0e39618 291
    /// struct's `table_idx` field).
4ba0db3 292
    pub table_idx: u32,
0e39618 293
    /// This closure literal's own env struct type index (fields = `free_vars`, in
0e39618 294
    /// order) — assigned once all closures are discovered, alongside every other
0e39618 295
    /// closure's env type and the shared closure-value struct, in one `rec` group.
0e39618 296
    pub env_type_idx: u32,
4ba0db3 297
    /// Closure param val types (NOT including the implicit leading env pointer).
4ba0db3 298
    pub param_vts: Vec<ValType>,
4ba0db3 299
    /// Closure param plum types (for the closure body's own type env).
4ba0db3 300
    pub param_ptypes: Vec<PlumType>,
4ba0db3 301
    /// Closure return val type (`None` for a `Unit`-returning closure).
4ba0db3 302
    pub ret_vt: Option<ValType>,
4ba0db3 303
    /// Free variables captured by value, in a stable (first-appearance) order; the
0e39618 304
    /// index into this vec IS the variable's field index in the env struct.
4ba0db3 305
    pub free_vars: Vec<(String, PlumType)>,
4ba0db3 306
}
4ba0db3 307
4ba0db3 308
/// Key for deduplicating `call_indirect` function-type indices: the full wasm
4ba0db3 309
/// signature (leading env-ptr param included) of a closure.
4ba0db3 310
type ClosureSigKey = (Vec<ValType>, Option<ValType>);
4ba0db3 311
5d8ada1 312
/// Global, read-only lookup tables shared by every function body being compiled.
4ba0db3 313
pub struct CompileCtx<'a> {
bb8ca38 314
    pub func_ids: HashMap<String, u32>,
bb8ca38 315
    pub func_sigs: HashMap<String, FuncSig>,
5d8ada1 316
    pub classes: ClassEnv,
5d8ada1 317
    pub methods: MethodEnv,
5d8ada1 318
    pub enum_variants: EnumVariants,
4fda634 319
    pub enum_params: EnumParams,
5d8ada1 320
    pub global_env: TypeEnv,
4ba0db3 321
    /// Closure literal (keyed by `&Expr::Closure` pointer identity) -> its `ClosureInfo`.
4ba0db3 322
    pub closures: HashMap<usize, ClosureInfo>,
4ba0db3 323
    /// The AST of each discovered closure literal, keyed the same way, so its body can
4ba0db3 324
    /// be compiled in a second pass after all closures are registered.
4ba0db3 325
    pub closure_asts: HashMap<usize, &'a ast::Closure>,
4ba0db3 326
    /// Closure wasm signature -> function-type index, for `call_indirect` at call sites.
4ba0db3 327
    pub closure_call_types: HashMap<ClosureSigKey, u32>,
0e39618 328
    /// Top-level function name -> global index holding its zero-capture "trampoline"
0e39618 329
    /// closure struct `{table_idx, env=null}`, for using a plain named function
35af6cf 330
    /// wherever a `fn(...)`-typed value is expected (e.g. `each(double)`). Since the
0e39618 331
    /// struct has no captures it never changes, so it's built once by the shared
0e39618 332
    /// `start` function instead of being reconstructed per reference.
35af6cf 333
    pub named_fn_values: HashMap<String, u32>,
0e39618 334
    /// Shared runtime helper `(a: ref Str, b: ref Str) -> ref Str`: allocates a new
0e39618 335
    /// `array<i8>` exactly long enough to hold `a`'s bytes followed by `b`'s, for
0e39618 336
    /// lowering string interpolation (`"{expr}"`).
35af6cf 337
    pub string_concat_func: u32,
0e39618 338
    /// Shared runtime helper `(n: i64) -> ref Str`: allocates a new `array<i8>`
0e39618 339
    /// holding `n`'s decimal representation, for interpolating an `Int`.
35af6cf 340
    pub int_to_string_func: u32,
0e39618 341
    /// wasm-gc type-section indices for this program's classes/enums/Str/closures —
0e39618 342
    /// every value's real representation (see `docs/superpowers/plans/2026-07-25-wasm-gc-migration.md`).
5ef4579 343
    pub gc_types: GcTypeRegistry,
0e39618 344
    /// Payload-free variant name (True/False/None/...) -> the global index holding
0e39618 345
    /// its one pre-allocated instance (see this migration plan's Decision 2).
0e39618 346
    pub singleton_globals: HashMap<String, u32>,
bb8ca38 347
}
bb8ca38 348
0e39618 349
/// Per-module state that accumulates as function bodies are compiled: every static
0e39618 350
/// string literal's bytes, staged here (rather than added straight to `WasmModule`)
0e39618 351
/// so `compileStaticString` can know a segment's final passive-data-section index —
0e39618 352
/// its position among ALL staged segments — before that section is actually
0e39618 353
/// assembled at the end of `compileSource`.
5d8ada1 354
struct ModuleState {
0e39618 355
    passive_segments: Vec<Vec<u8>>,
5d8ada1 356
}
5d8ada1 357
5d8ada1 358
struct LocalCtx<'a> {
5d8ada1 359
    locals: HashMap<String, u32>,
5d8ada1 360
    /// First local index reserved for `match` subject scratch temporaries.
5d8ada1 361
    match_scratch_base: u32,
5d8ada1 362
    /// `Match` stmt identity (pointer address) -> scratch slot offset.
5d8ada1 363
    match_scratch_index: HashMap<usize, u32>,
35af6cf 364
    /// First local index reserved for nested-constructor-pattern scratch temporaries
35af6cf 365
    /// (`Some(Some(v))`'s inner `Some(v)`); the outermost pattern uses a
35af6cf 366
    /// `match_scratch` slot instead, so this only covers depth >= 1.
35af6cf 367
    nested_class_scratch_base: u32,
35af6cf 368
    /// `CasePattern::Class` identity (pointer address) -> scratch slot offset.
35af6cf 369
    nested_class_scratch: HashMap<usize, u32>,
da1c377 370
    /// First local index reserved for variadic-`for` scratch temporaries (2 `i32`
da1c377 371
    /// slots per `for` statement that iterates a `TVariadic`: count, loop index).
da1c377 372
    variadic_for_scratch_base: u32,
da1c377 373
    /// `For` stmt identity (pointer address) -> slot number (multiply by 2 and add
da1c377 374
    /// `variadic_for_scratch_base` for the count local; +1 more for the index local).
da1c377 375
    variadic_for_scratch: HashMap<usize, u32>,
5d8ada1 376
    func_ids: &'a HashMap<String, u32>,
5d8ada1 377
    func_sigs: &'a HashMap<String, FuncSig>,
4ba0db3 378
    closures: &'a HashMap<usize, ClosureInfo>,
4ba0db3 379
    closure_call_types: &'a HashMap<ClosureSigKey, u32>,
35af6cf 380
    named_fn_values: &'a HashMap<String, u32>,
35af6cf 381
    string_concat_func: u32,
35af6cf 382
    int_to_string_func: u32,
5d8ada1 383
    classes: &'a ClassEnv,
5d8ada1 384
    methods: &'a MethodEnv,
5d8ada1 385
    enum_variants: &'a EnumVariants,
4fda634 386
    enum_params: &'a EnumParams,
0e39618 387
    gc_types: &'a GcTypeRegistry,
0e39618 388
    singleton_globals: &'a HashMap<String, u32>,
5d8ada1 389
    /// Tracks each binding's inferred type as compilation proceeds through
5d8ada1 390
    /// statements in order, mirroring `plum-checker`'s own env evolution — needed
5d8ada1 391
    /// to resolve `Attribute`/`ClassCall` targets and pick the right load/store width.
5d8ada1 392
    type_env: RefCell<TypeEnv>,
35af6cf 393
    /// Local name -> the exact wasm `call_indirect` signature of the closure literal
35af6cf 394
    /// assigned to it (populated when compiling that `Stmt::Assign`, straight from
35af6cf 395
    /// the already-correct `ClosureInfo` the discovery pass computed). Exists so
3d6f280 396
    /// `compileClosureCall` doesn't have to re-derive the signature via
3d6f280 397
    /// `plum_checker::inferExpr` on the closure a second time — which, unlike the
3d6f280 398
    /// discovery pass, doesn't have `resolveClosureParamTypesFromUsage`'s fix
35af6cf 399
    /// and would fall back to its old TVar-defaults-to-Int behavior, disagreeing
35af6cf 400
    /// with the (now correct) signature the closure's body was actually compiled with.
35af6cf 401
    closure_local_sigs: RefCell<HashMap<String, ClosureSigKey>>,
5d8ada1 402
}
5d8ada1 403
3d6f280 404
fn fnKey(f: &ast::Fn) -> String {
5d8ada1 405
    match &f.type_param {
5d8ada1 406
        Some(recv) => format!("{}::{}", recv, f.name),
5d8ada1 407
        None => f.name.clone(),
bb8ca38 408
    }
bb8ca38 409
}
bb8ca38 410
0000000 411
/// `f`'s full wasm param signature (implicit leading receiver param included, for
0000000 412
/// a method) — shared by real function registration and `extern fun` import
0000000 413
/// registration so both compute a param list the exact same way.
0000000 414
fn fnWasmParamTypes(f: &ast::Fn, gc_types: &GcTypeRegistry) -> Vec<ValType> {
0000000 415
    let mut param_types: Vec<ValType> = Vec::new();
0000000 416
    if let Some(recv) = &f.type_param {
0000000 417
        param_types.push(astTypeToWasm(recv).unwrap_or(ValType::I32));
0000000 418
    }
0000000 419
    for p in &f.params {
0000000 420
        let vt = match &p.ty {
0000000 421
            ast::ParamType::Variadic(t) => {
0000000 422
                let elem_vt = astTypeToWasm(&t.name).unwrap_or(ValType::I64);
0000000 423
                let arr_idx = *gc_types.variadic_array_type_idx.get(&elem_vt)
0000000 424
                    .expect("internal codegen error: variadic array type must be pre-registered for every elem type in the program");
0000000 425
                gcRef(arr_idx)
0000000 426
            }
0000000 427
            other => astTypeToWasm(paramTypeName(other)).unwrap_or(ValType::I32),
0000000 428
        };
0000000 429
        param_types.push(vt);
0000000 430
    }
0000000 431
    param_types
0000000 432
}
0000000 433
0000000 434
3d6f280 435
fn paramTypeName(pt: &ast::ParamType) -> &str {
5d8ada1 436
    match pt {
5d8ada1 437
        ast::ParamType::Type(t) => t.name.as_str(),
5d8ada1 438
        ast::ParamType::Variadic(t) => t.name.as_str(),
d7e5ff4 439
        // TODO: fn-value params aren't modeled as a wasm value type yet; treat as
d7e5ff4 440
        // an unmodeled type (pointer), same as a class instance.
d7e5ff4 441
        ast::ParamType::Fn(_, _) => "Fn",
bb8ca38 442
    }
bb8ca38 443
}
bb8ca38 444
0e39618 445
thread_local! {
0e39618 446
    /// The current `compileSource` call's wasm-gc type registry. `compileSource` is
0e39618 447
    /// the sole entry point and is never re-entrant/concurrent within one thread, so
0e39618 448
    /// a thread-local avoids threading an explicit `&GcTypeRegistry` parameter through
0e39618 449
    /// every one of `plumTypeToValtype`/`astTypeToWasm`'s ~25 call sites (many several
0e39618 450
    /// functions removed from anywhere a `CompileCtx`/`LocalCtx` is in scope, e.g.
0e39618 451
    /// function-signature registration that runs before any `LocalCtx` exists). Set
0e39618 452
    /// once near the top of `compileSource`, before anything below reads it.
0e39618 453
    static CURRENT_GC_TYPES: RefCell<Option<GcTypeRegistry>> = const { RefCell::new(None) };
0e39618 454
}
0e39618 455
0000000 456
thread_local! {
0000000 457
    /// Top-level `NAME = literal` const values, keyed by name — set once near the
0000000 458
    /// top of `compileSource`, alongside `CURRENT_GC_TYPES` (same non-reentrancy
0000000 459
    /// rationale). A bare `NAME` reference always lexes as a type_identifier (any
0000000 460
    /// uppercase-leading name does, there's no separate "constant" token — see
0000000 461
    /// `plum-checker`'s `inferExpr`'s matching `TypeName` comment), so `Expr::TypeName`
0000000 462
    /// consults this map before falling back to enum-variant/unmodeled-type handling.
0000000 463
    static CURRENT_CONSTS: RefCell<HashMap<String, ast::Expr>> = RefCell::new(HashMap::new());
0000000 464
}
0000000 465
0e39618 466
fn withGcTypes<R>(f: impl FnOnce(&GcTypeRegistry) -> R) -> R {
0e39618 467
    CURRENT_GC_TYPES.with(|c| {
0e39618 468
        let borrow = c.borrow();
0e39618 469
        let registry = borrow.as_ref().expect("internal codegen error: GC type registry read before compileSource initialized it");
0e39618 470
        f(registry)
0e39618 471
    })
0e39618 472
}
0e39618 473
0e39618 474
/// Resolves an `ast::Type`/`ast::ParamType`'s bare name (e.g. from a function
0e39618 475
/// signature, before any `PlumType`/checker involvement) to its wasm-gc `ValType`.
3d6f280 476
fn astTypeToWasm(name: &str) -> Option<ValType> {
bb8ca38 477
    match name {
bb8ca38 478
        "Int" => Some(ValType::I64),
bb8ca38 479
        "Float" => Some(ValType::F64),
bb8ca38 480
        "Unit" => None,
0e39618 481
        "Bool" => Some(plumTypeToValtype(&PlumType::TBool)),
0e39618 482
        "Str" => Some(plumTypeToValtype(&PlumType::TStr)),
0000000 483
        "Byte" => Some(plumTypeToValtype(&PlumType::TByte)),
0000000 484
        "[]Byte" => Some(plumTypeToValtype(&PlumType::TByteSlice)),
0000000 485
        // "ByteSlice" (as opposed to "[]Byte") is never written in source as a type
0000000 486
        // annotation — it's `methodReceiverName`'s name for `TByteSlice`, which is
0000000 487
        // how a `ByteSlice` method's `self` param and any `ByteSlice.foo(...)`
0000000 488
        // static-call receiver slot get resolved here (see `fnWasmParamTypes` and
0000000 489
        // the `is_static_call` site). Without this arm it would fall through to the
0000000 490
        // generic `TNamed` branch below and get treated as an ordinary (and, for
0000000 491
        // this name, orphaned/unused) class struct type instead of the shared
0000000 492
        // `array<i8>` ref every `[]Byte` value actually is.
0000000 493
        "ByteSlice" => Some(plumTypeToValtype(&PlumType::TByteSlice)),
0e39618 494
        other => Some(plumTypeToValtype(&PlumType::TNamed(other.to_string()))),
5d8ada1 495
    }
5d8ada1 496
}
5d8ada1 497
0e39618 498
/// A `ref null $Ty` — every heap value (class instance, enum/Bool variant, Str
0e39618 499
/// array, closure struct) is nullable-by-convention, matching how a bump-allocator
0e39618 500
/// i32 pointer could be "null" (0) too; nothing in this codegen currently relies on
0e39618 501
/// non-nullable refs for an optimization, so nullable everywhere keeps this simple.
0e39618 502
fn gcRef(idx: u32) -> ValType {
0e39618 503
    ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(idx) })
0e39618 504
}
0e39618 505
0000000 506
/// Pushes an arbitrary value of the right wasm type for a "self" slot nothing
0000000 507
/// will ever actually read — see the `is_static_call` case in `Expr::Attribute`'s
0000000 508
/// `AttrKind::Method` codegen. A GC ref's null is exactly as good as a real
0000000 509
/// instance when the callee's body has no `self` binding to dereference it with.
0000000 510
fn pushSelfPlaceholder(vt: ValType, body: &mut Vec<u8>) {
0000000 511
    match vt {
0000000 512
        ValType::I64 => Instruction::I64Const(0).encode(body),
0000000 513
        ValType::I32 => Instruction::I32Const(0).encode(body),
0000000 514
        ValType::F64 => Instruction::F64Const(0.0).encode(body),
0000000 515
        ValType::F32 => Instruction::F32Const(0.0).encode(body),
0000000 516
        ValType::Ref(rt) => Instruction::RefNull(rt.heap_type).encode(body),
0000000 517
        ValType::V128 => Instruction::V128Const(0).encode(body),
0000000 518
    }
0000000 519
}
0000000 520
3d6f280 521
fn plumTypeToValtype(t: &PlumType) -> ValType {
5d8ada1 522
    match t {
5d8ada1 523
        PlumType::TInt => ValType::I64,
5d8ada1 524
        PlumType::TFloat => ValType::F64,
0e39618 525
        PlumType::TBool => withGcTypes(|r| gcRef(*r.enum_super_type_idx.get("Bool").expect("Bool must be registered"))),
0e39618 526
        PlumType::TStr => withGcTypes(|r| gcRef(r.str_type_idx)),
0000000 527
        PlumType::TByte => ValType::I32,
0000000 528
        // `[]Byte` is represented by the EXACT SAME wasm-gc array type as `Str`
0000000 529
        // (a mutable `array<i8>`) — they're structurally identical, and nothing
0000000 530
        // in this codegen needs to distinguish them at the wasm-type level
0000000 531
        // (no runtime `ref.test`/dynamic dispatch keys off it), so reusing
0000000 532
        // `str_type_idx` avoids a second, redundant GC type-section entry.
0000000 533
        PlumType::TByteSlice => withGcTypes(|r| gcRef(r.str_type_idx)),
0e39618 534
        PlumType::TNamed(name) => withGcTypes(|r| {
0e39618 535
            match r.class_type_idx.get(name).or_else(|| r.enum_super_type_idx.get(name)) {
0e39618 536
                Some(idx) => gcRef(*idx),
0e39618 537
                // Genuinely unmodeled type name (not a real class/enum) — permissive
0e39618 538
                // fallback, matching this function's pre-wasm-gc "unmodeled type:
0e39618 539
                // pointer" behavior; codegen sites that actually need a concrete
0e39618 540
                // struct type still resolve it themselves and error clearly if absent.
0e39618 541
                None => ValType::Ref(RefType::ANYREF),
0e39618 542
            }
0e39618 543
        }),
0e39618 544
        // A closure value is a 2-field `{table_index: i32, env: anyref}` struct —
0e39618 545
        // its OWN concrete GC type, registered per Task 2e (closures), not through
0e39618 546
        // this general resolver; `anyref` here is a safe placeholder used only
0e39618 547
        // where a closure's exact struct type isn't being constructed/destructured
0e39618 548
        // directly (e.g. deciding a local's storage class), matching `TVariadic`'s
0e39618 549
        // existing "not modeled as a concrete shape here" treatment.
0e39618 550
        PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
4ba0db3 551
        PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
4ba0db3 552
    }
4ba0db3 553
}
4ba0db3 554
5ef4579 555
/// Type-section indices for every wasm-gc composite type this program's monomorphized
5ef4579 556
/// classes/enums/`Str` need. Populated once in `compileSource` from the checker's
0e39618 557
/// global tables.
0e39618 558
#[derive(Clone)]
5ef4579 559
pub struct GcTypeRegistry {
5ef4579 560
    /// Concrete class/struct name -> its one wasm-gc `struct` type index.
5ef4579 561
    pub class_type_idx: HashMap<String, u32>,
5ef4579 562
    /// Enum name -> its abstract supertype `struct` type index (every variant
5ef4579 563
    /// subtypes this) — includes the built-in `Bool` enum, which has no
5ef4579 564
    /// `ast::Item::Enum` of its own (`buildGlobalTables` hardcodes its
5ef4579 565
    /// `True`/`False` variants directly into `EnumVariants`).
5ef4579 566
    pub enum_super_type_idx: HashMap<String, u32>,
5ef4579 567
    /// Variant name (flat namespace, matching `EnumVariants`) -> its concrete
5ef4579 568
    /// subtype `struct` type index.
5ef4579 569
    pub variant_type_idx: HashMap<String, u32>,
5ef4579 570
    /// The single shared `array<i8>` type index every `Str` value uses.
5ef4579 571
    pub str_type_idx: u32,
0e39618 572
    /// The single shared closure-value struct type index — `{table_idx: i32, env:
0e39618 573
    /// anyref}` — every closure (and zero-capture "trampoline") uses regardless of
0e39618 574
    /// its own captures; only assigned once closures are discovered (see
0e39618 575
    /// `compileSource`), so `0` until then (nothing reads it earlier).
0e39618 576
    pub closure_type_idx: u32,
0e39618 577
    /// Wasm value type -> the shared `array<T>` type index used to pass a `...T`
0e39618 578
    /// variadic argument pack as one GC array (replacing the old bump-allocated
0e39618 579
    /// `[count][elem...]` blob — `array.len` replaces the explicit count). Populated
0e39618 580
    /// alongside `closure_type_idx`, from every `ParamType::Variadic` in the program.
0e39618 581
    pub variadic_array_type_idx: HashMap<ValType, u32>,
5ef4579 582
}
5ef4579 583
5ef4579 584
/// Resolves a plum type to the wasm-gc `ValType` its values are represented as, given
5ef4579 585
/// an ALREADY fully-populated `GcTypeRegistry` — every class/enum/Str index must exist
5ef4579 586
/// before this is called, since e.g. a class field of another class's type needs that
5ef4579 587
/// other class's index to already be assigned (see `buildGcTypeRegistry`'s two-pass
5ef4579 588
/// structure: this function is only ever called during its second pass).
5ef4579 589
fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
5ef4579 590
    match t {
5ef4579 591
        PlumType::TInt => ValType::I64,
5ef4579 592
        PlumType::TFloat => ValType::F64,
5ef4579 593
        PlumType::TBool => {
5ef4579 594
            let idx = *registry.enum_super_type_idx.get("Bool").expect("internal codegen error: Bool must be registered in the GC type registry");
5ef4579 595
            ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(idx) })
5ef4579 596
        }
5ef4579 597
        PlumType::TStr => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(registry.str_type_idx) }),
0000000 598
        PlumType::TByte => ValType::I32,
0000000 599
        // See the matching comment in `plumTypeToValtype` — `[]Byte` reuses `Str`'s
0000000 600
        // `array<i8>` GC type index rather than getting its own.
0000000 601
        PlumType::TByteSlice => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(registry.str_type_idx) }),
02b3582 602
        PlumType::TNamed(name) => match registry.class_type_idx.get(name).or_else(|| registry.enum_super_type_idx.get(name)) {
02b3582 603
            Some(idx) => ValType::Ref(RefType { nullable: true, heap_type: HeapType::Concrete(*idx) }),
02b3582 604
            // A field typed as a generic enum/class (e.g. `Node.next: Option[Node]`)
02b3582 605
            // resolves here to the BARE generic name — `ClassEnv`'s `plumTypeFromAst`
02b3582 606
            // has no representation for type arguments, so it can't know this means
02b3582 607
            // `Option$Int` once monomorphization specializes (and removes the
02b3582 608
            // unspecialized) `Option` — same permissive `anyref` fallback as
02b3582 609
            // `plumTypeToValtype` above, for the same "genuinely unmodeled" reason:
02b3582 610
            // `struct.get`/`ref.test`/`ref.cast` all work against `anyref` operands
02b3582 611
            // fine, so a field merely being STORED as `anyref` instead of the exact
02b3582 612
            // concrete type costs nothing but static precision.
02b3582 613
            None => ValType::Ref(RefType::ANYREF),
02b3582 614
        },
5ef4579 615
        // TFun (closures) and TVariadic get their own concrete representation once
5ef4579 616
        // Task 2 (closures/variadic calls) lands — `anyref` is a safe, valid-but-not-
5ef4579 617
        // yet-meaningful placeholder in the meantime, since nothing consumes it yet.
5ef4579 618
        PlumType::TFun(_, _) | PlumType::TVariadic(_) => ValType::Ref(RefType::ANYREF),
5ef4579 619
        PlumType::TVar(_) | PlumType::TUnit => ValType::I64,
5ef4579 620
    }
5ef4579 621
}
5ef4579 622
5ef4579 623
/// Builds the wasm-gc type registry for every concrete class/enum in `source`, plus
5ef4579 624
/// the built-in `Bool` enum and the shared `Str` array type, and declares them all as
5ef4579 625
/// ONE `rec` group via `module.addGcTypes` — a single group sidesteps every ordering
5ef4579 626
/// question about mutual/self-references (a class field of another class's type, an
5ef4579 627
/// enum variant field referencing its own enum, `Node.next: Option[Node]`, etc.),
5ef4579 628
/// since within one `rec` group members may reference each other regardless of
5ef4579 629
/// declaration order.
5ef4579 630
fn buildGcTypeRegistry(
5ef4579 631
    module: &mut WasmModule,
5ef4579 632
    source: &ast::Source,
5ef4579 633
    classes: &ClassEnv,
5ef4579 634
    enum_variants: &EnumVariants,
0e39618 635
    enum_params: &EnumParams,
5ef4579 636
) -> GcTypeRegistry {
5ef4579 637
    enum Slot {
5ef4579 638
        Str,
5ef4579 639
        Class(String),
0e39618 640
        EnumSuper(String),
5ef4579 641
        Variant(String),
5ef4579 642
    }
5ef4579 643
5ef4579 644
    // Pass 1: assign every entry a slot (and therefore a type index) up front, before
5ef4579 645
    // any field list is built, so field-type resolution can reference ANY other entry.
5ef4579 646
    let mut slots: Vec<Slot> = vec![Slot::Str];
5ef4579 647
    let mut class_type_idx: HashMap<String, u32> = HashMap::new();
5ef4579 648
    let mut enum_super_type_idx: HashMap<String, u32> = HashMap::new();
5ef4579 649
    let mut variant_type_idx: HashMap<String, u32> = HashMap::new();
5ef4579 650
5ef4579 651
    for item in &source.items {
5ef4579 652
        if let ast::Item::Class(c) = item {
5ef4579 653
            class_type_idx.insert(c.name.clone(), slots.len() as u32);
5ef4579 654
            slots.push(Slot::Class(c.name.clone()));
5ef4579 655
        }
5ef4579 656
    }
5ef4579 657
5ef4579 658
    // Bool is built into `EnumVariants` (True/False) by `buildGlobalTables` with no
5ef4579 659
    // `ast::Item::Enum` of its own (see `docs/superpowers/plans/2026-07-25-wasm-gc-migration.md`'s
5ef4579 660
    // Decision 1: Bool is a full wasm-gc struct, no special-casing) — register it
5ef4579 661
    // exactly like a real enum here, ahead of whatever the source actually declares.
5ef4579 662
    let mut enum_decls: Vec<(String, Vec<String>)> =
5ef4579 663
        vec![("Bool".to_string(), vec!["False".to_string(), "True".to_string()])];
5ef4579 664
    for item in &source.items {
0000000 665
        // A source file may re-"declare" `enum Bool = | True | False` purely to
0000000 666
        // give it a nesting site for methods (no other way exists to attach a
0000000 667
        // method to a builtin type) — see the identical skip, with the full
0000000 668
        // rationale, in `plum-checker`'s `buildGlobalTables`. Registering it
0000000 669
        // again here would give Bool a SECOND, orphaned GC struct (the first,
0000000 670
        // hardcoded one is still referenced by every OTHER already-registered
0000000 671
        // slot/type by index) and — worse — since slot assignment for a
0000000 672
        // specialized generic enum with a `Bool` field (e.g. `Result[Bool,
0000000 673
        // Str]`) resolves "Bool" by NAME at the point it's compiled, later
0000000 674
        // duplicate registrations can leave that field pointing at whichever
0000000 675
        // Bool slot was assigned last, an index that isn't guaranteed to
0000000 676
        // satisfy wasm-gc's "supertypes before subtypes" ordering rule.
5ef4579 677
        if let ast::Item::Enum(e) = item {
0000000 678
            if e.name != "Bool" {
0000000 679
                enum_decls.push((e.name.clone(), e.variants.iter().map(|v| v.name.clone()).collect()));
0000000 680
            }
5ef4579 681
        }
5ef4579 682
    }
5ef4579 683
    for (enum_name, variant_names) in &enum_decls {
5ef4579 684
        enum_super_type_idx.insert(enum_name.clone(), slots.len() as u32);
0e39618 685
        slots.push(Slot::EnumSuper(enum_name.clone()));
5ef4579 686
        for vname in variant_names {
5ef4579 687
            variant_type_idx.insert(vname.clone(), slots.len() as u32);
5ef4579 688
            slots.push(Slot::Variant(vname.clone()));
5ef4579 689
        }
5ef4579 690
    }
5ef4579 691
5ef4579 692
    // The registry is fully index-complete after pass 1 (every name has an assigned
5ef4579 693
    // slot) even though no field lists exist yet — safe to hand to `plumTypeToGcValtype`
5ef4579 694
    // for pass 2's field-type resolution.
5ef4579 695
    let registry = GcTypeRegistry {
5ef4579 696
        class_type_idx,
5ef4579 697
        enum_super_type_idx,
5ef4579 698
        variant_type_idx,
5ef4579 699
        str_type_idx: 0,
0e39618 700
        closure_type_idx: 0,
0e39618 701
        variadic_array_type_idx: HashMap::new(),
5ef4579 702
    };
5ef4579 703
5ef4579 704
    // Pass 2: build the real SubType for every slot, now that every cross-reference
5ef4579 705
    // resolves.
5ef4579 706
    let subtypes: Vec<SubType> = slots.iter().map(|slot| match slot {
5ef4579 707
        Slot::Str => SubType {
5ef4579 708
            is_final: true,
5ef4579 709
            supertype_idx: None,
5ef4579 710
            composite_type: CompositeType {
5ef4579 711
                inner: CompositeInnerType::Array(ArrayType(FieldType { element_type: StorageType::I8, mutable: true })),
5ef4579 712
                shared: false,
5ef4579 713
            },
5ef4579 714
        },
5ef4579 715
        Slot::Class(name) => {
5ef4579 716
            let fields = classes.get(name).cloned().unwrap_or_default();
5ef4579 717
            let field_types: Vec<FieldType> = fields.iter().map(|(_, ty)| FieldType {
5ef4579 718
                element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
5ef4579 719
                mutable: true,
5ef4579 720
            }).collect();
5ef4579 721
            SubType {
5ef4579 722
                is_final: true,
5ef4579 723
                supertype_idx: None,
5ef4579 724
                composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: field_types.into() }), shared: false },
5ef4579 725
            }
5ef4579 726
        }
0e39618 727
        // An ORDINARY enum's supertype declares zero fields (each variant adds its
0e39618 728
        // own distinct payload fields below it). A DISCRIMINANT enum (`enum
0e39618 729
        // Foo(n: Int) = ...`) is different: every variant shares the EXACT SAME
0e39618 730
        // field list (`plum-checker::buildGlobalTables` already gives every variant
0e39618 731
        // of such an enum identical `field_types`, equal to the shared params), so
0e39618 732
        // the supertype declares those fields directly — this is what lets `self.n`
0e39618 733
        // field access work on the plain supertype-typed reference with a
0e39618 734
        // `struct.get`, no `ref.cast` to one arbitrary variant required (which would
0e39618 735
        // trap at runtime whenever `self` isn't actually THAT variant).
0e39618 736
        Slot::EnumSuper(enum_name) => {
0e39618 737
            let params = enum_params.get(enum_name).cloned().unwrap_or_default();
0e39618 738
            let field_types: Vec<FieldType> = params.iter().map(|(_, ty)| FieldType {
0e39618 739
                element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
0e39618 740
                mutable: false,
0e39618 741
            }).collect();
0e39618 742
            SubType {
0e39618 743
                is_final: false,
0e39618 744
                supertype_idx: None,
0e39618 745
                composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: field_types.into() }), shared: false },
0e39618 746
            }
0e39618 747
        }
5ef4579 748
        Slot::Variant(vname) => {
5ef4579 749
            let info = enum_variants.get(vname)
5ef4579 750
                .unwrap_or_else(|| panic!("internal codegen error: variant '{}' missing from EnumVariants", vname));
5ef4579 751
            let super_idx = *registry.enum_super_type_idx.get(&info.enum_name)
5ef4579 752
                .unwrap_or_else(|| panic!("internal codegen error: enum '{}' missing its supertype slot", info.enum_name));
0e39618 753
            // For a discriminant enum, `info.field_types` is ALREADY identical to the
0e39618 754
            // supertype's own fields (see the `EnumSuper` arm above) — a variant with
0e39618 755
            // zero ADDED fields beyond its supertype is still valid wasm-gc
0e39618 756
            // subtyping, so no special-casing is needed here.
5ef4579 757
            let field_types: Vec<FieldType> = info.field_types.iter().map(|ty| FieldType {
5ef4579 758
                element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
5ef4579 759
                mutable: false,
5ef4579 760
            }).collect();
5ef4579 761
            SubType {
5ef4579 762
                is_final: true,
5ef4579 763
                supertype_idx: Some(super_idx),
5ef4579 764
                composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: field_types.into() }), shared: false },
5ef4579 765
            }
5ef4579 766
        }
5ef4579 767
    }).collect();
5ef4579 768
5ef4579 769
    module.addGcTypes(subtypes);
5ef4579 770
5ef4579 771
    registry
5ef4579 772
}
5ef4579 773
4ba0db3 774
/// Maps an `ast::ParamType::Fn(params, ret)` to the wasm signature of the *closure
0e39618 775
/// function* it compiles to: an implicit leading `env: anyref`, then one param per
4ba0db3 776
/// declared param type, returning `ret`'s val type (or nothing for `Unit`).
3d6f280 777
fn fnParamTypeToWasmSig(params: &[ast::Type], ret: &Option<Box<ast::Type>>) -> (Vec<ValType>, Option<ValType>) {
0e39618 778
    let mut vts = vec![ValType::Ref(RefType::ANYREF)]; // env pointer
4ba0db3 779
    for p in params {
3d6f280 780
        vts.push(astTypeToWasm(&p.name).unwrap_or(ValType::I32));
bb8ca38 781
    }
3d6f280 782
    let ret_vt = ret.as_ref().and_then(|t| astTypeToWasm(&t.name));
4ba0db3 783
    (vts, ret_vt)
bb8ca38 784
}
bb8ca38 785
3d6f280 786
fn blockTypeFor(result_vt: Option<ValType>) -> BlockType {
3254688 787
    result_vt.map(BlockType::Result).unwrap_or(BlockType::Empty)
3254688 788
}
3254688 789
3d6f280 790
fn retTypeToWasm(ret: Option<&ast::Type>) -> Option<ValType> {
3d6f280 791
    ret.and_then(|r| astTypeToWasm(&r.name))
bb8ca38 792
}
bb8ca38 793
3d6f280 794
fn encodeLeb128U32(mut val: u32) -> Vec<u8> {
bb8ca38 795
    let mut bytes = Vec::new();
bb8ca38 796
    loop {
bb8ca38 797
        let mut byte = (val & 0x7f) as u8;
bb8ca38 798
        val >>= 7;
bb8ca38 799
        if val != 0 {
bb8ca38 800
            byte |= 0x80;
bb8ca38 801
        }
bb8ca38 802
        bytes.push(byte);
bb8ca38 803
        if val == 0 {
bb8ca38 804
            break;
bb8ca38 805
        }
bb8ca38 806
    }
bb8ca38 807
    bytes
bb8ca38 808
}
bb8ca38 809
3d6f280 810
fn checkCtxOf<'a>(ctx_classes: &'a ClassEnv, ctx_methods: &'a MethodEnv, ctx_enum_variants: &'a EnumVariants, ctx_enum_params: &'a EnumParams) -> plum_checker::CheckCtx<'a> {
4fda634 811
    plum_checker::CheckCtx { classes: ctx_classes, methods: ctx_methods, enum_variants: ctx_enum_variants, enum_params: ctx_enum_params }
5d8ada1 812
}
5d8ada1 813
5d8ada1 814
/// Infers an expression's type using the function's current (mutable, evolving) type
5d8ada1 815
/// environment. Defaults to `TInt` if inference fails — codegen assumes the source was
3d6f280 816
/// already accepted by `plum_checker::checkSource`, so a failure here would indicate
5d8ada1 817
/// codegen is being driven directly on unchecked input (as some tests do).
3d6f280 818
fn inferLocalType(expr: &ast::Expr, ctx: &LocalCtx) -> PlumType {
5d8ada1 819
    let env = ctx.type_env.borrow();
3d6f280 820
    let cctx = checkCtxOf(ctx.classes, ctx.methods, ctx.enum_variants, ctx.enum_params);
3d6f280 821
    plum_checker::inferExpr(expr, &env, &cctx).unwrap_or(PlumType::TInt)
5d8ada1 822
}
5d8ada1 823
3d6f280 824
pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
3d6f280 825
    let source = &plum_checker::monomorphize::monomorphizeSource(source)?;
3d6f280 826
    let (global_env, classes, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
5d8ada1 827
5d8ada1 828
    let mut module = WasmModule::new();
0e39618 829
0e39618 830
    let mut gc_types = buildGcTypeRegistry(&mut module, source, &classes, &enum_variants, &enum_params);
0e39618 831
    CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
0e39618 832
0000000 833
    CURRENT_CONSTS.with(|c| {
0000000 834
        let mut consts = c.borrow_mut();
0000000 835
        for item in &source.items {
0000000 836
            if let ast::Item::Const(cst) = item {
0000000 837
                consts.insert(cst.name.clone(), cst.value.clone());
0000000 838
            }
0000000 839
        }
0000000 840
    });
0000000 841
0e39618 842
    // Register one shared `array<T>` GC type per distinct wasm value type used by a
0e39618 843
    // `...T` variadic parameter anywhere in the program (almost always just one, e.g.
0e39618 844
    // `Int...`) — replaces the old bump-allocated `[count][elem...]` blob (`array.len`
0e39618 845
    // replaces the explicit count word). Must happen before function signatures are
0e39618 846
    // registered below, since a variadic param's wasm type is this array's `ref`.
0e39618 847
    let mut variadic_elem_vts: Vec<ValType> = Vec::new();
0e39618 848
    for item in &source.items {
0e39618 849
        if let ast::Item::Fn(f) = item {
0e39618 850
            for p in &f.params {
0e39618 851
                if let ast::ParamType::Variadic(t) = &p.ty {
0e39618 852
                    let vt = astTypeToWasm(&t.name).unwrap_or(ValType::I64);
0e39618 853
                    if !variadic_elem_vts.contains(&vt) {
0e39618 854
                        variadic_elem_vts.push(vt);
0e39618 855
                    }
0e39618 856
                }
0e39618 857
            }
0e39618 858
        }
0e39618 859
    }
0e39618 860
    let variadic_array_subtypes: Vec<SubType> = variadic_elem_vts.iter().map(|vt| SubType {
0e39618 861
        is_final: true,
0e39618 862
        supertype_idx: None,
0e39618 863
        composite_type: CompositeType {
0e39618 864
            inner: CompositeInnerType::Array(ArrayType(FieldType { element_type: StorageType::Val(*vt), mutable: false })),
0e39618 865
            shared: false,
0e39618 866
        },
0e39618 867
    }).collect();
0e39618 868
    if !variadic_array_subtypes.is_empty() {
0e39618 869
        let indices = module.addGcTypes(variadic_array_subtypes);
0e39618 870
        for (vt, idx) in variadic_elem_vts.iter().zip(indices) {
0e39618 871
            gc_types.variadic_array_type_idx.insert(*vt, idx);
0e39618 872
        }
0e39618 873
        CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
0e39618 874
    }
0e39618 875
0000000 876
    // Register every `extern fun` (e.g. `libs/std/os.plum`'s `printLn`) as a
0000000 877
    // genuine wasm import BEFORE any function (including the `start` function
0000000 878
    // set up right below) — imports must occupy the low end of the function
0000000 879
    // index space for every later `addFunction`'s index arithmetic to stay
0000000 880
    // correct. `plum-checker` has already confirmed every extern fn has no
0000000 881
    // receiver and no body, so `f.name` alone (no `fnKey` receiver-mangling) is
0000000 882
    // always its unique key.
0000000 883
    let mut func_ids: HashMap<String, u32> = HashMap::new();
0000000 884
    let mut func_sigs: HashMap<String, FuncSig> = HashMap::new();
0000000 885
    for item in &source.items {
0000000 886
        if let ast::Item::Fn(f) = item {
0000000 887
            if !f.is_extern {
0000000 888
                continue;
0000000 889
            }
0000000 890
            let param_types = fnWasmParamTypes(f, &gc_types);
0000000 891
            let ret = retTypeToWasm(f.returns.as_ref());
0000000 892
            let results_vec: Vec<ValType> = ret.into_iter().collect();
0000000 893
            let type_idx = module.addType(&param_types, &results_vec);
0000000 894
            let func_idx = module.addImport("plum", &f.name, type_idx);
0000000 895
            func_ids.insert(f.name.clone(), func_idx);
0000000 896
            func_sigs.insert(f.name.clone(), FuncSig { params: param_types, ret });
0000000 897
        }
0000000 898
    }
0000000 899
0e39618 900
    // Pre-allocate one instance of every payload-free variant (True/False/None/...)
0e39618 901
    // as a global, populated once by a `start` function rather than reconstructed on
0e39618 902
    // every reference — see this migration plan's Decision 2. `struct.new` isn't
0e39618 903
    // allowed inside a global's own const-expr initializer (confirmed empirically in
0e39618 904
    // Task 1), so each global starts `ref.null` and a `start` function fills it in
0e39618 905
    // before any export is callable. The zero-capture "trampoline" closures
0e39618 906
    // registered below (once closures are discovered) append to this SAME start
0e39618 907
    // function, so its body isn't finalized/patched into the module until then.
0e39618 908
    let mut singleton_globals: HashMap<String, u32> = HashMap::new();
0e39618 909
    let mut start_body = Vec::new();
0e39618 910
    for (name, info) in &enum_variants {
0e39618 911
        if !info.field_types.is_empty() {
0e39618 912
            continue;
0e39618 913
        }
0e39618 914
        let variant_idx = *gc_types.variant_type_idx.get(name)
0e39618 915
            .unwrap_or_else(|| panic!("internal codegen error: payload-free variant '{}' missing from GC type registry", name));
0e39618 916
        let mut init = Vec::new();
0e39618 917
        Instruction::RefNull(HeapType::Concrete(variant_idx)).encode(&mut init);
0e39618 918
        let global_idx = module.addGlobal(gcRef(variant_idx), true, &init);
0e39618 919
        singleton_globals.insert(name.clone(), global_idx);
0e39618 920
0e39618 921
        Instruction::StructNewDefault(variant_idx).encode(&mut start_body);
0e39618 922
        Instruction::GlobalSet(global_idx).encode(&mut start_body);
0e39618 923
    }
0e39618 924
    let start_type_idx = module.addType(&[], &[]);
0e39618 925
    let start_func_idx = module.addFunction(start_type_idx, &[]);
0e39618 926
    module.setStartFunction(start_func_idx);
5ef4579 927
4ba0db3 928
    // Closure wasm signature -> function-type index, deduped so every closure/call site
4ba0db3 929
    // of the same shape shares one `call_indirect` type.
4ba0db3 930
    let mut closure_call_types: HashMap<ClosureSigKey, u32> = HashMap::new();
5d8ada1 931
5d8ada1 932
    // Register every function AND method signature up front (methods get an implicit
5d8ada1 933
    // leading `self: pointer` param and are keyed as "Receiver::method").
bb8ca38 934
    for item in &source.items {
bb8ca38 935
        if let ast::Item::Fn(f) = item {
0000000 936
            if f.is_extern {
0000000 937
                continue;
5d8ada1 938
            }
0000000 939
            let param_types = fnWasmParamTypes(f, &gc_types);
3d6f280 940
            let ret = retTypeToWasm(f.returns.as_ref());
bb8ca38 941
            let results_vec: Vec<ValType> = ret.into_iter().collect();
3d6f280 942
            let type_idx = module.addType(&param_types, &results_vec);
3d6f280 943
            let func_idx = module.addFunction(type_idx, &[]);
3d6f280 944
            let key = fnKey(f);
5d8ada1 945
            func_ids.insert(key.clone(), func_idx);
5d8ada1 946
            func_sigs.insert(key, FuncSig { params: param_types, ret });
4ba0db3 947
4ba0db3 948
            // Any `fn(...) -> ...`-typed param is callable via `call_indirect`; register
4ba0db3 949
            // its wasm signature (leading env-ptr param included) so call sites can
4ba0db3 950
            // resolve a consistent type index even if no matching closure literal exists.
4ba0db3 951
            for p in &f.params {
4ba0db3 952
                if let ast::ParamType::Fn(params, ret) = &p.ty {
3d6f280 953
                    let (sig_params, ret_vt) = fnParamTypeToWasmSig(params, ret);
4ba0db3 954
                    let sig_key: ClosureSigKey = (sig_params.clone(), ret_vt);
4ba0db3 955
                    if !closure_call_types.contains_key(&sig_key) {
4ba0db3 956
                        let results: Vec<ValType> = ret_vt.into_iter().collect();
3d6f280 957
                        let tidx = module.addType(&sig_params, &results);
4ba0db3 958
                        closure_call_types.insert(sig_key, tidx);
4ba0db3 959
                    }
4ba0db3 960
                }
4ba0db3 961
            }
bb8ca38 962
        }
bb8ca38 963
    }
bb8ca38 964
5d8ada1 965
    let fns: Vec<&ast::Fn> = source.items.iter().filter_map(|item| match item {
0000000 966
        ast::Item::Fn(f) if !f.is_extern => Some(f),
5d8ada1 967
        _ => None,
5d8ada1 968
    }).collect();
5d8ada1 969
4ba0db3 970
    // ---- Discovery pre-pass: find every closure literal in every function body. ----
4ba0db3 971
    // (Runs on the monomorphized source, so any generic types in a closure's context
4ba0db3 972
    // are already concrete.) Registers each closure as its own wasm function + table
4ba0db3 973
    // element and records the free variables it must capture.
4ba0db3 974
    let fn_decls: HashMap<String, &ast::Fn> = fns.iter().map(|f| (f.name.clone(), *f)).collect();
4ba0db3 975
    let mut raw_closures: Vec<RawClosure> = Vec::new();
35af6cf 976
    let mut named_fn_refs: HashSet<String> = HashSet::new();
4ba0db3 977
    for f in &fns {
4ba0db3 978
        let mut env = global_env.clone();
4ba0db3 979
        if let Some(recv) = &f.type_param {
0000000 980
            env.insert("self".to_string(), TypeScheme::mono(plum_checker::plumTypeFromName(recv)));
4ba0db3 981
        }
35af6cf 982
        let mut locals: HashSet<String> = HashSet::new();
4ba0db3 983
        for p in &f.params {
3d6f280 984
            env.insert(p.name.clone(), TypeScheme::mono(paramPlumType(&p.ty)));
35af6cf 985
            locals.insert(p.name.clone());
4ba0db3 986
        }
4ba0db3 987
        let mut walker = ClosureWalker {
4ba0db3 988
            env,
3d6f280 989
            cctx: checkCtxOf(&classes, &methods, &enum_variants, &enum_params),
4ba0db3 990
            fn_decls: &fn_decls,
4ba0db3 991
            found: Vec::new(),
35af6cf 992
            locals,
35af6cf 993
            named_fn_refs: HashSet::new(),
4ba0db3 994
        };
4ba0db3 995
        match &f.body {
3d6f280 996
            ast::FnBody::Block(block) => walker.walkBlock(block),
3d6f280 997
            ast::FnBody::Expr(e) => walker.walkExpr(e, None),
0000000 998
            // `fns` excludes every `extern fun` (no body to discover closures in).
0000000 999
            ast::FnBody::Extern => unreachable!("extern fns are excluded from `fns`"),
4ba0db3 1000
        }
4ba0db3 1001
        raw_closures.extend(walker.found);
35af6cf 1002
        named_fn_refs.extend(walker.named_fn_refs);
4ba0db3 1003
    }
4ba0db3 1004
0e39618 1005
    // Register every closure literal's own env struct type, plus the ONE shared
0e39618 1006
    // closure-value struct `{table_idx: i32, env: anyref}` every closure (and
0e39618 1007
    // trampoline, below) wraps its env in — declared together as a single `rec`
0e39618 1008
    // group now that every closure's free-variable types are known. The shared
0e39618 1009
    // struct is always slot 0; closure `i`'s env type is slot `i + 1`.
0e39618 1010
    let mut closure_subtypes: Vec<SubType> = vec![SubType {
0e39618 1011
        is_final: true,
0e39618 1012
        supertype_idx: None,
0e39618 1013
        composite_type: CompositeType {
0e39618 1014
            inner: CompositeInnerType::Struct(StructType {
0e39618 1015
                fields: vec![
0e39618 1016
                    FieldType { element_type: StorageType::Val(ValType::I32), mutable: false },
0e39618 1017
                    FieldType { element_type: StorageType::Val(ValType::Ref(RefType::ANYREF)), mutable: false },
0e39618 1018
                ].into(),
0e39618 1019
            }),
0e39618 1020
            shared: false,
0e39618 1021
        },
0e39618 1022
    }];
0e39618 1023
    for rc in &raw_closures {
0e39618 1024
        let field_types: Vec<FieldType> = rc.free_vars.iter().map(|(_, ty)| FieldType {
0e39618 1025
            element_type: StorageType::Val(plumTypeToValtype(ty)),
0e39618 1026
            mutable: false,
0e39618 1027
        }).collect();
0e39618 1028
        closure_subtypes.push(SubType {
0e39618 1029
            is_final: true,
0e39618 1030
            supertype_idx: None,
0e39618 1031
            composite_type: CompositeType { inner: CompositeInnerType::Struct(StructType { fields: field_types.into() }), shared: false },
0e39618 1032
        });
0e39618 1033
    }
0e39618 1034
    let closure_type_indices = module.addGcTypes(closure_subtypes);
0e39618 1035
    gc_types.closure_type_idx = closure_type_indices[0];
0e39618 1036
    CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
0e39618 1037
4ba0db3 1038
    let mut closures: HashMap<usize, ClosureInfo> = HashMap::new();
4ba0db3 1039
    let mut closure_asts: HashMap<usize, &ast::Closure> = HashMap::new();
0e39618 1040
    for (i, rc) in raw_closures.iter().enumerate() {
0e39618 1041
        let mut sig_params = vec![ValType::Ref(RefType::ANYREF)]; // env pointer
4ba0db3 1042
        sig_params.extend(rc.param_vts.iter().copied());
4ba0db3 1043
        let sig_key: ClosureSigKey = (sig_params.clone(), rc.ret_vt);
4ba0db3 1044
        let type_idx = match closure_call_types.get(&sig_key) {
4ba0db3 1045
            Some(t) => *t,
4ba0db3 1046
            None => {
4ba0db3 1047
                let results: Vec<ValType> = rc.ret_vt.into_iter().collect();
3d6f280 1048
                let t = module.addType(&sig_params, &results);
4ba0db3 1049
                closure_call_types.insert(sig_key, t);
4ba0db3 1050
                t
4ba0db3 1051
            }
4ba0db3 1052
        };
3d6f280 1053
        let func_idx = module.addFunction(type_idx, &[]);
3d6f280 1054
        let table_idx = module.addTableElement(func_idx);
4ba0db3 1055
        closures.insert(rc.ptr, ClosureInfo {
4ba0db3 1056
            func_idx,
4ba0db3 1057
            table_idx,
0e39618 1058
            env_type_idx: closure_type_indices[i + 1],
4ba0db3 1059
            param_vts: rc.param_vts.clone(),
4ba0db3 1060
            param_ptypes: rc.param_ptypes.clone(),
4ba0db3 1061
            ret_vt: rc.ret_vt,
4ba0db3 1062
            free_vars: rc.free_vars.clone(),
4ba0db3 1063
        });
4ba0db3 1064
        closure_asts.insert(rc.ptr, rc.closure);
4ba0db3 1065
    }
4ba0db3 1066
35af6cf 1067
    // Register a zero-capture "trampoline" closure for every top-level function
35af6cf 1068
    // referenced as a bare value (e.g. `each(double)`): a real wasm function with the
0e39618 1069
    // closure calling convention `(env, ...real_params) -> ret` that ignores its env
0e39618 1070
    // and forwards straight to the real function, plus a funcref-table entry for it.
0e39618 1071
    // Since it never captures anything, its `{table_idx, env=null}` closure struct is
0e39618 1072
    // a compile-time constant — like every payload-free enum variant, built once by
0e39618 1073
    // the shared `start` function and stored in its own global.
35af6cf 1074
    let mut named_fn_values: HashMap<String, u32> = HashMap::new();
35af6cf 1075
    for name in &named_fn_refs {
35af6cf 1076
        let sig = func_sigs.get(name).expect("named fn ref must be a registered top-level function");
35af6cf 1077
        let real_func_idx = *func_ids.get(name).expect("named fn ref must be a registered top-level function");
35af6cf 1078
0e39618 1079
        let mut sig_params = vec![ValType::Ref(RefType::ANYREF)]; // env pointer
35af6cf 1080
        sig_params.extend(sig.params.iter().copied());
35af6cf 1081
        let sig_key: ClosureSigKey = (sig_params.clone(), sig.ret);
35af6cf 1082
        let type_idx = match closure_call_types.get(&sig_key) {
35af6cf 1083
            Some(t) => *t,
35af6cf 1084
            None => {
35af6cf 1085
                let results: Vec<ValType> = sig.ret.into_iter().collect();
3d6f280 1086
                let t = module.addType(&sig_params, &results);
35af6cf 1087
                closure_call_types.insert(sig_key, t);
35af6cf 1088
                t
35af6cf 1089
            }
35af6cf 1090
        };
35af6cf 1091
35af6cf 1092
        let mut tbody = Vec::new();
35af6cf 1093
        tbody.push(0u8); // no locals beyond the params already in the signature
35af6cf 1094
        for i in 0..sig.params.len() {
0e39618 1095
            Instruction::LocalGet((i + 1) as u32).encode(&mut tbody); // local 0 is env, ignored
35af6cf 1096
        }
35af6cf 1097
        Instruction::Call(real_func_idx).encode(&mut tbody);
35af6cf 1098
        Instruction::End.encode(&mut tbody);
3d6f280 1099
        let func_idx = module.addFunction(type_idx, &tbody);
3d6f280 1100
        let table_idx = module.addTableElement(func_idx);
35af6cf 1101
0e39618 1102
        let mut init = Vec::new();
0e39618 1103
        Instruction::RefNull(HeapType::Concrete(gc_types.closure_type_idx)).encode(&mut init);
0e39618 1104
        let global_idx = module.addGlobal(gcRef(gc_types.closure_type_idx), true, &init);
0e39618 1105
        named_fn_values.insert(name.clone(), global_idx);
0e39618 1106
0e39618 1107
        Instruction::I32Const(table_idx as i32).encode(&mut start_body);
0e39618 1108
        Instruction::RefNull(HeapType::ANY).encode(&mut start_body);
0e39618 1109
        Instruction::StructNew(gc_types.closure_type_idx).encode(&mut start_body);
0e39618 1110
        Instruction::GlobalSet(global_idx).encode(&mut start_body);
35af6cf 1111
    }
35af6cf 1112
0e39618 1113
    // The `start` function is now complete — every payload-free variant singleton
0e39618 1114
    // (above) and every trampoline closure (above) has appended its own init code.
0e39618 1115
    // It declares no locals of its own, but a function body's raw bytes must still
0e39618 1116
    // start with the (empty) locals-declaration vector — a single `0x00` — before
0e39618 1117
    // the instruction stream, exactly like every other compiled function body below.
0e39618 1118
    Instruction::End.encode(&mut start_body);
0e39618 1119
    let mut final_start_body = vec![0u8];
0e39618 1120
    final_start_body.extend(start_body);
0e39618 1121
    let start_slot = (start_func_idx - module.func_import_count) as usize;
0e39618 1122
    module.functions[start_slot].1 = final_start_body;
0e39618 1123
35af6cf 1124
    // Runtime helpers backing string interpolation (`"{expr}"`): always registered
35af6cf 1125
    // (unconditionally, for simplicity) since they're cheap and self-contained.
0e39618 1126
    let string_concat_func = registerStringConcatHelper(&mut module, gc_types.str_type_idx);
0e39618 1127
    let int_to_string_func = registerIntToStringHelper(&mut module, gc_types.str_type_idx);
35af6cf 1128
4ba0db3 1129
    let ctx = CompileCtx {
0e39618 1130
        func_ids, func_sigs, classes, methods, enum_variants, enum_params, global_env,
35af6cf 1131
        closures, closure_asts, closure_call_types, named_fn_values,
0e39618 1132
        string_concat_func, int_to_string_func, gc_types, singleton_globals,
4ba0db3 1133
    };
4ba0db3 1134
0e39618 1135
    let mut state = ModuleState { passive_segments: Vec::new() };
bb8ca38 1136
bb8ca38 1137
    let mut compiled_bodies: Vec<(String, Vec<u8>)> = Vec::new();
bb8ca38 1138
    for f in &fns {
0000000 1139
        let body = match compileIntrinsicFnBody(f) {
0000000 1140
            Some(body) => body,
0000000 1141
            None => compileFnBody(f, &ctx, &mut state)?,
0000000 1142
        };
3d6f280 1143
        compiled_bodies.push((fnKey(f), body));
5d8ada1 1144
    }
5d8ada1 1145
5d8ada1 1146
    // Patch compiled bodies back into their pre-registered function slots.
5d8ada1 1147
    for (name, body) in &compiled_bodies {
5d8ada1 1148
        let idx = *ctx.func_ids.get(name).expect("function was registered in the first pass");
5d8ada1 1149
        let slot = (idx - module.func_import_count) as usize;
5d8ada1 1150
        module.functions[slot].1 = body.clone();
bb8ca38 1151
    }
bb8ca38 1152
4ba0db3 1153
    // Compile each closure literal's own body into its reserved function slot.
4ba0db3 1154
    let mut closure_bodies: Vec<(u32, Vec<u8>)> = Vec::new();
4ba0db3 1155
    for (ptr, info) in &ctx.closures {
4ba0db3 1156
        let cl = ctx.closure_asts.get(ptr).expect("every registered closure has its AST recorded");
3d6f280 1157
        let body = compileClosureBody(cl, info, &ctx, &mut state)?;
4ba0db3 1158
        closure_bodies.push((info.func_idx, body));
4ba0db3 1159
    }
4ba0db3 1160
    for (func_idx, body) in &closure_bodies {
4ba0db3 1161
        let slot = (*func_idx - module.func_import_count) as usize;
4ba0db3 1162
        module.functions[slot].1 = body.clone();
4ba0db3 1163
    }
4ba0db3 1164
0e39618 1165
    // Flush every staged string literal into the module in the SAME order they were
0e39618 1166
    // staged — `compileStaticString` already baked each one's index (its position in
0e39618 1167
    // `state.passive_segments` at staging time) into an `array.new_data` instruction,
0e39618 1168
    // so that order must be preserved exactly for those indices to still be correct.
0e39618 1169
    for bytes in &state.passive_segments {
0e39618 1170
        module.addPassiveDataSegment(bytes);
bb8ca38 1171
    }
bb8ca38 1172
1e3672d 1173
    if let Some(&main_idx) = ctx.func_ids.get("main") {
3d6f280 1174
        module.addExport("main", ExportKind::Func, main_idx);
bb8ca38 1175
    }
bb8ca38 1176
5d8ada1 1177
    Ok(module.finish())
bb8ca38 1178
}
bb8ca38 1179
0e39618 1180
/// Registers `__string_concat(a: ref Str, b: ref Str) -> ref Str`, a hand-written
0e39618 1181
/// runtime helper backing string interpolation. `Str` is a wasm-gc `array<i8>`,
0e39618 1182
/// which tracks its own length (`array.len`) — building the concatenation is:
0e39618 1183
/// allocate a new array sized `len(a) + len(b)`, then two `array.copy`s (a
0e39618 1184
/// whole-array-in-one-instruction bulk copy).
0e39618 1185
fn registerStringConcatHelper(module: &mut WasmModule, str_type_idx: u32) -> u32 {
0e39618 1186
    let str_ref = gcRef(str_type_idx);
0e39618 1187
    let type_idx = module.addType(&[str_ref, str_ref], &[str_ref]);
35af6cf 1188
0e39618 1189
    // locals: 0=a (param), 1=b (param), 2=len_a, 3=len_b, 4=result
35af6cf 1190
    const A: u32 = 0;
35af6cf 1191
    const B: u32 = 1;
35af6cf 1192
    const LEN_A: u32 = 2;
35af6cf 1193
    const LEN_B: u32 = 3;
35af6cf 1194
    const RESULT: u32 = 4;
35af6cf 1195
35af6cf 1196
    let mut body = Vec::new();
0e39618 1197
    body.extend(encodeLeb128U32(2)); // two locals groups
0e39618 1198
    body.extend(encodeLeb128U32(2)); // len_a, len_b: i32
35af6cf 1199
    ValType::I32.encode(&mut body);
0e39618 1200
    body.extend(encodeLeb128U32(1)); // result: ref
0e39618 1201
    str_ref.encode(&mut body);
35af6cf 1202
0e39618 1203
    // len_a = array.len(a); len_b = array.len(b)
35af6cf 1204
    Instruction::LocalGet(A).encode(&mut body);
0e39618 1205
    Instruction::ArrayLen.encode(&mut body);
35af6cf 1206
    Instruction::LocalSet(LEN_A).encode(&mut body);
35af6cf 1207
    Instruction::LocalGet(B).encode(&mut body);
0e39618 1208
    Instruction::ArrayLen.encode(&mut body);
35af6cf 1209
    Instruction::LocalSet(LEN_B).encode(&mut body);
35af6cf 1210
0e39618 1211
    // result = array.new_default(str_type_idx, len_a + len_b)
35af6cf 1212
    Instruction::LocalGet(LEN_A).encode(&mut body);
35af6cf 1213
    Instruction::LocalGet(LEN_B).encode(&mut body);
35af6cf 1214
    Instruction::I32Add.encode(&mut body);
0e39618 1215
    Instruction::ArrayNewDefault(str_type_idx).encode(&mut body);
0e39618 1216
    Instruction::LocalSet(RESULT).encode(&mut body);
35af6cf 1217
0e39618 1218
    // array.copy(dst: result, dst_offset: 0, src: a, src_offset: 0, len: len_a)
35af6cf 1219
    Instruction::LocalGet(RESULT).encode(&mut body);
35af6cf 1220
    Instruction::I32Const(0).encode(&mut body);
35af6cf 1221
    Instruction::LocalGet(A).encode(&mut body);
35af6cf 1222
    Instruction::I32Const(0).encode(&mut body);
0e39618 1223
    Instruction::LocalGet(LEN_A).encode(&mut body);
0e39618 1224
    Instruction::ArrayCopy { array_type_index_dst: str_type_idx, array_type_index_src: str_type_idx }.encode(&mut body);
0e39618 1225
0e39618 1226
    // array.copy(dst: result, dst_offset: len_a, src: b, src_offset: 0, len: len_b)
35af6cf 1227
    Instruction::LocalGet(RESULT).encode(&mut body);
35af6cf 1228
    Instruction::LocalGet(LEN_A).encode(&mut body);
35af6cf 1229
    Instruction::LocalGet(B).encode(&mut body);
0e39618 1230
    Instruction::I32Const(0).encode(&mut body);
0e39618 1231
    Instruction::LocalGet(LEN_B).encode(&mut body);
0e39618 1232
    Instruction::ArrayCopy { array_type_index_dst: str_type_idx, array_type_index_src: str_type_idx }.encode(&mut body);
35af6cf 1233
35af6cf 1234
    Instruction::LocalGet(RESULT).encode(&mut body);
35af6cf 1235
    Instruction::End.encode(&mut body);
35af6cf 1236
3d6f280 1237
    module.addFunction(type_idx, &body)
35af6cf 1238
}
35af6cf 1239
0e39618 1240
/// Registers `__int_to_string(n: i64) -> ref Str`, a hand-written runtime helper
0e39618 1241
/// backing string interpolation: allocates a new `array<i8>` holding `n`'s decimal
0e39618 1242
/// representation (handling a leading `-` for negatives, and `0` correctly via a
0e39618 1243
/// do-while digit count that always runs at least once).
0e39618 1244
fn registerIntToStringHelper(module: &mut WasmModule, str_type_idx: u32) -> u32 {
0e39618 1245
    let str_ref = gcRef(str_type_idx);
0e39618 1246
    let type_idx = module.addType(&[ValType::I64], &[str_ref]);
35af6cf 1247
0e39618 1248
    // locals: 0=n (param, i64), 1=is_neg (i32), 2=count (i32), 3=total_len (i32),
0e39618 1249
    // 4=pos (i32), 5=result (ref), 6=abs_n (i64), 7=temp (i64)
0e39618 1250
    // Locals are declared as one group of 4 `i32`s, one group of 1 `ref`, then one
0e39618 1251
    // group of 2 `i64`s (see below), so indices must stay grouped by type in that
0e39618 1252
    // same order — NOT in whatever order reads best logically.
35af6cf 1253
    const N: u32 = 0;
35af6cf 1254
    const IS_NEG: u32 = 1;
35af6cf 1255
    const COUNT: u32 = 2;
35af6cf 1256
    const TOTAL_LEN: u32 = 3;
0e39618 1257
    const POS: u32 = 4;
0e39618 1258
    const RESULT: u32 = 5;
35af6cf 1259
    const ABS_N: u32 = 6;
35af6cf 1260
    const TEMP: u32 = 7;
35af6cf 1261
35af6cf 1262
    let mut body = Vec::new();
0e39618 1263
    body.extend(encodeLeb128U32(3)); // three locals groups
0e39618 1264
    body.extend(encodeLeb128U32(4)); // is_neg, count, total_len, pos: i32
35af6cf 1265
    ValType::I32.encode(&mut body);
0e39618 1266
    body.extend(encodeLeb128U32(1)); // result: ref
0e39618 1267
    str_ref.encode(&mut body);
3d6f280 1268
    body.extend(encodeLeb128U32(2)); // abs_n, temp: i64
35af6cf 1269
    ValType::I64.encode(&mut body);
35af6cf 1270
35af6cf 1271
    // is_neg = n < 0
35af6cf 1272
    Instruction::LocalGet(N).encode(&mut body);
35af6cf 1273
    Instruction::I64Const(0).encode(&mut body);
35af6cf 1274
    Instruction::I64LtS.encode(&mut body);
35af6cf 1275
    Instruction::LocalSet(IS_NEG).encode(&mut body);
35af6cf 1276
35af6cf 1277
    // abs_n = is_neg ? (0 - n) : n
35af6cf 1278
    Instruction::LocalGet(IS_NEG).encode(&mut body);
35af6cf 1279
    Instruction::If(BlockType::Result(ValType::I64)).encode(&mut body);
35af6cf 1280
    Instruction::I64Const(0).encode(&mut body);
35af6cf 1281
    Instruction::LocalGet(N).encode(&mut body);
35af6cf 1282
    Instruction::I64Sub.encode(&mut body);
35af6cf 1283
    Instruction::Else.encode(&mut body);
35af6cf 1284
    Instruction::LocalGet(N).encode(&mut body);
35af6cf 1285
    Instruction::End.encode(&mut body);
35af6cf 1286
    Instruction::LocalSet(ABS_N).encode(&mut body);
35af6cf 1287
35af6cf 1288
    // count digits: do { temp /= 10; count++ } while (temp != 0); temp starts as abs_n
35af6cf 1289
    Instruction::I32Const(0).encode(&mut body);
35af6cf 1290
    Instruction::LocalSet(COUNT).encode(&mut body);
35af6cf 1291
    Instruction::LocalGet(ABS_N).encode(&mut body);
35af6cf 1292
    Instruction::LocalSet(TEMP).encode(&mut body);
35af6cf 1293
    Instruction::Loop(BlockType::Empty).encode(&mut body);
35af6cf 1294
    Instruction::LocalGet(TEMP).encode(&mut body);
35af6cf 1295
    Instruction::I64Const(10).encode(&mut body);
35af6cf 1296
    Instruction::I64DivS.encode(&mut body);
35af6cf 1297
    Instruction::LocalSet(TEMP).encode(&mut body);
35af6cf 1298
    Instruction::LocalGet(COUNT).encode(&mut body);
35af6cf 1299
    Instruction::I32Const(1).encode(&mut body);
35af6cf 1300
    Instruction::I32Add.encode(&mut body);
35af6cf 1301
    Instruction::LocalSet(COUNT).encode(&mut body);
35af6cf 1302
    Instruction::LocalGet(TEMP).encode(&mut body);
35af6cf 1303
    Instruction::I64Const(0).encode(&mut body);
35af6cf 1304
    Instruction::I64Ne.encode(&mut body);
35af6cf 1305
    Instruction::BrIf(0).encode(&mut body);
35af6cf 1306
    Instruction::End.encode(&mut body);
35af6cf 1307
35af6cf 1308
    // total_len = count + is_neg
35af6cf 1309
    Instruction::LocalGet(COUNT).encode(&mut body);
35af6cf 1310
    Instruction::LocalGet(IS_NEG).encode(&mut body);
35af6cf 1311
    Instruction::I32Add.encode(&mut body);
35af6cf 1312
    Instruction::LocalSet(TOTAL_LEN).encode(&mut body);
35af6cf 1313
0e39618 1314
    // result = array.new_default(str_type_idx, total_len) — no length prefix needed,
0e39618 1315
    // `array.len` reads it back natively.
35af6cf 1316
    Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
0e39618 1317
    Instruction::ArrayNewDefault(str_type_idx).encode(&mut body);
0e39618 1318
    Instruction::LocalSet(RESULT).encode(&mut body);
35af6cf 1319
0e39618 1320
    // pos = total_len; temp = abs_n
35af6cf 1321
    Instruction::LocalGet(TOTAL_LEN).encode(&mut body);
35af6cf 1322
    Instruction::LocalSet(POS).encode(&mut body);
35af6cf 1323
    Instruction::LocalGet(ABS_N).encode(&mut body);
35af6cf 1324
    Instruction::LocalSet(TEMP).encode(&mut body);
35af6cf 1325
0e39618 1326
    // do { pos--; result[pos] = '0' + temp % 10; temp /= 10 } while (pos > is_neg)
35af6cf 1327
    Instruction::Loop(BlockType::Empty).encode(&mut body);
35af6cf 1328
    Instruction::LocalGet(POS).encode(&mut body);
35af6cf 1329
    Instruction::I32Const(1).encode(&mut body);
35af6cf 1330
    Instruction::I32Sub.encode(&mut body);
35af6cf 1331
    Instruction::LocalSet(POS).encode(&mut body);
0e39618 1332
    // array.set(result, pos, value) — stack order [array_ref, index, value]
35af6cf 1333
    Instruction::LocalGet(RESULT).encode(&mut body);
35af6cf 1334
    Instruction::LocalGet(POS).encode(&mut body);
35af6cf 1335
    // value = '0' + (temp % 10)
35af6cf 1336
    Instruction::LocalGet(TEMP).encode(&mut body);
35af6cf 1337
    Instruction::I64Const(10).encode(&mut body);
35af6cf 1338
    Instruction::I64RemS.encode(&mut body);
35af6cf 1339
    Instruction::I64Const(48).encode(&mut body);
35af6cf 1340
    Instruction::I64Add.encode(&mut body);
35af6cf 1341
    Instruction::I32WrapI64.encode(&mut body);
0e39618 1342
    Instruction::ArraySet(str_type_idx).encode(&mut body);
35af6cf 1343
    // temp /= 10
35af6cf 1344
    Instruction::LocalGet(TEMP).encode(&mut body);
35af6cf 1345
    Instruction::I64Const(10).encode(&mut body);
35af6cf 1346
    Instruction::I64DivS.encode(&mut body);
35af6cf 1347
    Instruction::LocalSet(TEMP).encode(&mut body);
0e39618 1348
    // while (pos > is_neg)
35af6cf 1349
    Instruction::LocalGet(POS).encode(&mut body);
35af6cf 1350
    Instruction::LocalGet(IS_NEG).encode(&mut body);
35af6cf 1351
    Instruction::I32GtU.encode(&mut body);
35af6cf 1352
    Instruction::BrIf(0).encode(&mut body);
35af6cf 1353
    Instruction::End.encode(&mut body);
35af6cf 1354
0e39618 1355
    // if (is_neg) result[0] = '-'
35af6cf 1356
    Instruction::LocalGet(IS_NEG).encode(&mut body);
35af6cf 1357
    Instruction::If(BlockType::Empty).encode(&mut body);
35af6cf 1358
    Instruction::LocalGet(RESULT).encode(&mut body);
0e39618 1359
    Instruction::I32Const(0).encode(&mut body);
35af6cf 1360
    Instruction::I32Const(45).encode(&mut body); // '-'
0e39618 1361
    Instruction::ArraySet(str_type_idx).encode(&mut body);
35af6cf 1362
    Instruction::End.encode(&mut body);
35af6cf 1363
35af6cf 1364
    Instruction::LocalGet(RESULT).encode(&mut body);
35af6cf 1365
    Instruction::End.encode(&mut body);
35af6cf 1366
3d6f280 1367
    module.addFunction(type_idx, &body)
35af6cf 1368
}
35af6cf 1369
0000000 1370
/// Hand-written bodies for a handful of `libs/std/str.plum` primitives that can't
0000000 1371
/// be expressed in Plum source at all (byte-level array access, building a new
0000000 1372
/// one-element array) — these three are the whole reason every other `Str` method
0000000 1373
/// (case conversion, trim, split, ...) can now be written in pure Plum on top of
0000000 1374
/// them. Declared normally in `str.plum` (with `= todo` bodies so the checker
0000000 1375
/// registers their real signature and validates call sites), then intercepted
0000000 1376
/// here — by `fnKey` — instead of compiling their `todo` body to `unreachable`.
0000000 1377
/// Returns `None` for any other function, meaning "compile it normally."
0000000 1378
fn compileIntrinsicFnBody(f: &ast::Fn) -> Option<Vec<u8>> {
0000000 1379
    let str_type_idx = withGcTypes(|r| r.str_type_idx);
0000000 1380
    match (f.type_param.as_deref(), f.name.as_str()) {
0000000 1381
        // Str.length(self) -> Int
0000000 1382
        (Some("Str"), "length") => {
0000000 1383
            let mut body = vec![0u8]; // no locals
0000000 1384
            Instruction::LocalGet(0).encode(&mut body); // self
0000000 1385
            Instruction::ArrayLen.encode(&mut body);
0000000 1386
            Instruction::I64ExtendI32U.encode(&mut body);
0000000 1387
            Instruction::End.encode(&mut body);
0000000 1388
            Some(body)
0000000 1389
        }
0000000 1390
        // Str.byteAt(self, i: Int) -> Int — the byte's unsigned value (0-255).
0000000 1391
        (Some("Str"), "byteAt") => {
0000000 1392
            let mut body = vec![0u8];
0000000 1393
            Instruction::LocalGet(0).encode(&mut body); // self
0000000 1394
            Instruction::LocalGet(1).encode(&mut body); // i
0000000 1395
            Instruction::I32WrapI64.encode(&mut body);
0000000 1396
            Instruction::ArrayGetU(str_type_idx).encode(&mut body);
0000000 1397
            Instruction::I64ExtendI32U.encode(&mut body);
0000000 1398
            Instruction::End.encode(&mut body);
0000000 1399
            Some(body)
0000000 1400
        }
0000000 1401
        // byteToStr(b: Int) -> Str — a new 1-byte Str holding `b`'s low 8 bits.
0000000 1402
        (None, "byteToStr") => {
0000000 1403
            let mut body = vec![0u8];
0000000 1404
            Instruction::LocalGet(0).encode(&mut body); // b
0000000 1405
            Instruction::I32WrapI64.encode(&mut body);
0000000 1406
            Instruction::ArrayNewFixed { array_type_index: str_type_idx, array_size: 1 }.encode(&mut body);
0000000 1407
            Instruction::End.encode(&mut body);
0000000 1408
            Some(body)
0000000 1409
        }
0000000 1410
        // ByteSlice.length(self) -> Int — `[]Byte` reuses `Str`'s array<i8> wasm
0000000 1411
        // type (see the comment on `PlumType::TByteSlice` in `plumTypeToValtype`),
0000000 1412
        // so this is byte-for-byte identical to `Str.length` above.
0000000 1413
        (Some("ByteSlice"), "length") => {
0000000 1414
            let mut body = vec![0u8];
0000000 1415
            Instruction::LocalGet(0).encode(&mut body); // self
0000000 1416
            Instruction::ArrayLen.encode(&mut body);
0000000 1417
            Instruction::I64ExtendI32U.encode(&mut body);
0000000 1418
            Instruction::End.encode(&mut body);
0000000 1419
            Some(body)
0000000 1420
        }
0000000 1421
        // ByteSlice.get(self, i: Int) -> Byte — unlike `Str.byteAt`, the result
0000000 1422
        // is already `Byte`'s wasm representation (i32), so no `i64.extend` here.
0000000 1423
        (Some("ByteSlice"), "get") => {
0000000 1424
            let mut body = vec![0u8];
0000000 1425
            Instruction::LocalGet(0).encode(&mut body); // self
0000000 1426
            Instruction::LocalGet(1).encode(&mut body); // i
0000000 1427
            Instruction::I32WrapI64.encode(&mut body);
0000000 1428
            Instruction::ArrayGetU(str_type_idx).encode(&mut body);
0000000 1429
            Instruction::End.encode(&mut body);
0000000 1430
            Some(body)
0000000 1431
        }
0000000 1432
        // ByteSlice.set(self, i: Int, b: Byte) -> Unit
0000000 1433
        (Some("ByteSlice"), "set") => {
0000000 1434
            let mut body = vec![0u8];
0000000 1435
            Instruction::LocalGet(0).encode(&mut body); // self
0000000 1436
            Instruction::LocalGet(1).encode(&mut body); // i
0000000 1437
            Instruction::I32WrapI64.encode(&mut body);
0000000 1438
            Instruction::LocalGet(2).encode(&mut body); // b (already i32)
0000000 1439
            Instruction::ArraySet(str_type_idx).encode(&mut body);
0000000 1440
            Instruction::End.encode(&mut body);
0000000 1441
            Some(body)
0000000 1442
        }
0000000 1443
        // makeBytes(n: Int) -> []Byte — a fresh, zero-filled byte slice of length `n`.
0000000 1444
        (None, "makeBytes") => {
0000000 1445
            let mut body = vec![0u8];
0000000 1446
            Instruction::LocalGet(0).encode(&mut body); // n
0000000 1447
            Instruction::I32WrapI64.encode(&mut body);
0000000 1448
            Instruction::ArrayNewDefault(str_type_idx).encode(&mut body);
0000000 1449
            Instruction::End.encode(&mut body);
0000000 1450
            Some(body)
0000000 1451
        }
0000000 1452
        // copyBytes(dst: []Byte, dstStart: Int, src: []Byte, srcStart: Int, n: Int) -> Unit
0000000 1453
        // copyStrToBytes(dst: []Byte, dstStart: Int, src: Str, srcStart: Int, n: Int) -> Unit
0000000 1454
        // Both compile to the exact same `array.copy` — `[]Byte` and `Str` share
0000000 1455
        // one underlying wasm-gc array type, so a bulk copy between them needs no
0000000 1456
        // conversion, just the one instruction. Two Plum-level names exist only so
0000000 1457
        // the checker can enforce each argument's declared type.
0000000 1458
        (None, "copyBytes") | (None, "copyStrToBytes") => {
0000000 1459
            let mut body = vec![0u8];
0000000 1460
            Instruction::LocalGet(0).encode(&mut body); // dst
0000000 1461
            Instruction::LocalGet(1).encode(&mut body); // dstStart
0000000 1462
            Instruction::I32WrapI64.encode(&mut body);
0000000 1463
            Instruction::LocalGet(2).encode(&mut body); // src
0000000 1464
            Instruction::LocalGet(3).encode(&mut body); // srcStart
0000000 1465
            Instruction::I32WrapI64.encode(&mut body);
0000000 1466
            Instruction::LocalGet(4).encode(&mut body); // n
0000000 1467
            Instruction::I32WrapI64.encode(&mut body);
0000000 1468
            Instruction::ArrayCopy { array_type_index_dst: str_type_idx, array_type_index_src: str_type_idx }.encode(&mut body);
0000000 1469
            Instruction::End.encode(&mut body);
0000000 1470
            Some(body)
0000000 1471
        }
0000000 1472
        // bytesToStr(src: []Byte, start: Int, n: Int) -> Str — copies out `n` bytes
0000000 1473
        // starting at `start` into a fresh `Str`, rather than aliasing `src`
0000000 1474
        // directly, so a later mutation of `src` (e.g. `Buffer` reusing/growing its
0000000 1475
        // backing slice) can never retroactively change an already-returned `Str`.
0000000 1476
        (None, "bytesToStr") => {
0000000 1477
            const SRC: u32 = 0;
0000000 1478
            const START: u32 = 1;
0000000 1479
            const N: u32 = 2;
0000000 1480
            const RESULT: u32 = 3;
0000000 1481
            let str_ref = gcRef(str_type_idx);
0000000 1482
            let mut body = Vec::new();
0000000 1483
            body.extend(encodeLeb128U32(1)); // one locals group
0000000 1484
            body.extend(encodeLeb128U32(1)); // result: ref
0000000 1485
            str_ref.encode(&mut body);
0000000 1486
0000000 1487
            Instruction::LocalGet(N).encode(&mut body);
0000000 1488
            Instruction::I32WrapI64.encode(&mut body);
0000000 1489
            Instruction::ArrayNewDefault(str_type_idx).encode(&mut body);
0000000 1490
            Instruction::LocalSet(RESULT).encode(&mut body);
0000000 1491
0000000 1492
            Instruction::LocalGet(RESULT).encode(&mut body);
0000000 1493
            Instruction::I32Const(0).encode(&mut body);
0000000 1494
            Instruction::LocalGet(SRC).encode(&mut body);
0000000 1495
            Instruction::LocalGet(START).encode(&mut body);
0000000 1496
            Instruction::I32WrapI64.encode(&mut body);
0000000 1497
            Instruction::LocalGet(N).encode(&mut body);
0000000 1498
            Instruction::I32WrapI64.encode(&mut body);
0000000 1499
            Instruction::ArrayCopy { array_type_index_dst: str_type_idx, array_type_index_src: str_type_idx }.encode(&mut body);
0000000 1500
0000000 1501
            Instruction::LocalGet(RESULT).encode(&mut body);
0000000 1502
            Instruction::End.encode(&mut body);
0000000 1503
            Some(body)
0000000 1504
        }
0000000 1505
        _ => None,
0000000 1506
    }
0000000 1507
}
0000000 1508
4ba0db3 1509
/// The `PlumType` of a declared parameter, including `fn(...)`-typed params as `TFun`.
3d6f280 1510
fn paramPlumType(pt: &ast::ParamType) -> PlumType {
4ba0db3 1511
    match pt {
3d6f280 1512
        ast::ParamType::Type(t) => plum_checker::plumTypeFromAst(t),
3d6f280 1513
        ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plum_checker::plumTypeFromAst(t))),
4ba0db3 1514
        ast::ParamType::Fn(params, ret) => {
3d6f280 1515
            let param_types = params.iter().map(plum_checker::plumTypeFromAst).collect();
3d6f280 1516
            let ret_ty = ret.as_ref().map(|r| plum_checker::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
4ba0db3 1517
            PlumType::TFun(param_types, Box::new(ret_ty))
4ba0db3 1518
        }
4ba0db3 1519
    }
4ba0db3 1520
}
4ba0db3 1521
4ba0db3 1522
/// One closure literal discovered by the pre-pass, before it is registered as a wasm
4ba0db3 1523
/// function. `ptr` is the closure's `&ast::Closure` pointer identity (its stable key).
4ba0db3 1524
struct RawClosure<'a> {
4ba0db3 1525
    ptr: usize,
4ba0db3 1526
    closure: &'a ast::Closure,
4ba0db3 1527
    param_vts: Vec<ValType>,
4ba0db3 1528
    param_ptypes: Vec<PlumType>,
4ba0db3 1529
    ret_vt: Option<ValType>,
4ba0db3 1530
    free_vars: Vec<(String, PlumType)>,
4ba0db3 1531
}
4ba0db3 1532
4ba0db3 1533
/// Walks a function body (maintaining an evolving type env, exactly like `Collector`)
4ba0db3 1534
/// to find every closure literal and determine its concrete signature and captured
4ba0db3 1535
/// free variables. A closure passed directly as a `fn(...)`-typed call argument takes
4ba0db3 1536
/// its signature from that declared param type; any other closure (e.g. one assigned to
4ba0db3 1537
/// a local) falls back to the checker's inference of the closure expression itself.
4ba0db3 1538
struct ClosureWalker<'a, 'c> {
4ba0db3 1539
    env: TypeEnv,
4ba0db3 1540
    cctx: plum_checker::CheckCtx<'c>,
4ba0db3 1541
    fn_decls: &'a HashMap<String, &'a ast::Fn>,
4ba0db3 1542
    found: Vec<RawClosure<'a>>,
35af6cf 1543
    /// Local names bound in the function currently being walked (params, assign
35af6cf 1544
    /// targets, for-loop vars) — used to tell a local variable reference apart from a
35af6cf 1545
    /// bare reference to a top-level function name (see `named_fn_refs`).
35af6cf 1546
    locals: HashSet<String>,
35af6cf 1547
    /// Top-level (non-method) function names referenced as a bare value (e.g.
35af6cf 1548
    /// `each(double)`) rather than called directly (`double(x)`, which compiles via
35af6cf 1549
    /// `Expr::FnCall` and never reaches here). Each one needs a zero-capture
35af6cf 1550
    /// "trampoline" closure so it can be used wherever a `fn(...)`-typed value is
35af6cf 1551
    /// expected.
35af6cf 1552
    named_fn_refs: HashSet<String>,
4ba0db3 1553
}
4ba0db3 1554
4ba0db3 1555
impl<'a, 'c> ClosureWalker<'a, 'c> {
3d6f280 1556
    fn walkBlock(&mut self, block: &'a ast::Block) {
4ba0db3 1557
        for s in &block.stmts {
3d6f280 1558
            self.walkStmt(s);
4ba0db3 1559
        }
4ba0db3 1560
    }
4ba0db3 1561
3d6f280 1562
    fn walkStmt(&mut self, stmt: &'a ast::Stmt) {
4ba0db3 1563
        match stmt {
4ba0db3 1564
            ast::Stmt::Assign(a) => {
4ba0db3 1565
                for (target, value) in a.targets.iter().zip(a.values.iter()) {
3d6f280 1566
                    self.walkExpr(value, None);
01f9be3 1567
                    match target {
01f9be3 1568
                        ast::AssignTarget::Var(name) => {
3d6f280 1569
                            let ty = plum_checker::inferExpr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
01f9be3 1570
                            self.env.insert(name.clone(), TypeScheme::mono(ty));
01f9be3 1571
                            self.locals.insert(name.clone());
01f9be3 1572
                        }
01f9be3 1573
                        ast::AssignTarget::Field(object, _) => {
3d6f280 1574
                            self.walkExpr(object, None);
01f9be3 1575
                        }
01f9be3 1576
                    }
4ba0db3 1577
                }
4ba0db3 1578
            }
3d6f280 1579
            ast::Stmt::Return(Some(e)) => self.walkExpr(e, None),
4ba0db3 1580
            ast::Stmt::Return(None) => {}
4ba0db3 1581
            ast::Stmt::If(i) => {
3d6f280 1582
                self.walkExpr(&i.condition, None);
3d6f280 1583
                self.walkBlock(&i.body);
4ba0db3 1584
                for ei in &i.else_ifs {
3d6f280 1585
                    self.walkExpr(&ei.condition, None);
3d6f280 1586
                    self.walkBlock(&ei.body);
4ba0db3 1587
                }
4ba0db3 1588
                if let Some(e) = &i.else_ {
3d6f280 1589
                    self.walkBlock(e);
4ba0db3 1590
                }
4ba0db3 1591
            }
4ba0db3 1592
            ast::Stmt::While(w) => {
3d6f280 1593
                self.walkExpr(&w.condition, None);
3d6f280 1594
                self.walkBlock(&w.body);
4ba0db3 1595
            }
4ba0db3 1596
            ast::Stmt::For(f) => {
3d6f280 1597
                self.walkExpr(&f.iter, None);
3d6f280 1598
                let elem_ty = match plum_checker::inferExpr(&f.iter, &self.env, &self.cctx) {
da1c377 1599
                    Ok(PlumType::TVariadic(elem)) => *elem,
da1c377 1600
                    _ => PlumType::TInt,
da1c377 1601
                };
4ba0db3 1602
                for v in &f.vars {
da1c377 1603
                    self.env.insert(v.clone(), TypeScheme::mono(elem_ty.clone()));
35af6cf 1604
                    self.locals.insert(v.clone());
4ba0db3 1605
                }
3d6f280 1606
                self.walkBlock(&f.body);
4ba0db3 1607
            }
3d6f280 1608
            ast::Stmt::Expr(e) => self.walkExpr(e, None),
3d6f280 1609
            ast::Stmt::Assert(e) => self.walkExpr(e, None),
4ba0db3 1610
            ast::Stmt::Match(m) => {
4ba0db3 1611
                for s in &m.subjects {
3d6f280 1612
                    self.walkExpr(s, None);
4ba0db3 1613
                }
4ba0db3 1614
                for case in &m.cases {
3d6f280 1615
                    self.walkBlock(&case.body);
4ba0db3 1616
                }
4ba0db3 1617
            }
4ba0db3 1618
            ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
4ba0db3 1619
        }
4ba0db3 1620
    }
4ba0db3 1621
4ba0db3 1622
    /// `expected_fn` carries the declared `fn(params) -> ret` type when this expression is
4ba0db3 1623
    /// a call argument in a `fn`-typed parameter position, giving a closure literal its
4ba0db3 1624
    /// concrete signature.
3d6f280 1625
    fn walkExpr(&mut self, expr: &'a ast::Expr, expected_fn: Option<&'a ast::ParamType>) {
4ba0db3 1626
        match expr {
3d6f280 1627
            ast::Expr::Closure(cl) => self.recordClosure(cl, expected_fn),
3d6f280 1628
            ast::Expr::Binary(b) => { self.walkExpr(&b.left, None); self.walkExpr(&b.right, None); }
3d6f280 1629
            ast::Expr::Bool(b) => { self.walkExpr(&b.left, None); self.walkExpr(&b.right, None); }
3d6f280 1630
            ast::Expr::Compare(c) => { self.walkExpr(&c.left, None); self.walkExpr(&c.right, None); }
3d6f280 1631
            ast::Expr::Not(inner) => self.walkExpr(inner, None),
3d6f280 1632
            ast::Expr::Unary(u) => self.walkExpr(&u.operand, None),
3d6f280 1633
            ast::Expr::Paren(inner) => self.walkExpr(inner, None),
4ba0db3 1634
            ast::Expr::Ternary(t) => {
3d6f280 1635
                self.walkExpr(&t.condition, None);
3d6f280 1636
                self.walkExpr(&t.then, None);
3d6f280 1637
                self.walkExpr(&t.else_, None);
4ba0db3 1638
            }
4ba0db3 1639
            ast::Expr::FnCall(call) => {
4ba0db3 1640
                let callee = self.fn_decls.get(&call.name).copied();
4ba0db3 1641
                for (i, arg) in call.args.iter().enumerate() {
3d6f280 1642
                    let arg_expr = argExprOf(arg);
4ba0db3 1643
                    let expected = callee.and_then(|f| f.params.get(i)).map(|p| &p.ty)
4ba0db3 1644
                        .filter(|pt| matches!(pt, ast::ParamType::Fn(_, _)));
3d6f280 1645
                    self.walkExpr(arg_expr, expected);
4ba0db3 1646
                }
4ba0db3 1647
            }
4ba0db3 1648
            ast::Expr::ClassCall(call) => {
4ba0db3 1649
                for fa in &call.fields {
3d6f280 1650
                    self.walkExpr(&fa.value, None);
4ba0db3 1651
                }
4ba0db3 1652
            }
4ba0db3 1653
            ast::Expr::Attribute(a) => {
3d6f280 1654
                self.walkExpr(&a.object, None);
4ba0db3 1655
                if let ast::AttrKind::Method(call) = &a.attr {
4ba0db3 1656
                    for arg in &call.args {
3d6f280 1657
                        self.walkExpr(argExprOf(arg), None);
4ba0db3 1658
                    }
4ba0db3 1659
                }
4ba0db3 1660
            }
35af6cf 1661
            ast::Expr::Var(name) => {
35af6cf 1662
                // A bare name that isn't a local in this scope but does name a
35af6cf 1663
                // top-level (non-method) function is a reference to that function as
35af6cf 1664
                // a value (e.g. `each(double)`), not a direct call — a direct call
35af6cf 1665
                // compiles via `Expr::FnCall` and never reaches this arm.
35af6cf 1666
                if !self.locals.contains(name) {
35af6cf 1667
                    if let Some(f) = self.fn_decls.get(name) {
35af6cf 1668
                        if f.type_param.is_none() {
35af6cf 1669
                            self.named_fn_refs.insert(name.clone());
35af6cf 1670
                        }
35af6cf 1671
                    }
35af6cf 1672
                }
35af6cf 1673
            }
4ba0db3 1674
            ast::Expr::Int(_)
4ba0db3 1675
            | ast::Expr::Float(_)
4ba0db3 1676
            | ast::Expr::String(_)
4ba0db3 1677
            | ast::Expr::Self_
4ba0db3 1678
            | ast::Expr::TypeName(_) => {}
4ba0db3 1679
        }
4ba0db3 1680
    }
4ba0db3 1681
3d6f280 1682
    fn recordClosure(&mut self, cl: &'a ast::Closure, expected_fn: Option<&'a ast::ParamType>) {
4ba0db3 1683
        let (param_vts, param_ptypes, ret_vt) = match expected_fn {
4ba0db3 1684
            Some(ast::ParamType::Fn(params, ret)) => {
3d6f280 1685
                let param_vts = params.iter().map(|t| astTypeToWasm(&t.name).unwrap_or(ValType::I32)).collect();
3d6f280 1686
                let param_ptypes = params.iter().map(plum_checker::plumTypeFromAst).collect();
3d6f280 1687
                let ret_vt = ret.as_ref().and_then(|t| astTypeToWasm(&t.name));
4ba0db3 1688
                (param_vts, param_ptypes, ret_vt)
4ba0db3 1689
            }
4ba0db3 1690
            _ => {
35af6cf 1691
                // Not a direct `fn`-typed call argument (e.g. assigned to a local
35af6cf 1692
                // first, then called/passed on later): the checker's own closure
3d6f280 1693
                // inference (`plum_checker::inferExpr` on `Expr::Closure`) gives
35af6cf 1694
                // each param a fresh `TVar` and never actually unifies it against
35af6cf 1695
                // how the param is used in the body — `unify` is a no-op for any
35af6cf 1696
                // `TVar` — so a param that's genuinely Float or a class/pointer
35af6cf 1697
                // silently comes back as an unresolved `TVar`, which
3d6f280 1698
                // `plumTypeToValtype` then defaults to `Int`. If the closure is
35af6cf 1699
                // later called at its real (non-Int) type, the wasm function actually
35af6cf 1700
                // compiled for its body (built from this wrong, Int-assumed signature)
35af6cf 1701
                // won't match the `call_indirect` type the real call site expects —
35af6cf 1702
                // a mismatch `call_indirect` only traps on at runtime, not at compile
35af6cf 1703
                // or validation time.
35af6cf 1704
                //
35af6cf 1705
                // Fix: resolve each param's type from a direct usage in the body
3d6f280 1706
                // first (see `resolveClosureParamTypesFromUsage`) — e.g. `cb = |v|
35af6cf 1707
                // x + v` with a captured `Float` `x` resolves `v` to `Float` from that
35af6cf 1708
                // `Binary` op — then infer the return type against an env where the
35af6cf 1709
                // params are already bound concretely (bypassing the checker's
35af6cf 1710
                // Closure-inference arm entirely, since it always re-binds params to
35af6cf 1711
                // fresh TVars regardless of what's already in the env passed to it).
3d6f280 1712
                let resolved_params = resolveClosureParamTypesFromUsage(cl, &self.env, &self.cctx);
35af6cf 1713
                let param_ptypes: Vec<PlumType> = cl.params.iter()
35af6cf 1714
                    .map(|p| resolved_params.get(p).cloned().unwrap_or(PlumType::TInt))
35af6cf 1715
                    .collect();
3d6f280 1716
                let param_vts: Vec<ValType> = param_ptypes.iter().map(plumTypeToValtype).collect();
35af6cf 1717
35af6cf 1718
                let mut body_env = self.env.clone();
35af6cf 1719
                for (p, ty) in cl.params.iter().zip(param_ptypes.iter()) {
35af6cf 1720
                    body_env.insert(p.clone(), TypeScheme::mono(ty.clone()));
4ba0db3 1721
                }
35af6cf 1722
                let ret_ty = match cl.body.stmts.last() {
3d6f280 1723
                    Some(ast::Stmt::Expr(e)) => plum_checker::inferExpr(e, &body_env, &self.cctx).ok(),
3d6f280 1724
                    Some(ast::Stmt::Return(Some(e))) => plum_checker::inferExpr(e, &body_env, &self.cctx).ok(),
35af6cf 1725
                    _ => Some(PlumType::TUnit),
35af6cf 1726
                };
35af6cf 1727
                let ret_vt = match ret_ty {
35af6cf 1728
                    Some(PlumType::TUnit) => None,
35af6cf 1729
                    Some(PlumType::TVar(_)) | None => Some(ValType::I64), // unresolved: preserve prior Int-default behavior
3d6f280 1730
                    Some(other) => Some(plumTypeToValtype(&other)),
35af6cf 1731
                };
35af6cf 1732
                (param_vts, param_ptypes, ret_vt)
4ba0db3 1733
            }
4ba0db3 1734
        };
3d6f280 1735
        let free_vars = collectFreeVars(cl, &self.env, self.fn_decls);
4ba0db3 1736
        self.found.push(RawClosure {
4ba0db3 1737
            ptr: cl as *const ast::Closure as usize,
4ba0db3 1738
            closure: cl,
4ba0db3 1739
            param_vts,
35af6cf 1740
            param_ptypes: param_ptypes.clone(),
4ba0db3 1741
            ret_vt,
4ba0db3 1742
            free_vars,
4ba0db3 1743
        });
35af6cf 1744
35af6cf 1745
        // Recurse into this closure's own body to find any closure literals nested
35af6cf 1746
        // inside it (`|v| |w| v + w`, or a closure literal used inside a `match`/`if`
35af6cf 1747
        // within this one's body). Each nested closure gets registered exactly like a
35af6cf 1748
        // top-level one, seeing this closure's own params as locals in scope — which
35af6cf 1749
        // is also what makes its free-variable analysis correctly capture a name from
35af6cf 1750
        // *this* closure's scope (rather than silently missing it): once discovery
35af6cf 1751
        // finishes, `ctx.closures` holds every closure at every depth before any body
35af6cf 1752
        // is compiled, so the single fixed "compile each registered closure" pass in
3d6f280 1753
        // `compileSource` already handles arbitrary nesting with no further changes.
35af6cf 1754
        let saved_env = self.env.clone();
35af6cf 1755
        let saved_locals = self.locals.clone();
35af6cf 1756
        for (p, ty) in cl.params.iter().zip(param_ptypes.iter()) {
35af6cf 1757
            self.env.insert(p.clone(), TypeScheme::mono(ty.clone()));
35af6cf 1758
            self.locals.insert(p.clone());
35af6cf 1759
        }
3d6f280 1760
        self.walkBlock(&cl.body);
35af6cf 1761
        self.env = saved_env;
35af6cf 1762
        self.locals = saved_locals;
4ba0db3 1763
    }
4ba0db3 1764
}
4ba0db3 1765
3d6f280 1766
fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
4ba0db3 1767
    match arg {
4ba0db3 1768
        ast::Arg::Positional(e) => e,
4ba0db3 1769
        ast::Arg::Keyword { value, .. } => value,
4ba0db3 1770
        ast::Arg::Pair { value, .. } => value,
4ba0db3 1771
    }
4ba0db3 1772
}
4ba0db3 1773
35af6cf 1774
/// Resolves as many of a closure's param types as possible from how they're actually
35af6cf 1775
/// used in its body — e.g. `|v| x + v` with a captured `Float` `x` resolves `v` to
35af6cf 1776
/// `Float` from that `Binary` op, or `|v| helper(v)` where `helper`'s declared param
35af6cf 1777
/// type is concrete resolves `v` to that. A param never used in a way that pins down
35af6cf 1778
/// a concrete type simply doesn't appear in the returned map (callers fall back to
35af6cf 1779
/// `Int`, matching the prior default). This is intentionally a shallow, best-effort
35af6cf 1780
/// scan — not full unification — scoped to fixing the specific `call_indirect`
35af6cf 1781
/// signature-mismatch gap this exists for, not replacing the checker's inference.
3d6f280 1782
fn resolveClosureParamTypesFromUsage(
35af6cf 1783
    cl: &ast::Closure,
35af6cf 1784
    env: &TypeEnv,
35af6cf 1785
    cctx: &plum_checker::CheckCtx,
35af6cf 1786
) -> HashMap<String, PlumType> {
35af6cf 1787
    let params: std::collections::HashSet<String> = cl.params.iter().cloned().collect();
35af6cf 1788
    let mut resolved: HashMap<String, PlumType> = HashMap::new();
35af6cf 1789
    for stmt in &cl.body.stmts {
3d6f280 1790
        scanStmtForParamTypes(stmt, &params, env, cctx, &mut resolved);
35af6cf 1791
    }
35af6cf 1792
    resolved
35af6cf 1793
}
35af6cf 1794
3d6f280 1795
fn scanStmtForParamTypes(
35af6cf 1796
    stmt: &ast::Stmt,
35af6cf 1797
    params: &std::collections::HashSet<String>,
35af6cf 1798
    env: &TypeEnv,
35af6cf 1799
    cctx: &plum_checker::CheckCtx,
35af6cf 1800
    resolved: &mut HashMap<String, PlumType>,
35af6cf 1801
) {
35af6cf 1802
    match stmt {
35af6cf 1803
        ast::Stmt::Assign(a) => {
35af6cf 1804
            for v in &a.values {
3d6f280 1805
                scanExprForParamTypes(v, params, env, cctx, resolved);
35af6cf 1806
            }
35af6cf 1807
        }
35af6cf 1808
        ast::Stmt::Return(Some(e)) | ast::Stmt::Expr(e) | ast::Stmt::Assert(e) => {
3d6f280 1809
            scanExprForParamTypes(e, params, env, cctx, resolved);
35af6cf 1810
        }
35af6cf 1811
        ast::Stmt::If(i) => {
3d6f280 1812
            scanExprForParamTypes(&i.condition, params, env, cctx, resolved);
35af6cf 1813
            for s in &i.body.stmts {
3d6f280 1814
                scanStmtForParamTypes(s, params, env, cctx, resolved);
35af6cf 1815
            }
35af6cf 1816
            for ei in &i.else_ifs {
3d6f280 1817
                scanExprForParamTypes(&ei.condition, params, env, cctx, resolved);
35af6cf 1818
                for s in &ei.body.stmts {
3d6f280 1819
                    scanStmtForParamTypes(s, params, env, cctx, resolved);
35af6cf 1820
                }
35af6cf 1821
            }
35af6cf 1822
            if let Some(e) = &i.else_ {
35af6cf 1823
                for s in &e.stmts {
3d6f280 1824
                    scanStmtForParamTypes(s, params, env, cctx, resolved);
35af6cf 1825
                }
35af6cf 1826
            }
35af6cf 1827
        }
35af6cf 1828
        ast::Stmt::While(w) => {
3d6f280 1829
            scanExprForParamTypes(&w.condition, params, env, cctx, resolved);
35af6cf 1830
            for s in &w.body.stmts {
3d6f280 1831
                scanStmtForParamTypes(s, params, env, cctx, resolved);
35af6cf 1832
            }
35af6cf 1833
        }
35af6cf 1834
        ast::Stmt::For(f) => {
3d6f280 1835
            scanExprForParamTypes(&f.iter, params, env, cctx, resolved);
35af6cf 1836
            for s in &f.body.stmts {
3d6f280 1837
                scanStmtForParamTypes(s, params, env, cctx, resolved);
35af6cf 1838
            }
35af6cf 1839
        }
35af6cf 1840
        _ => {}
35af6cf 1841
    }
35af6cf 1842
}
35af6cf 1843
3d6f280 1844
fn scanExprForParamTypes(
35af6cf 1845
    expr: &ast::Expr,
35af6cf 1846
    params: &std::collections::HashSet<String>,
35af6cf 1847
    env: &TypeEnv,
35af6cf 1848
    cctx: &plum_checker::CheckCtx,
35af6cf 1849
    resolved: &mut HashMap<String, PlumType>,
35af6cf 1850
) {
35af6cf 1851
    match expr {
35af6cf 1852
        ast::Expr::Binary(b) => {
3d6f280 1853
            tryResolveParamFromPair(&b.left, &b.right, params, env, cctx, resolved);
3d6f280 1854
            scanExprForParamTypes(&b.left, params, env, cctx, resolved);
3d6f280 1855
            scanExprForParamTypes(&b.right, params, env, cctx, resolved);
35af6cf 1856
        }
35af6cf 1857
        ast::Expr::Compare(c) => {
3d6f280 1858
            tryResolveParamFromPair(&c.left, &c.right, params, env, cctx, resolved);
3d6f280 1859
            scanExprForParamTypes(&c.left, params, env, cctx, resolved);
3d6f280 1860
            scanExprForParamTypes(&c.right, params, env, cctx, resolved);
35af6cf 1861
        }
35af6cf 1862
        ast::Expr::Bool(b) => {
3d6f280 1863
            scanExprForParamTypes(&b.left, params, env, cctx, resolved);
3d6f280 1864
            scanExprForParamTypes(&b.right, params, env, cctx, resolved);
35af6cf 1865
        }
3d6f280 1866
        ast::Expr::Not(inner) => scanExprForParamTypes(inner, params, env, cctx, resolved),
3d6f280 1867
        ast::Expr::Unary(u) => scanExprForParamTypes(&u.operand, params, env, cctx, resolved),
3d6f280 1868
        ast::Expr::Paren(inner) => scanExprForParamTypes(inner, params, env, cctx, resolved),
35af6cf 1869
        ast::Expr::Ternary(t) => {
3d6f280 1870
            scanExprForParamTypes(&t.condition, params, env, cctx, resolved);
3d6f280 1871
            scanExprForParamTypes(&t.then, params, env, cctx, resolved);
3d6f280 1872
            scanExprForParamTypes(&t.else_, params, env, cctx, resolved);
35af6cf 1873
        }
35af6cf 1874
        ast::Expr::FnCall(call) => {
35af6cf 1875
            if let Ok(PlumType::TFun(param_types, _)) = plum_checker::lookup(env, &call.name) {
35af6cf 1876
                for (arg, expected) in call.args.iter().zip(param_types.iter()) {
3d6f280 1877
                    let arg_expr = argExprOf(arg);
35af6cf 1878
                    if let ast::Expr::Var(n) = arg_expr {
35af6cf 1879
                        if params.contains(n) && !resolved.contains_key(n) && !matches!(expected, PlumType::TVar(_)) {
35af6cf 1880
                            resolved.insert(n.clone(), expected.clone());
35af6cf 1881
                        }
35af6cf 1882
                    }
35af6cf 1883
                }
35af6cf 1884
            }
35af6cf 1885
            for arg in &call.args {
3d6f280 1886
                scanExprForParamTypes(argExprOf(arg), params, env, cctx, resolved);
35af6cf 1887
            }
35af6cf 1888
        }
35af6cf 1889
        ast::Expr::ClassCall(call) => {
35af6cf 1890
            for fa in &call.fields {
3d6f280 1891
                scanExprForParamTypes(&fa.value, params, env, cctx, resolved);
35af6cf 1892
            }
35af6cf 1893
        }
35af6cf 1894
        ast::Expr::Attribute(a) => {
35af6cf 1895
            // `c.field` on a bare, unresolved param implies `c`'s type is whichever
35af6cf 1896
            // class declares that field name — ambiguous if more than one class has
35af6cf 1897
            // a field by that name, but resolvable in the common case.
35af6cf 1898
            if let ast::AttrKind::Field(field_name) = &a.attr {
35af6cf 1899
                if let ast::Expr::Var(n) = &a.object {
35af6cf 1900
                    if params.contains(n) && !resolved.contains_key(n) {
35af6cf 1901
                        let mut matches = cctx.classes.iter().filter(|(_, fields)| fields.iter().any(|(fname, _)| fname == field_name));
35af6cf 1902
                        if let (Some((class_name, _)), None) = (matches.next(), matches.next()) {
35af6cf 1903
                            resolved.insert(n.clone(), PlumType::TNamed(class_name.clone()));
35af6cf 1904
                        }
35af6cf 1905
                    }
35af6cf 1906
                }
35af6cf 1907
            }
3d6f280 1908
            scanExprForParamTypes(&a.object, params, env, cctx, resolved);
35af6cf 1909
            if let ast::AttrKind::Method(call) = &a.attr {
35af6cf 1910
                for arg in &call.args {
3d6f280 1911
                    scanExprForParamTypes(argExprOf(arg), params, env, cctx, resolved);
35af6cf 1912
                }
35af6cf 1913
            }
35af6cf 1914
        }
35af6cf 1915
        _ => {}
35af6cf 1916
    }
35af6cf 1917
}
35af6cf 1918
35af6cf 1919
/// If either side of a `Binary`/`Compare` operand pair is a bare reference to an
35af6cf 1920
/// unresolved param and the *other* side has a concrete (non-`TVar`) inferred type,
35af6cf 1921
/// binds the param to that type.
3d6f280 1922
fn tryResolveParamFromPair(
35af6cf 1923
    left: &ast::Expr,
35af6cf 1924
    right: &ast::Expr,
35af6cf 1925
    params: &std::collections::HashSet<String>,
35af6cf 1926
    env: &TypeEnv,
35af6cf 1927
    cctx: &plum_checker::CheckCtx,
35af6cf 1928
    resolved: &mut HashMap<String, PlumType>,
35af6cf 1929
) {
35af6cf 1930
    if let ast::Expr::Var(n) = left {
35af6cf 1931
        if params.contains(n) && !resolved.contains_key(n) {
3d6f280 1932
            if let Ok(ty) = plum_checker::inferExpr(right, env, cctx) {
35af6cf 1933
                if !matches!(ty, PlumType::TVar(_)) {
35af6cf 1934
                    resolved.insert(n.clone(), ty);
35af6cf 1935
                }
35af6cf 1936
            }
35af6cf 1937
        }
35af6cf 1938
    }
35af6cf 1939
    if let ast::Expr::Var(n) = right {
35af6cf 1940
        if params.contains(n) && !resolved.contains_key(n) {
3d6f280 1941
            if let Ok(ty) = plum_checker::inferExpr(left, env, cctx) {
35af6cf 1942
                if !matches!(ty, PlumType::TVar(_)) {
35af6cf 1943
                    resolved.insert(n.clone(), ty);
35af6cf 1944
                }
35af6cf 1945
            }
35af6cf 1946
        }
35af6cf 1947
    }
35af6cf 1948
}
35af6cf 1949
4ba0db3 1950
/// Determines a closure's captured free variables: every `Var` referenced in its body
4ba0db3 1951
/// that is neither one of the closure's own params nor assigned locally inside the body,
4ba0db3 1952
/// in first-appearance order. Each free variable's type is looked up in the *enclosing*
4ba0db3 1953
/// scope's type env.
3d6f280 1954
fn collectFreeVars(
35af6cf 1955
    cl: &ast::Closure,
35af6cf 1956
    env: &TypeEnv,
35af6cf 1957
    fn_decls: &HashMap<String, &ast::Fn>,
35af6cf 1958
) -> Vec<(String, PlumType)> {
4ba0db3 1959
    let mut bound: std::collections::HashSet<String> = cl.params.iter().cloned().collect();
3d6f280 1960
    fvCollectBoundBlock(&cl.body, &mut bound);
4ba0db3 1961
4ba0db3 1962
    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
4ba0db3 1963
    let mut free: Vec<(String, PlumType)> = Vec::new();
3d6f280 1964
    fvCollectRefsBlock(&cl.body, &bound, &mut seen, &mut free, env, fn_decls);
4ba0db3 1965
    free
4ba0db3 1966
}
4ba0db3 1967
3d6f280 1968
fn fvCollectBoundBlock(block: &ast::Block, bound: &mut std::collections::HashSet<String>) {
4ba0db3 1969
    for s in &block.stmts {
4ba0db3 1970
        match s {
4ba0db3 1971
            ast::Stmt::Assign(a) => {
4ba0db3 1972
                for t in &a.targets {
01f9be3 1973
                    if let ast::AssignTarget::Var(name) = t {
01f9be3 1974
                        bound.insert(name.clone());
01f9be3 1975
                    }
4ba0db3 1976
                }
4ba0db3 1977
            }
4ba0db3 1978
            ast::Stmt::For(f) => {
4ba0db3 1979
                for v in &f.vars {
4ba0db3 1980
                    bound.insert(v.clone());
4ba0db3 1981
                }
3d6f280 1982
                fvCollectBoundBlock(&f.body, bound);
4ba0db3 1983
            }
4ba0db3 1984
            ast::Stmt::If(i) => {
3d6f280 1985
                fvCollectBoundBlock(&i.body, bound);
4ba0db3 1986
                for ei in &i.else_ifs {
3d6f280 1987
                    fvCollectBoundBlock(&ei.body, bound);
4ba0db3 1988
                }
4ba0db3 1989
                if let Some(e) = &i.else_ {
3d6f280 1990
                    fvCollectBoundBlock(e, bound);
4ba0db3 1991
                }
4ba0db3 1992
            }
3d6f280 1993
            ast::Stmt::While(w) => fvCollectBoundBlock(&w.body, bound),
4ba0db3 1994
            ast::Stmt::Match(m) => {
4ba0db3 1995
                for case in &m.cases {
4ba0db3 1996
                    for p in &case.patterns {
3d6f280 1997
                        fvCollectPatternBindings(p, bound);
4ba0db3 1998
                    }
3d6f280 1999
                    fvCollectBoundBlock(&case.body, bound);
4ba0db3 2000
                }
4ba0db3 2001
            }
4ba0db3 2002
            _ => {}
4ba0db3 2003
        }
4ba0db3 2004
    }
4ba0db3 2005
}
4ba0db3 2006
3d6f280 2007
fn fvCollectPatternBindings(pat: &ast::CasePattern, bound: &mut std::collections::HashSet<String>) {
4ba0db3 2008
    match pat {
4ba0db3 2009
        ast::CasePattern::Name(n) => { bound.insert(n.clone()); }
4ba0db3 2010
        ast::CasePattern::Class { fields, .. } => {
4ba0db3 2011
            for f in fields {
3d6f280 2012
                fvCollectPatternBindings(f, bound);
4ba0db3 2013
            }
4ba0db3 2014
        }
4ba0db3 2015
        _ => {}
4ba0db3 2016
    }
4ba0db3 2017
}
4ba0db3 2018
3d6f280 2019
fn fvCollectRefsBlock(
4ba0db3 2020
    block: &ast::Block,
4ba0db3 2021
    bound: &std::collections::HashSet<String>,
4ba0db3 2022
    seen: &mut std::collections::HashSet<String>,
4ba0db3 2023
    free: &mut Vec<(String, PlumType)>,
4ba0db3 2024
    env: &TypeEnv,
35af6cf 2025
    fn_decls: &HashMap<String, &ast::Fn>,
4ba0db3 2026
) {
4ba0db3 2027
    for s in &block.stmts {
4ba0db3 2028
        match s {
4ba0db3 2029
            ast::Stmt::Assign(a) => {
4ba0db3 2030
                for v in &a.values {
3d6f280 2031
                    fvCollectRefsExpr(v, bound, seen, free, env, fn_decls);
4ba0db3 2032
                }
01f9be3 2033
                for t in &a.targets {
01f9be3 2034
                    if let ast::AssignTarget::Field(object, _) = t {
3d6f280 2035
                        fvCollectRefsExpr(object, bound, seen, free, env, fn_decls);
01f9be3 2036
                    }
01f9be3 2037
                }
4ba0db3 2038
            }
4ba0db3 2039
            ast::Stmt::Return(Some(e)) | ast::Stmt::Expr(e) | ast::Stmt::Assert(e) => {
3d6f280 2040
                fvCollectRefsExpr(e, bound, seen, free, env, fn_decls);
4ba0db3 2041
            }
4ba0db3 2042
            ast::Stmt::If(i) => {
3d6f280 2043
                fvCollectRefsExpr(&i.condition, bound, seen, free, env, fn_decls);
3d6f280 2044
                fvCollectRefsBlock(&i.body, bound, seen, free, env, fn_decls);
4ba0db3 2045
                for ei in &i.else_ifs {
3d6f280 2046
                    fvCollectRefsExpr(&ei.condition, bound, seen, free, env, fn_decls);
3d6f280 2047
                    fvCollectRefsBlock(&ei.body, bound, seen, free, env, fn_decls);
4ba0db3 2048
                }
4ba0db3 2049
                if let Some(e) = &i.else_ {
3d6f280 2050
                    fvCollectRefsBlock(e, bound, seen, free, env, fn_decls);
4ba0db3 2051
                }
4ba0db3 2052
            }
4ba0db3 2053
            ast::Stmt::While(w) => {
3d6f280 2054
                fvCollectRefsExpr(&w.condition, bound, seen, free, env, fn_decls);
3d6f280 2055
                fvCollectRefsBlock(&w.body, bound, seen, free, env, fn_decls);
4ba0db3 2056
            }
4ba0db3 2057
            ast::Stmt::For(f) => {
3d6f280 2058
                fvCollectRefsExpr(&f.iter, bound, seen, free, env, fn_decls);
3d6f280 2059
                fvCollectRefsBlock(&f.body, bound, seen, free, env, fn_decls);
4ba0db3 2060
            }
4ba0db3 2061
            ast::Stmt::Match(m) => {
4ba0db3 2062
                for subj in &m.subjects {
3d6f280 2063
                    fvCollectRefsExpr(subj, bound, seen, free, env, fn_decls);
4ba0db3 2064
                }
4ba0db3 2065
                for case in &m.cases {
3d6f280 2066
                    fvCollectRefsBlock(&case.body, bound, seen, free, env, fn_decls);
4ba0db3 2067
                }
4ba0db3 2068
            }
4ba0db3 2069
            _ => {}
4ba0db3 2070
        }
4ba0db3 2071
    }
4ba0db3 2072
}
4ba0db3 2073
3d6f280 2074
fn fvCollectRefsExpr(
4ba0db3 2075
    expr: &ast::Expr,
4ba0db3 2076
    bound: &std::collections::HashSet<String>,
4ba0db3 2077
    seen: &mut std::collections::HashSet<String>,
4ba0db3 2078
    free: &mut Vec<(String, PlumType)>,
4ba0db3 2079
    env: &TypeEnv,
35af6cf 2080
    fn_decls: &HashMap<String, &ast::Fn>,
4ba0db3 2081
) {
4ba0db3 2082
    match expr {
4ba0db3 2083
        ast::Expr::Var(name) => {
35af6cf 2084
            // A top-level (non-method) function referenced bare (e.g. `each(double)`)
35af6cf 2085
            // is not a captured variable — it's compiled as a static trampoline
35af6cf 2086
            // reference (see `named_fn_values`), not loaded from an enclosing local.
35af6cf 2087
            let is_named_fn_ref = fn_decls.get(name).is_some_and(|f| f.type_param.is_none());
35af6cf 2088
            if !is_named_fn_ref && !bound.contains(name) && seen.insert(name.clone()) {
4ba0db3 2089
                let ty = plum_checker::lookup(env, name).unwrap_or(PlumType::TInt);
4ba0db3 2090
                free.push((name.clone(), ty));
4ba0db3 2091
            }
4ba0db3 2092
        }
3d6f280 2093
        ast::Expr::Binary(b) => { fvCollectRefsExpr(&b.left, bound, seen, free, env, fn_decls); fvCollectRefsExpr(&b.right, bound, seen, free, env, fn_decls); }
3d6f280 2094
        ast::Expr::Bool(b) => { fvCollectRefsExpr(&b.left, bound, seen, free, env, fn_decls); fvCollectRefsExpr(&b.right, bound, seen, free, env, fn_decls); }
3d6f280 2095
        ast::Expr::Compare(c) => { fvCollectRefsExpr(&c.left, bound, seen, free, env, fn_decls); fvCollectRefsExpr(&c.right, bound, seen, free, env, fn_decls); }
3d6f280 2096
        ast::Expr::Not(inner) => fvCollectRefsExpr(inner, bound, seen, free, env, fn_decls),
3d6f280 2097
        ast::Expr::Unary(u) => fvCollectRefsExpr(&u.operand, bound, seen, free, env, fn_decls),
3d6f280 2098
        ast::Expr::Paren(inner) => fvCollectRefsExpr(inner, bound, seen, free, env, fn_decls),
4ba0db3 2099
        ast::Expr::Ternary(t) => {
3d6f280 2100
            fvCollectRefsExpr(&t.condition, bound, seen, free, env, fn_decls);
3d6f280 2101
            fvCollectRefsExpr(&t.then, bound, seen, free, env, fn_decls);
3d6f280 2102
            fvCollectRefsExpr(&t.else_, bound, seen, free, env, fn_decls);
4ba0db3 2103
        }
4ba0db3 2104
        ast::Expr::FnCall(call) => {
4ba0db3 2105
            for arg in &call.args {
3d6f280 2106
                fvCollectRefsExpr(argExprOf(arg), bound, seen, free, env, fn_decls);
4ba0db3 2107
            }
4ba0db3 2108
        }
4ba0db3 2109
        ast::Expr::ClassCall(call) => {
4ba0db3 2110
            for fa in &call.fields {
3d6f280 2111
                fvCollectRefsExpr(&fa.value, bound, seen, free, env, fn_decls);
4ba0db3 2112
            }
4ba0db3 2113
        }
4ba0db3 2114
        ast::Expr::Attribute(a) => {
3d6f280 2115
            fvCollectRefsExpr(&a.object, bound, seen, free, env, fn_decls);
4ba0db3 2116
            if let ast::AttrKind::Method(call) = &a.attr {
4ba0db3 2117
                for arg in &call.args {
3d6f280 2118
                    fvCollectRefsExpr(argExprOf(arg), bound, seen, free, env, fn_decls);
4ba0db3 2119
                }
4ba0db3 2120
            }
4ba0db3 2121
        }
35af6cf 2122
        ast::Expr::Closure(inner) => {
35af6cf 2123
            // A name the *inner* closure references that isn't bound by the inner
35af6cf 2124
            // itself (its own params/locals) and isn't bound by *this* (outer)
35af6cf 2125
            // closure either is a genuine multi-level capture: this outer closure
35af6cf 2126
            // also needs to capture it from its own enclosing scope, in order to
35af6cf 2127
            // pass it down when it later constructs the inner closure. A name the
35af6cf 2128
            // inner references that the outer already binds (e.g. one of the
35af6cf 2129
            // outer's own params) needs no such propagation — the outer's compiled
35af6cf 2130
            // body can already reference it as an ordinary local when snapshotting
35af6cf 2131
            // the inner closure's env, so it's deliberately excluded here by
35af6cf 2132
            // unioning `bound` (outer) with the inner's own bound set below, rather
35af6cf 2133
            // than passing the inner's bound set alone.
35af6cf 2134
            let mut inner_bound = bound.clone();
35af6cf 2135
            for p in &inner.params {
35af6cf 2136
                inner_bound.insert(p.clone());
35af6cf 2137
            }
3d6f280 2138
            fvCollectBoundBlock(&inner.body, &mut inner_bound);
3d6f280 2139
            fvCollectRefsBlock(&inner.body, &inner_bound, seen, free, env, fn_decls);
35af6cf 2140
        }
4ba0db3 2141
        _ => {}
4ba0db3 2142
    }
4ba0db3 2143
}
4ba0db3 2144
5d8ada1 2145
/// Walks a function body once to determine: (1) every locally-assigned/bound name and
5d8ada1 2146
/// its inferred type, (2) how many `ClassCall` scratch temporaries it needs, and (3)
5d8ada1 2147
/// the subject type for every `match` statement (for its own scratch temporary).
5d8ada1 2148
struct Collector<'a> {
5d8ada1 2149
    env: TypeEnv,
5d8ada1 2150
    cctx: plum_checker::CheckCtx<'a>,
5d8ada1 2151
    named: Vec<(String, PlumType)>,
5d8ada1 2152
    named_set: std::collections::HashSet<String>,
35af6cf 2153
    /// One scratch-local type per subject (usually one, more for `match a, b, ...`).
35af6cf 2154
    match_scratch: HashMap<usize, Vec<PlumType>>,
0e39618 2155
    /// `CasePattern::Class` identity -> its scratch local. Covers EVERY constructor
0e39618 2156
    /// pattern, including top-level ones — under wasm-gc, matching `Some(v)` needs a
0e39618 2157
    /// `ref.cast` from the subject's static supertype down to the concrete variant
0e39618 2158
    /// type before any `struct.get` on it validates, and that narrowed value needs
0e39618 2159
    /// its OWN local (declared with the concrete variant's ref type) distinct from
0e39618 2160
    /// the original wide-typed subject local, which stays declared at the
0e39618 2161
    /// supertype's type for the whole function. (Before wasm-gc, this only covered
0e39618 2162
    /// patterns nested inside another constructor pattern's fields, depth >= 1,
0e39618 2163
    /// since a plain i32 pointer needed no per-pattern static type at all.)
35af6cf 2164
    nested_class_scratch: HashMap<usize, u32>,
0e39618 2165
    /// Slot number (the `u32` values in `nested_class_scratch`) -> the variant name
0e39618 2166
    /// it narrows to, so its scratch local can be declared with that variant's exact
0e39618 2167
    /// concrete ref type instead of a uniform placeholder type.
0e39618 2168
    nested_class_scratch_types: Vec<String>,
35af6cf 2169
    next_nested_class_slot: u32,
da1c377 2170
    /// `For` stmt identity (pointer address) -> a slot number; each slot reserves 2
da1c377 2171
    /// consecutive `i32` scratch locals for variadic iteration (`for v in nums`):
da1c377 2172
    /// [count, loop index]. Only `for` statements whose iterable is a `TVariadic`
da1c377 2173
    /// use this — an ordinary range `for` reuses its own loop var as the counter
da1c377 2174
    /// and needs no extra scratch locals.
da1c377 2175
    variadic_for_scratch: HashMap<usize, u32>,
da1c377 2176
    next_variadic_for_slot: u32,
bb8ca38 2177
}
bb8ca38 2178
5d8ada1 2179
impl<'a> Collector<'a> {
5d8ada1 2180
    fn bind(&mut self, name: &str, ty: PlumType) {
5d8ada1 2181
        if self.named_set.insert(name.to_string()) {
5d8ada1 2182
            self.named.push((name.to_string(), ty.clone()));
5d8ada1 2183
        }
5d8ada1 2184
        self.env.insert(name.to_string(), TypeScheme::mono(ty));
bb8ca38 2185
    }
bb8ca38 2186
35af6cf 2187
    /// Binds every `Name` sub-pattern anywhere inside `pat` (at any nesting depth) to
35af6cf 2188
    /// its correct field type, and reserves a scratch local for every `Class`
35af6cf 2189
    /// sub-pattern found *nested* inside another constructor pattern's fields (the
35af6cf 2190
    /// outermost, per-subject pattern doesn't need one — see `nested_class_scratch`).
0e39618 2191
    fn collectPattern(&mut self, pat: &ast::CasePattern, ty: &PlumType) {
35af6cf 2192
        match pat {
35af6cf 2193
            ast::CasePattern::Name(n) => {
35af6cf 2194
                let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
35af6cf 2195
                    && self.cctx.enum_variants.contains_key(n);
35af6cf 2196
                if !is_variant {
35af6cf 2197
                    self.bind(n, ty.clone());
35af6cf 2198
                }
35af6cf 2199
            }
35af6cf 2200
            ast::CasePattern::Class { name, fields } => {
0e39618 2201
                let key = pat as *const ast::CasePattern as usize;
0e39618 2202
                let slot = self.next_nested_class_slot;
0e39618 2203
                self.next_nested_class_slot += 1;
0e39618 2204
                self.nested_class_scratch.insert(key, slot);
0e39618 2205
                self.nested_class_scratch_types.push(name.clone());
35af6cf 2206
                if let Some(info) = self.cctx.enum_variants.get(name) {
35af6cf 2207
                    let field_types = info.field_types.clone();
35af6cf 2208
                    for (f, fty) in fields.iter().zip(field_types.iter()) {
0e39618 2209
                        self.collectPattern(f, fty);
35af6cf 2210
                    }
35af6cf 2211
                }
35af6cf 2212
            }
35af6cf 2213
            _ => {}
35af6cf 2214
        }
35af6cf 2215
    }
35af6cf 2216
3d6f280 2217
    fn walkBlock(&mut self, block: &ast::Block) {
5d8ada1 2218
        for s in &block.stmts {
3d6f280 2219
            self.walkStmt(s);
5d8ada1 2220
        }
5d8ada1 2221
    }
5d8ada1 2222
3d6f280 2223
    fn walkStmt(&mut self, stmt: &ast::Stmt) {
5d8ada1 2224
        match stmt {
5d8ada1 2225
            ast::Stmt::Assign(a) => {
5d8ada1 2226
                for (target, value) in a.targets.iter().zip(a.values.iter()) {
3d6f280 2227
                    self.walkExpr(value);
01f9be3 2228
                    match target {
01f9be3 2229
                        ast::AssignTarget::Var(name) => {
01f9be3 2230
                            let ty = if matches!(value, ast::Expr::Closure(_)) {
3d6f280 2231
                                // The checker's own closure inference (`inferExpr` on
01f9be3 2232
                                // `Expr::Closure`) infers the return type by recursively
01f9be3 2233
                                // inferring the body's tail expression with each param bound
01f9be3 2234
                                // to a fresh, unconstrained `TVar` — e.g. a captured/param
01f9be3 2235
                                // attribute access (`c.age`) on a `TVar`-typed object isn't a
01f9be3 2236
                                // known class, so it errors out entirely, and this call site
01f9be3 2237
                                // then silently defaults to `TInt` — the *wrong* wasm local
01f9be3 2238
                                // width for what's actually always an `i32` pointer. All that
01f9be3 2239
                                // actually matters here is the local's wasm width, and every
01f9be3 2240
                                // closure value is an i32 pointer regardless of its
01f9be3 2241
                                // parameter/return types, so skip inference entirely.
01f9be3 2242
                                PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
01f9be3 2243
                            } else {
3d6f280 2244
                                plum_checker::inferExpr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt)
01f9be3 2245
                            };
01f9be3 2246
                            self.bind(name, ty);
01f9be3 2247
                        }
01f9be3 2248
                        ast::AssignTarget::Field(object, _) => {
3d6f280 2249
                            self.walkExpr(object);
01f9be3 2250
                        }
01f9be3 2251
                    }
bb8ca38 2252
                }
bb8ca38 2253
            }
3d6f280 2254
            ast::Stmt::Return(Some(e)) => self.walkExpr(e),
5d8ada1 2255
            ast::Stmt::Return(None) => {}
5d8ada1 2256
            ast::Stmt::If(i) => {
3d6f280 2257
                self.walkExpr(&i.condition);
3d6f280 2258
                self.walkBlock(&i.body);
5d8ada1 2259
                for ei in &i.else_ifs {
3d6f280 2260
                    self.walkExpr(&ei.condition);
3d6f280 2261
                    self.walkBlock(&ei.body);
5d8ada1 2262
                }
5d8ada1 2263
                if let Some(e) = &i.else_ {
3d6f280 2264
                    self.walkBlock(e);
5d8ada1 2265
                }
bb8ca38 2266
            }
5d8ada1 2267
            ast::Stmt::While(w) => {
3d6f280 2268
                self.walkExpr(&w.condition);
3d6f280 2269
                self.walkBlock(&w.body);
5d8ada1 2270
            }
5d8ada1 2271
            ast::Stmt::For(f) => {
3d6f280 2272
                self.walkExpr(&f.iter);
3d6f280 2273
                let iter_ty = plum_checker::inferExpr(&f.iter, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
da1c377 2274
                if let PlumType::TVariadic(elem) = &iter_ty {
da1c377 2275
                    let idx = self.next_variadic_for_slot;
da1c377 2276
                    self.next_variadic_for_slot += 1;
da1c377 2277
                    self.variadic_for_scratch.insert(f as *const ast::For as usize, idx);
da1c377 2278
                    for v in &f.vars {
da1c377 2279
                        self.bind(v, (**elem).clone());
da1c377 2280
                    }
da1c377 2281
                } else {
da1c377 2282
                    for v in &f.vars {
da1c377 2283
                        self.bind(v, PlumType::TInt);
da1c377 2284
                    }
5d8ada1 2285
                }
3d6f280 2286
                self.walkBlock(&f.body);
bb8ca38 2287
            }
3d6f280 2288
            ast::Stmt::Expr(e) => self.walkExpr(e),
3d6f280 2289
            ast::Stmt::Assert(e) => self.walkExpr(e),
5d8ada1 2290
            ast::Stmt::Match(m) => {
35af6cf 2291
                let subject_types: Vec<PlumType> = m.subjects.iter().map(|s| {
3d6f280 2292
                    self.walkExpr(s);
3d6f280 2293
                    plum_checker::inferExpr(s, &self.env, &self.cctx).unwrap_or(PlumType::TInt)
35af6cf 2294
                }).collect();
35af6cf 2295
                self.match_scratch.insert(m as *const ast::Match as usize, subject_types.clone());
5d8ada1 2296
                for case in &m.cases {
5d8ada1 2297
                    let saved = self.env.clone();
35af6cf 2298
                    for (pat, subject_ty) in case.patterns.iter().zip(subject_types.iter()) {
0e39618 2299
                        self.collectPattern(pat, subject_ty);
5d8ada1 2300
                    }
3d6f280 2301
                    self.walkBlock(&case.body);
5d8ada1 2302
                    self.env = saved;
5d8ada1 2303
                }
5d8ada1 2304
            }
5d8ada1 2305
            ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
bb8ca38 2306
        }
5d8ada1 2307
    }
5d8ada1 2308
3d6f280 2309
    fn walkExpr(&mut self, expr: &ast::Expr) {
5d8ada1 2310
        match expr {
5d8ada1 2311
            ast::Expr::ClassCall(call) => {
5d8ada1 2312
                for fa in &call.fields {
3d6f280 2313
                    self.walkExpr(&fa.value);
5d8ada1 2314
                }
5d8ada1 2315
            }
5d8ada1 2316
            ast::Expr::Binary(b) => {
3d6f280 2317
                self.walkExpr(&b.left);
3d6f280 2318
                self.walkExpr(&b.right);
5d8ada1 2319
            }
5d8ada1 2320
            ast::Expr::Bool(b) => {
3d6f280 2321
                self.walkExpr(&b.left);
3d6f280 2322
                self.walkExpr(&b.right);
5d8ada1 2323
            }
5d8ada1 2324
            ast::Expr::Compare(c) => {
3d6f280 2325
                self.walkExpr(&c.left);
3d6f280 2326
                self.walkExpr(&c.right);
5d8ada1 2327
            }
3d6f280 2328
            ast::Expr::Not(inner) => self.walkExpr(inner),
3d6f280 2329
            ast::Expr::Unary(u) => self.walkExpr(&u.operand),
3d6f280 2330
            ast::Expr::Paren(inner) => self.walkExpr(inner),
5d8ada1 2331
            ast::Expr::Ternary(t) => {
3d6f280 2332
                self.walkExpr(&t.condition);
3d6f280 2333
                self.walkExpr(&t.then);
3d6f280 2334
                self.walkExpr(&t.else_);
5d8ada1 2335
            }
5d8ada1 2336
            ast::Expr::FnCall(call) => {
5d8ada1 2337
                for arg in &call.args {
3d6f280 2338
                    self.walkArg(arg);
5d8ada1 2339
                }
5d8ada1 2340
            }
5d8ada1 2341
            ast::Expr::Attribute(a) => {
3d6f280 2342
                self.walkExpr(&a.object);
5d8ada1 2343
                if let ast::AttrKind::Method(call) = &a.attr {
5d8ada1 2344
                    for arg in &call.args {
3d6f280 2345
                        self.walkArg(arg);
5d8ada1 2346
                    }
bb8ca38 2347
                }
bb8ca38 2348
            }
0e39618 2349
            ast::Expr::TypeName(_) => {}
5d8ada1 2350
            ast::Expr::Int(_)
5d8ada1 2351
            | ast::Expr::Float(_)
5d8ada1 2352
            | ast::Expr::String(_)
5d8ada1 2353
            | ast::Expr::Self_
d2640d2 2354
            | ast::Expr::Var(_) => {}
0e39618 2355
            // A closure literal's body has its own locals, belonging to the separate
0e39618 2356
            // closure function it compiles to — nothing to recurse into here.
0e39618 2357
            ast::Expr::Closure(_) => {}
5d8ada1 2358
        }
5d8ada1 2359
    }
5d8ada1 2360
3d6f280 2361
    fn walkArg(&mut self, arg: &ast::Arg) {
5d8ada1 2362
        match arg {
3d6f280 2363
            ast::Arg::Positional(e) => self.walkExpr(e),
3d6f280 2364
            ast::Arg::Keyword { value, .. } => self.walkExpr(value),
3d6f280 2365
            ast::Arg::Pair { value, .. } => self.walkExpr(value),
bb8ca38 2366
        }
bb8ca38 2367
    }
bb8ca38 2368
}
bb8ca38 2369
3d6f280 2370
fn compileFnBody(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Result<Vec<u8>, String> {
bb8ca38 2371
    let mut body = Vec::new();
bb8ca38 2372
5d8ada1 2373
    let mut base_env = ctx.global_env.clone();
5d8ada1 2374
    if let Some(recv) = &f.type_param {
0000000 2375
        base_env.insert("self".to_string(), TypeScheme::mono(plum_checker::plumTypeFromName(recv)));
5d8ada1 2376
    }
5d8ada1 2377
    for p in &f.params {
5d8ada1 2378
        let ty = match &p.ty {
3d6f280 2379
            ast::ParamType::Type(t) => plum_checker::plumTypeFromAst(t),
3d6f280 2380
            ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plum_checker::plumTypeFromAst(t))),
d7e5ff4 2381
            ast::ParamType::Fn(params, ret) => {
3d6f280 2382
                let param_types = params.iter().map(plum_checker::plumTypeFromAst).collect();
3d6f280 2383
                let ret_ty = ret.as_ref().map(|r| plum_checker::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
d7e5ff4 2384
                PlumType::TFun(param_types, Box::new(ret_ty))
d7e5ff4 2385
            }
5d8ada1 2386
        };
5d8ada1 2387
        base_env.insert(p.name.clone(), TypeScheme::mono(ty));
5d8ada1 2388
    }
bb8ca38 2389
5d8ada1 2390
    let mut collector = Collector {
5d8ada1 2391
        env: base_env.clone(),
3d6f280 2392
        cctx: checkCtxOf(&ctx.classes, &ctx.methods, &ctx.enum_variants, &ctx.enum_params),
5d8ada1 2393
        named: Vec::new(),
5d8ada1 2394
        named_set: Default::default(),
5d8ada1 2395
        match_scratch: HashMap::new(),
35af6cf 2396
        nested_class_scratch: HashMap::new(),
0e39618 2397
        nested_class_scratch_types: Vec::new(),
35af6cf 2398
        next_nested_class_slot: 0,
da1c377 2399
        variadic_for_scratch: HashMap::new(),
da1c377 2400
        next_variadic_for_slot: 0,
5d8ada1 2401
    };
5d8ada1 2402
    if let ast::FnBody::Block(block) = &f.body {
3d6f280 2403
        collector.walkBlock(block);
4ba0db3 2404
    } else if let ast::FnBody::Expr(e) = &f.body {
4ba0db3 2405
        // An expression-bodied fn can still contain a closure literal (e.g.
4ba0db3 2406
        // `main() -> Int = each(|v| v)`), which needs construction scratch slots.
3d6f280 2407
        collector.walkExpr(e);
bb8ca38 2408
    }
5d8ada1 2409
    // ---- assign local indices: [self?][params][named...][classcall scratch...][match scratch...] ----
bb8ca38 2410
    let mut locals: HashMap<String, u32> = HashMap::new();
5d8ada1 2411
    let mut groups: Vec<ValType> = Vec::new();
5d8ada1 2412
    let mut idx = 0u32;
5d8ada1 2413
5d8ada1 2414
    if f.type_param.is_some() {
5d8ada1 2415
        locals.insert("self".to_string(), idx);
5d8ada1 2416
        idx += 1;
5d8ada1 2417
    }
5d8ada1 2418
    for p in &f.params {
5d8ada1 2419
        locals.insert(p.name.clone(), idx);
5d8ada1 2420
        idx += 1;
5d8ada1 2421
    }
5d8ada1 2422
    for (name, ty) in &collector.named {
3d6f280 2423
        let vt = plumTypeToValtype(ty);
5d8ada1 2424
        locals.insert(name.clone(), idx);
5d8ada1 2425
        groups.push(vt);
5d8ada1 2426
        idx += 1;
5d8ada1 2427
    }
5d8ada1 2428
5d8ada1 2429
    let match_scratch_base = idx;
5d8ada1 2430
    let mut match_scratch_index: HashMap<usize, u32> = HashMap::new();
35af6cf 2431
    for (ptr, types) in collector.match_scratch.iter() {
5d8ada1 2432
        match_scratch_index.insert(*ptr, idx - match_scratch_base);
35af6cf 2433
        for ty in types {
3d6f280 2434
            groups.push(plumTypeToValtype(ty));
35af6cf 2435
            idx += 1;
35af6cf 2436
        }
35af6cf 2437
    }
35af6cf 2438
35af6cf 2439
    let nested_class_scratch_base = idx;
0e39618 2440
    // Each slot is declared with its OWN concrete variant ref type (not a uniform
0e39618 2441
    // placeholder) — `struct.get` on a constructor-pattern match requires the local
0e39618 2442
    // holding the narrowed (`ref.cast`) value to be statically typed as that exact
0e39618 2443
    // variant, and different slots very likely narrow to different variants.
0e39618 2444
    for vname in &collector.nested_class_scratch_types {
0e39618 2445
        let variant_idx = withGcTypes(|r| *r.variant_type_idx.get(vname)
0e39618 2446
            .unwrap_or_else(|| panic!("internal codegen error: variant '{}' missing from the GC type registry", vname)));
0e39618 2447
        groups.push(gcRef(variant_idx));
5d8ada1 2448
        idx += 1;
bb8ca38 2449
    }
5d8ada1 2450
da1c377 2451
    let variadic_for_scratch_base = idx;
da1c377 2452
    let variadic_for_scratch_count = collector.variadic_for_scratch.values().copied().max().map(|m| m + 1).unwrap_or(0);
da1c377 2453
    for _ in 0..variadic_for_scratch_count {
da1c377 2454
        groups.push(ValType::I32); // count
da1c377 2455
        groups.push(ValType::I32); // loop index
da1c377 2456
        idx += 2;
da1c377 2457
    }
da1c377 2458
5d8ada1 2459
    if groups.is_empty() {
5d8ada1 2460
        body.push(0);
5d8ada1 2461
    } else {
3d6f280 2462
        body.extend(encodeLeb128U32(groups.len() as u32));
5d8ada1 2463
        for g in &groups {
3d6f280 2464
            body.extend(encodeLeb128U32(1));
5d8ada1 2465
            g.encode(&mut body);
5d8ada1 2466
        }
bb8ca38 2467
    }
bb8ca38 2468
bb8ca38 2469
    let local_ctx = LocalCtx {
bb8ca38 2470
        locals,
5d8ada1 2471
        match_scratch_base,
5d8ada1 2472
        match_scratch_index,
35af6cf 2473
        nested_class_scratch_base,
35af6cf 2474
        nested_class_scratch: collector.nested_class_scratch,
da1c377 2475
        variadic_for_scratch_base,
da1c377 2476
        variadic_for_scratch: collector.variadic_for_scratch,
bb8ca38 2477
        func_ids: &ctx.func_ids,
bb8ca38 2478
        func_sigs: &ctx.func_sigs,
4ba0db3 2479
        closures: &ctx.closures,
4ba0db3 2480
        closure_call_types: &ctx.closure_call_types,
35af6cf 2481
        named_fn_values: &ctx.named_fn_values,
35af6cf 2482
        string_concat_func: ctx.string_concat_func,
35af6cf 2483
        int_to_string_func: ctx.int_to_string_func,
5d8ada1 2484
        classes: &ctx.classes,
5d8ada1 2485
        methods: &ctx.methods,
5d8ada1 2486
        enum_variants: &ctx.enum_variants,
4fda634 2487
        enum_params: &ctx.enum_params,
0e39618 2488
        gc_types: &ctx.gc_types,
0e39618 2489
        singleton_globals: &ctx.singleton_globals,
5d8ada1 2490
        type_env: RefCell::new(base_env),
35af6cf 2491
        closure_local_sigs: RefCell::new(HashMap::new()),
bb8ca38 2492
    };
bb8ca38 2493
3d6f280 2494
    let result_vt = retTypeToWasm(f.returns.as_ref());
bb8ca38 2495
bb8ca38 2496
    match &f.body {
bb8ca38 2497
        ast::FnBody::Expr(e) => {
3d6f280 2498
            compileExpr(e, &mut body, &local_ctx, state)?;
bb8ca38 2499
        }
bb8ca38 2500
        ast::FnBody::Block(block) => {
3d6f280 2501
            compileBlockAsFnBody(block, &mut body, &local_ctx, state, result_vt)?;
bb8ca38 2502
        }
0000000 2503
        // `fns` excludes every `extern fun` (no body to compile).
0000000 2504
        ast::FnBody::Extern => unreachable!("extern fns are excluded from `fns`"),
bb8ca38 2505
    }
bb8ca38 2506
bb8ca38 2507
    Instruction::End.encode(&mut body);
bb8ca38 2508
    Ok(body)
bb8ca38 2509
}
bb8ca38 2510
3d6f280 2511
fn compileBlock(block: &ast::Block, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
bb8ca38 2512
    for stmt in &block.stmts {
3d6f280 2513
        compileStmt(stmt, body, ctx, state)?;
bb8ca38 2514
    }
bb8ca38 2515
    Ok(())
bb8ca38 2516
}
bb8ca38 2517
3254688 2518
/// Compiles a case/branch body either as an ordinary statement block (`result_vt: None`)
3d6f280 2519
/// or, when in value position, via `compileBlockInValuePosition` so its own tail
3254688 2520
/// statement propagates a value instead of being dropped.
3d6f280 2521
fn compileCaseBody(
3254688 2522
    block: &ast::Block,
3254688 2523
    result_vt: Option<ValType>,
3254688 2524
    body: &mut Vec<u8>,
3254688 2525
    ctx: &LocalCtx,
3254688 2526
    state: &mut ModuleState,
3254688 2527
) -> Result<(), String> {
3254688 2528
    match result_vt {
3d6f280 2529
        Some(vt) => compileBlockInValuePosition(block, vt, body, ctx, state),
3d6f280 2530
        None => compileBlock(block, body, ctx, state),
3254688 2531
    }
3254688 2532
}
3254688 2533
3254688 2534
/// Compiles a block whose value must be produced when control reaches its end — every
3254688 2535
/// statement except the last compiles normally; the last is compiled via
3d6f280 2536
/// `compileStmtInValuePosition`.
3d6f280 2537
fn compileBlockInValuePosition(
3254688 2538
    block: &ast::Block,
3254688 2539
    result_vt: ValType,
3254688 2540
    body: &mut Vec<u8>,
3254688 2541
    ctx: &LocalCtx,
3254688 2542
    state: &mut ModuleState,
3254688 2543
) -> Result<(), String> {
3254688 2544
    let (last, rest) = block.stmts.split_last().ok_or_else(|| {
3254688 2545
        "codegen: function has a control-flow path that doesn't produce a return value (empty branch)".to_string()
3254688 2546
    })?;
3254688 2547
    for stmt in rest {
3d6f280 2548
        compileStmt(stmt, body, ctx, state)?;
3254688 2549
    }
3d6f280 2550
    compileStmtInValuePosition(last, result_vt, body, ctx, state)
3254688 2551
}
3254688 2552
3254688 2553
/// Compiles a single statement in value position: a bare expression is left on the stack
3254688 2554
/// (not dropped); `return`/`todo` compile normally (both are stack-polymorphic in wasm —
3254688 2555
/// control never falls through past them, so no value is needed on this path); `if`/`match`
3254688 2556
/// recurse so every arm/branch resolves the same way. Any other statement kind can't
3254688 2557
/// produce a value, so this returns a clear error instead of ever emitting wasm that
3254688 2558
/// would fail validation.
3d6f280 2559
fn compileStmtInValuePosition(
3254688 2560
    stmt: &ast::Stmt,
3254688 2561
    result_vt: ValType,
3254688 2562
    body: &mut Vec<u8>,
3254688 2563
    ctx: &LocalCtx,
3254688 2564
    state: &mut ModuleState,
3254688 2565
) -> Result<(), String> {
12537c4 2566
    match stmt {
3d6f280 2567
        ast::Stmt::Expr(e) => compileExpr(e, body, ctx, state),
3d6f280 2568
        ast::Stmt::Return(_) | ast::Stmt::Todo => compileStmt(stmt, body, ctx, state),
3d6f280 2569
        ast::Stmt::If(if_) => compileIf(if_, Some(result_vt), body, ctx, state),
3d6f280 2570
        ast::Stmt::Match(m) => compileMatch(m, body, ctx, state, Some(result_vt)),
3254688 2571
        _ => Err(
3254688 2572
            "codegen: function has a control-flow path that doesn't produce a return value".to_string(),
3254688 2573
        ),
12537c4 2574
    }
12537c4 2575
}
12537c4 2576
3254688 2577
/// Compiles an `if`/`else if`/`else` chain. `result_vt` is `None` for an ordinary statement
3254688 2578
/// (each branch is `BlockType::Empty`, nothing left on the stack) or `Some(vt)` when this
3254688 2579
/// `if` is in value position — every branch must then leave a `vt` value on the stack, which
3254688 2580
/// requires an `else` (a value can't be produced on a path that doesn't exist).
0e39618 2581
/// Compiles a `Bool`-typed expression, then a `ref.test` against the `True` variant's
0e39618 2582
/// concrete type, leaving a plain `i32` (1/0) on the stack. Every place a `Bool` value
0e39618 2583
/// drives wasm's OWN native control flow (`if`/`br_if`, which require a raw `i32`
0e39618 2584
/// condition, not a `ref`) goes through this — see this migration plan's Decision 1
0e39618 2585
/// (Bool is a full wasm-gc struct, no special-casing).
0e39618 2586
fn compileBoolConditionAsI32(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
0000000 2587
    // `&&`/`||` short-circuit: the right operand must not even be COMPILED (let
0000000 2588
    // alone executed) unless the left side's result already needs it — guarding
0000000 2589
    // `i < len && s.byteAt(i) != ...` on the length check only works if `byteAt`
0000000 2590
    // is never reached once `i < len` is false. Handled here (not just as a
0000000 2591
    // generic `Expr::Bool` case in `compileExpr` below) so a boolean used
0000000 2592
    // directly as an `if`/`while` condition never pays for constructing a real
0000000 2593
    // `Bool` ref just to immediately `ref.test` it back into an `i32`.
0000000 2594
    if let ast::Expr::Bool(b) = expr {
0000000 2595
        return compileShortCircuitBoolI32(b, body, ctx, state);
0000000 2596
    }
0e39618 2597
    compileExpr(expr, body, ctx, state)?;
0e39618 2598
    let true_idx = *ctx.gc_types.variant_type_idx.get("True")
0e39618 2599
        .expect("internal codegen error: True must be registered in the GC type registry");
0e39618 2600
    Instruction::RefTestNonNull(HeapType::Concrete(true_idx)).encode(body);
0e39618 2601
    Ok(())
0e39618 2602
}
0e39618 2603
0000000 2604
/// Leaves a short-circuited `i32` (1/0) on the stack for `b.left op b.right`:
0000000 2605
/// `b.right` is compiled inside a wasm `if` guarded by `b.left`'s result, so for
0000000 2606
/// `&&` it's skipped entirely once the left side is already false (and for `||`,
0000000 2607
/// once the left side is already true) — exactly like every source language's
0000000 2608
/// `&&`/`||`, but requiring real branching since wasm has no lazy operand
0000000 2609
/// evaluation of its own.
0000000 2610
fn compileShortCircuitBoolI32(b: &ast::BoolExpr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
0000000 2611
    compileBoolConditionAsI32(&b.left, body, ctx, state)?;
0000000 2612
    Instruction::If(BlockType::Result(ValType::I32)).encode(body);
0000000 2613
    match b.op {
0000000 2614
        ast::BoolOp::And => compileBoolConditionAsI32(&b.right, body, ctx, state)?,
0000000 2615
        ast::BoolOp::Or => Instruction::I32Const(1).encode(body),
0000000 2616
    }
0000000 2617
    Instruction::Else.encode(body);
0000000 2618
    match b.op {
0000000 2619
        ast::BoolOp::And => Instruction::I32Const(0).encode(body),
0000000 2620
        ast::BoolOp::Or => compileBoolConditionAsI32(&b.right, body, ctx, state)?,
0000000 2621
    }
0000000 2622
    Instruction::End.encode(body);
0000000 2623
    Ok(())
0000000 2624
}
0000000 2625
0e39618 2626
/// Given an `i32` boolean (1/0) already on the stack, converts it into a `Bool` ref by
0e39618 2627
/// selecting the pre-allocated `True`/`False` singleton (this migration plan's
0e39618 2628
/// Decision 2) — the reverse of `compileBoolConditionAsI32`. Used wherever a native
0e39618 2629
/// wasm comparison/logical-op instruction just left a raw `i32` predicate on the
0e39618 2630
/// stack that needs to become a proper `Bool` value.
0e39618 2631
fn pushBoolRefFromI32Flag(body: &mut Vec<u8>, ctx: &LocalCtx) {
0e39618 2632
    let bool_ref_ty = plumTypeToValtype(&PlumType::TBool);
0e39618 2633
    let true_global = *ctx.singleton_globals.get("True").expect("internal codegen error: True singleton global missing");
0e39618 2634
    let false_global = *ctx.singleton_globals.get("False").expect("internal codegen error: False singleton global missing");
0e39618 2635
    Instruction::If(BlockType::Result(bool_ref_ty)).encode(body);
0e39618 2636
    Instruction::GlobalGet(true_global).encode(body);
0e39618 2637
    Instruction::Else.encode(body);
0e39618 2638
    Instruction::GlobalGet(false_global).encode(body);
0e39618 2639
    Instruction::End.encode(body);
0e39618 2640
}
0e39618 2641
3d6f280 2642
fn compileIf(
3254688 2643
    if_: &ast::If,
3254688 2644
    result_vt: Option<ValType>,
3254688 2645
    body: &mut Vec<u8>,
3254688 2646
    ctx: &LocalCtx,
3254688 2647
    state: &mut ModuleState,
3254688 2648
) -> Result<(), String> {
3254688 2649
    if result_vt.is_some() && if_.else_.is_none() {
3254688 2650
        return Err(
3254688 2651
            "codegen: function has a control-flow path that doesn't produce a return value (if without else)".to_string(),
3254688 2652
        );
3254688 2653
    }
3d6f280 2654
    let bt = blockTypeFor(result_vt);
0e39618 2655
    compileBoolConditionAsI32(&if_.condition, body, ctx, state)?;
3254688 2656
    Instruction::If(bt).encode(body);
3d6f280 2657
    compileCaseBody(&if_.body, result_vt, body, ctx, state)?;
3254688 2658
    if !if_.else_ifs.is_empty() || if_.else_.is_some() {
3254688 2659
        Instruction::Else.encode(body);
3254688 2660
        for ei in &if_.else_ifs {
0e39618 2661
            compileBoolConditionAsI32(&ei.condition, body, ctx, state)?;
3254688 2662
            Instruction::If(bt).encode(body);
3d6f280 2663
            compileCaseBody(&ei.body, result_vt, body, ctx, state)?;
3254688 2664
            Instruction::Else.encode(body);
3254688 2665
        }
3254688 2666
        if let Some(else_block) = &if_.else_ {
3d6f280 2667
            compileCaseBody(else_block, result_vt, body, ctx, state)?;
3254688 2668
        }
3254688 2669
        for _ in &if_.else_ifs {
3254688 2670
            Instruction::End.encode(body);
3254688 2671
        }
3254688 2672
    }
3254688 2673
    Instruction::End.encode(body);
3254688 2674
    Ok(())
12537c4 2675
}
12537c4 2676
3254688 2677
/// Compiles a block that is the body of a function. If the function returns a value,
3d6f280 2678
/// its tail statement is compiled in value position (see `compileStmtInValuePosition`)
3254688 2679
/// so a bare expression, or an `if`/`match` whose arms resolve to one, propagates that
3254688 2680
/// value instead of being dropped.
3d6f280 2681
fn compileBlockAsFnBody(
bb8ca38 2682
    block: &ast::Block,
bb8ca38 2683
    body: &mut Vec<u8>,
bb8ca38 2684
    ctx: &LocalCtx,
5d8ada1 2685
    state: &mut ModuleState,
3254688 2686
    result_vt: Option<ValType>,
bb8ca38 2687
) -> Result<(), String> {
3254688 2688
    match result_vt {
3d6f280 2689
        Some(vt) => compileBlockInValuePosition(block, vt, body, ctx, state),
3d6f280 2690
        None => compileBlock(block, body, ctx, state),
bb8ca38 2691
    }
bb8ca38 2692
}
bb8ca38 2693
3d6f280 2694
fn compileStmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
bb8ca38 2695
    match stmt {
bb8ca38 2696
        ast::Stmt::Assign(a) => {
bb8ca38 2697
            for (target, value) in a.targets.iter().zip(a.values.iter()) {
01f9be3 2698
                match target {
01f9be3 2699
                    ast::AssignTarget::Var(name) => {
3d6f280 2700
                        // See the matching comment in `Collector::walkStmt`: the checker's
01f9be3 2701
                        // closure inference is unreliable (can error out entirely depending
01f9be3 2702
                        // on the body), but every closure value is an i32 pointer regardless
01f9be3 2703
                        // of its real signature, so don't bother inferring it at all here.
01f9be3 2704
                        let vty = if matches!(value, ast::Expr::Closure(_)) {
01f9be3 2705
                            PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
01f9be3 2706
                        } else {
3d6f280 2707
                            inferLocalType(value, ctx)
01f9be3 2708
                        };
3d6f280 2709
                        compileExpr(value, body, ctx, state)?;
01f9be3 2710
                        let idx = ctx
01f9be3 2711
                            .locals
01f9be3 2712
                            .get(name)
01f9be3 2713
                            .copied()
01f9be3 2714
                            .ok_or_else(|| format!("undeclared local '{}'", name))?;
01f9be3 2715
                        Instruction::LocalSet(idx).encode(body);
01f9be3 2716
                        ctx.type_env.borrow_mut().insert(name.clone(), TypeScheme::mono(vty));
01f9be3 2717
                        // If this assigns a closure *literal*, remember its exact, already-
01f9be3 2718
                        // correct signature (computed by the discovery pass) so a later call
01f9be3 2719
                        // to it doesn't have to re-derive one — see `closure_local_sigs`.
01f9be3 2720
                        if let ast::Expr::Closure(cl) = value {
01f9be3 2721
                            let key = cl.as_ref() as *const ast::Closure as usize;
01f9be3 2722
                            if let Some(info) = ctx.closures.get(&key) {
0e39618 2723
                                let mut sig_params = vec![ValType::Ref(RefType::ANYREF)];
01f9be3 2724
                                sig_params.extend(info.param_vts.iter().copied());
01f9be3 2725
                                ctx.closure_local_sigs.borrow_mut().insert(name.clone(), (sig_params, info.ret_vt));
01f9be3 2726
                            }
01f9be3 2727
                        }
01f9be3 2728
                    }
01f9be3 2729
                    ast::AssignTarget::Field(object, field_name) => {
3d6f280 2730
                        let obj_ty = inferLocalType(object, ctx);
01f9be3 2731
                        let class_name = match &obj_ty {
01f9be3 2732
                            PlumType::TNamed(n) => n.clone(),
01f9be3 2733
                            other => return Err(format!("codegen: cannot assign field '{}' on non-class type {}", field_name, other)),
01f9be3 2734
                        };
01f9be3 2735
                        let fields = ctx
01f9be3 2736
                            .classes
01f9be3 2737
                            .get(&class_name)
01f9be3 2738
                            .ok_or_else(|| format!("codegen: unknown class '{}'", class_name))?;
0e39618 2739
                        let field_idx = fields
01f9be3 2740
                            .iter()
01f9be3 2741
                            .position(|(n, _)| n == field_name)
01f9be3 2742
                            .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
0e39618 2743
                        let class_type_idx = *ctx.gc_types.class_type_idx.get(&class_name)
0e39618 2744
                            .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", class_name))?;
0e39618 2745
                        // struct.set expects [(ref null $t) value] on the stack (ref
0e39618 2746
                        // pushed first/deeper, value second/on top) — same push order
0e39618 2747
                        // this already used for the old memory store.
3d6f280 2748
                        compileExpr(object, body, ctx, state)?;
3d6f280 2749
                        compileExpr(value, body, ctx, state)?;
0e39618 2750
                        Instruction::StructSet { struct_type_index: class_type_idx, field_index: field_idx as u32 }.encode(body);
35af6cf 2751
                    }
35af6cf 2752
                }
bb8ca38 2753
            }
bb8ca38 2754
        }
bb8ca38 2755
        ast::Stmt::Return(Some(e)) => {
3d6f280 2756
            compileExpr(e, body, ctx, state)?;
bb8ca38 2757
            Instruction::Return.encode(body);
bb8ca38 2758
        }
bb8ca38 2759
        ast::Stmt::Return(None) => {
bb8ca38 2760
            Instruction::Return.encode(body);
bb8ca38 2761
        }
bb8ca38 2762
        ast::Stmt::If(if_) => {
3d6f280 2763
            compileIf(if_, None, body, ctx, state)?;
bb8ca38 2764
        }
bb8ca38 2765
        ast::Stmt::While(w) => {
bb8ca38 2766
            Instruction::Block(BlockType::Empty).encode(body);
bb8ca38 2767
            Instruction::Loop(BlockType::Empty).encode(body);
0e39618 2768
            compileBoolConditionAsI32(&w.condition, body, ctx, state)?;
bb8ca38 2769
            Instruction::I32Eqz.encode(body);
bb8ca38 2770
            Instruction::BrIf(1).encode(body);
3d6f280 2771
            compileBlock(&w.body, body, ctx, state)?;
bb8ca38 2772
            Instruction::Br(0).encode(body);
bb8ca38 2773
            Instruction::End.encode(body);
bb8ca38 2774
            Instruction::End.encode(body);
bb8ca38 2775
        }
bb8ca38 2776
        ast::Stmt::For(f) => {
0000000 2777
            // `for i := range n` (`n: Int`) — Go-1.22-style counting loop over
0000000 2778
            // `0..n` (exclusive), with no separate range-literal syntax needed.
0000000 2779
            if matches!(inferLocalType(&f.iter, ctx), PlumType::TInt) && f.vars.len() == 1 {
0000000 2780
                let var_name = &f.vars[0];
0000000 2781
                let var_idx = ctx
0000000 2782
                    .locals
0000000 2783
                    .get(var_name)
0000000 2784
                    .copied()
0000000 2785
                    .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
0000000 2786
                ctx.type_env.borrow_mut().insert(var_name.clone(), TypeScheme::mono(PlumType::TInt));
0000000 2787
                Instruction::I64Const(0).encode(body);
0000000 2788
                Instruction::LocalSet(var_idx).encode(body);
0000000 2789
                Instruction::Block(BlockType::Empty).encode(body);
0000000 2790
                Instruction::Loop(BlockType::Empty).encode(body);
0000000 2791
                Instruction::LocalGet(var_idx).encode(body);
0000000 2792
                compileExpr(&f.iter, body, ctx, state)?;
0000000 2793
                Instruction::I64GeS.encode(body);
0000000 2794
                Instruction::BrIf(1).encode(body);
0000000 2795
                compileBlock(&f.body, body, ctx, state)?;
0000000 2796
                Instruction::LocalGet(var_idx).encode(body);
0000000 2797
                Instruction::I64Const(1).encode(body);
0000000 2798
                Instruction::I64Add.encode(body);
0000000 2799
                Instruction::LocalSet(var_idx).encode(body);
0000000 2800
                Instruction::Br(0).encode(body);
0000000 2801
                Instruction::End.encode(body);
0000000 2802
                Instruction::End.encode(body);
0000000 2803
                return Ok(());
bb8ca38 2804
            }
3d6f280 2805
            if let PlumType::TVariadic(elem_ty) = inferLocalType(&f.iter, ctx) {
da1c377 2806
                if f.vars.len() != 1 {
da1c377 2807
                    return Err("codegen: for-loop over a variadic param must bind exactly one variable".to_string());
da1c377 2808
                }
da1c377 2809
                let var_name = &f.vars[0];
da1c377 2810
                let var_idx = ctx
da1c377 2811
                    .locals
da1c377 2812
                    .get(var_name)
da1c377 2813
                    .copied()
da1c377 2814
                    .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
da1c377 2815
                ctx.type_env.borrow_mut().insert(var_name.clone(), TypeScheme::mono((*elem_ty).clone()));
da1c377 2816
da1c377 2817
                let scratch_key = f as *const ast::For as usize;
da1c377 2818
                let slot = *ctx
da1c377 2819
                    .variadic_for_scratch
da1c377 2820
                    .get(&scratch_key)
da1c377 2821
                    .ok_or_else(|| "internal codegen error: missing variadic-for scratch slot".to_string())?;
da1c377 2822
                let count_local = ctx.variadic_for_scratch_base + slot * 2;
da1c377 2823
                let index_local = count_local + 1;
3d6f280 2824
                let elem_vt = plumTypeToValtype(&elem_ty);
0e39618 2825
                let array_type_idx = *ctx
0e39618 2826
                    .gc_types
0e39618 2827
                    .variadic_array_type_idx
0e39618 2828
                    .get(&elem_vt)
0e39618 2829
                    .ok_or_else(|| "internal codegen error: no variadic array type registered for this elem type".to_string())?;
da1c377 2830
0e39618 2831
                // count_local = array.len(iter)
3d6f280 2832
                compileExpr(&f.iter, body, ctx, state)?;
0e39618 2833
                Instruction::ArrayLen.encode(body);
da1c377 2834
                Instruction::LocalSet(count_local).encode(body);
da1c377 2835
da1c377 2836
                // index_local = 0
da1c377 2837
                Instruction::I32Const(0).encode(body);
da1c377 2838
                Instruction::LocalSet(index_local).encode(body);
da1c377 2839
da1c377 2840
                Instruction::Block(BlockType::Empty).encode(body);
da1c377 2841
                Instruction::Loop(BlockType::Empty).encode(body);
da1c377 2842
                Instruction::LocalGet(index_local).encode(body);
da1c377 2843
                Instruction::LocalGet(count_local).encode(body);
da1c377 2844
                Instruction::I32GeS.encode(body);
da1c377 2845
                Instruction::BrIf(1).encode(body);
da1c377 2846
0e39618 2847
                // var = array.get(iter, index)
3d6f280 2848
                compileExpr(&f.iter, body, ctx, state)?;
da1c377 2849
                Instruction::LocalGet(index_local).encode(body);
0e39618 2850
                Instruction::ArrayGet(array_type_idx).encode(body);
da1c377 2851
                Instruction::LocalSet(var_idx).encode(body);
da1c377 2852
3d6f280 2853
                compileBlock(&f.body, body, ctx, state)?;
da1c377 2854
da1c377 2855
                Instruction::LocalGet(index_local).encode(body);
da1c377 2856
                Instruction::I32Const(1).encode(body);
da1c377 2857
                Instruction::I32Add.encode(body);
da1c377 2858
                Instruction::LocalSet(index_local).encode(body);
da1c377 2859
                Instruction::Br(0).encode(body);
da1c377 2860
                Instruction::End.encode(body);
da1c377 2861
                Instruction::End.encode(body);
da1c377 2862
                return Ok(());
da1c377 2863
            }
3d6f280 2864
            compileExpr(&f.iter, body, ctx, state)?;
bb8ca38 2865
            Instruction::Drop.encode(body);
bb8ca38 2866
        }
bb8ca38 2867
        ast::Stmt::Expr(e) => {
3d6f280 2868
            let has_result = exprHasResult(e, ctx);
3d6f280 2869
            compileExpr(e, body, ctx, state)?;
bb8ca38 2870
            if has_result {
bb8ca38 2871
                Instruction::Drop.encode(body);
bb8ca38 2872
            }
bb8ca38 2873
        }
bb8ca38 2874
        ast::Stmt::Break => {
bb8ca38 2875
            Instruction::Br(1).encode(body);
bb8ca38 2876
        }
bb8ca38 2877
        ast::Stmt::Continue => {
bb8ca38 2878
            Instruction::Br(0).encode(body);
bb8ca38 2879
        }
5d8ada1 2880
        ast::Stmt::Match(m) => {
3d6f280 2881
            compileMatch(m, body, ctx, state, None)?;
5d8ada1 2882
        }
12537c4 2883
        ast::Stmt::Assert(e) => {
0e39618 2884
            compileBoolConditionAsI32(e, body, ctx, state)?;
12537c4 2885
            Instruction::I32Eqz.encode(body);
12537c4 2886
            Instruction::If(BlockType::Empty).encode(body);
12537c4 2887
            Instruction::Unreachable.encode(body);
12537c4 2888
            Instruction::End.encode(body);
12537c4 2889
        }
12537c4 2890
        ast::Stmt::Todo => {
12537c4 2891
            // Marks an unimplemented body — trap rather than silently continuing.
12537c4 2892
            Instruction::Unreachable.encode(body);
12537c4 2893
        }
bb8ca38 2894
    }
bb8ca38 2895
    Ok(())
bb8ca38 2896
}
bb8ca38 2897
bb8ca38 2898
/// Returns true if the expression leaves a value on the wasm stack.
3d6f280 2899
fn exprHasResult(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
bb8ca38 2900
    match expr {
4ba0db3 2901
        ast::Expr::FnCall(call) => {
4ba0db3 2902
            if ctx.locals.contains_key(&call.name) {
3d6f280 2903
                if let PlumType::TFun(_, ret) = inferLocalType(&ast::Expr::Var(call.name.clone()), ctx) {
4ba0db3 2904
                    return !matches!(*ret, PlumType::TUnit);
4ba0db3 2905
                }
4ba0db3 2906
            }
4ba0db3 2907
            ctx.func_sigs.get(&call.name).map(|s| s.ret.is_some()).unwrap_or(true)
4ba0db3 2908
        }
5d8ada1 2909
        ast::Expr::Attribute(attr) => match &attr.attr {
5d8ada1 2910
            ast::AttrKind::Method(call) => {
0000000 2911
                // `methodReceiverName` (not a bare `TNamed` match) so this also
0000000 2912
                // covers a Unit-returning method called in statement position on
0000000 2913
                // a BUILTIN primitive receiver (`Int`/`Float`/`Bool`/`Str`/`Byte`/
0000000 2914
                // `[]Byte`) — e.g. `self.data.set(...)` on a `[]Byte` field — not
0000000 2915
                // just an ordinary class. Without this, such a call was wrongly
0000000 2916
                // assumed to leave a value on the stack, emitting a `Drop` with
0000000 2917
                // nothing to drop.
0000000 2918
                if let Some(class_name) = plum_checker::methodReceiverName(&inferLocalType(&attr.object, ctx)) {
5d8ada1 2919
                    let key = format!("{}::{}", class_name, call.name);
5d8ada1 2920
                    ctx.func_sigs.get(&key).map(|s| s.ret.is_some()).unwrap_or(true)
5d8ada1 2921
                } else {
5d8ada1 2922
                    true
5d8ada1 2923
                }
5d8ada1 2924
            }
5d8ada1 2925
            ast::AttrKind::Field(_) => true,
5d8ada1 2926
        },
bb8ca38 2927
        _ => true,
bb8ca38 2928
    }
bb8ca38 2929
}
bb8ca38 2930
3254688 2931
/// True if `cases` consists solely of enum-tag patterns (bare variant names or
3254688 2932
/// constructor patterns, no wildcard/binding/int/etc.) that between them cover every
3254688 2933
/// variant of a single enum type. When that holds, a match compiled in value position
3254688 2934
/// can never actually fall through past the last arm at runtime — even though the
3254688 2935
/// patterns don't include an explicit wildcard/binding catch-all — so the "ran out of
3254688 2936
/// patterns" fallback in `compile_match_arms` is provably unreachable code, not a real
3d6f280 2937
/// gap. `compileMatch` uses this to append a synthetic trap-and-never-fall-through
3254688 2938
/// wildcard arm (rather than let the arms recursion hit its non-exhaustive-match error)
3254688 2939
/// so previously-working exhaustive enum matches (e.g. `Some`/`None`, `True`/`False`)
3254688 2940
/// keep compiling even without a trailing wildcard, while a genuinely non-exhaustive
3254688 2941
/// match (an `Int` match, or an enum match missing a variant) still gets a clear error.
35af6cf 2942
/// True if `cases` already covers every combination of enum variants across all
35af6cf 2943
/// subject positions (by explicit tag/constructor patterns only — no binding or
35af6cf 2944
/// wildcard in any position), i.e. the match is exhaustive at runtime even though
3d6f280 2945
/// `compileMatchArmsMulti` can't see that from the remaining-cases slice alone.
35af6cf 2946
/// For a single subject this is "every variant of its enum is named somewhere";
35af6cf 2947
/// for `match a, b, ...` it's the full cross product (e.g. `Bool, Bool` needs all
35af6cf 2948
/// 4 combinations named, matching `libs/std/bool.plum`'s `and`/`or`).
3d6f280 2949
fn matchCoversEveryEnumVariant(cases: &[ast::Case], subject_vts: &[ValType], ctx: &LocalCtx) -> bool {
0e39618 2950
    if subject_vts.is_empty() || subject_vts.iter().any(|vt| !matches!(vt, ValType::Ref(_))) {
3254688 2951
        return false;
3254688 2952
    }
35af6cf 2953
    let n = subject_vts.len();
35af6cf 2954
    let mut enum_names: Vec<Option<String>> = vec![None; n];
35af6cf 2955
    let mut tuples_seen: std::collections::BTreeSet<Vec<i32>> = std::collections::BTreeSet::new();
3254688 2956
    for case in cases {
35af6cf 2957
        if case.patterns.len() != n {
35af6cf 2958
            return false;
35af6cf 2959
        }
35af6cf 2960
        let mut tuple = Vec::with_capacity(n);
35af6cf 2961
        for (i, pat) in case.patterns.iter().enumerate() {
35af6cf 2962
            let variant_name = match pat {
35af6cf 2963
                ast::CasePattern::Name(nm)
35af6cf 2964
                    if nm.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) && ctx.enum_variants.contains_key(nm) =>
35af6cf 2965
                {
35af6cf 2966
                    nm.as_str()
35af6cf 2967
                }
35af6cf 2968
                ast::CasePattern::Class { name, .. } => name.as_str(),
35af6cf 2969
                // A binding or wildcard at any position could match variants we
35af6cf 2970
                // haven't otherwise named, so we can't prove full coverage this way.
35af6cf 2971
                _ => return false,
35af6cf 2972
            };
35af6cf 2973
            let info = match ctx.enum_variants.get(variant_name) {
35af6cf 2974
                Some(info) => info,
35af6cf 2975
                None => return false,
35af6cf 2976
            };
35af6cf 2977
            match &enum_names[i] {
35af6cf 2978
                Some(en) if en != &info.enum_name => return false,
35af6cf 2979
                Some(_) => {}
35af6cf 2980
                None => enum_names[i] = Some(info.enum_name.clone()),
35af6cf 2981
            }
35af6cf 2982
            tuple.push(info.tag);
35af6cf 2983
        }
35af6cf 2984
        tuples_seen.insert(tuple);
35af6cf 2985
    }
35af6cf 2986
    let mut total_combinations: usize = 1;
35af6cf 2987
    for en in &enum_names {
35af6cf 2988
        match en {
35af6cf 2989
            Some(name) => {
35af6cf 2990
                let count = ctx.enum_variants.values().filter(|v| &v.enum_name == name).count();
35af6cf 2991
                total_combinations = match total_combinations.checked_mul(count) {
35af6cf 2992
                    Some(t) => t,
35af6cf 2993
                    None => return false,
35af6cf 2994
                };
35af6cf 2995
            }
3254688 2996
            None => return false,
3254688 2997
        }
3254688 2998
    }
35af6cf 2999
    !tuples_seen.is_empty() && tuples_seen.len() == total_combinations
3254688 3000
}
3254688 3001
3d6f280 3002
fn compileMatch(
3254688 3003
    m: &ast::Match,
3254688 3004
    body: &mut Vec<u8>,
3254688 3005
    ctx: &LocalCtx,
3254688 3006
    state: &mut ModuleState,
3254688 3007
    result_vt: Option<ValType>,
3254688 3008
) -> Result<(), String> {
5d8ada1 3009
    let key = m as *const ast::Match as usize;
35af6cf 3010
    let base_slot = *ctx
5d8ada1 3011
        .match_scratch_index
5d8ada1 3012
        .get(&key)
5d8ada1 3013
        .ok_or_else(|| "internal codegen error: missing match scratch slot".to_string())?;
3254688 3014
35af6cf 3015
    // Evaluate every subject into its own consecutive scratch local (one per
35af6cf 3016
    // subject, in `match a, b, ...` order) before checking any pattern.
35af6cf 3017
    let mut all_subjects: Vec<(ValType, u32)> = Vec::with_capacity(m.subjects.len());
35af6cf 3018
    for (i, subject) in m.subjects.iter().enumerate() {
3d6f280 3019
        let subject_ty = inferLocalType(subject, ctx);
3d6f280 3020
        let subject_vt = plumTypeToValtype(&subject_ty);
35af6cf 3021
        let scratch_local = ctx.match_scratch_base + base_slot + i as u32;
3d6f280 3022
        compileExpr(subject, body, ctx, state)?;
35af6cf 3023
        Instruction::LocalSet(scratch_local).encode(body);
35af6cf 3024
        all_subjects.push((subject_vt, scratch_local));
35af6cf 3025
    }
35af6cf 3026
35af6cf 3027
    // A match in value position whose arms already cover every combination of
35af6cf 3028
    // enum variants across all subjects (by explicit tag/constructor patterns, no
3d6f280 3029
    // wildcard) is exhaustive at runtime even though `compileMatchArmsMulti` can't
35af6cf 3030
    // see that from the remaining-cases slice alone. `exhaustive_fallback` tells it to
35af6cf 3031
    // compile the "ran out of cases" path as an (unreachable, but valid) trap instead
35af6cf 3032
    // of a spurious non-exhaustive-match error — threaded through as a flag, rather
35af6cf 3033
    // than appending a synthetic wildcard case by cloning `m.cases`, because cloning
35af6cf 3034
    // would reallocate every nested `CasePattern::Class` node at a new address and
35af6cf 3035
    // break `nested_class_scratch`'s pointer-identity-keyed lookup.
35af6cf 3036
    let subject_vts: Vec<ValType> = all_subjects.iter().map(|(vt, _)| *vt).collect();
3d6f280 3037
    let exhaustive_fallback = result_vt.is_some() && matchCoversEveryEnumVariant(&m.cases, &subject_vts, ctx);
35af6cf 3038
3d6f280 3039
    compileMatchArmsMulti(&m.cases, &all_subjects, result_vt, exhaustive_fallback, body, ctx, state)
5d8ada1 3040
}
5d8ada1 3041
35af6cf 3042
/// Tries each case in turn (in source order); a case that fails to match falls
35af6cf 3043
/// through to the next one. `all_subjects` is the full `(valtype, scratch_local)`
35af6cf 3044
/// list for every subject of the enclosing `match`, shared unchanged across every
35af6cf 3045
/// case (each case's own pattern list is checked position-by-position against it
3d6f280 3046
/// via `compileCasePositions`). `exhaustive_fallback` (see `compileMatch`) says
35af6cf 3047
/// what to do once `cases` runs out: trap (proven exhaustive) or report a
35af6cf 3048
/// non-exhaustive-match error.
3d6f280 3049
fn compileMatchArmsMulti(
5d8ada1 3050
    cases: &[ast::Case],
35af6cf 3051
    all_subjects: &[(ValType, u32)],
3254688 3052
    result_vt: Option<ValType>,
35af6cf 3053
    exhaustive_fallback: bool,
5d8ada1 3054
    body: &mut Vec<u8>,
5d8ada1 3055
    ctx: &LocalCtx,
5d8ada1 3056
    state: &mut ModuleState,
5d8ada1 3057
) -> Result<(), String> {
5d8ada1 3058
    let (case, rest) = match cases.split_first() {
3254688 3059
        None => {
3254688 3060
            return match result_vt {
35af6cf 3061
                Some(_) if exhaustive_fallback => {
35af6cf 3062
                    Instruction::Unreachable.encode(body);
35af6cf 3063
                    Ok(())
35af6cf 3064
                }
3254688 3065
                Some(_) => Err(
3254688 3066
                    "codegen: function has a control-flow path that doesn't produce a return value (non-exhaustive match)".to_string(),
3254688 3067
                ),
3254688 3068
                None => Ok(()),
3254688 3069
            };
3254688 3070
        }
5d8ada1 3071
        Some(pair) => pair,
5d8ada1 3072
    };
35af6cf 3073
    if case.patterns.len() != all_subjects.len() {
35af6cf 3074
        return Err(format!(
35af6cf 3075
            "codegen: match case has {} pattern(s), expected {} (one per subject)",
35af6cf 3076
            case.patterns.len(), all_subjects.len()
35af6cf 3077
        ));
35af6cf 3078
    }
3d6f280 3079
    compileCasePositions(case, 0, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
35af6cf 3080
}
35af6cf 3081
35af6cf 3082
/// Checks `case.patterns[pos]` against `all_subjects[pos]`; on success, recurses to
35af6cf 3083
/// `pos + 1` (or, once every position has matched, compiles the case body). On
3d6f280 3084
/// failure at any position, falls through to `compileMatchArmsMulti(rest, ...)`
35af6cf 3085
/// — i.e. the *entire next case*, restarting from its own position 0, not the next
35af6cf 3086
/// position of this case. This is what gives `match a, b` its "all positions must
35af6cf 3087
/// match" (AND) semantics while still trying cases in order.
35af6cf 3088
#[allow(clippy::too_many_arguments)]
3d6f280 3089
fn compileCasePositions(
35af6cf 3090
    case: &ast::Case,
35af6cf 3091
    pos: usize,
35af6cf 3092
    all_subjects: &[(ValType, u32)],
35af6cf 3093
    rest: &[ast::Case],
35af6cf 3094
    result_vt: Option<ValType>,
35af6cf 3095
    exhaustive_fallback: bool,
35af6cf 3096
    body: &mut Vec<u8>,
35af6cf 3097
    ctx: &LocalCtx,
35af6cf 3098
    state: &mut ModuleState,
35af6cf 3099
) -> Result<(), String> {
35af6cf 3100
    if pos == case.patterns.len() {
35af6cf 3101
        // Every position matched.
3d6f280 3102
        return compileCaseBody(&case.body, result_vt, body, ctx, state);
35af6cf 3103
    }
35af6cf 3104
    let pat = &case.patterns[pos];
35af6cf 3105
    let (subject_vt, scratch_local) = all_subjects[pos];
5d8ada1 3106
    match pat {
5d8ada1 3107
        ast::CasePattern::Wildcard => {
35af6cf 3108
            // Always matches this position; move on to the next one.
3d6f280 3109
            compileCasePositions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
5d8ada1 3110
        }
5d8ada1 3111
        ast::CasePattern::Name(n) => {
5d8ada1 3112
            let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
5d8ada1 3113
                && ctx.enum_variants.contains_key(n);
5d8ada1 3114
            if is_variant {
3d6f280 3115
                compileVariantEqArm(n, subject_vt, scratch_local, case, pos, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
5d8ada1 3116
            } else {
5d8ada1 3117
                let idx = ctx
5d8ada1 3118
                    .locals
5d8ada1 3119
                    .get(n)
5d8ada1 3120
                    .copied()
5d8ada1 3121
                    .ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?;
5d8ada1 3122
                Instruction::LocalGet(scratch_local).encode(body);
5d8ada1 3123
                Instruction::LocalSet(idx).encode(body);
3d6f280 3124
                ctx.type_env.borrow_mut().insert(n.clone(), TypeScheme::mono(plumTypeFromValtypeHint(subject_vt)));
35af6cf 3125
                // A binding always matches this position; move on to the next one.
3d6f280 3126
                compileCasePositions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
5d8ada1 3127
            }
5d8ada1 3128
        }
5d8ada1 3129
        ast::CasePattern::Int(n) => {
5d8ada1 3130
            if subject_vt != ValType::I64 {
5d8ada1 3131
                return Err("codegen: integer match pattern against a non-Int subject".to_string());
5d8ada1 3132
            }
5d8ada1 3133
            Instruction::LocalGet(scratch_local).encode(body);
5d8ada1 3134
            Instruction::I64Const(*n).encode(body);
5d8ada1 3135
            Instruction::I64Eq.encode(body);
3d6f280 3136
            Instruction::If(blockTypeFor(result_vt)).encode(body);
3d6f280 3137
            compileCasePositions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)?;
5d8ada1 3138
            Instruction::Else.encode(body);
3d6f280 3139
            compileMatchArmsMulti(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
5d8ada1 3140
            Instruction::End.encode(body);
5d8ada1 3141
            Ok(())
5d8ada1 3142
        }
5d8ada1 3143
        ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
5d8ada1 3144
        ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
8ecbf56 3145
        ast::CasePattern::Class { name, fields } => {
0e39618 3146
            compileVariantConstructorArm(pat, name, fields, subject_vt, scratch_local, case, pos, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)
8ecbf56 3147
        }
5d8ada1 3148
    }
5d8ada1 3149
}
5d8ada1 3150
5d8ada1 3151
#[allow(clippy::too_many_arguments)]
3d6f280 3152
fn compileVariantEqArm(
5d8ada1 3153
    name: &str,
5d8ada1 3154
    subject_vt: ValType,
5d8ada1 3155
    scratch_local: u32,
5d8ada1 3156
    case: &ast::Case,
35af6cf 3157
    pos: usize,
35af6cf 3158
    all_subjects: &[(ValType, u32)],
5d8ada1 3159
    rest: &[ast::Case],
35af6cf 3160
    result_vt: Option<ValType>,
35af6cf 3161
    exhaustive_fallback: bool,
5d8ada1 3162
    body: &mut Vec<u8>,
5d8ada1 3163
    ctx: &LocalCtx,
5d8ada1 3164
    state: &mut ModuleState,
5d8ada1 3165
) -> Result<(), String> {
0e39618 3166
    ctx.enum_variants
8ecbf56 3167
        .get(name)
8ecbf56 3168
        .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
0e39618 3169
    if !matches!(subject_vt, ValType::Ref(_)) {
8ecbf56 3170
        return Err(format!("codegen: enum tag pattern '{}' against a non-enum subject", name));
5d8ada1 3171
    }
0e39618 3172
    let variant_idx = *ctx.gc_types.variant_type_idx.get(name)
0e39618 3173
        .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", name))?;
0e39618 3174
    // A single ref.test against the variant's exact concrete type replaces the old
0e39618 3175
    // "range-check against HEAP_BASE, then conditionally load+compare a tag" dance —
0e39618 3176
    // there's no tag to load at all anymore, the type itself IS the discriminant.
5d8ada1 3177
    Instruction::LocalGet(scratch_local).encode(body);
0e39618 3178
    Instruction::RefTestNonNull(HeapType::Concrete(variant_idx)).encode(body);
3d6f280 3179
    Instruction::If(blockTypeFor(result_vt)).encode(body);
3d6f280 3180
    compileCasePositions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state)?;
5d8ada1 3181
    Instruction::Else.encode(body);
3d6f280 3182
    compileMatchArmsMulti(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
5d8ada1 3183
    Instruction::End.encode(body);
5d8ada1 3184
    Ok(())
5d8ada1 3185
}
5d8ada1 3186
0e39618 3187
#[allow(clippy::too_many_arguments)]
8ecbf56 3188
#[allow(clippy::too_many_arguments)]
3d6f280 3189
fn compileVariantConstructorArm(
0e39618 3190
    pat: &ast::CasePattern,
8ecbf56 3191
    name: &str,
8ecbf56 3192
    fields: &[ast::CasePattern],
8ecbf56 3193
    subject_vt: ValType,
8ecbf56 3194
    scratch_local: u32,
8ecbf56 3195
    case: &ast::Case,
35af6cf 3196
    pos: usize,
35af6cf 3197
    all_subjects: &[(ValType, u32)],
8ecbf56 3198
    rest: &[ast::Case],
35af6cf 3199
    result_vt: Option<ValType>,
35af6cf 3200
    exhaustive_fallback: bool,
8ecbf56 3201
    body: &mut Vec<u8>,
8ecbf56 3202
    ctx: &LocalCtx,
8ecbf56 3203
    state: &mut ModuleState,
8ecbf56 3204
) -> Result<(), String> {
8ecbf56 3205
    let info = ctx
8ecbf56 3206
        .enum_variants
8ecbf56 3207
        .get(name)
8ecbf56 3208
        .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
0e39618 3209
    if !matches!(subject_vt, ValType::Ref(_)) {
8ecbf56 3210
        return Err(format!("codegen: constructor pattern '{}' against a non-enum subject", name));
8ecbf56 3211
    }
8ecbf56 3212
    if fields.len() != info.field_types.len() {
8ecbf56 3213
        return Err(format!(
8ecbf56 3214
            "codegen: constructor pattern '{}' expects {} field(s), got {}",
8ecbf56 3215
            name, info.field_types.len(), fields.len()
8ecbf56 3216
        ));
8ecbf56 3217
    }
0e39618 3218
    let variant_idx = *ctx.gc_types.variant_type_idx.get(name)
0e39618 3219
        .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", name))?;
8ecbf56 3220
    let field_types = info.field_types.clone();
8ecbf56 3221
0e39618 3222
    // This pattern's own narrowly-typed scratch local (declared with variant's exact
0e39618 3223
    // concrete ref type — see `nested_class_scratch_types`), distinct from
0e39618 3224
    // `scratch_local` (which stays declared at the subject's wide supertype type for
0e39618 3225
    // the whole function). `struct.get` on the fields below requires this narrowed
0e39618 3226
    // static type; the old bump-allocator version needed no such narrowing since
0e39618 3227
    // every heap reference was a uniformly-typed, untyped-at-the-wasm-level i32.
0e39618 3228
    let narrow_key = pat as *const ast::CasePattern as usize;
0e39618 3229
    let narrow_slot = *ctx.nested_class_scratch.get(&narrow_key)
0e39618 3230
        .ok_or_else(|| "internal codegen error: missing constructor-pattern scratch slot".to_string())?;
0e39618 3231
    let narrow_local = ctx.nested_class_scratch_base + narrow_slot;
0e39618 3232
0e39618 3233
    // A single ref.test against the variant's exact concrete type replaces the old
0e39618 3234
    // "range-check against HEAP_BASE, then conditionally load+compare a tag" dance.
8ecbf56 3235
    Instruction::LocalGet(scratch_local).encode(body);
0e39618 3236
    Instruction::RefTestNonNull(HeapType::Concrete(variant_idx)).encode(body);
3d6f280 3237
    Instruction::If(blockTypeFor(result_vt)).encode(body);
0e39618 3238
    // Narrow the subject down to this variant's concrete type before destructuring
0e39618 3239
    // its fields — valid here specifically because the ref.test just above proved it.
0e39618 3240
    Instruction::LocalGet(scratch_local).encode(body);
0e39618 3241
    Instruction::RefCastNonNull(HeapType::Concrete(variant_idx)).encode(body);
0e39618 3242
    Instruction::LocalSet(narrow_local).encode(body);
3d6f280 3243
    compileFieldPatterns(
0e39618 3244
        fields, &field_types, 0, narrow_local, variant_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state,
3d6f280 3245
        &mut |body, state| compileCasePositions(case, pos + 1, all_subjects, rest, result_vt, exhaustive_fallback, body, ctx, state),
35af6cf 3246
    )?;
35af6cf 3247
    Instruction::Else.encode(body);
3d6f280 3248
    compileMatchArmsMulti(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
35af6cf 3249
    Instruction::End.encode(body);
35af6cf 3250
    Ok(())
35af6cf 3251
}
35af6cf 3252
35af6cf 3253
/// Checks `fields[fpos..]` (a constructor pattern's own sub-patterns, e.g. the `v` in
35af6cf 3254
/// `Some(v)`, or — recursively — the `Some(v)` in `Wrap(Some(v))`) against the
35af6cf 3255
/// already-loaded value in `container_local`, one field at a time. Once every field
35af6cf 3256
/// has matched, calls `on_match` (typically: proceed to the next top-level subject
35af6cf 3257
/// position). A mismatch at any field — at any nesting depth — falls through to
3d6f280 3258
/// `compileMatchArmsMulti(rest, ...)`, exactly like a top-level pattern mismatch.
35af6cf 3259
#[allow(clippy::too_many_arguments)]
0e39618 3260
#[allow(clippy::too_many_arguments)]
3d6f280 3261
fn compileFieldPatterns(
35af6cf 3262
    fields: &[ast::CasePattern],
35af6cf 3263
    field_types: &[PlumType],
35af6cf 3264
    fpos: usize,
35af6cf 3265
    container_local: u32,
0e39618 3266
    container_type_idx: u32,
35af6cf 3267
    rest: &[ast::Case],
35af6cf 3268
    all_subjects: &[(ValType, u32)],
35af6cf 3269
    result_vt: Option<ValType>,
35af6cf 3270
    exhaustive_fallback: bool,
35af6cf 3271
    body: &mut Vec<u8>,
35af6cf 3272
    ctx: &LocalCtx,
35af6cf 3273
    state: &mut ModuleState,
35af6cf 3274
    on_match: &mut dyn FnMut(&mut Vec<u8>, &mut ModuleState) -> Result<(), String>,
35af6cf 3275
) -> Result<(), String> {
35af6cf 3276
    if fpos == fields.len() {
35af6cf 3277
        return on_match(body, state);
35af6cf 3278
    }
35af6cf 3279
    let pat = &fields[fpos];
35af6cf 3280
    let field_ty = &field_types[fpos];
35af6cf 3281
35af6cf 3282
    match pat {
35af6cf 3283
        ast::CasePattern::Wildcard => {
0e39618 3284
            compileFieldPatterns(fields, field_types, fpos + 1, container_local, container_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)
35af6cf 3285
        }
35af6cf 3286
        ast::CasePattern::Name(n) if !(n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) && ctx.enum_variants.contains_key(n)) => {
35af6cf 3287
            // A plain binding always matches this field; load it straight into its
35af6cf 3288
            // binding local and move on to the next field.
8ecbf56 3289
            let idx = ctx
8ecbf56 3290
                .locals
8ecbf56 3291
                .get(n)
8ecbf56 3292
                .copied()
8ecbf56 3293
                .ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?;
35af6cf 3294
            Instruction::LocalGet(container_local).encode(body);
0e39618 3295
            Instruction::StructGet { struct_type_index: container_type_idx, field_index: fpos as u32 }.encode(body);
8ecbf56 3296
            Instruction::LocalSet(idx).encode(body);
8ecbf56 3297
            ctx.type_env.borrow_mut().insert(n.to_string(), TypeScheme::mono(field_ty.clone()));
0e39618 3298
            compileFieldPatterns(fields, field_types, fpos + 1, container_local, container_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)
35af6cf 3299
        }
35af6cf 3300
        ast::CasePattern::Name(n) => {
35af6cf 3301
            // An uppercase, payload-free variant name used as a field pattern (e.g.
35af6cf 3302
            // matching a nested `None` rather than binding a name to it).
0e39618 3303
            ctx.enum_variants
35af6cf 3304
                .get(n)
35af6cf 3305
                .ok_or_else(|| format!("codegen: unknown enum variant '{}'", n))?;
0e39618 3306
            if !matches!(plumTypeToValtype(field_ty), ValType::Ref(_)) {
35af6cf 3307
                return Err(format!("codegen: enum tag pattern '{}' against a non-enum field", n));
35af6cf 3308
            }
0e39618 3309
            let variant_idx = *ctx.gc_types.variant_type_idx.get(n)
0e39618 3310
                .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", n))?;
35af6cf 3311
            Instruction::LocalGet(container_local).encode(body);
0e39618 3312
            Instruction::StructGet { struct_type_index: container_type_idx, field_index: fpos as u32 }.encode(body);
0e39618 3313
            Instruction::RefTestNonNull(HeapType::Concrete(variant_idx)).encode(body);
3d6f280 3314
            Instruction::If(blockTypeFor(result_vt)).encode(body);
0e39618 3315
            compileFieldPatterns(fields, field_types, fpos + 1, container_local, container_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)?;
35af6cf 3316
            Instruction::Else.encode(body);
3d6f280 3317
            compileMatchArmsMulti(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
35af6cf 3318
            Instruction::End.encode(body);
35af6cf 3319
            Ok(())
35af6cf 3320
        }
35af6cf 3321
        ast::CasePattern::Int(n) => {
0e39618 3322
            if plumTypeToValtype(field_ty) != ValType::I64 {
35af6cf 3323
                return Err("codegen: integer match pattern against a non-Int field".to_string());
35af6cf 3324
            }
35af6cf 3325
            Instruction::LocalGet(container_local).encode(body);
0e39618 3326
            Instruction::StructGet { struct_type_index: container_type_idx, field_index: fpos as u32 }.encode(body);
35af6cf 3327
            Instruction::I64Const(*n).encode(body);
35af6cf 3328
            Instruction::I64Eq.encode(body);
3d6f280 3329
            Instruction::If(blockTypeFor(result_vt)).encode(body);
0e39618 3330
            compileFieldPatterns(fields, field_types, fpos + 1, container_local, container_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)?;
35af6cf 3331
            Instruction::Else.encode(body);
3d6f280 3332
            compileMatchArmsMulti(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
35af6cf 3333
            Instruction::End.encode(body);
35af6cf 3334
            Ok(())
35af6cf 3335
        }
35af6cf 3336
        ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
35af6cf 3337
        ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
35af6cf 3338
        ast::CasePattern::Class { name, fields: inner_fields } => {
35af6cf 3339
            let info = ctx
35af6cf 3340
                .enum_variants
35af6cf 3341
                .get(name)
35af6cf 3342
                .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?;
0e39618 3343
            if !matches!(plumTypeToValtype(field_ty), ValType::Ref(_)) {
35af6cf 3344
                return Err(format!("codegen: constructor pattern '{}' against a non-enum field", name));
35af6cf 3345
            }
35af6cf 3346
            if inner_fields.len() != info.field_types.len() {
35af6cf 3347
                return Err(format!(
35af6cf 3348
                    "codegen: constructor pattern '{}' expects {} field(s), got {}",
35af6cf 3349
                    name, info.field_types.len(), inner_fields.len()
35af6cf 3350
                ));
35af6cf 3351
            }
0e39618 3352
            let inner_variant_idx = *ctx.gc_types.variant_type_idx.get(name)
0e39618 3353
                .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", name))?;
35af6cf 3354
            let key = pat as *const ast::CasePattern as usize;
35af6cf 3355
            let slot = *ctx
35af6cf 3356
                .nested_class_scratch
35af6cf 3357
                .get(&key)
35af6cf 3358
                .ok_or_else(|| "internal codegen error: missing nested constructor pattern scratch slot".to_string())?;
35af6cf 3359
            let nested_local = ctx.nested_class_scratch_base + slot;
35af6cf 3360
            let inner_field_types = info.field_types.clone();
35af6cf 3361
35af6cf 3362
            Instruction::LocalGet(container_local).encode(body);
0e39618 3363
            Instruction::StructGet { struct_type_index: container_type_idx, field_index: fpos as u32 }.encode(body);
0e39618 3364
            Instruction::RefTestNonNull(HeapType::Concrete(inner_variant_idx)).encode(body);
3d6f280 3365
            Instruction::If(blockTypeFor(result_vt)).encode(body);
0e39618 3366
            // Narrow into `nested_local` now that ref.test just above proved it's safe.
0e39618 3367
            Instruction::LocalGet(container_local).encode(body);
0e39618 3368
            Instruction::StructGet { struct_type_index: container_type_idx, field_index: fpos as u32 }.encode(body);
0e39618 3369
            Instruction::RefCastNonNull(HeapType::Concrete(inner_variant_idx)).encode(body);
0e39618 3370
            Instruction::LocalSet(nested_local).encode(body);
0e39618 3371
            compileFieldPatterns(inner_fields, &inner_field_types, 0, nested_local, inner_variant_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, &mut |body, state| {
0e39618 3372
                compileFieldPatterns(fields, field_types, fpos + 1, container_local, container_type_idx, rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state, on_match)
35af6cf 3373
            })?;
35af6cf 3374
            Instruction::Else.encode(body);
3d6f280 3375
            compileMatchArmsMulti(rest, all_subjects, result_vt, exhaustive_fallback, body, ctx, state)?;
35af6cf 3376
            Instruction::End.encode(body);
35af6cf 3377
            Ok(())
8ecbf56 3378
        }
8ecbf56 3379
    }
8ecbf56 3380
}
8ecbf56 3381
3d6f280 3382
fn plumTypeFromValtypeHint(vt: ValType) -> PlumType {
5d8ada1 3383
    match vt {
5d8ada1 3384
        ValType::I64 => PlumType::TInt,
5d8ada1 3385
        ValType::F64 => PlumType::TFloat,
5d8ada1 3386
        _ => PlumType::TVar("_".to_string()),
5d8ada1 3387
    }
5d8ada1 3388
}
5d8ada1 3389
3d6f280 3390
fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
bb8ca38 3391
    match expr {
bb8ca38 3392
        ast::Expr::Int(n) => {
bb8ca38 3393
            Instruction::I64Const(*n).encode(body);
bb8ca38 3394
        }
bb8ca38 3395
        ast::Expr::Float(f) => {
bb8ca38 3396
            Instruction::F64Const(*f).encode(body);
bb8ca38 3397
        }
bb8ca38 3398
        ast::Expr::Var(name) => {
35af6cf 3399
            match ctx.locals.get(name.as_str()) {
35af6cf 3400
                Some(idx) => Instruction::LocalGet(*idx).encode(body),
35af6cf 3401
                // Not a local: a bare reference to a top-level function used as a
35af6cf 3402
                // value (e.g. `each(double)`) — push its zero-capture trampoline
0e39618 3403
                // closure's (pre-built by the `start` function) global.
35af6cf 3404
                None => {
0e39618 3405
                    let global_idx = *ctx
35af6cf 3406
                        .named_fn_values
35af6cf 3407
                        .get(name.as_str())
35af6cf 3408
                        .ok_or_else(|| format!("undeclared variable '{}'", name))?;
0e39618 3409
                    Instruction::GlobalGet(global_idx).encode(body);
35af6cf 3410
                }
35af6cf 3411
            }
bb8ca38 3412
        }
bb8ca38 3413
        ast::Expr::Paren(inner) => {
3d6f280 3414
            compileExpr(inner, body, ctx, state)?;
bb8ca38 3415
        }
5d8ada1 3416
        ast::Expr::Unary(u) => match u.op {
5d8ada1 3417
            ast::UnOp::Neg => {
3d6f280 3418
                if matches!(inferLocalType(&u.operand, ctx), PlumType::TFloat) {
3d6f280 3419
                    compileExpr(&u.operand, body, ctx, state)?;
5d8ada1 3420
                    Instruction::F64Neg.encode(body);
5d8ada1 3421
                } else {
5d8ada1 3422
                    // WASM has no i64.neg; use 0 - operand.
bb8ca38 3423
                    Instruction::I64Const(0).encode(body);
3d6f280 3424
                    compileExpr(&u.operand, body, ctx, state)?;
bb8ca38 3425
                    Instruction::I64Sub.encode(body);
bb8ca38 3426
                }
bb8ca38 3427
            }
5d8ada1 3428
            ast::UnOp::Pos => {
3d6f280 3429
                compileExpr(&u.operand, body, ctx, state)?;
5d8ada1 3430
            }
5d8ada1 3431
        },
bb8ca38 3432
        ast::Expr::Binary(b) => {
0000000 3433
            let left_ty = inferLocalType(&b.left, ctx);
0000000 3434
            let is_float = matches!(left_ty, PlumType::TFloat);
0000000 3435
            let is_str = matches!(left_ty, PlumType::TStr);
3d6f280 3436
            compileExpr(&b.left, body, ctx, state)?;
3d6f280 3437
            compileExpr(&b.right, body, ctx, state)?;
bb8ca38 3438
            match b.op {
0000000 3439
                // `Str + Str` (e.g. `libs/std/str.plum`'s `concat`) allocates a new
0000000 3440
                // array holding both operands' bytes via the same runtime helper
0000000 3441
                // string interpolation uses — there's no native wasm "add" for a
0000000 3442
                // GC ref.
0000000 3443
                ast::BinOp::Add if is_str => Instruction::Call(ctx.string_concat_func).encode(body),
5d8ada1 3444
                ast::BinOp::Add => if is_float { Instruction::F64Add } else { Instruction::I64Add }.encode(body),
5d8ada1 3445
                ast::BinOp::Sub => if is_float { Instruction::F64Sub } else { Instruction::I64Sub }.encode(body),
5d8ada1 3446
                ast::BinOp::Mul => if is_float { Instruction::F64Mul } else { Instruction::I64Mul }.encode(body),
5d8ada1 3447
                ast::BinOp::Div => if is_float { Instruction::F64Div } else { Instruction::I64DivS }.encode(body),
bb8ca38 3448
                ast::BinOp::Mod => Instruction::I64RemS.encode(body),
bb8ca38 3449
                ast::BinOp::BitOr => Instruction::I64Or.encode(body),
bb8ca38 3450
                ast::BinOp::BitAnd => Instruction::I64And.encode(body),
bb8ca38 3451
                ast::BinOp::Xor => Instruction::I64Xor.encode(body),
bb8ca38 3452
                ast::BinOp::Shl => Instruction::I64Shl.encode(body),
bb8ca38 3453
                ast::BinOp::Shr => Instruction::I64ShrS.encode(body),
bb8ca38 3454
            }
bb8ca38 3455
        }
bb8ca38 3456
        ast::Expr::Bool(b) => {
0000000 3457
            compileShortCircuitBoolI32(b, body, ctx, state)?;
0e39618 3458
            pushBoolRefFromI32Flag(body, ctx);
bb8ca38 3459
        }
bb8ca38 3460
        ast::Expr::Not(inner) => {
0e39618 3461
            compileBoolConditionAsI32(inner, body, ctx, state)?;
bb8ca38 3462
            Instruction::I32Eqz.encode(body);
0e39618 3463
            pushBoolRefFromI32Flag(body, ctx);
bb8ca38 3464
        }
bb8ca38 3465
        ast::Expr::Compare(c) => {
02b3582 3466
            let left_ty = inferLocalType(&c.left, ctx);
3d6f280 3467
            compileExpr(&c.left, body, ctx, state)?;
3d6f280 3468
            compileExpr(&c.right, body, ctx, state)?;
02b3582 3469
            match left_ty {
02b3582 3470
                PlumType::TFloat => {
02b3582 3471
                    match c.op {
02b3582 3472
                        ast::CmpOp::Lt => Instruction::F64Lt,
02b3582 3473
                        ast::CmpOp::Lte => Instruction::F64Le,
02b3582 3474
                        ast::CmpOp::Eq => Instruction::F64Eq,
02b3582 3475
                        ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::F64Ne,
02b3582 3476
                        ast::CmpOp::Gte => Instruction::F64Ge,
02b3582 3477
                        ast::CmpOp::Gt => Instruction::F64Gt,
02b3582 3478
                    }
02b3582 3479
                    .encode(body);
5d8ada1 3480
                }
02b3582 3481
                // `TVar`/`TUnit` share `Int`'s `i64` wasm representation (see
02b3582 3482
                // `plumTypeToValtype`) — an unresolved generic defaults the same way.
02b3582 3483
                PlumType::TInt | PlumType::TVar(_) | PlumType::TUnit => {
02b3582 3484
                    match c.op {
02b3582 3485
                        ast::CmpOp::Lt => Instruction::I64LtS,
02b3582 3486
                        ast::CmpOp::Lte => Instruction::I64LeS,
02b3582 3487
                        ast::CmpOp::Eq => Instruction::I64Eq,
02b3582 3488
                        ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I64Ne,
02b3582 3489
                        ast::CmpOp::Gte => Instruction::I64GeS,
02b3582 3490
                        ast::CmpOp::Gt => Instruction::I64GtS,
02b3582 3491
                    }
02b3582 3492
                    .encode(body);
5d8ada1 3493
                }
02b3582 3494
                // Bool/Str/class/enum values are wasm-gc refs — `==`/`!=` compares
02b3582 3495
                // reference identity via `ref.eq`. That's exactly right for a
02b3582 3496
                // payload-free singleton (`None`/`True`/`False`, this migration
02b3582 3497
                // plan's Decision 2) and for class-instance identity; it's NOT a
02b3582 3498
                // deep/structural comparison (two distinct `Str` values holding
02b3582 3499
                // equal text compare unequal) — the same caveat this codegen
02b3582 3500
                // already had pre-wasm-gc, when it was an i32 POINTER comparison.
02b3582 3501
                // Ordering a ref type has no meaning and was never valid.
02b3582 3502
                _ => match &c.op {
02b3582 3503
                    ast::CmpOp::Eq => Instruction::RefEq.encode(body),
02b3582 3504
                    ast::CmpOp::Neq | ast::CmpOp::NotEq2 => {
02b3582 3505
                        Instruction::RefEq.encode(body);
02b3582 3506
                        Instruction::I32Eqz.encode(body);
02b3582 3507
                    }
02b3582 3508
                    other => return Err(format!("codegen: '{:?}' is not supported between reference-typed values", other)),
02b3582 3509
                },
bb8ca38 3510
            }
0e39618 3511
            pushBoolRefFromI32Flag(body, ctx);
bb8ca38 3512
        }
bb8ca38 3513
        ast::Expr::Ternary(t) => {
3d6f280 3514
            let result_vt = plumTypeToValtype(&inferLocalType(&t.then, ctx));
0e39618 3515
            compileBoolConditionAsI32(&t.condition, body, ctx, state)?;
5d8ada1 3516
            Instruction::If(BlockType::Result(result_vt)).encode(body);
3d6f280 3517
            compileExpr(&t.then, body, ctx, state)?;
bb8ca38 3518
            Instruction::Else.encode(body);
3d6f280 3519
            compileExpr(&t.else_, body, ctx, state)?;
bb8ca38 3520
            Instruction::End.encode(body);
bb8ca38 3521
        }
bb8ca38 3522
        ast::Expr::FnCall(call) => {
4ba0db3 3523
            // A call whose callee name is a *local* of function type is a closure call,
4ba0db3 3524
            // dispatched via `call_indirect` — not a direct `Call` to a named function.
4ba0db3 3525
            let is_closure_call = ctx.locals.contains_key(&call.name)
3d6f280 3526
                && matches!(inferLocalType(&ast::Expr::Var(call.name.clone()), ctx), PlumType::TFun(_, _));
0000000 3527
            if (call.name == "Int" || call.name == "Float" || call.name == "Byte") && call.args.len() == 1 && !ctx.func_ids.contains_key(&call.name) {
0000000 3528
                let arg_expr = match &call.args[0] {
0000000 3529
                    ast::Arg::Positional(e) => e,
0000000 3530
                    ast::Arg::Keyword { value, .. } => value,
0000000 3531
                    ast::Arg::Pair { value, .. } => value,
0000000 3532
                };
0000000 3533
                let arg_ty = inferLocalType(arg_expr, ctx);
0000000 3534
                compileExpr(arg_expr, body, ctx, state)?;
0000000 3535
                match (call.name.as_str(), &arg_ty) {
0000000 3536
                    ("Float", PlumType::TInt) => Instruction::F64ConvertI64S.encode(body),
0000000 3537
                    ("Int", PlumType::TFloat) => Instruction::I64TruncSatF64S.encode(body),
0000000 3538
                    // `Byte(intExpr)` truncates to the low 32 bits then masks to a
0000000 3539
                    // single byte (0-255) — an `Int` outside that range wraps, matching
0000000 3540
                    // Go's `byte(x)` conversion semantics rather than trapping.
0000000 3541
                    ("Byte", PlumType::TInt) => {
0000000 3542
                        Instruction::I32WrapI64.encode(body);
0000000 3543
                        Instruction::I32Const(0xFF).encode(body);
0000000 3544
                        Instruction::I32And.encode(body);
0000000 3545
                    }
0000000 3546
                    ("Int", PlumType::TByte) => Instruction::I64ExtendI32U.encode(body),
0000000 3547
                    // Same-type conversion (`Int(intExpr)`/`Float(floatExpr)`/`Byte(byteExpr)`) is a no-op.
0000000 3548
                    _ => {}
0000000 3549
                }
0000000 3550
            } else if is_closure_call {
3d6f280 3551
                compileClosureCall(call, body, ctx, state)?;
4ba0db3 3552
            } else if let Some(info) = ctx.enum_variants.get(&call.name) {
3d6f280 3553
                compileVariantConstruction(info, call, expr, body, ctx, state)?;
380a51c 3554
            } else {
3d6f280 3555
                fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
d6b1f95 3556
                    match arg {
380a51c 3557
                        ast::Arg::Positional(e) => e,
380a51c 3558
                        ast::Arg::Keyword { value, .. } => value,
380a51c 3559
                        ast::Arg::Pair { value, .. } => value,
d6b1f95 3560
                    }
d6b1f95 3561
                }
3d6f280 3562
                let callee_sig = inferLocalType(&ast::Expr::Var(call.name.clone()), ctx);
d6b1f95 3563
                let variadic_split = match &callee_sig {
d6b1f95 3564
                    PlumType::TFun(params, _) => match params.last() {
d6b1f95 3565
                        Some(PlumType::TVariadic(elem)) => Some(((**elem).clone(), params.len() - 1)),
d6b1f95 3566
                        _ => None,
d6b1f95 3567
                    },
d6b1f95 3568
                    _ => None,
d6b1f95 3569
                };
d6b1f95 3570
                match variadic_split {
d6b1f95 3571
                    Some((elem_ty, fixed_count)) => {
d6b1f95 3572
                        for arg in call.args.iter().take(fixed_count) {
3d6f280 3573
                            compileExpr(argExprOf(arg), body, ctx, state)?;
d6b1f95 3574
                        }
3d6f280 3575
                        let trailing: Vec<&ast::Expr> = call.args.iter().skip(fixed_count).map(argExprOf).collect();
3d6f280 3576
                        let elem_vt = plumTypeToValtype(&elem_ty);
0e39618 3577
                        let array_type_idx = *ctx
0e39618 3578
                            .gc_types
0e39618 3579
                            .variadic_array_type_idx
0e39618 3580
                            .get(&elem_vt)
0e39618 3581
                            .ok_or_else(|| "internal codegen error: no variadic array type registered for this elem type".to_string())?;
0e39618 3582
                        for arg_expr in &trailing {
3d6f280 3583
                            compileExpr(arg_expr, body, ctx, state)?;
d6b1f95 3584
                        }
0e39618 3585
                        Instruction::ArrayNewFixed { array_type_index: array_type_idx, array_size: trailing.len() as u32 }.encode(body);
d6b1f95 3586
d6b1f95 3587
                        let func_idx = ctx
d6b1f95 3588
                            .func_ids
d6b1f95 3589
                            .get(&call.name)
d6b1f95 3590
                            .ok_or_else(|| format!("unknown function '{}'", call.name))?;
d6b1f95 3591
                        Instruction::Call(*func_idx).encode(body);
d6b1f95 3592
                    }
d6b1f95 3593
                    None => {
d6b1f95 3594
                        for arg in &call.args {
3d6f280 3595
                            compileExpr(argExprOf(arg), body, ctx, state)?;
d6b1f95 3596
                        }
d6b1f95 3597
                        let func_idx = ctx
d6b1f95 3598
                            .func_ids
d6b1f95 3599
                            .get(&call.name)
d6b1f95 3600
                            .ok_or_else(|| format!("unknown function '{}'", call.name))?;
d6b1f95 3601
                        Instruction::Call(*func_idx).encode(body);
d6b1f95 3602
                    }
380a51c 3603
                }
bb8ca38 3604
            }
bb8ca38 3605
        }
bb8ca38 3606
        ast::Expr::Self_ => {
5d8ada1 3607
            let idx = ctx
5d8ada1 3608
                .locals
5d8ada1 3609
                .get("self")
5d8ada1 3610
                .copied()
5d8ada1 3611
                .ok_or_else(|| "codegen: 'self' used outside a method".to_string())?;
5d8ada1 3612
            Instruction::LocalGet(idx).encode(body);
bb8ca38 3613
        }
380a51c 3614
        ast::Expr::TypeName(n) => match ctx.enum_variants.get(n) {
380a51c 3615
            Some(info) if info.field_types.is_empty() => {
0e39618 3616
                let global_idx = *ctx.singleton_globals.get(n)
0e39618 3617
                    .unwrap_or_else(|| panic!("internal codegen error: payload-free variant '{}' has no singleton global", n));
0e39618 3618
                Instruction::GlobalGet(global_idx).encode(body);
380a51c 3619
            }
d2640d2 3620
            Some(info) if !info.values.is_empty() => {
d2640d2 3621
                // A discriminant variant's own declared literal values ARE its
d2640d2 3622
                // construction arguments — there is no call site to take them from, so
d2640d2 3623
                // build one synthetically and reuse the existing payload-variant path.
d2640d2 3624
                let synthetic_call = ast::FnCall {
d2640d2 3625
                    name: n.clone(),
d2640d2 3626
                    args: info.values.iter().cloned().map(ast::Arg::Positional).collect(),
d2640d2 3627
                };
3d6f280 3628
                compileVariantConstruction(info, &synthetic_call, expr, body, ctx, state)?;
d2640d2 3629
            }
380a51c 3630
            Some(_) => return Err(format!("codegen: '{}' carries a payload — construct it with '{}(...)'", n, n)),
0000000 3631
            None => {
0000000 3632
                let const_value = CURRENT_CONSTS.with(|c| c.borrow().get(n).cloned());
0000000 3633
                match const_value {
0000000 3634
                    Some(value) => compileExpr(&value, body, ctx, state)?,
0000000 3635
                    None => return Err(format!("codegen: type name '{}' is not yet supported as a value", n)),
0000000 3636
                }
0000000 3637
            }
5d8ada1 3638
        },
5d8ada1 3639
        ast::Expr::ClassCall(call) => {
0e39618 3640
            // struct.new needs every field value pushed in DECLARATION order (not
0e39618 3641
            // `call.fields`'s written order) immediately before the single
0e39618 3642
            // construction instruction — no intermediate scratch pointer needed at
0e39618 3643
            // all, unlike the old bump-pointer-then-store approach.
5d8ada1 3644
            let fields = ctx
5d8ada1 3645
                .classes
5d8ada1 3646
                .get(&call.type_name)
5d8ada1 3647
                .ok_or_else(|| format!("codegen: unknown class '{}'", call.type_name))?
5d8ada1 3648
                .clone();
0e39618 3649
            let class_type_idx = *ctx.gc_types.class_type_idx.get(&call.type_name)
0e39618 3650
                .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", call.type_name))?;
5d8ada1 3651
0e39618 3652
            for (field_name, _) in &fields {
0e39618 3653
                let fa = call.fields.iter().find(|fa| &fa.name == field_name)
0e39618 3654
                    .ok_or_else(|| format!("codegen: class '{}' missing field '{}'", call.type_name, field_name))?;
3d6f280 3655
                compileExpr(&fa.value, body, ctx, state)?;
5d8ada1 3656
            }
0e39618 3657
            Instruction::StructNew(class_type_idx).encode(body);
bb8ca38 3658
        }
5d8ada1 3659
        ast::Expr::Attribute(attr) => {
3d6f280 3660
            let obj_ty = inferLocalType(&attr.object, ctx);
5d8ada1 3661
            match &attr.attr {
5d8ada1 3662
                ast::AttrKind::Field(field_name) => {
5d8ada1 3663
                    let class_name = match &obj_ty {
5d8ada1 3664
                        PlumType::TNamed(n) => n.clone(),
5d8ada1 3665
                        other => return Err(format!("codegen: cannot access field '{}' on non-class type {}", field_name, other)),
5d8ada1 3666
                    };
d2640d2 3667
                    match ctx.classes.get(&class_name) {
d2640d2 3668
                        Some(fields) => {
0e39618 3669
                            let field_idx = fields
d2640d2 3670
                                .iter()
d2640d2 3671
                                .position(|(n, _)| n == field_name)
d2640d2 3672
                                .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
0e39618 3673
                            let class_type_idx = *ctx.gc_types.class_type_idx.get(&class_name)
0e39618 3674
                                .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", class_name))?;
3d6f280 3675
                            compileExpr(&attr.object, body, ctx, state)?;
0e39618 3676
                            Instruction::StructGet { struct_type_index: class_type_idx, field_index: field_idx as u32 }.encode(body);
d2640d2 3677
                        }
0e39618 3678
                        // Not a class: fall back to a discriminant enum's shared params,
0e39618 3679
                        // declared directly on the enum's SUPERTYPE (see
0e39618 3680
                        // `buildGcTypeRegistry`'s `EnumSuper` arm) — no `ref.cast` to any
0e39618 3681
                        // particular variant needed, since every variant has the exact
0e39618 3682
                        // same field list as the supertype itself.
d2640d2 3683
                        None => {
d2640d2 3684
                            let params = ctx
d2640d2 3685
                                .enum_params
d2640d2 3686
                                .get(&class_name)
d2640d2 3687
                                .ok_or_else(|| format!("codegen: unknown class '{}'", class_name))?;
0e39618 3688
                            let field_idx = params
d2640d2 3689
                                .iter()
d2640d2 3690
                                .position(|(n, _)| n == field_name)
d2640d2 3691
                                .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
0e39618 3692
                            let super_type_idx = *ctx.gc_types.enum_super_type_idx.get(&class_name)
0e39618 3693
                                .ok_or_else(|| format!("codegen: enum '{}' missing from the GC type registry", class_name))?;
3d6f280 3694
                            compileExpr(&attr.object, body, ctx, state)?;
0e39618 3695
                            Instruction::StructGet { struct_type_index: super_type_idx, field_index: field_idx as u32 }.encode(body);
d2640d2 3696
                        }
d2640d2 3697
                    }
5d8ada1 3698
                }
5d8ada1 3699
                ast::AttrKind::Method(call) => {
5d8ada1 3700
                    let class_name = match &obj_ty {
5d8ada1 3701
                        PlumType::TNamed(n) => n.clone(),
0000000 3702
                        // Builtin primitive types (`Int`/`Float`/`Bool`/`Str`) declare
0000000 3703
                        // methods the same way classes do (`type Int = fun ...` in
0000000 3704
                        // `libs/std`) — they're just never `TNamed`, so map them back
0000000 3705
                        // to the receiver name `ctx.func_ids`/`ctx.methods` use.
0000000 3706
                        PlumType::TInt => "Int".to_string(),
0000000 3707
                        PlumType::TFloat => "Float".to_string(),
0000000 3708
                        PlumType::TBool => "Bool".to_string(),
0000000 3709
                        PlumType::TStr => "Str".to_string(),
0000000 3710
                        PlumType::TByte => "Byte".to_string(),
0000000 3711
                        PlumType::TByteSlice => "ByteSlice".to_string(),
5d8ada1 3712
                        other => return Err(format!("codegen: cannot call method '{}' on non-class type {}", call.name, other)),
5d8ada1 3713
                    };
5d8ada1 3714
                    let key = format!("{}::{}", class_name, call.name);
5d8ada1 3715
                    let func_idx = *ctx
5d8ada1 3716
                        .func_ids
5d8ada1 3717
                        .get(&key)
5d8ada1 3718
                        .ok_or_else(|| format!("codegen: unknown method '{}.{}'", class_name, call.name))?;
0000000 3719
                    // A "static"-style call (`Bool.parse("true")`, `Float.fromStr("3.14")`)
0000000 3720
                    // on a self-less method declared inside a `type`/`enum` body — its
0000000 3721
                    // receiver is a bare reference to the type's own name, not a real
0000000 3722
                    // value. `attr.object`'s only possible resolution to exactly
0000000 3723
                    // `TNamed(class_name)` via a bare `TypeName` is this pattern (a real
0000000 3724
                    // enum-variant/const reference resolves to some OTHER concrete type,
0000000 3725
                    // per the checker's `inferExpr`). Every method still reserves a
0000000 3726
                    // leading self slot in its wasm signature regardless of whether its
0000000 3727
                    // Plum source declares a `self` param (see `fnWasmParamTypes`), so
0000000 3728
                    // something of the right type must still be pushed — the body simply
0000000 3729
                    // never reads it (no `self` binding exists for it to read).
0000000 3730
                    let is_static_call = matches!(&attr.object, ast::Expr::TypeName(n) if *n == class_name);
0000000 3731
                    if is_static_call {
0000000 3732
                        // Use `class_name`'s wasm type (via `astTypeToWasm`, exactly
0000000 3733
                        // like `fnWasmParamTypes` computed the callee's actual self
0000000 3734
                        // slot type), NOT `obj_ty` — for a builtin primitive receiver
0000000 3735
                        // (`Int.fromStr`), `obj_ty` is `TNamed("Int")` (the checker's
0000000 3736
                        // bare-`TypeName` fallback doesn't know about primitives), whose
0000000 3737
                        // `plumTypeToValtype` would wrongly resolve to a GC ref instead
0000000 3738
                        // of `i64`.
0000000 3739
                        let vt = astTypeToWasm(&class_name).unwrap_or(ValType::I32);
0000000 3740
                        pushSelfPlaceholder(vt, body);
0000000 3741
                    } else {
0000000 3742
                        compileExpr(&attr.object, body, ctx, state)?; // push self
0000000 3743
                    }
02b3582 3744
02b3582 3745
                    fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
02b3582 3746
                        match arg {
5d8ada1 3747
                            ast::Arg::Positional(e) => e,
5d8ada1 3748
                            ast::Arg::Keyword { value, .. } => value,
5d8ada1 3749
                            ast::Arg::Pair { value, .. } => value,
02b3582 3750
                        }
02b3582 3751
                    }
02b3582 3752
                    // A trailing `...T` param (e.g. `add(self, values: ...T)`) packs its
02b3582 3753
                    // trailing args into one GC array, exactly like a plain function call
02b3582 3754
                    // (see the `Expr::FnCall` variadic-call arm) — `call.args` doesn't
02b3582 3755
                    // include `self`, matching `ctx.methods`' own param list.
02b3582 3756
                    let variadic_split = match ctx.methods.get(&(class_name.clone(), call.name.clone())) {
02b3582 3757
                        Some(PlumType::TFun(params, _)) => match params.last() {
02b3582 3758
                            Some(PlumType::TVariadic(elem)) => Some(((**elem).clone(), params.len() - 1)),
02b3582 3759
                            _ => None,
02b3582 3760
                        },
02b3582 3761
                        _ => None,
02b3582 3762
                    };
02b3582 3763
                    match variadic_split {
02b3582 3764
                        Some((elem_ty, fixed_count)) => {
02b3582 3765
                            for arg in call.args.iter().take(fixed_count) {
02b3582 3766
                                compileExpr(argExprOf(arg), body, ctx, state)?;
02b3582 3767
                            }
02b3582 3768
                            let trailing: Vec<&ast::Expr> = call.args.iter().skip(fixed_count).map(argExprOf).collect();
02b3582 3769
                            let elem_vt = plumTypeToValtype(&elem_ty);
02b3582 3770
                            let array_type_idx = *ctx
02b3582 3771
                                .gc_types
02b3582 3772
                                .variadic_array_type_idx
02b3582 3773
                                .get(&elem_vt)
02b3582 3774
                                .ok_or_else(|| "internal codegen error: no variadic array type registered for this elem type".to_string())?;
02b3582 3775
                            for arg_expr in &trailing {
02b3582 3776
                                compileExpr(arg_expr, body, ctx, state)?;
02b3582 3777
                            }
02b3582 3778
                            Instruction::ArrayNewFixed { array_type_index: array_type_idx, array_size: trailing.len() as u32 }.encode(body);
02b3582 3779
                        }
02b3582 3780
                        None => {
02b3582 3781
                            for arg in &call.args {
02b3582 3782
                                compileExpr(argExprOf(arg), body, ctx, state)?;
02b3582 3783
                            }
02b3582 3784
                        }
5d8ada1 3785
                    }
5d8ada1 3786
                    Instruction::Call(func_idx).encode(body);
5d8ada1 3787
                }
5d8ada1 3788
            }
5d8ada1 3789
        }
5d8ada1 3790
        ast::Expr::String(s) => {
35af6cf 3791
            let has_interp = s.parts.iter().any(|p| matches!(p, ast::StringPart::Interp(_)));
35af6cf 3792
            if !has_interp {
35af6cf 3793
                // Fast path: every part is static text, so the whole literal is one
35af6cf 3794
                // fixed byte blob known at compile time — no runtime work at all.
35af6cf 3795
                let mut text = String::new();
35af6cf 3796
                for part in &s.parts {
35af6cf 3797
                    if let ast::StringPart::Text(t) = part {
35af6cf 3798
                        text.push_str(t);
5d8ada1 3799
                    }
5d8ada1 3800
                }
3d6f280 3801
                compileStaticString(&text, body, state);
35af6cf 3802
            } else {
3d6f280 3803
                compileInterpolatedString(s, body, ctx, state)?;
5d8ada1 3804
            }
bb8ca38 3805
        }
4ba0db3 3806
        ast::Expr::Closure(cl) => {
3d6f280 3807
            compileClosureLiteral(cl, body, ctx, state)?;
d7e5ff4 3808
        }
bb8ca38 3809
    }
bb8ca38 3810
    Ok(())
bb8ca38 3811
}
380a51c 3812
380a51c 3813
/// Compiles a variant-construction call. A payload-free variant (`None`, called as
0e39618 3814
/// `None()` rather than used bare) is its pre-allocated singleton global. A payload
0e39618 3815
/// variant pushes its field values in order and does `struct.new` into its own
0e39618 3816
/// concrete variant type.
3d6f280 3817
fn compileVariantConstruction(
380a51c 3818
    info: &EnumVariantInfo,
380a51c 3819
    call: &ast::FnCall,
0e39618 3820
    _expr: &ast::Expr,
380a51c 3821
    body: &mut Vec<u8>,
380a51c 3822
    ctx: &LocalCtx,
380a51c 3823
    state: &mut ModuleState,
380a51c 3824
) -> Result<(), String> {
380a51c 3825
    if call.args.len() != info.field_types.len() {
380a51c 3826
        return Err(format!(
380a51c 3827
            "codegen: variant '{}' expects {} arg(s), got {}",
380a51c 3828
            call.name, info.field_types.len(), call.args.len()
380a51c 3829
        ));
380a51c 3830
    }
0e39618 3831
    let variant_type_idx = *ctx.gc_types.variant_type_idx.get(&call.name)
0e39618 3832
        .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", call.name))?;
380a51c 3833
    if info.field_types.is_empty() {
0e39618 3834
        // Pre-allocated singleton (this migration plan's Decision 2) — not a fresh
0e39618 3835
        // struct.new per reference.
0e39618 3836
        let global_idx = *ctx.singleton_globals.get(&call.name)
0e39618 3837
            .ok_or_else(|| format!("codegen: payload-free variant '{}' has no singleton global", call.name))?;
0e39618 3838
        Instruction::GlobalGet(global_idx).encode(body);
380a51c 3839
        return Ok(());
380a51c 3840
    }
380a51c 3841
0e39618 3842
    // struct.new needs every field value pushed, in order, immediately before the
0e39618 3843
    // single construction instruction — positional args already match field
0e39618 3844
    // declaration order (unlike named class-field construction), so no reordering
0e39618 3845
    // is needed here.
0e39618 3846
    for arg in &call.args {
380a51c 3847
        let arg_expr = match arg {
380a51c 3848
            ast::Arg::Positional(e) => e,
380a51c 3849
            ast::Arg::Keyword { value, .. } => value,
380a51c 3850
            ast::Arg::Pair { value, .. } => value,
380a51c 3851
        };
3d6f280 3852
        compileExpr(arg_expr, body, ctx, state)?;
380a51c 3853
    }
0e39618 3854
    Instruction::StructNew(variant_type_idx).encode(body);
380a51c 3855
    Ok(())
380a51c 3856
}
4ba0db3 3857
0e39618 3858
/// Emits a static (compile-time-known) string literal as a fresh passive data
0e39618 3859
/// segment, pushing a `Str` array built from it via `array.new_data`. `Str`'s wasm-gc
0e39618 3860
/// representation is a plain `array<i8>` (see Decision 4 of the wasm-gc migration
0e39618 3861
/// plan) — no length prefix needed, unlike the old bump-allocator layout.
3d6f280 3862
fn compileStaticString(text: &str, body: &mut Vec<u8>, state: &mut ModuleState) {
35af6cf 3863
    let bytes = text.as_bytes();
0e39618 3864
    let data_index = state.passive_segments.len() as u32;
0e39618 3865
    state.passive_segments.push(bytes.to_vec());
0e39618 3866
    let str_type_idx = withGcTypes(|r| r.str_type_idx);
0e39618 3867
    Instruction::I32Const(0).encode(body);
0e39618 3868
    Instruction::I32Const(bytes.len() as i32).encode(body);
0e39618 3869
    Instruction::ArrayNewData { array_type_index: str_type_idx, array_data_index: data_index }.encode(body);
35af6cf 3870
}
35af6cf 3871
35af6cf 3872
/// Lowers a string literal that contains at least one `{expr}` interpolation.
35af6cf 3873
/// Every part becomes a string-pointer-valued expression (static text via
3d6f280 3874
/// `compileStaticString`; `Str`/`Int`/`Bool` interpolated values converted at
35af6cf 3875
/// runtime), then all parts are left-folded together with the `__string_concat`
35af6cf 3876
/// runtime helper.
3d6f280 3877
fn compileInterpolatedString(
35af6cf 3878
    s: &ast::StringExpr,
35af6cf 3879
    body: &mut Vec<u8>,
35af6cf 3880
    ctx: &LocalCtx,
35af6cf 3881
    state: &mut ModuleState,
35af6cf 3882
) -> Result<(), String> {
35af6cf 3883
    let mut first = true;
35af6cf 3884
    for part in &s.parts {
35af6cf 3885
        match part {
35af6cf 3886
            ast::StringPart::Text(t) => {
3d6f280 3887
                compileStaticString(t, body, state);
35af6cf 3888
            }
35af6cf 3889
            ast::StringPart::Interp(expr) => {
3d6f280 3890
                let ty = inferLocalType(expr, ctx);
35af6cf 3891
                match ty {
35af6cf 3892
                    PlumType::TStr => {
3d6f280 3893
                        compileExpr(expr, body, ctx, state)?;
35af6cf 3894
                    }
35af6cf 3895
                    PlumType::TInt => {
3d6f280 3896
                        compileExpr(expr, body, ctx, state)?;
35af6cf 3897
                        Instruction::Call(ctx.int_to_string_func).encode(body);
35af6cf 3898
                    }
35af6cf 3899
                    PlumType::TBool => {
0e39618 3900
                        let str_ref = withGcTypes(|r| gcRef(r.str_type_idx));
0e39618 3901
                        compileBoolConditionAsI32(expr, body, ctx, state)?;
0e39618 3902
                        Instruction::If(BlockType::Result(str_ref)).encode(body);
3d6f280 3903
                        compileStaticString("True", body, state);
35af6cf 3904
                        Instruction::Else.encode(body);
3d6f280 3905
                        compileStaticString("False", body, state);
35af6cf 3906
                        Instruction::End.encode(body);
35af6cf 3907
                    }
35af6cf 3908
                    PlumType::TFloat => {
35af6cf 3909
                        return Err("codegen: interpolating a Float value is not yet supported".to_string());
35af6cf 3910
                    }
35af6cf 3911
                    other => {
35af6cf 3912
                        return Err(format!(
35af6cf 3913
                            "codegen: interpolating a value of type {} is not yet supported",
35af6cf 3914
                            other
35af6cf 3915
                        ));
35af6cf 3916
                    }
35af6cf 3917
                }
35af6cf 3918
            }
35af6cf 3919
        }
35af6cf 3920
        if !first {
35af6cf 3921
            Instruction::Call(ctx.string_concat_func).encode(body);
35af6cf 3922
        }
35af6cf 3923
        first = false;
35af6cf 3924
    }
35af6cf 3925
    Ok(())
35af6cf 3926
}
35af6cf 3927
0e39618 3928
/// Compiles a closure *literal* at its construction site. Snapshots each captured
0e39618 3929
/// free variable's CURRENT value into a fresh env struct (`struct.new`), then wraps
0e39618 3930
/// it with the closure's table index into the shared `{table_idx, env}` closure
0e39618 3931
/// struct and leaves its ref on the stack.
3d6f280 3932
fn compileClosureLiteral(
4ba0db3 3933
    cl: &ast::Closure,
4ba0db3 3934
    body: &mut Vec<u8>,
4ba0db3 3935
    ctx: &LocalCtx,
4ba0db3 3936
    _state: &mut ModuleState,
4ba0db3 3937
) -> Result<(), String> {
4ba0db3 3938
    let key = cl as *const ast::Closure as usize;
4ba0db3 3939
    let info = ctx
4ba0db3 3940
        .closures
4ba0db3 3941
        .get(&key)
35af6cf 3942
        .ok_or_else(|| "internal codegen error: closure literal was not discovered by the discovery pre-pass".to_string())?;
0e39618 3943
0e39618 3944
    // Build the closure struct directly on the stack: push table_idx, then build the
0e39618 3945
    // env struct (snapshotting each captured free variable's current value from the
0e39618 3946
    // enclosing function's local), then wrap both into the shared closure struct.
0e39618 3947
    Instruction::I32Const(info.table_idx as i32).encode(body);
0e39618 3948
    for (name, _) in &info.free_vars {
4ba0db3 3949
        let local_idx = *ctx
4ba0db3 3950
            .locals
4ba0db3 3951
            .get(name)
4ba0db3 3952
            .ok_or_else(|| format!("codegen: captured variable '{}' is not a local in the enclosing scope", name))?;
4ba0db3 3953
        Instruction::LocalGet(local_idx).encode(body);
4ba0db3 3954
    }
0e39618 3955
    Instruction::StructNew(info.env_type_idx).encode(body);
0e39618 3956
    Instruction::StructNew(ctx.gc_types.closure_type_idx).encode(body);
4ba0db3 3957
    Ok(())
4ba0db3 3958
}
4ba0db3 3959
0e39618 3960
/// Compiles a call to a closure-typed local via `call_indirect`. The local is
0e39618 3961
/// statically typed `anyref` (closures share that placeholder type — see
0e39618 3962
/// `plumTypeToValtype`), so every read of its fields `ref.cast`s down to the shared
0e39618 3963
/// concrete closure struct type first. Stack order matches the closure function's
0e39618 3964
/// signature `(env, ...args)`: push the env, then each argument, then the table
0e39618 3965
/// index (the `call_indirect` operand).
3d6f280 3966
fn compileClosureCall(
4ba0db3 3967
    call: &ast::FnCall,
4ba0db3 3968
    body: &mut Vec<u8>,
4ba0db3 3969
    ctx: &LocalCtx,
4ba0db3 3970
    state: &mut ModuleState,
4ba0db3 3971
) -> Result<(), String> {
4ba0db3 3972
    let closure_local = *ctx
4ba0db3 3973
        .locals
4ba0db3 3974
        .get(&call.name)
4ba0db3 3975
        .ok_or_else(|| format!("codegen: closure '{}' is not a local", call.name))?;
0e39618 3976
    let closure_type_idx = ctx.gc_types.closure_type_idx;
4ba0db3 3977
35af6cf 3978
    // Prefer the exact signature recorded when this local was assigned a closure
35af6cf 3979
    // *literal* (see `closure_local_sigs`) — it's already correct. Otherwise (e.g.
35af6cf 3980
    // `call.name` is a `fn(...)`-typed parameter, whose declared type is reliable on
35af6cf 3981
    // its own) fall back to re-deriving it from the type env.
35af6cf 3982
    let sig_key: ClosureSigKey = match ctx.closure_local_sigs.borrow().get(&call.name) {
35af6cf 3983
        Some(key) => key.clone(),
35af6cf 3984
        None => {
3d6f280 3985
            let (param_ptypes, ret_ptype) = match inferLocalType(&ast::Expr::Var(call.name.clone()), ctx) {
35af6cf 3986
                PlumType::TFun(p, r) => (p, *r),
35af6cf 3987
                other => return Err(format!("codegen: '{}' is not callable (type {:?})", call.name, other)),
35af6cf 3988
            };
0e39618 3989
            let mut sig_params = vec![ValType::Ref(RefType::ANYREF)]; // env pointer
35af6cf 3990
            for p in &param_ptypes {
3d6f280 3991
                sig_params.push(plumTypeToValtype(p));
35af6cf 3992
            }
35af6cf 3993
            let ret_vt = match ret_ptype {
35af6cf 3994
                PlumType::TUnit => None,
3d6f280 3995
                other => Some(plumTypeToValtype(&other)),
35af6cf 3996
            };
35af6cf 3997
            (sig_params, ret_vt)
35af6cf 3998
        }
4ba0db3 3999
    };
4ba0db3 4000
    let type_index = *ctx
4ba0db3 4001
        .closure_call_types
4ba0db3 4002
        .get(&sig_key)
4ba0db3 4003
        .ok_or_else(|| format!("internal codegen error: no call_indirect type for closure '{}'", call.name))?;
4ba0db3 4004
0e39618 4005
    // env (closure struct field 1)
4ba0db3 4006
    Instruction::LocalGet(closure_local).encode(body);
0e39618 4007
    Instruction::RefCastNonNull(HeapType::Concrete(closure_type_idx)).encode(body);
0e39618 4008
    Instruction::StructGet { struct_type_index: closure_type_idx, field_index: 1 }.encode(body);
4ba0db3 4009
    // real arguments
4ba0db3 4010
    for arg in &call.args {
3d6f280 4011
        compileExpr(argExprOf(arg), body, ctx, state)?;
4ba0db3 4012
    }
0e39618 4013
    // table index (closure struct field 0) — the call_indirect operand
4ba0db3 4014
    Instruction::LocalGet(closure_local).encode(body);
0e39618 4015
    Instruction::RefCastNonNull(HeapType::Concrete(closure_type_idx)).encode(body);
0e39618 4016
    Instruction::StructGet { struct_type_index: closure_type_idx, field_index: 0 }.encode(body);
4ba0db3 4017
    Instruction::CallIndirect { type_index, table_index: 0 }.encode(body);
4ba0db3 4018
    Ok(())
4ba0db3 4019
}
4ba0db3 4020
4ba0db3 4021
/// Compiles a closure literal's own body into a standalone wasm function. Local 0 is the
4ba0db3 4022
/// implicit env pointer; the closure's params follow; then each captured free variable
4ba0db3 4023
/// gets a local loaded from the env struct at function entry (restoring the snapshot).
3d6f280 4024
fn compileClosureBody(
4ba0db3 4025
    cl: &ast::Closure,
4ba0db3 4026
    info: &ClosureInfo,
4ba0db3 4027
    ctx: &CompileCtx,
4ba0db3 4028
    state: &mut ModuleState,
4ba0db3 4029
) -> Result<Vec<u8>, String> {
4ba0db3 4030
    let mut body = Vec::new();
4ba0db3 4031
4ba0db3 4032
    // Base type env: globals + captured free vars + closure params.
4ba0db3 4033
    let mut base_env = ctx.global_env.clone();
4ba0db3 4034
    for (name, ty) in &info.free_vars {
4ba0db3 4035
        base_env.insert(name.clone(), TypeScheme::mono(ty.clone()));
4ba0db3 4036
    }
4ba0db3 4037
    for (name, pty) in cl.params.iter().zip(info.param_ptypes.iter()) {
4ba0db3 4038
        base_env.insert(name.clone(), TypeScheme::mono(pty.clone()));
4ba0db3 4039
    }
4ba0db3 4040
4ba0db3 4041
    let mut collector = Collector {
4ba0db3 4042
        env: base_env.clone(),
3d6f280 4043
        cctx: checkCtxOf(&ctx.classes, &ctx.methods, &ctx.enum_variants, &ctx.enum_params),
4ba0db3 4044
        named: Vec::new(),
4ba0db3 4045
        named_set: Default::default(),
4ba0db3 4046
        match_scratch: HashMap::new(),
35af6cf 4047
        nested_class_scratch: HashMap::new(),
0e39618 4048
        nested_class_scratch_types: Vec::new(),
35af6cf 4049
        next_nested_class_slot: 0,
da1c377 4050
        variadic_for_scratch: HashMap::new(),
da1c377 4051
        next_variadic_for_slot: 0,
4ba0db3 4052
    };
3d6f280 4053
    collector.walkBlock(&cl.body);
4ba0db3 4054
4ba0db3 4055
    // ---- local index layout ----
4ba0db3 4056
    // [env_ptr][closure params][free-var locals][named...][classcall][match][closure scratch]
4ba0db3 4057
    let mut locals: HashMap<String, u32> = HashMap::new();
4ba0db3 4058
    let mut groups: Vec<ValType> = Vec::new();
4ba0db3 4059
    let mut idx = 0u32;
4ba0db3 4060
4ba0db3 4061
    idx += 1; // local 0 = env pointer (a param, so not declared below)
4ba0db3 4062
    for name in &cl.params {
4ba0db3 4063
        locals.insert(name.clone(), idx);
4ba0db3 4064
        idx += 1;
4ba0db3 4065
    }
4ba0db3 4066
    for (name, ty) in &info.free_vars {
4ba0db3 4067
        locals.insert(name.clone(), idx);
3d6f280 4068
        groups.push(plumTypeToValtype(ty));
4ba0db3 4069
        idx += 1;
4ba0db3 4070
    }
4ba0db3 4071
    for (name, ty) in &collector.named {
4ba0db3 4072
        if locals.contains_key(name) {
4ba0db3 4073
            continue;
4ba0db3 4074
        }
4ba0db3 4075
        locals.insert(name.clone(), idx);
3d6f280 4076
        groups.push(plumTypeToValtype(ty));
4ba0db3 4077
        idx += 1;
4ba0db3 4078
    }
4ba0db3 4079
4ba0db3 4080
    let match_scratch_base = idx;
4ba0db3 4081
    let mut match_scratch_index: HashMap<usize, u32> = HashMap::new();
35af6cf 4082
    for (ptr, types) in collector.match_scratch.iter() {
4ba0db3 4083
        match_scratch_index.insert(*ptr, idx - match_scratch_base);
35af6cf 4084
        for ty in types {
3d6f280 4085
            groups.push(plumTypeToValtype(ty));
35af6cf 4086
            idx += 1;
35af6cf 4087
        }
35af6cf 4088
    }
35af6cf 4089
35af6cf 4090
    let nested_class_scratch_base = idx;
0e39618 4091
    // Each slot is declared with its OWN concrete variant ref type (not a uniform
0e39618 4092
    // placeholder) — `struct.get` on a constructor-pattern match requires the local
0e39618 4093
    // holding the narrowed (`ref.cast`) value to be statically typed as that exact
0e39618 4094
    // variant, and different slots very likely narrow to different variants.
0e39618 4095
    for vname in &collector.nested_class_scratch_types {
0e39618 4096
        let variant_idx = withGcTypes(|r| *r.variant_type_idx.get(vname)
0e39618 4097
            .unwrap_or_else(|| panic!("internal codegen error: variant '{}' missing from the GC type registry", vname)));
0e39618 4098
        groups.push(gcRef(variant_idx));
4ba0db3 4099
        idx += 1;
4ba0db3 4100
    }
4ba0db3 4101
da1c377 4102
    let variadic_for_scratch_base = idx;
da1c377 4103
    let variadic_for_scratch_count = collector.variadic_for_scratch.values().copied().max().map(|m| m + 1).unwrap_or(0);
da1c377 4104
    for _ in 0..variadic_for_scratch_count {
da1c377 4105
        groups.push(ValType::I32); // count
da1c377 4106
        groups.push(ValType::I32); // loop index
da1c377 4107
        idx += 2;
da1c377 4108
    }
da1c377 4109
4ba0db3 4110
    if groups.is_empty() {
4ba0db3 4111
        body.push(0);
4ba0db3 4112
    } else {
3d6f280 4113
        body.extend(encodeLeb128U32(groups.len() as u32));
4ba0db3 4114
        for g in &groups {
3d6f280 4115
            body.extend(encodeLeb128U32(1));
4ba0db3 4116
            g.encode(&mut body);
4ba0db3 4117
        }
4ba0db3 4118
    }
4ba0db3 4119
0e39618 4120
    // Restore each captured free variable from the env struct (local 0, statically
0e39618 4121
    // `anyref` — `ref.cast` down to THIS closure's own concrete env type) at entry.
0e39618 4122
    for (i, (name, _)) in info.free_vars.iter().enumerate() {
4ba0db3 4123
        let local_idx = *locals.get(name).expect("free var local was assigned above");
4ba0db3 4124
        Instruction::LocalGet(0).encode(&mut body); // env pointer
0e39618 4125
        Instruction::RefCastNonNull(HeapType::Concrete(info.env_type_idx)).encode(&mut body);
0e39618 4126
        Instruction::StructGet { struct_type_index: info.env_type_idx, field_index: i as u32 }.encode(&mut body);
4ba0db3 4127
        Instruction::LocalSet(local_idx).encode(&mut body);
4ba0db3 4128
    }
4ba0db3 4129
4ba0db3 4130
    let local_ctx = LocalCtx {
4ba0db3 4131
        locals,
4ba0db3 4132
        match_scratch_base,
4ba0db3 4133
        match_scratch_index,
35af6cf 4134
        nested_class_scratch_base,
35af6cf 4135
        nested_class_scratch: collector.nested_class_scratch,
da1c377 4136
        variadic_for_scratch_base,
da1c377 4137
        variadic_for_scratch: collector.variadic_for_scratch,
4ba0db3 4138
        func_ids: &ctx.func_ids,
4ba0db3 4139
        func_sigs: &ctx.func_sigs,
4ba0db3 4140
        closures: &ctx.closures,
4ba0db3 4141
        closure_call_types: &ctx.closure_call_types,
35af6cf 4142
        named_fn_values: &ctx.named_fn_values,
35af6cf 4143
        string_concat_func: ctx.string_concat_func,
35af6cf 4144
        int_to_string_func: ctx.int_to_string_func,
4ba0db3 4145
        classes: &ctx.classes,
4ba0db3 4146
        methods: &ctx.methods,
4ba0db3 4147
        enum_variants: &ctx.enum_variants,
4fda634 4148
        enum_params: &ctx.enum_params,
0e39618 4149
        gc_types: &ctx.gc_types,
0e39618 4150
        singleton_globals: &ctx.singleton_globals,
4ba0db3 4151
        type_env: RefCell::new(base_env),
35af6cf 4152
        closure_local_sigs: RefCell::new(HashMap::new()),
4ba0db3 4153
    };
4ba0db3 4154
3d6f280 4155
    compileBlockAsFnBody(&cl.body, &mut body, &local_ctx, state, info.ret_vt)?;
4ba0db3 4156
4ba0db3 4157
    Instruction::End.encode(&mut body);
4ba0db3 4158
    Ok(body)
4ba0db3 4159
}