1
//! `CompileContext` — the per-module wasm assembly state shared across
2
//! every codegen path.
3
//!
4
//! Split across topic-focused submodules so each file stays under the
5
//! ~500-line CLAUDE.md guideline:
6
//! - [`host_fn`] — host-fn import registration + lookup.
7
//! - [`types`] — concrete + abstract heap-type accessors
8
//!   (`ratio_ref`, `commodity_ref`, `pair_ref`, `anyref`, ...).
9
//! - [`registry`] — section-level registration: types, imports,
10
//!   functions, exports, data segments, and the local-pool
11
//!   allocator.
12
//! - [`pair`] — `$pair`/`$i8_array` setup + entity allocators +
13
//!   string-eq helper.
14
//! - [`commodity`] — commodity-arithmetic helpers
15
//!   (`commodity_new`, `commodity_add`/sub/neg/scale/cmp/...).
16
//! - [`ratio`] — ratio-arithmetic helpers (`ratio_new`, `gcd`,
17
//!   binops, comparisons).
18
//!
19
//! Construction lives here in `mod.rs`: every entry point assembles
20
//! the engine by calling submodule helpers in order.
21

            
22
pub(crate) mod closure;
23
mod commodity;
24
mod commodity_compound;
25
mod entity_registry;
26
mod exception;
27
mod host_fn;
28
mod ids;
29
pub(in crate::compiler) mod monomorph;
30
mod pair;
31
mod ratio;
32
mod registry;
33
mod snapshot;
34
mod types;
35
mod unit_term;
36

            
37
#[cfg(test)]
38
mod tests;
39

            
40
#[cfg(test)]
41
mod snapshot_tests;
42

            
43
use std::collections::{HashMap, HashSet};
44

            
45
use tracing::debug;
46
use wasm_encoder::{
47
    CodeSection, DataCountSection, DataSection, ElementSection, Elements, ExportKind,
48
    ExportSection, Function, FunctionSection, ImportSection, Instruction, MemorySection,
49
    MemoryType, Module, TagSection, TypeSection, ValType,
50
};
51

            
52
use super::layout::OutputSerializer;
53
use crate::ast::{Expr, WasmType};
54
use crate::error::Result;
55
use crate::host_fn::HostFnSpec;
56

            
57
pub(crate) use closure::CLOSURE_LITERAL_LEN;
58

            
59
#[derive(Debug, Clone)]
60
pub(crate) struct HostFnEntry {
61
    pub func_idx: u32,
62
    pub params: Vec<WasmType>,
63
    pub result: Option<WasmType>,
64
}
65

            
66
/// Base index for user-allocatable locals (after pre-allocated slots)
67
pub const LOCAL_POOL_BASE: u32 = 6;
68

            
69
pub struct CompileContext {
70
    pub(in crate::compiler::context) types: TypeSection,
71
    pub(in crate::compiler::context) imports: ImportSection,
72
    pub(in crate::compiler::context) functions: FunctionSection,
73
    pub(in crate::compiler::context) memories: MemorySection,
74
    pub(in crate::compiler::context) tags: TagSection,
75
    pub(in crate::compiler::context) exports: ExportSection,
76
    pub(in crate::compiler::context) data: DataSection,
77
    pub(in crate::compiler::context) codes: CodeSection,
78
    pub(in crate::compiler::context) data_count: u32,
79
    pub(in crate::compiler::context) type_count: u32,
80
    pub(in crate::compiler::context) tag_count: u32,
81
    pub(in crate::compiler::context) import_func_count: u32,
82
    pub(in crate::compiler::context) local_func_count: u32,
83
    pub(in crate::compiler::context) type_cache: HashMap<Vec<ValType>, HashMap<Vec<ValType>, u32>>,
84
    /// Maps a declared helper-function / import NAME to its wasm function index.
85
    /// Written during the declare phase; read once by [`Self::resolve_ids`] to
86
    /// populate the typed `WasmIds`, and by `export_func` /
87
    /// `declared_func_index`. Emit code never touches it — it reads `self.ids`.
88
    pub(in crate::compiler::context) func_names: HashMap<String, u32>,
89
    pub(in crate::compiler::context) host_fns: HashMap<String, HostFnEntry>,
90
    pub(in crate::compiler::context) pending_helpers: Vec<Function>,
91
    pub(in crate::compiler::context) next_local: u32,
92
    pub(in crate::compiler::context) local_types: Vec<(WasmType, u32)>,
93
    pub(in crate::compiler::context) serializer: OutputSerializer,
94
    pub(in crate::compiler::context) closures: closure::ClosureRegistry,
95
    /// Source `(params, body)` of each value-position lambda, keyed by the wasm
96
    /// local index its closure value was bound to. A let-bound closure is
97
    /// emitted with FIXED (param-usage-inferred / default-Ratio) param types, so
98
    /// applying it via `call_ref` over a list whose element type differs is a
99
    /// type mismatch. A higher-order native (FOLD/MAP/FILTER) recovers the body
100
    /// here and INLINES it per element instead — binding the iteration param to
101
    /// the actual element type — so the closure works over String/I32/Entity
102
    /// lists, not just Ratio. Keyed by local index (unique per binding); the
103
    /// closure SIG is shared across structurally-identical lambdas and can't
104
    /// distinguish bodies.
105
    pub(in crate::compiler::context) closure_bodies: HashMap<u32, (crate::ast::LambdaParams, Expr)>,
106
    pub(in crate::compiler::context) declared_funcs: Vec<u32>,
107
    pub(in crate::compiler::context) monomorphs: monomorph::MonomorphCache,
108
    pub(in crate::compiler::context) nomi_error_tag: Option<u32>,
109
    /// Resolved wasm fn/type indices for emit. Seeded poison
110
    /// ([`ids::WasmIds::UNRESOLVED`]) in `new_skeleton`, overwritten with the
111
    /// real indices by `resolve_ids` at the end of each public constructor —
112
    /// before any emit can read it. See `context/ids.rs`.
113
    pub(in crate::compiler) ids: ids::WasmIds,
114
    inlining_stack: Vec<String>,
115
    inlining_set: HashSet<String>,
116
    block_labels: Vec<BlockLabel>,
117
    tag_frames: Vec<TagFrame>,
118
    unwind_frames: Vec<UnwindFrame>,
119
}
120

            
121
/// One active `(unwind-protect)` frame, live while its protected body
122
/// compiles. A non-local lexical exit — `(return-from)` / `(go)` whose
123
/// target lies *outside* this unwind-protect — must run this cleanup before
124
/// branching (Common-Lisp semantics), so the exit-site codegen walks the
125
/// frame stack and emits the cleanup of every crossed frame inline, then
126
/// `br`s. `threshold` is the `block_depth()` the unwind-protect sat at
127
/// (`parent_depth`); an exit whose target wasm-depth is `<= threshold`
128
/// leaves this frame and so triggers its cleanup. `cleanup` is the cleanup
129
/// forms' AST, recompiled for effect at each crossing exit (the normal /
130
/// exceptional convergence path emits its own copy independently).
131
#[derive(Debug, Clone)]
132
pub(in crate::compiler) struct UnwindFrame {
133
    pub threshold: u32,
134
    pub cleanup: Vec<Expr>,
135
}
136

            
137
/// One frame on the lexical BLOCK stack. `wasm_depth` is the
138
/// `FunctionEmitter::block_depth()` reading captured *after* the
139
/// matching `block` instruction was emitted, so a `(return-from name v)`
140
/// sees a stable br target across nested IF / DOLIST / DO frames inside
141
/// the body. The frame is popped when control leaves the BLOCK form.
142
///
143
/// The block's result type is discovered at emit time: each reachable
144
/// `(return-from name v)` compiled inside the body records `v`'s
145
/// `WasmType` in `recorded_exits`. Because only *compiled* (reachable)
146
/// exits record — code stored as a value (quote / lambda / labels bodies,
147
/// discarded macro args) is never compiled in value position — the set is
148
/// exactly the live exits, with no syntactic pre-scan.
149
#[derive(Debug, Clone)]
150
pub(in crate::compiler) struct BlockLabel {
151
    pub name: String,
152
    pub wasm_depth: u32,
153
    pub recorded_exits: Vec<WasmType>,
154
}
155

            
156
/// One TAGBODY frame. Each tag in the body has a stable ordinal `pc` (its
157
/// position in the source order, base-zero) and a wasm-block depth captured
158
/// when emit reaches its segment. `(go tag)` resolves the named tag,
159
/// stores the next `pc` into `pc_local`, and branches to the dispatcher
160
/// `loop` so `br_table` re-enters at the right segment.
161
#[derive(Debug, Clone)]
162
pub(in crate::compiler) struct TagFrame {
163
    pub tags: Vec<TagEntry>,
164
    pub pc_local: u32,
165
    pub loop_depth: u32,
166
}
167

            
168
#[derive(Debug, Clone)]
169
pub(in crate::compiler) struct TagEntry {
170
    pub name: String,
171
    pub pc: u32,
172
}
173

            
174
impl CompileContext {
175
    /// Builds an eval-mode compile context: same types and ratio helpers as
176
    /// script mode, but exports `nomi-eval` (returning `(ref null any)`)
177
    /// instead of `should_apply`/`process`. Each entry in `host_fns`
178
    /// gets a wasm import declared up-front (so the function index is
179
    /// stable for codegen) plus an entry in the lookup table the native
180
    /// dispatcher consults when emitting calls. The host decodes the
181
    /// nomi-eval return via `scripting::runtime::decode_eval_result`.
182
166999
    pub fn new_eval_with_host_fns(host_fns: &[HostFnSpec]) -> Result<Self> {
183
166999
        let mut ctx = Self::new_skeleton()?;
184
166999
        ctx.register_raise_import()?;
185
166999
        ctx.register_catch_each_import()?;
186
        // `env.log` is the output channel for PRINT / DISPLAY / NEWLINE / DEBUG.
187
        // It was script-mode-only; without it those natives' `ctx.func("log")`
188
        // had no func index in eval mode. Declared before the local `nomi-eval`
189
        // fn so the import index space stays contiguous. Host side: the rpc
190
        // Session linker wires `env.log` (mirrors `scripting::host`).
191
166999
        ctx.register_log_import()?;
192
7983247
        for spec in host_fns {
193
7983242
            ctx.register_host_fn(spec)?;
194
        }
195
166998
        let anyref = ctx.anyref();
196
166998
        ctx.register_function("nomi-eval", &[], &[anyref])?;
197
        // Two-phase (declare → resolve → build): declare every helper signature
198
        // so all indices exist, snapshot them into `ids`, then emit the bodies
199
        // reading `ids`. Declare order fixes the function-index space; build
200
        // order fixes `pending_helpers` + data-segment indices — both preserved
201
        // exactly as the pre-split single-pass layout, so the wasm is identical.
202
166998
        ctx.declare_ratio_helpers()?;
203
166998
        ctx.declare_unit_term_helpers()?;
204
166998
        ctx.declare_commodity_helpers()?;
205
166998
        ctx.declare_pair_helpers()?;
206
166998
        ctx.declare_string_eq_helper()?;
207
166998
        ctx.declare_entity_allocators()?;
208
166998
        ctx.export_func("nomi-eval")?;
209
166998
        ctx.ids = ctx.resolve_ids()?;
210
166998
        ctx.build_ratio_helpers()?;
211
166998
        ctx.build_unit_term_helpers();
212
166998
        ctx.build_commodity_helpers()?;
213
166998
        ctx.build_pair_helpers();
214
166998
        ctx.build_string_eq_helper();
215
166998
        ctx.build_entity_allocators()?;
216
166998
        debug!(
217
            host_fn_count = host_fns.len(),
218
            "eval compile context initialized"
219
        );
220
166998
        Ok(ctx)
221
166999
    }
222

            
223
    /// Declares the `(error 'code "msg")` lowering target up-front so
224
    /// the function index is stable before any user host fn is wired.
225
    /// The native is declared as `(string-ref, string-ref) -> ()` —
226
    /// it never returns normally; the host always produces
227
    /// `Err(__nomi_raise:CODE:MSG)` which the classifier parses before
228
    /// the unreachable-trap branch (ADR-0014). See
229
    /// `rpc::natives::raise` for the host side.
230
252751
    fn register_raise_import(&mut self) -> Result<()> {
231
252751
        let arr = self.array_ref();
232
252751
        self.register_import("nomi", "__nomi_raise", &[arr, arr], &[])?;
233
252751
        Ok(())
234
252751
    }
235

            
236
    /// Declares the `(catch-each items var body)` lowering target up-front.
237
    /// The body is compiled to a real wasm fn (Tier 1.5 closure machinery)
238
    /// whose funcref + env are extracted at the call site and passed to
239
    /// `__nomi_catch_each` along with the items list. The host walks the
240
    /// chain in Rust, calls the funcref per item recovering per-call
241
    /// `wasmtime::Error`, and returns a heterogeneous `pair<anyref>`
242
    /// chain of `(ok . v)` / `(err . (code . msg))` cells. Engine-bound
243
    /// `OutOfFuel` / `EpochInterrupt` traps re-throw straight through —
244
    /// they never appear as `err` cells (ADR-0025).
245
166999
    fn register_catch_each_import(&mut self) -> Result<()> {
246
        // Items + result go on the wire as abstract `(ref null struct)`.
247
        // The host registers the fn with `Rooted<StructRef>` whose
248
        // `WasmTy::valtype()` is the abstract heap type — declaring the
249
        // concrete `$pair` here would fail the import-type check at
250
        // instantiation. Compiler emits a `ref.cast` after the call to
251
        // recover the concrete `$pair` for the downstream pipeline; the
252
        // items-arg side accepts a `(ref null $pair)` because the struct
253
        // subtype relationship is one-way (concrete <: abstract).
254
166999
        let funcref = ValType::Ref(wasm_encoder::RefType::FUNCREF);
255
166999
        let any = self.anyref();
256
166999
        let abstract_struct = self.struct_ref();
257
166999
        self.register_import(
258
166999
            "nomi",
259
166999
            "__nomi_catch_each",
260
166999
            &[funcref, any, abstract_struct],
261
166999
            &[abstract_struct],
262
        )?;
263
166999
        Ok(())
264
166999
    }
265

            
266
252751
    fn new_skeleton() -> Result<Self> {
267
252751
        let mut ctx = Self {
268
252751
            types: TypeSection::new(),
269
252751
            imports: ImportSection::new(),
270
252751
            functions: FunctionSection::new(),
271
252751
            memories: MemorySection::new(),
272
252751
            tags: TagSection::new(),
273
252751
            exports: ExportSection::new(),
274
252751
            codes: CodeSection::new(),
275
252751
            data: DataSection::new(),
276
252751
            data_count: 0,
277
252751
            type_count: 0,
278
252751
            tag_count: 0,
279
252751
            import_func_count: 0,
280
252751
            local_func_count: 0,
281
252751
            type_cache: HashMap::new(),
282
252751
            func_names: HashMap::new(),
283
252751
            host_fns: HashMap::new(),
284
252751
            pending_helpers: Vec::new(),
285
252751
            next_local: LOCAL_POOL_BASE,
286
252751
            local_types: Vec::new(),
287
252751
            serializer: OutputSerializer::new(super::expr::LOCAL_OUTPUT_BASE),
288
252751
            closures: closure::ClosureRegistry::default(),
289
252751
            closure_bodies: HashMap::new(),
290
252751
            declared_funcs: Vec::new(),
291
252751
            monomorphs: monomorph::MonomorphCache::default(),
292
252751
            nomi_error_tag: None,
293
252751
            ids: ids::WasmIds::UNRESOLVED,
294
252751
            inlining_stack: Vec::new(),
295
252751
            inlining_set: HashSet::new(),
296
252751
            block_labels: Vec::new(),
297
252751
            tag_frames: Vec::new(),
298
252751
            unwind_frames: Vec::new(),
299
252751
        };
300
        // Type indices are captured into `ids.ty_*` AS each type registers, so
301
        // the type-ref accessors (`ratio_ref`, `pair_ref`, …) read `ids` even
302
        // mid-skeleton — before the helper-fn `resolve_ids` runs. The struct
303
        // layouts + registration order are unchanged, so the wasm is identical.
304
252751
        ctx.ids.ty_i8_array = ctx.register_type()?;
305
252751
        ctx.ids.ty_ratio = ctx.register_struct_type(&[ValType::I64, ValType::I64])?;
306
252751
        ctx.ids.ty_pair = ctx.register_pair_type()?;
307
        // Commodity unit-term (ADR-0028): a flat `(array (mut i64))` of sorted
308
        // canonical `(hi, lo, exp)` triples. Registered before `commodity` so
309
        // the struct's 5th field can reference it. A NULL term means ATOMIC
310
        // `[(atom, 1)]`, so atomic money leaves the field unset and round-trips
311
        // exactly as before this widening.
312
252751
        ctx.ids.ty_unit_term = ctx.register_i64_array_type()?;
313
        // Commodity = (i64 numer, i64 denom, i64 commodity_hi, i64 commodity_lo,
314
        // (ref null $unit_term) term). commodity_hi/lo carry the two halves of
315
        // the UUID, stored as i64 to match wasmtime host-fn primitive types
316
        // (reassembled to Uuid at the host boundary via `Uuid::from_u128`).
317
        // Fields 0-3 are byte-identical to the pre-ADR-0028 layout.
318
252751
        let unit_term_ref = ctx.unit_term_ref();
319
252751
        ctx.ids.ty_commodity = ctx.register_struct_type(&[
320
252751
            ValType::I64,
321
252751
            ValType::I64,
322
252751
            ValType::I64,
323
252751
            ValType::I64,
324
252751
            unit_term_ref,
325
252751
        ])?;
326
        // Server-entity wasm struct types. Field layouts read from
327
        // the single-source-of-truth `ENTITY_SPECS` table in
328
        // `entity_registry.rs` — both this site and
329
        // `register_entity_allocators` walk the same const so the
330
        // struct layout and the allocator signature can't drift.
331
        // Adding a new entity kind: extend `EntityKind` in =ast.rs= +
332
        // add one row to `ENTITY_SPECS`.
333
1769257
        for spec in entity_registry::ENTITY_SPECS {
334
7329779
            let fields: Vec<ValType> = spec.fields.iter().map(|f| f.as_val_type(&ctx)).collect();
335
1769257
            let idx = ctx.register_struct_type(&fields)?;
336
1769257
            ctx.ids.set_entity_type(spec.kind, idx);
337
        }
338
        // `$nomi_condition` struct + `$nomi_error` exception tag (Tier 3,
339
        // ADR-0026). Registered here so the type/tag indices are stable
340
        // before any body emits a `throw` / `try_table`. The tag's payload
341
        // is a single `(ref null $nomi_condition)` so a catch hands the
342
        // handler the condition ref directly.
343
252751
        ctx.register_exception_support()?;
344
252751
        ctx.memories.memory(MemoryType {
345
252751
            minimum: 1,
346
252751
            maximum: None,
347
252751
            memory64: false,
348
252751
            shared: false,
349
252751
            page_size_log2: None,
350
252751
        });
351
252751
        ctx.exports.export("memory", ExportKind::Memory, 0);
352
252751
        Ok(ctx)
353
252751
    }
354

            
355
85752
    pub fn new() -> Result<Self> {
356
85752
        debug!("initializing compile context");
357
85752
        let mut ctx = Self::new_skeleton()?;
358
85752
        ctx.register_script_imports()?;
359
        // Tier 3: the boundary wrapper around `process` / `should_apply`
360
        // bridges an uncaught `$nomi_error` to `__nomi_raise`, so script
361
        // mode must declare the import too (it was eval-mode-only). Must
362
        // precede the first `register_function` so the import index space
363
        // stays contiguous. Host side: `scripting::host::define_host_functions`.
364
85752
        ctx.register_raise_import()?;
365
85752
        ctx.register_function("should_apply", &[], &[ValType::I32])?;
366
        // Two-phase (declare → resolve → build); see `new_eval_with_host_fns`.
367
        // `should_apply` / `process` keep their function-index slots (declared
368
        // around the helper declarations exactly as before); helper BODIES land
369
        // on `pending_helpers` in build order, which the `should_apply`
370
        // split-drain (bootstrap count) consumes unchanged.
371
85752
        ctx.declare_ratio_helpers()?;
372
85752
        ctx.declare_unit_term_helpers()?;
373
85752
        ctx.declare_commodity_helpers()?;
374
85752
        ctx.declare_pair_helpers()?;
375
85752
        ctx.declare_string_eq_helper()?;
376
85752
        ctx.register_function("process", &[], &[])?;
377
85752
        ctx.ids = ctx.resolve_ids()?;
378
85752
        ctx.build_ratio_helpers()?;
379
85752
        ctx.build_unit_term_helpers();
380
85752
        ctx.build_commodity_helpers()?;
381
85752
        ctx.build_pair_helpers();
382
85752
        ctx.build_string_eq_helper();
383
85752
        ctx.export_func("should_apply")?;
384
85752
        ctx.export_func("process")?;
385
85752
        ctx.ids = ctx.resolve_ids()?;
386
85752
        debug!("compile context initialized");
387
85752
        Ok(ctx)
388
85752
    }
389

            
390
    /// Declares the `env.log` import — the host output channel shared by
391
    /// PRINT / DISPLAY / NEWLINE / DEBUG (`(i32 level, i32 ptr, i32 len) -> ()`).
392
    /// Used by both module modes so the `log` func index always exists wherever
393
    /// those natives can be emitted.
394
252751
    fn register_log_import(&mut self) -> Result<()> {
395
252751
        self.register_import(
396
252751
            "env",
397
252751
            "log",
398
252751
            &[ValType::I32, ValType::I32, ValType::I32],
399
252751
            &[],
400
        )?;
401
252751
        Ok(())
402
252751
    }
403

            
404
85752
    fn register_script_imports(&mut self) -> Result<()> {
405
85752
        self.register_import("env", "get_output_offset", &[], &[ValType::I32])?;
406
85752
        self.register_import("env", "symbol_resolve", &[ValType::I32, ValType::I32], &[])?;
407
85752
        self.register_log_import()?;
408
85752
        self.register_import("env", "get_input_offset", &[], &[ValType::I32])?;
409
85752
        self.register_import("env", "get_strings_offset", &[], &[ValType::I32])?;
410
85752
        self.register_import("env", "get_input_entities_count", &[], &[ValType::I32])?;
411
85752
        self.register_import("env", "get_timestamp", &[], &[ValType::I64])?;
412
85752
        self.register_import("env", "generate_uuid", &[ValType::I32], &[])?;
413
85752
        self.register_import(
414
85752
            "env",
415
85752
            "write_string",
416
85752
            &[ValType::I32, ValType::I32],
417
85752
            &[ValType::I32],
418
        )?;
419
85752
        self.register_import(
420
85752
            "env",
421
85752
            "write_bytes",
422
85752
            &[ValType::I32, ValType::I32, ValType::I32],
423
85752
            &[ValType::I32],
424
        )?;
425
85752
        Ok(())
426
85752
    }
427

            
428
165576
    pub fn add_nomi_eval(&mut self, f: Function) {
429
165576
        debug!("emitting nomi-eval function");
430
165576
        self.codes.function(&f);
431

            
432
6126951
        for helper in self.pending_helpers.drain(..) {
433
6126951
            self.codes.function(&helper);
434
6126951
        }
435
165576
    }
436

            
437
    /// Emits `should_apply`, then drains the first `bootstrap_count`
438
    /// queued helpers so their code-section slots line up with the
439
    /// function-section slots reserved between `should_apply` and
440
    /// `process` during context bootstrap. Helpers the user's `process`
441
    /// body queued (e.g. a real wasm fn for a `(lambda ...)` value) stay
442
    /// in the queue until [`add_process`] drains them after the
443
    /// `process` body lands. Pass `usize::MAX` (or any value `>=` the
444
    /// pending count) to drain everything — the helper bootstrap path
445
    /// in tests goes that route.
446
75060
    pub fn add_should_apply(&mut self, f: Function, bootstrap_count: usize) {
447
75060
        debug!(
448
            bootstrap_count,
449
            "emitting should_apply with split-drain helpers"
450
        );
451
75060
        self.codes.function(&f);
452

            
453
75060
        let take = bootstrap_count.min(self.pending_helpers.len());
454
2251800
        for helper in self.pending_helpers.drain(..take) {
455
2251800
            self.codes.function(&helper);
456
2251800
        }
457
75060
    }
458

            
459
85713
    pub fn pending_helper_count(&self) -> usize {
460
85713
        self.pending_helpers.len()
461
85713
    }
462

            
463
73853
    pub fn default_should_apply() -> Function {
464
73853
        let mut f = Function::new([]);
465
73853
        f.instruction(&Instruction::I32Const(1));
466
73853
        f.instruction(&Instruction::End);
467
73853
        f
468
73853
    }
469

            
470
75060
    pub fn add_process(&mut self, f: Function) {
471
75060
        debug!("emitting process function");
472
75060
        self.codes.function(&f);
473

            
474
        // User code in `process` may register lambda / defun helpers
475
        // whose function-section indices land *after* `process`. Their
476
        // bodies have to appear in the code section in the matching
477
        // order, so drain after the `process` body — `add_should_apply`
478
        // earlier already drained the bootstrap-time helpers
479
        // (ratio/commodity/pair) that were registered before `process`.
480
75060
        for helper in self.pending_helpers.drain(..) {
481
3621
            self.codes.function(&helper);
482
3621
        }
483
75060
    }
484

            
485
    /// Tracks active defun / lambda inlining frames so a body that
486
    /// re-enters itself can be flagged before the compiler stack
487
    /// overflows. The inline-call path is load-bearing const-fold (see
488
    /// ADR-0027 / Tier 1.5 reframe): recursion terminates only because
489
    /// each walk reduces args toward a base case via constant folding.
490
    /// With a runtime arg the walk re-enters the same body without
491
    /// progress; the runtime-call path that would terminate the
492
    /// recursion at one level is the next-tier follow-up.
493
49984
    pub(in crate::compiler) fn is_inlining(&self, name: &str) -> bool {
494
49984
        self.inlining_set.contains(name)
495
49984
    }
496

            
497
    /// Push an inlining frame, erroring past [`MAX_INLINE_DEPTH`]. The codegen
498
    /// inline path (`compile_lambda_call`) walks a defun body recursively;
499
    /// const recursion with no foldable base case (or simply very deep
500
    /// const-folded recursion) would otherwise recurse the compiler's native
501
    /// stack until it overflows. This turns that into a structured compile
502
    /// error — the codegen-path analogue of `SymbolTable::enter_inline`.
503
14200
    pub(in crate::compiler) fn push_inlining_frame(&mut self, name: &str) -> Result<()> {
504
14200
        if self.inlining_stack.len() >= crate::runtime::MAX_INLINE_DEPTH {
505
71
            return Err(crate::error::Error::Compile(format!(
506
71
                "function inlining exceeded depth {} \
507
71
                 (recursive or non-terminating call to '{name}'?)",
508
71
                crate::runtime::MAX_INLINE_DEPTH
509
71
            )));
510
14129
        }
511
14129
        self.inlining_stack.push(name.to_string());
512
14129
        self.inlining_set.insert(name.to_string());
513
14129
        Ok(())
514
14200
    }
515

            
516
14129
    pub(in crate::compiler) fn pop_inlining_frame(&mut self, name: &str) {
517
14129
        if let Some(top) = self.inlining_stack.pop() {
518
14129
            debug_assert_eq!(top, name, "inlining stack imbalance");
519
14129
            if !self.inlining_stack.iter().any(|n| n == name) {
520
9514
                self.inlining_set.remove(name);
521
9514
            }
522
        }
523
14129
    }
524

            
525
    /// Records `func_idx` as ref-able via `ref.func`. Wasm requires every
526
    /// `ref.func` operand to appear in either an import/export, a global
527
    /// initializer, or a declared element segment — lambda helpers are
528
    /// none of those, so we collect their indices and emit a single
529
    /// declared-funcref segment in [`Self::finish`].
530
3623
    pub(crate) fn declare_funcref(&mut self, func_idx: u32) {
531
3623
        if !self.declared_funcs.contains(&func_idx) {
532
3623
            self.declared_funcs.push(func_idx);
533
3623
        }
534
3623
    }
535

            
536
    /// Push a lexical BLOCK frame so a `(return-from name v)` walking
537
    /// the stack from innermost to outermost can resolve the wasm-block
538
    /// it refers to. `wasm_depth` is the emitter's depth the body is
539
    /// compiled at (the depth the `block` frame will occupy once spliced).
540
3905
    pub(in crate::compiler) fn push_block_label(&mut self, name: &str, wasm_depth: u32) {
541
3905
        self.block_labels.push(BlockLabel {
542
3905
            name: name.to_string(),
543
3905
            wasm_depth,
544
3905
            recorded_exits: Vec::new(),
545
3905
        });
546
3905
    }
547

            
548
    /// Pop the named BLOCK frame, returning the exit types its reachable
549
    /// `(return-from)`s recorded during body emit. The caller unifies them
550
    /// (with the fall-through tail, if any) into the block's result type.
551
3905
    pub(in crate::compiler) fn pop_block_label(&mut self, name: &str) -> Result<Vec<WasmType>> {
552
3905
        match self.block_labels.pop() {
553
3905
            Some(top) if top.name == name => Ok(top.recorded_exits),
554
            Some(top) => Err(crate::error::Error::Compile(format!(
555
                "BLOCK label stack imbalance: expected '{name}' on top, found '{}'",
556
                top.name
557
            ))),
558
            None => Err(crate::error::Error::Compile(format!(
559
                "BLOCK label stack imbalance: expected '{name}' on top, found empty stack"
560
            ))),
561
        }
562
3905
    }
563

            
564
3053
    pub(in crate::compiler) fn lookup_block_label(&self, name: &str) -> Option<&BlockLabel> {
565
3124
        self.block_labels.iter().rev().find(|b| b.name == name)
566
3053
    }
567

            
568
    /// Record a reachable `(return-from name v)` exit's value type into the
569
    /// innermost matching BLOCK frame, so emit-time discovery can unify the
570
    /// block's result type from exactly the exits that compile.
571
2911
    pub(in crate::compiler) fn record_block_exit(&mut self, name: &str, ty: WasmType) {
572
3053
        if let Some(frame) = self.block_labels.iter_mut().rev().find(|b| b.name == name) {
573
2911
            frame.recorded_exits.push(ty);
574
2911
        }
575
2911
    }
576

            
577
    /// Push an `(unwind-protect)` frame so a non-local exit crossing it runs
578
    /// its cleanup. `threshold` is the unwind-protect's `parent_depth`; an
579
    /// exit to a wasm-depth `<= threshold` leaves the frame.
580
2627
    pub(in crate::compiler) fn push_unwind_frame(&mut self, threshold: u32, cleanup: Vec<Expr>) {
581
2627
        self.unwind_frames.push(UnwindFrame { threshold, cleanup });
582
2627
    }
583

            
584
2627
    pub(in crate::compiler) fn pop_unwind_frame(&mut self) -> Result<()> {
585
2627
        match self.unwind_frames.pop() {
586
2627
            Some(_) => Ok(()),
587
            None => Err(crate::error::Error::Compile(
588
                "UNWIND-PROTECT frame stack imbalance: pop on empty stack".to_string(),
589
            )),
590
        }
591
2627
    }
592

            
593
    /// REMOVE and return every active unwind-protect frame an exit to
594
    /// `target_depth` crosses, innermost-first. A frame is crossed when the
595
    /// exit lands at or above (shallower than) its `threshold`. Removing them
596
    /// (rather than cloning) MASKS them for the duration of cleanup emission:
597
    /// a `(return-from)` / `(go)` *inside* one of these cleanups then only
598
    /// sees the OUTER frames, so a cleanup can't re-schedule itself (which
599
    /// would recurse forever / double-run). The caller MUST call
600
    /// [`Self::restore_unwind_frames`] with the returned frames afterward, so
601
    /// sibling branches of the protected body (compiled later on the same
602
    /// frame stack) still see them.
603
    ///
604
    /// Because the frame stack is monotonic in `threshold` (a deeper-nested
605
    /// unwind-protect pushes a higher `parent_depth`), the crossed frames are
606
    /// exactly the contiguous top run with `threshold >= target_depth`.
607
3905
    pub(in crate::compiler) fn take_unwind_frames_crossing(
608
3905
        &mut self,
609
3905
        target_depth: u32,
610
3905
    ) -> Vec<UnwindFrame> {
611
3905
        let keep = self
612
3905
            .unwind_frames
613
3905
            .iter()
614
3905
            .take_while(|frame| target_depth > frame.threshold)
615
3905
            .count();
616
3905
        self.unwind_frames.split_off(keep)
617
3905
    }
618

            
619
    /// Restore frames removed by [`Self::take_unwind_frames_crossing`], in
620
    /// their original (bottom-to-top) order.
621
3905
    pub(in crate::compiler) fn restore_unwind_frames(&mut self, frames: Vec<UnwindFrame>) {
622
3905
        self.unwind_frames.extend(frames);
623
3905
    }
624

            
625
    /// The cleanup ASTs of `frames`, innermost-first (the order they must run).
626
3905
    pub(in crate::compiler) fn unwind_frame_cleanups(frames: &[UnwindFrame]) -> Vec<Vec<Expr>> {
627
3905
        frames.iter().rev().map(|f| f.cleanup.clone()).collect()
628
3905
    }
629

            
630
    /// Push a TAGBODY frame so `(go tag)` walking the stack from innermost
631
    /// outward can resolve the wasm-loop it has to jump back to. Returned
632
    /// after the matching `loop` instruction is emitted (so `loop_depth`
633
    /// records the emitter's depth at that moment).
634
1136
    pub(in crate::compiler) fn push_tag_frame(&mut self, frame: TagFrame) {
635
1136
        self.tag_frames.push(frame);
636
1136
    }
637

            
638
1065
    pub(in crate::compiler) fn pop_tag_frame(&mut self) -> Result<()> {
639
1065
        match self.tag_frames.pop() {
640
1065
            Some(_) => Ok(()),
641
            None => Err(crate::error::Error::Compile(
642
                "TAGBODY frame stack imbalance: pop on empty stack".to_string(),
643
            )),
644
        }
645
1065
    }
646

            
647
1136
    pub(in crate::compiler) fn lookup_tag(&self, name: &str) -> Option<(&TagFrame, u32)> {
648
1136
        for frame in self.tag_frames.iter().rev() {
649
1207
            if let Some(entry) = frame.tags.iter().find(|t| t.name == name) {
650
994
                return Some((frame, entry.pc));
651
71
            }
652
        }
653
142
        None
654
1136
    }
655

            
656
240636
    pub fn finish(self) -> Vec<u8> {
657
240636
        debug!(data_segments = self.data_count, "assembling WASM module");
658
240636
        let mut module = Module::new();
659
240636
        module.section(&self.types);
660
240636
        module.section(&self.imports);
661
240636
        module.section(&self.functions);
662
240636
        module.section(&self.memories);
663
        // Tag section (ID 13) sits after Memory and before Export in the
664
        // exception-handling proposal's section order.
665
240636
        module.section(&self.tags);
666
240636
        module.section(&self.exports);
667
240636
        let mut elements = ElementSection::new();
668
240636
        if !self.declared_funcs.is_empty() {
669
3266
            elements.declared(Elements::Functions(std::borrow::Cow::Borrowed(
670
3266
                &self.declared_funcs,
671
3266
            )));
672
3266
            module.section(&elements);
673
237370
        }
674
240636
        module.section(&DataCountSection {
675
240636
            count: self.data_count,
676
240636
        });
677
240636
        module.section(&self.codes);
678
240636
        module.section(&self.data);
679
240636
        module.finish()
680
240636
    }
681
}