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
137027
    pub fn new_eval_with_host_fns(host_fns: &[HostFnSpec]) -> Result<Self> {
183
137027
        let mut ctx = Self::new_skeleton()?;
184
137027
        ctx.register_raise_import()?;
185
137027
        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
137027
        ctx.register_log_import()?;
192
5860383
        for spec in host_fns {
193
5860378
            ctx.register_host_fn(spec)?;
194
        }
195
137026
        let anyref = ctx.anyref();
196
137026
        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
137026
        ctx.declare_ratio_helpers()?;
203
137026
        ctx.declare_unit_term_helpers()?;
204
137026
        ctx.declare_commodity_helpers()?;
205
137026
        ctx.declare_pair_helpers()?;
206
137026
        ctx.declare_string_eq_helper()?;
207
137026
        ctx.declare_entity_allocators()?;
208
137026
        ctx.export_func("nomi-eval")?;
209
137026
        ctx.ids = ctx.resolve_ids()?;
210
137026
        ctx.build_ratio_helpers()?;
211
137026
        ctx.build_unit_term_helpers();
212
137026
        ctx.build_commodity_helpers()?;
213
137026
        ctx.build_pair_helpers();
214
137026
        ctx.build_string_eq_helper();
215
137026
        ctx.build_entity_allocators()?;
216
137026
        debug!(
217
            host_fn_count = host_fns.len(),
218
            "eval compile context initialized"
219
        );
220
137026
        Ok(ctx)
221
137027
    }
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
219162
    fn register_raise_import(&mut self) -> Result<()> {
231
219162
        let arr = self.array_ref();
232
219162
        self.register_import("nomi", "__nomi_raise", &[arr, arr], &[])?;
233
219162
        Ok(())
234
219162
    }
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
137027
    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
137027
        let funcref = ValType::Ref(wasm_encoder::RefType::FUNCREF);
255
137027
        let any = self.anyref();
256
137027
        let abstract_struct = self.struct_ref();
257
137027
        self.register_import(
258
137027
            "nomi",
259
137027
            "__nomi_catch_each",
260
137027
            &[funcref, any, abstract_struct],
261
137027
            &[abstract_struct],
262
        )?;
263
137027
        Ok(())
264
137027
    }
265

            
266
219162
    fn new_skeleton() -> Result<Self> {
267
219162
        let mut ctx = Self {
268
219162
            types: TypeSection::new(),
269
219162
            imports: ImportSection::new(),
270
219162
            functions: FunctionSection::new(),
271
219162
            memories: MemorySection::new(),
272
219162
            tags: TagSection::new(),
273
219162
            exports: ExportSection::new(),
274
219162
            codes: CodeSection::new(),
275
219162
            data: DataSection::new(),
276
219162
            data_count: 0,
277
219162
            type_count: 0,
278
219162
            tag_count: 0,
279
219162
            import_func_count: 0,
280
219162
            local_func_count: 0,
281
219162
            type_cache: HashMap::new(),
282
219162
            func_names: HashMap::new(),
283
219162
            host_fns: HashMap::new(),
284
219162
            pending_helpers: Vec::new(),
285
219162
            next_local: LOCAL_POOL_BASE,
286
219162
            local_types: Vec::new(),
287
219162
            serializer: OutputSerializer::new(super::expr::LOCAL_OUTPUT_BASE),
288
219162
            closures: closure::ClosureRegistry::default(),
289
219162
            closure_bodies: HashMap::new(),
290
219162
            declared_funcs: Vec::new(),
291
219162
            monomorphs: monomorph::MonomorphCache::default(),
292
219162
            nomi_error_tag: None,
293
219162
            ids: ids::WasmIds::UNRESOLVED,
294
219162
            inlining_stack: Vec::new(),
295
219162
            inlining_set: HashSet::new(),
296
219162
            block_labels: Vec::new(),
297
219162
            tag_frames: Vec::new(),
298
219162
            unwind_frames: Vec::new(),
299
219162
        };
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
219162
        ctx.ids.ty_i8_array = ctx.register_type()?;
305
219162
        ctx.ids.ty_ratio = ctx.register_struct_type(&[ValType::I64, ValType::I64])?;
306
219162
        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
219162
        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
219162
        let unit_term_ref = ctx.unit_term_ref();
319
219162
        ctx.ids.ty_commodity = ctx.register_struct_type(&[
320
219162
            ValType::I64,
321
219162
            ValType::I64,
322
219162
            ValType::I64,
323
219162
            ValType::I64,
324
219162
            unit_term_ref,
325
219162
        ])?;
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`. The recursive `ReportNode`
333
        // case is no longer special-cased here — its `PairRef` field
334
        // resolves to `(ref null $pair)` which `$pair` (registered
335
        // earlier in this skeleton) already supplies.
336
1753296
        for spec in entity_registry::ENTITY_SPECS {
337
6355698
            let fields: Vec<ValType> = spec.fields.iter().map(|f| f.as_val_type(&ctx)).collect();
338
1753296
            let idx = ctx.register_struct_type(&fields)?;
339
1753296
            ctx.ids.set_entity_type(spec.kind, idx);
340
        }
341
        // `$nomi_condition` struct + `$nomi_error` exception tag (Tier 3,
342
        // ADR-0026). Registered here so the type/tag indices are stable
343
        // before any body emits a `throw` / `try_table`. The tag's payload
344
        // is a single `(ref null $nomi_condition)` so a catch hands the
345
        // handler the condition ref directly.
346
219162
        ctx.register_exception_support()?;
347
219162
        ctx.memories.memory(MemoryType {
348
219162
            minimum: 1,
349
219162
            maximum: None,
350
219162
            memory64: false,
351
219162
            shared: false,
352
219162
            page_size_log2: None,
353
219162
        });
354
219162
        ctx.exports.export("memory", ExportKind::Memory, 0);
355
219162
        Ok(ctx)
356
219162
    }
357

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

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

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

            
431
135664
    pub fn add_nomi_eval(&mut self, f: Function) {
432
135664
        debug!("emitting nomi-eval function");
433
135664
        self.codes.function(&f);
434

            
435
5155844
        for helper in self.pending_helpers.drain(..) {
436
5155844
            self.codes.function(&helper);
437
5155844
        }
438
135664
    }
439

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

            
456
71893
        let take = bootstrap_count.min(self.pending_helpers.len());
457
2156790
        for helper in self.pending_helpers.drain(..take) {
458
2156790
            self.codes.function(&helper);
459
2156790
        }
460
71893
    }
461

            
462
82096
    pub fn pending_helper_count(&self) -> usize {
463
82096
        self.pending_helpers.len()
464
82096
    }
465

            
466
70733
    pub fn default_should_apply() -> Function {
467
70733
        let mut f = Function::new([]);
468
70733
        f.instruction(&Instruction::I32Const(1));
469
70733
        f.instruction(&Instruction::End);
470
70733
        f
471
70733
    }
472

            
473
71893
    pub fn add_process(&mut self, f: Function) {
474
71893
        debug!("emitting process function");
475
71893
        self.codes.function(&f);
476

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

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

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

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

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

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

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

            
567
2924
    pub(in crate::compiler) fn lookup_block_label(&self, name: &str) -> Option<&BlockLabel> {
568
2992
        self.block_labels.iter().rev().find(|b| b.name == name)
569
2924
    }
570

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

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

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

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

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

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

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

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

            
650
1088
    pub(in crate::compiler) fn lookup_tag(&self, name: &str) -> Option<(&TagFrame, u32)> {
651
1088
        for frame in self.tag_frames.iter().rev() {
652
1156
            if let Some(entry) = frame.tags.iter().find(|t| t.name == name) {
653
952
                return Some((frame, entry.pc));
654
68
            }
655
        }
656
136
        None
657
1088
    }
658

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