plum
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, ®istry)), |
| 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, ®istry)), |
| 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, ®istry)), |
| 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(¶m_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(¶m_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, ¶ms, 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 ¶m_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 | } |