1
//! Lambda body emit — production-side path that lifts a value-position
2
//! `(lambda ...)` / `(function ...)` from compile-time stringification
3
//! to a real wasm function plus a `$closure_<sig>` GC-struct value
4
//! pushed onto the stack.
5
//!
6
//! Scope of this slice (Tier 1.5 v1, ADR-0027): required-only params
7
//! typed as Ratio, no keyword/optional/rest/aux. Free-variable captures
8
//! are now lowered: each captured name with a stable local storage
9
//! location rides through a per-scope `$env_<id>` GC struct that the
10
//! helper-fn body unpacks in its prologue. Globally-resolvable names
11
//! (host-fn placeholders, special forms) need no env transport — they
12
//! resolve identically inside the helper through the cloned symbol
13
//! table.
14
//!
15
//! The caller observes a `WasmType::Closure(sig)` on the stack — sig'd
16
//! through the per-context `ClosureRegistry` so distinct lambdas of the
17
//! same shape share `$fn_<sig>` and `$closure_<sig>`. Each capturing
18
//! lambda still gets its own `$env_<id>` so two closures with disjoint
19
//! captures don't collide.
20

            
21
use crate::ast::{ClosureSigId, Expr, LambdaParams, WasmType};
22
use crate::compiler::context::CompileContext;
23
use crate::compiler::context::closure::{EnvField, EnvStructLayout};
24
use crate::compiler::emit::FunctionEmitter;
25
use crate::compiler::expr::{compile_for_stack, emit_box_anyref};
26
use crate::error::{Error, Result};
27
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
28

            
29
use super::captures::{CaptureSet, compute_captures};
30

            
31
/// Wire-level param types for the closure body fn. Catch-each (and other
32
/// future host-iteration callers) pass items as raw anyref values, so
33
/// the body fn must accept anyref params and downcast inside the
34
/// prologue to the declared user-visible param type. The lambda value
35
/// path (FUNCALL/APPLY/etc.) keeps the original typed signature so the
36
/// `call_ref` site doesn't need any boxing.
37
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38
pub(in crate::compiler) enum CallingConvention {
39
    /// Wasm-side typed call_ref: param wasm types match user-visible types.
40
    Typed,
41
    /// Host-side anyref call: every user param is wired as anyref + downcast
42
    /// in the prologue to the user-visible type.
43
    HostAnyref,
44
}
45

            
46
/// Emits a real wasm function for `params`/`body` and pushes a
47
/// `(ref $closure_<sig>)` value onto the caller's stack. Returns the
48
/// signature id so the caller can record `WasmType::Closure(sig)` for
49
/// the stack arm.
50
4067
pub(super) fn emit_lambda_value(
51
4067
    ctx: &mut CompileContext,
52
4067
    emit: &mut FunctionEmitter,
53
4067
    symbols: &SymbolTable,
54
4067
    params: &LambdaParams,
55
4067
    body: &Expr,
56
4067
) -> Result<ClosureSigId> {
57
4067
    let user_param_types = super::param_infer::infer_param_types(params, body);
58
4067
    emit_lambda_value_typed(
59
4067
        ctx,
60
4067
        emit,
61
4067
        symbols,
62
4067
        params,
63
4067
        body,
64
4067
        &user_param_types,
65
4067
        CallingConvention::Typed,
66
    )
67
4067
}
68

            
69
/// Variant that lets the caller declare the user-visible param types
70
/// (defaults are Ratio). With `HostAnyref` calling convention every
71
/// param is wired as anyref on the wasm side and the prologue downcasts
72
/// to the declared user-visible type before the body runs.
73
5644
pub(in crate::compiler) fn emit_lambda_value_typed(
74
5644
    ctx: &mut CompileContext,
75
5644
    emit: &mut FunctionEmitter,
76
5644
    symbols: &SymbolTable,
77
5644
    params: &LambdaParams,
78
5644
    body: &Expr,
79
5644
    user_param_types: &[WasmType],
80
5644
    cc: CallingConvention,
81
5644
) -> Result<ClosureSigId> {
82
5644
    if !has_v1_param_shape(params) {
83
        return Err(Error::Compile(
84
            "lambda value with &optional/&rest/&key/&aux not yet supported as a \
85
             first-class wasm closure; rewrite the lambda to take only required \
86
             parameters or invoke it inline (the const-fold path still handles \
87
             keyword args)"
88
                .to_string(),
89
        ));
90
5644
    }
91
5644
    if user_param_types.len() != params.required.len() {
92
        return Err(Error::Compile(format!(
93
            "lambda emit: param-type count {} does not match required-param count {}",
94
            user_param_types.len(),
95
            params.required.len()
96
        )));
97
5644
    }
98

            
99
5644
    let captures = compute_captures(symbols, params, body);
100
5644
    let env_fields = collect_env_fields(symbols, &captures)?;
101

            
102
5644
    let (result_ty, helper_func_idx, env_layout) = compile_body_into_helper(
103
5644
        ctx,
104
5644
        symbols,
105
5644
        &params.required,
106
5644
        user_param_types,
107
5644
        body,
108
5644
        &env_fields,
109
5644
        cc,
110
581
    )?;
111
5063
    let wire_param_types: Vec<WasmType> = match cc {
112
4067
        CallingConvention::Typed => user_param_types.to_vec(),
113
        CallingConvention::HostAnyref => {
114
996
            user_param_types.iter().map(|_| WasmType::AnyRef).collect()
115
        }
116
    };
117
5063
    let wire_result_ty = match cc {
118
4067
        CallingConvention::Typed => result_ty,
119
996
        CallingConvention::HostAnyref => WasmType::AnyRef,
120
    };
121
5063
    let sig = ctx.intern_closure_signature(&wire_param_types, wire_result_ty)?;
122

            
123
5063
    push_closure_value(
124
5063
        ctx,
125
5063
        emit,
126
5063
        symbols,
127
5063
        sig,
128
5063
        helper_func_idx,
129
5063
        env_layout.as_ref(),
130
    )?;
131
5063
    Ok(sig)
132
5644
}
133

            
134
/// Predicate: whether `params` / `body` fit the Tier 1.5 v1 emit slice.
135
/// V1 covers required-only params; captures of stable locals are
136
/// supported. Lambdas with keyword/optional/rest/aux still fall back
137
/// to stringification.
138
5810
pub(super) fn is_v1_eligible(_symbols: &SymbolTable, params: &LambdaParams, _body: &Expr) -> bool {
139
5810
    has_v1_param_shape(params)
140
5810
}
141

            
142
11454
fn has_v1_param_shape(params: &LambdaParams) -> bool {
143
11454
    params.optional.is_empty()
144
11371
        && params.rest.is_none()
145
11288
        && params.key.is_empty()
146
11288
        && params.aux.is_empty()
147
11454
}
148

            
149
/// Walks `captures` against `symbols`, keeping only names that resolve
150
/// to a `WasmLocal(idx, ty)` — those have a stable outer-scope storage
151
/// location to copy from. `WasmRuntime` placeholders (e.g. registered
152
/// host-fn symbols) are globally resolvable and need no env transport;
153
/// the helper's cloned symbol table sees them unchanged.
154
5644
fn collect_env_fields(symbols: &SymbolTable, captures: &CaptureSet) -> Result<Vec<EnvField>> {
155
5644
    let mut fields = Vec::new();
156
5644
    for name in captures.iter() {
157
415
        let sym = symbols.lookup(name).ok_or_else(|| {
158
            Error::Compile(format!(
159
                "lambda body references undefined name {name:?} during capture analysis"
160
            ))
161
        })?;
162
415
        if let Some(Expr::WasmLocal(_, ty)) = sym.value() {
163
415
            fields.push(EnvField {
164
415
                name: name.to_string(),
165
415
                ty: *ty,
166
415
            });
167
415
        }
168
    }
169
5644
    Ok(fields)
170
5644
}
171

            
172
/// Body emit pipeline:
173
/// 1. Snapshot caller's local-pool state.
174
/// 2. Build a fresh `FunctionEmitter`, bind params as `WasmLocal`s
175
///    (slot 0 = env anyref, 1..=N = user params).
176
/// 3. If the body captures anything: register an `$env_<id>` struct,
177
///    allocate one helper-side local per field, emit the prologue
178
///    that downcasts param-0 and unpacks the env into those locals,
179
///    rebind captured names in the helper's local symbol table.
180
/// 4. Walk body via `compile_for_stack` to produce the value the helper
181
///    returns.
182
/// 5. End the body, drop the locals declaration, register the helper
183
///    fn (reserves a stable wasm fn idx), queue the body into
184
///    `pending_helpers`.
185
/// 6. Restore caller's local-pool snapshot.
186
5644
fn compile_body_into_helper(
187
5644
    ctx: &mut CompileContext,
188
5644
    symbols: &SymbolTable,
189
5644
    param_names: &[String],
190
5644
    user_param_types: &[WasmType],
191
5644
    body: &Expr,
192
5644
    env_fields: &[EnvField],
193
5644
    cc: CallingConvention,
194
5644
) -> Result<(WasmType, u32, Option<EnvStructLayout>)> {
195
5644
    let env_anyref = ctx.anyref();
196
5644
    let mut helper_param_vts = Vec::with_capacity(user_param_types.len() + 1);
197
5644
    helper_param_vts.push(env_anyref);
198
6889
    for &ty in user_param_types {
199
6889
        let wire_ty = match cc {
200
5312
            CallingConvention::Typed => ty,
201
1577
            CallingConvention::HostAnyref => WasmType::AnyRef,
202
        };
203
6889
        helper_param_vts.push(ctx.wasm_val_type(wire_ty));
204
    }
205
5644
    let param_count = u32::try_from(helper_param_vts.len())
206
5644
        .map_err(|_| Error::Compile("lambda parameter count exceeds u32 range".to_string()))?;
207

            
208
5644
    let env_layout = if env_fields.is_empty() {
209
5229
        None
210
    } else {
211
415
        Some(ctx.intern_env_struct(env_fields)?)
212
    };
213

            
214
5644
    let snapshot = ctx.take_local_pool(param_count);
215

            
216
5644
    let mut local_symbols = symbols.clone();
217
5644
    let mut helper_emit = FunctionEmitter::new();
218

            
219
5644
    bind_user_params(
220
5644
        ctx,
221
5644
        &mut helper_emit,
222
5644
        &mut local_symbols,
223
5644
        param_names,
224
5644
        user_param_types,
225
5644
        cc,
226
    )?;
227

            
228
5644
    if let Some(layout) = env_layout.as_ref() {
229
415
        emit_env_unpack_prologue(ctx, &mut helper_emit, &mut local_symbols, layout)?;
230
5229
    }
231

            
232
    // A `HostAnyref` callback is host-invoked (catch-each), so an
233
    // uncaught `(error)` throw inside it must be bridged to `__nomi_raise`
234
    // at this boundary — wrap the body in the Tier 3 boundary `try_table`
235
    // (ADR-0026). The wire result is always anyref here, known up-front.
236
    // `Typed` lambdas are in-module calls: NO wrapper, so an enclosing
237
    // `(handler-case)` can still catch their throws.
238
5644
    let result_ty = match cc {
239
        CallingConvention::Typed => {
240
4067
            match compile_for_stack(ctx, &mut helper_emit, &mut local_symbols, body) {
241
4067
                Ok(ty) => ty,
242
                Err(e) => {
243
                    ctx.restore_local_pool(snapshot);
244
                    return Err(e);
245
                }
246
            }
247
        }
248
        CallingConvention::HostAnyref => {
249
1577
            let wrap =
250
1577
                ctx.emit_boundary_wrapper(&mut helper_emit, Some(WasmType::AnyRef), |ctx, emit| {
251
1577
                    let ty = compile_for_stack(ctx, emit, &mut local_symbols, body)?;
252
996
                    promote_result_to_anyref(ctx, emit, ty)?;
253
996
                    Ok(())
254
1577
                });
255
1577
            if let Err(e) = wrap {
256
581
                ctx.restore_local_pool(snapshot);
257
581
                return Err(e);
258
996
            }
259
996
            WasmType::AnyRef
260
        }
261
    };
262
5063
    helper_emit.end();
263

            
264
5063
    let helper_locals = ctx.build_helper_locals(param_count);
265
5063
    let helper_fn = helper_emit.finish(&helper_locals);
266

            
267
5063
    ctx.restore_local_pool(snapshot);
268

            
269
5063
    let result_vt = ctx.wasm_val_type(result_ty);
270
5063
    let helper_name = ctx.next_lambda_helper_name()?;
271
5063
    let func_idx = ctx.register_function(&helper_name, &helper_param_vts, &[result_vt])?;
272
5063
    ctx.declare_funcref(func_idx);
273
5063
    ctx.queue_helper(helper_fn);
274
5063
    Ok((result_ty, func_idx, env_layout))
275
5644
}
276

            
277
/// Binds each user-facing param symbol so the body sees a `WasmLocal`
278
/// of the declared user type. With `Typed` the wasm param already holds
279
/// a typed value — bind directly. With `HostAnyref` the wasm param is
280
/// anyref; allocate a fresh helper-side typed local, downcast the
281
/// anyref into it, and bind the user name to that local.
282
5644
fn bind_user_params(
283
5644
    ctx: &mut CompileContext,
284
5644
    helper: &mut FunctionEmitter,
285
5644
    local_symbols: &mut SymbolTable,
286
5644
    param_names: &[String],
287
5644
    user_param_types: &[WasmType],
288
5644
    cc: CallingConvention,
289
5644
) -> Result<()> {
290
6889
    for (idx, (name, ty)) in param_names.iter().zip(user_param_types.iter()).enumerate() {
291
6889
        let wire_local_idx = u32::try_from(idx + 1)
292
6889
            .map_err(|_| Error::Compile("lambda parameter count exceeds u32 range".to_string()))?;
293
6889
        let bound_idx = match cc {
294
5312
            CallingConvention::Typed => wire_local_idx,
295
            CallingConvention::HostAnyref => {
296
1577
                let typed_local = ctx.alloc_local(*ty)?;
297
1577
                helper.local_get(wire_local_idx);
298
1577
                emit_anyref_to_typed_downcast(ctx, helper, *ty)?;
299
1577
                helper.local_set(typed_local);
300
1577
                typed_local
301
            }
302
        };
303
6889
        local_symbols.define(
304
6889
            Symbol::new(name, SymbolKind::Variable).with_value(Expr::WasmLocal(bound_idx, *ty)),
305
        );
306
    }
307
5644
    Ok(())
308
5644
}
309

            
310
/// Emits the wasm to convert an `anyref` on the stack to the declared
311
/// user-visible type: a reference `ref.cast`s to its concrete type, an i31-boxed
312
/// index or bool unboxes to its i32. The variable keeps the element's own
313
/// dimension — no int ↔ ratio ↔ commodity bridging (ADR-0014). A `Closure`
314
/// never rides a list cell, so it is refused.
315
1577
fn emit_anyref_to_typed_downcast(
316
1577
    ctx: &CompileContext,
317
1577
    helper: &mut FunctionEmitter,
318
1577
    ty: WasmType,
319
1577
) -> Result<()> {
320
1577
    match ty {
321
581
        WasmType::Ratio => helper.ref_cast(ctx.ids.ty_ratio),
322
        WasmType::Commodity => helper.ref_cast(ctx.ids.ty_commodity),
323
        WasmType::PairRef(_) => helper.ref_cast(ctx.ids.ty_pair),
324
        WasmType::StringRef => helper.ref_cast(ctx.ids.ty_i8_array),
325
166
        WasmType::EntityRef(kind) => helper.ref_cast(ctx.ids.entity_type(kind)),
326
498
        WasmType::I32 | WasmType::Bool => {
327
498
            helper.ref_cast_i31();
328
498
            helper.i31_get_s();
329
498
        }
330
332
        WasmType::AnyRef => {}
331
        WasmType::Closure(_) => {
332
            return Err(Error::Compile(format!(
333
                "host-anyref calling convention cannot downcast to {ty}; \
334
                 a closure cannot ride a list cell"
335
            )));
336
        }
337
    }
338
1577
    Ok(())
339
1577
}
340

            
341
/// Wraps the body's typed result as the anyref the host fn receives: an index
342
/// through the range-checked `box_index`, a bool as nil-null / i31 1, a
343
/// reference as itself. A body that diverged left a polymorphic stack, which
344
/// the same boxing satisfies.
345
996
fn promote_result_to_anyref(
346
996
    ctx: &CompileContext,
347
996
    helper: &mut FunctionEmitter,
348
996
    ty: WasmType,
349
996
) -> Result<()> {
350
996
    match ty {
351
166
        WasmType::AnyRef => Ok(()),
352
        WasmType::I32 | WasmType::Bool => {
353
581
            emit_box_anyref(ctx, helper, ty);
354
581
            Ok(())
355
        }
356
        WasmType::Ratio
357
        | WasmType::Commodity
358
        | WasmType::PairRef(_)
359
        | WasmType::StringRef
360
        | WasmType::EntityRef(_) => {
361
            // Refs widen to anyref freely (subtyping); no instruction needed.
362
249
            let _ = ctx;
363
249
            let _ = helper;
364
249
            Ok(())
365
        }
366
        WasmType::Closure(_) => Err(Error::Compile(format!(
367
            "host-anyref calling convention cannot widen body return type {ty} to anyref"
368
        ))),
369
    }
370
996
}
371

            
372
/// Emits the helper-fn prologue that unpacks an env-struct into fresh
373
/// helper-side locals. For each captured field: downcast param-0
374
/// (`(ref null any)`) to the concrete env-struct type, read the field,
375
/// stash it in a freshly allocated helper-side local, then rebind the
376
/// captured name in `local_symbols` to that local so the body's
377
/// `compile_for_stack` walks resolve captures via `local.get`.
378
415
fn emit_env_unpack_prologue(
379
415
    ctx: &mut CompileContext,
380
415
    helper: &mut FunctionEmitter,
381
415
    local_symbols: &mut SymbolTable,
382
415
    layout: &EnvStructLayout,
383
415
) -> Result<()> {
384
415
    for (field_idx, field) in layout.fields.iter().enumerate() {
385
415
        let local_idx = ctx.alloc_local(field.ty)?;
386
415
        let field_idx_u32 = u32::try_from(field_idx)
387
415
            .map_err(|_| Error::Compile("env-struct field count exceeds u32 range".to_string()))?;
388
415
        helper.local_get(0);
389
415
        helper.ref_cast(layout.type_idx);
390
415
        helper.struct_get(layout.type_idx, field_idx_u32);
391
415
        helper.local_set(local_idx);
392
415
        local_symbols.define(
393
415
            Symbol::new(&field.name, SymbolKind::Variable)
394
415
                .with_value(Expr::WasmLocal(local_idx, field.ty)),
395
        );
396
    }
397
415
    Ok(())
398
415
}
399

            
400
/// Emits the closure construction sequence at the caller's stack:
401
/// `ref.func $helper` + (env-struct construction or `ref.null any`) +
402
/// `struct.new $closure_<sig>`. Captures are loaded from the caller's
403
/// scope via `local.get` on each captured name's outer-scope local
404
/// index, then bundled into a `$env_<id>` struct whose ref slots into
405
/// the closure's nullable env field.
406
5063
fn push_closure_value(
407
5063
    ctx: &CompileContext,
408
5063
    emit: &mut FunctionEmitter,
409
5063
    symbols: &SymbolTable,
410
5063
    sig: ClosureSigId,
411
5063
    func_idx: u32,
412
5063
    env_layout: Option<&EnvStructLayout>,
413
5063
) -> Result<()> {
414
5063
    let closure_type_idx = ctx.closure_sig(sig).closure_type_idx;
415
5063
    emit.ref_func(func_idx);
416
5063
    match env_layout {
417
415
        Some(layout) => emit_env_struct_construction(emit, symbols, layout)?,
418
4648
        None => emit.ref_null_any(),
419
    }
420
5063
    emit.struct_new(closure_type_idx);
421
5063
    Ok(())
422
5063
}
423

            
424
415
fn emit_env_struct_construction(
425
415
    emit: &mut FunctionEmitter,
426
415
    symbols: &SymbolTable,
427
415
    layout: &EnvStructLayout,
428
415
) -> Result<()> {
429
415
    for field in &layout.fields {
430
415
        let sym = symbols.lookup(&field.name).ok_or_else(|| {
431
            Error::Compile(format!(
432
                "captured name {:?} disappeared from symbol table before env-struct \
433
                 construction",
434
                field.name
435
            ))
436
        })?;
437
415
        let Some(Expr::WasmLocal(idx, _)) = sym.value() else {
438
            return Err(Error::Compile(format!(
439
                "captured name {:?} no longer resolves to a local at env-struct \
440
                 construction time",
441
                field.name
442
            )));
443
        };
444
415
        emit.local_get(*idx);
445
    }
446
415
    emit.struct_new(layout.type_idx);
447
415
    Ok(())
448
415
}