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
//! - [`index`] — range-checked i31 boxing + throwing index division.
15
//! - [`commodity`] — commodity-arithmetic helpers
16
//!   (`commodity_new`, `commodity_add`/sub/neg/scale/cmp/...).
17
//! - [`ratio`] — ratio-arithmetic helpers (`ratio_new`, `gcd`,
18
//!   binops, comparisons).
19
//!
20
//! Construction lives here in `mod.rs`: every entry point assembles
21
//! the engine by calling submodule helpers in order.
22

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

            
39
#[cfg(test)]
40
mod tests;
41

            
42
#[cfg(test)]
43
mod snapshot_tests;
44

            
45
use std::collections::{HashMap, HashSet};
46

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

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

            
59
pub(crate) use closure::CLOSURE_LITERAL_LEN;
60

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

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

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

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

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

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

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

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

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

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

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

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

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

            
410
100329
    fn register_script_imports(&mut self) -> Result<()> {
411
100329
        self.register_import("env", "get_output_offset", &[], &[ValType::I32])?;
412
100329
        self.register_import("env", "symbol_resolve", &[ValType::I32, ValType::I32], &[])?;
413
100329
        self.register_log_import()?;
414
100329
        self.register_import("env", "get_input_offset", &[], &[ValType::I32])?;
415
100329
        self.register_import("env", "get_strings_offset", &[], &[ValType::I32])?;
416
100329
        self.register_import("env", "get_input_entities_count", &[], &[ValType::I32])?;
417
100329
        self.register_import("env", "get_timestamp", &[], &[ValType::I64])?;
418
100329
        self.register_import("env", "generate_uuid", &[ValType::I32], &[])?;
419
100329
        self.register_import(
420
100329
            "env",
421
100329
            "write_string",
422
100329
            &[ValType::I32, ValType::I32],
423
100329
            &[ValType::I32],
424
        )?;
425
100329
        self.register_import(
426
100329
            "env",
427
100329
            "write_bytes",
428
100329
            &[ValType::I32, ValType::I32, ValType::I32],
429
100329
            &[ValType::I32],
430
        )?;
431
100329
        Ok(())
432
100329
    }
433

            
434
374749
    pub fn add_nomi_eval(&mut self, f: Function) {
435
374749
        debug!("emitting nomi-eval function");
436
374749
        self.codes.function(&f);
437

            
438
16115618
        for helper in self.pending_helpers.drain(..) {
439
16115618
            self.codes.function(&helper);
440
16115618
        }
441
374749
    }
442

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

            
459
87910
        let take = bootstrap_count.min(self.pending_helpers.len());
460
3164760
        for helper in self.pending_helpers.drain(..take) {
461
3164760
            self.codes.function(&helper);
462
3164760
        }
463
87910
    }
464

            
465
100280
    pub fn pending_helper_count(&self) -> usize {
466
100280
        self.pending_helpers.len()
467
100280
    }
468

            
469
86499
    pub fn default_should_apply() -> Function {
470
86499
        let mut f = Function::new([]);
471
86499
        f.instruction(&Instruction::I32Const(1));
472
86499
        f.instruction(&Instruction::End);
473
86499
        f
474
86499
    }
475

            
476
87910
    pub fn add_process(&mut self, f: Function) {
477
87910
        debug!("emitting process function");
478
87910
        self.codes.function(&f);
479

            
480
        // User code in `process` may register lambda / defun helpers
481
        // whose function-section indices land *after* `process`. Their
482
        // bodies have to appear in the code section in the matching
483
        // order, so drain after the `process` body — `add_should_apply`
484
        // earlier already drained the bootstrap-time helpers
485
        // (ratio/commodity/pair) that were registered before `process`.
486
87910
        for helper in self.pending_helpers.drain(..) {
487
4233
            self.codes.function(&helper);
488
4233
        }
489
87910
    }
490

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

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

            
522
17015
    pub(in crate::compiler) fn pop_inlining_frame(&mut self, name: &str) {
523
17015
        if let Some(top) = self.inlining_stack.pop() {
524
17015
            debug_assert_eq!(top, name, "inlining stack imbalance");
525
17015
            if !self.inlining_stack.iter().any(|n| n == name) {
526
11620
                self.inlining_set.remove(name);
527
11620
            }
528
        }
529
17015
    }
530

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

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

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

            
570
3569
    pub(in crate::compiler) fn lookup_block_label(&self, name: &str) -> Option<&BlockLabel> {
571
3652
        self.block_labels.iter().rev().find(|b| b.name == name)
572
3569
    }
573

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

            
583
    /// Push an `(unwind-protect)` frame so a non-local exit crossing it runs
584
    /// its cleanup. `threshold` is the unwind-protect's `parent_depth`; an
585
    /// exit to a wasm-depth `<= threshold` leaves the frame.
586
3071
    pub(in crate::compiler) fn push_unwind_frame(&mut self, threshold: u32, cleanup: Vec<Expr>) {
587
3071
        self.unwind_frames.push(UnwindFrame { threshold, cleanup });
588
3071
    }
589

            
590
3071
    pub(in crate::compiler) fn pop_unwind_frame(&mut self) -> Result<()> {
591
3071
        match self.unwind_frames.pop() {
592
3071
            Some(_) => Ok(()),
593
            None => Err(crate::error::Error::Compile(
594
                "UNWIND-PROTECT frame stack imbalance: pop on empty stack".to_string(),
595
            )),
596
        }
597
3071
    }
598

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

            
625
    /// Restore frames removed by [`Self::take_unwind_frames_crossing`], in
626
    /// their original (bottom-to-top) order.
627
4565
    pub(in crate::compiler) fn restore_unwind_frames(&mut self, frames: Vec<UnwindFrame>) {
628
4565
        self.unwind_frames.extend(frames);
629
4565
    }
630

            
631
    /// The cleanup ASTs of `frames`, innermost-first (the order they must run).
632
4565
    pub(in crate::compiler) fn unwind_frame_cleanups(frames: &[UnwindFrame]) -> Vec<Vec<Expr>> {
633
4565
        frames.iter().rev().map(|f| f.cleanup.clone()).collect()
634
4565
    }
635

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

            
644
1245
    pub(in crate::compiler) fn pop_tag_frame(&mut self) -> Result<()> {
645
1245
        match self.tag_frames.pop() {
646
1245
            Some(_) => Ok(()),
647
            None => Err(crate::error::Error::Compile(
648
                "TAGBODY frame stack imbalance: pop on empty stack".to_string(),
649
            )),
650
        }
651
1245
    }
652

            
653
1328
    pub(in crate::compiler) fn lookup_tag(&self, name: &str) -> Option<(&TagFrame, u32)> {
654
1328
        for frame in self.tag_frames.iter().rev() {
655
1411
            if let Some(entry) = frame.tags.iter().find(|t| t.name == name) {
656
1162
                return Some((frame, entry.pc));
657
83
            }
658
        }
659
166
        None
660
1328
    }
661

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