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;
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
2856
pub(super) fn emit_lambda_value(
51
2856
    ctx: &mut CompileContext,
52
2856
    emit: &mut FunctionEmitter,
53
2856
    symbols: &SymbolTable,
54
2856
    params: &LambdaParams,
55
2856
    body: &Expr,
56
2856
) -> Result<ClosureSigId> {
57
2856
    let user_param_types = super::param_infer::infer_param_types(params, body);
58
2856
    emit_lambda_value_typed(
59
2856
        ctx,
60
2856
        emit,
61
2856
        symbols,
62
2856
        params,
63
2856
        body,
64
2856
        &user_param_types,
65
2856
        CallingConvention::Typed,
66
    )
67
2856
}
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
3944
pub(in crate::compiler) fn emit_lambda_value_typed(
74
3944
    ctx: &mut CompileContext,
75
3944
    emit: &mut FunctionEmitter,
76
3944
    symbols: &SymbolTable,
77
3944
    params: &LambdaParams,
78
3944
    body: &Expr,
79
3944
    user_param_types: &[WasmType],
80
3944
    cc: CallingConvention,
81
3944
) -> Result<ClosureSigId> {
82
3944
    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
3944
    }
91
3944
    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
3944
    }
98

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

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

            
123
3468
    push_closure_value(
124
3468
        ctx,
125
3468
        emit,
126
3468
        symbols,
127
3468
        sig,
128
3468
        helper_func_idx,
129
3468
        env_layout.as_ref(),
130
    )?;
131
3468
    Ok(sig)
132
3944
}
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
4080
pub(super) fn is_v1_eligible(_symbols: &SymbolTable, params: &LambdaParams, _body: &Expr) -> bool {
139
4080
    has_v1_param_shape(params)
140
4080
}
141

            
142
8024
fn has_v1_param_shape(params: &LambdaParams) -> bool {
143
8024
    params.optional.is_empty()
144
7956
        && params.rest.is_none()
145
7888
        && params.key.is_empty()
146
7888
        && params.aux.is_empty()
147
8024
}
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
3944
fn collect_env_fields(symbols: &SymbolTable, captures: &CaptureSet) -> Result<Vec<EnvField>> {
155
3944
    let mut fields = Vec::new();
156
3944
    for name in captures.iter() {
157
340
        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
340
        if let Some(Expr::WasmLocal(_, ty)) = sym.value() {
163
340
            fields.push(EnvField {
164
340
                name: name.to_string(),
165
340
                ty: *ty,
166
340
            });
167
340
        }
168
    }
169
3944
    Ok(fields)
170
3944
}
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
3944
fn compile_body_into_helper(
187
3944
    ctx: &mut CompileContext,
188
3944
    symbols: &SymbolTable,
189
3944
    param_names: &[String],
190
3944
    user_param_types: &[WasmType],
191
3944
    body: &Expr,
192
3944
    env_fields: &[EnvField],
193
3944
    cc: CallingConvention,
194
3944
) -> Result<(WasmType, u32, Option<EnvStructLayout>)> {
195
3944
    let env_anyref = ctx.anyref();
196
3944
    let mut helper_param_vts = Vec::with_capacity(user_param_types.len() + 1);
197
3944
    helper_param_vts.push(env_anyref);
198
4964
    for &ty in user_param_types {
199
4964
        let wire_ty = match cc {
200
3876
            CallingConvention::Typed => ty,
201
1088
            CallingConvention::HostAnyref => WasmType::AnyRef,
202
        };
203
4964
        helper_param_vts.push(ctx.wasm_val_type(wire_ty));
204
    }
205
3944
    let param_count = u32::try_from(helper_param_vts.len())
206
3944
        .map_err(|_| Error::Compile("lambda parameter count exceeds u32 range".to_string()))?;
207

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

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

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

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

            
228
3808
    if let Some(layout) = env_layout.as_ref() {
229
340
        emit_env_unpack_prologue(ctx, &mut helper_emit, &mut local_symbols, layout)?;
230
3468
    }
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
3808
    let result_ty = match cc {
239
        CallingConvention::Typed => {
240
2856
            match compile_for_stack(ctx, &mut helper_emit, &mut local_symbols, body) {
241
2856
                Ok(ty) => ty,
242
                Err(e) => {
243
                    ctx.restore_local_pool(snapshot);
244
                    return Err(e);
245
                }
246
            }
247
        }
248
        CallingConvention::HostAnyref => {
249
952
            let wrap =
250
952
                ctx.emit_boundary_wrapper(&mut helper_emit, Some(WasmType::AnyRef), |ctx, emit| {
251
952
                    let ty = compile_for_stack(ctx, emit, &mut local_symbols, body)?;
252
612
                    promote_result_to_anyref(ctx, emit, ty)?;
253
612
                    Ok(())
254
952
                });
255
952
            if let Err(e) = wrap {
256
340
                ctx.restore_local_pool(snapshot);
257
340
                return Err(e);
258
612
            }
259
612
            WasmType::AnyRef
260
        }
261
    };
262
3468
    helper_emit.end();
263

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

            
267
3468
    ctx.restore_local_pool(snapshot);
268

            
269
3468
    let result_vt = ctx.wasm_val_type(result_ty);
270
3468
    let helper_name = ctx.next_lambda_helper_name()?;
271
3468
    let func_idx = ctx.register_function(&helper_name, &helper_param_vts, &[result_vt])?;
272
3468
    ctx.declare_funcref(func_idx);
273
3468
    ctx.queue_helper(helper_fn);
274
3468
    Ok((result_ty, func_idx, env_layout))
275
3944
}
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
3944
fn bind_user_params(
283
3944
    ctx: &mut CompileContext,
284
3944
    helper: &mut FunctionEmitter,
285
3944
    local_symbols: &mut SymbolTable,
286
3944
    param_names: &[String],
287
3944
    user_param_types: &[WasmType],
288
3944
    cc: CallingConvention,
289
3944
) -> Result<()> {
290
4964
    for (idx, (name, ty)) in param_names.iter().zip(user_param_types.iter()).enumerate() {
291
4964
        let wire_local_idx = u32::try_from(idx + 1)
292
4964
            .map_err(|_| Error::Compile("lambda parameter count exceeds u32 range".to_string()))?;
293
4964
        let bound_idx = match cc {
294
3876
            CallingConvention::Typed => wire_local_idx,
295
            CallingConvention::HostAnyref => {
296
1088
                let typed_local = ctx.alloc_local(*ty)?;
297
1088
                helper.local_get(wire_local_idx);
298
1088
                emit_anyref_to_typed_downcast(ctx, helper, *ty)?;
299
952
                helper.local_set(typed_local);
300
952
                typed_local
301
            }
302
        };
303
4828
        local_symbols.define(
304
4828
            Symbol::new(name, SymbolKind::Variable).with_value(Expr::WasmLocal(bound_idx, *ty)),
305
        );
306
    }
307
3808
    Ok(())
308
3944
}
309

            
310
/// Emits the wasm to convert an `anyref` on the stack to the declared
311
/// user-visible type. Reference-typed values ride a `ref.cast` to the
312
/// concrete type; nothing else is allowed — `I32` (i31-boxed integer)
313
/// and `Closure` deliberately fail here so the catch-each body can't
314
/// silently coerce small ints into a Ratio / Commodity slot or vice
315
/// versa. Cross-type bridging (int ↔ ratio ↔ commodity) is the
316
/// caller's job at the script level (ADR-0014).
317
1088
fn emit_anyref_to_typed_downcast(
318
1088
    ctx: &CompileContext,
319
1088
    helper: &mut FunctionEmitter,
320
1088
    ty: WasmType,
321
1088
) -> Result<()> {
322
1088
    match ty {
323
544
        WasmType::Ratio => helper.ref_cast(ctx.ids.ty_ratio),
324
        WasmType::Commodity => helper.ref_cast(ctx.ids.ty_commodity),
325
        WasmType::PairRef(_) => helper.ref_cast(ctx.ids.ty_pair),
326
        WasmType::StringRef => helper.ref_cast(ctx.ids.ty_i8_array),
327
136
        WasmType::EntityRef(kind) => helper.ref_cast(ctx.ids.entity_type(kind)),
328
272
        WasmType::AnyRef => {}
329
        WasmType::I32 | WasmType::Bool | WasmType::Closure(_) => {
330
136
            return Err(Error::Compile(format!(
331
136
                "host-anyref calling convention cannot downcast to {ty}; \
332
136
                 the iteration variable's element type must be a reference-typed \
333
136
                 value (Ratio / Commodity / Pair / String / Entity / AnyRef). \
334
136
                 Cross-type bridging (e.g. integer literal lists into a Ratio \
335
136
                 body) is forbidden — adjust the items list to the type the \
336
136
                 body expects, or rebox via an explicit conversion native"
337
136
            )));
338
        }
339
    }
340
952
    Ok(())
341
1088
}
342

            
343
/// Wraps a typed value on the stack as an anyref so the host fn can
344
/// receive it via the funcref's anyref result slot. I32 here means the
345
/// body terminated in `unreachable` (`(error ...)`), so wasm's
346
/// stack-polymorphism after the trap covers the typing — no real value
347
/// will ever be returned.
348
612
fn promote_result_to_anyref(
349
612
    ctx: &CompileContext,
350
612
    helper: &mut FunctionEmitter,
351
612
    ty: WasmType,
352
612
) -> Result<()> {
353
612
    match ty {
354
136
        WasmType::AnyRef => Ok(()),
355
        WasmType::Bool => {
356
            // A boolean body result is a value type (i32-repr), so box it into
357
            // an `(ref i31)` to ride the anyref result slot — the same boxing
358
            // the host-anyref convention uses for small ints elsewhere.
359
136
            let _ = ctx;
360
136
            helper.ref_i31();
361
136
            Ok(())
362
        }
363
        WasmType::I32 => {
364
            // Body lowered through (error ...) which ended in `unreachable`.
365
            // Drop the dummy I32 the unreachable left on the synthetic
366
            // polymorphic stack; nothing actually flows past this point.
367
136
            let _ = ctx;
368
136
            let _ = helper;
369
136
            Ok(())
370
        }
371
        WasmType::Ratio
372
        | WasmType::Commodity
373
        | WasmType::PairRef(_)
374
        | WasmType::StringRef
375
        | WasmType::EntityRef(_) => {
376
            // Refs widen to anyref freely (subtyping); no instruction needed.
377
204
            let _ = ctx;
378
204
            let _ = helper;
379
204
            Ok(())
380
        }
381
        WasmType::Closure(_) => Err(Error::Compile(format!(
382
            "host-anyref calling convention cannot widen body return type {ty} to anyref"
383
        ))),
384
    }
385
612
}
386

            
387
/// Emits the helper-fn prologue that unpacks an env-struct into fresh
388
/// helper-side locals. For each captured field: downcast param-0
389
/// (`(ref null any)`) to the concrete env-struct type, read the field,
390
/// stash it in a freshly allocated helper-side local, then rebind the
391
/// captured name in `local_symbols` to that local so the body's
392
/// `compile_for_stack` walks resolve captures via `local.get`.
393
340
fn emit_env_unpack_prologue(
394
340
    ctx: &mut CompileContext,
395
340
    helper: &mut FunctionEmitter,
396
340
    local_symbols: &mut SymbolTable,
397
340
    layout: &EnvStructLayout,
398
340
) -> Result<()> {
399
340
    for (field_idx, field) in layout.fields.iter().enumerate() {
400
340
        let local_idx = ctx.alloc_local(field.ty)?;
401
340
        let field_idx_u32 = u32::try_from(field_idx)
402
340
            .map_err(|_| Error::Compile("env-struct field count exceeds u32 range".to_string()))?;
403
340
        helper.local_get(0);
404
340
        helper.ref_cast(layout.type_idx);
405
340
        helper.struct_get(layout.type_idx, field_idx_u32);
406
340
        helper.local_set(local_idx);
407
340
        local_symbols.define(
408
340
            Symbol::new(&field.name, SymbolKind::Variable)
409
340
                .with_value(Expr::WasmLocal(local_idx, field.ty)),
410
        );
411
    }
412
340
    Ok(())
413
340
}
414

            
415
/// Emits the closure construction sequence at the caller's stack:
416
/// `ref.func $helper` + (env-struct construction or `ref.null any`) +
417
/// `struct.new $closure_<sig>`. Captures are loaded from the caller's
418
/// scope via `local.get` on each captured name's outer-scope local
419
/// index, then bundled into a `$env_<id>` struct whose ref slots into
420
/// the closure's nullable env field.
421
3468
fn push_closure_value(
422
3468
    ctx: &CompileContext,
423
3468
    emit: &mut FunctionEmitter,
424
3468
    symbols: &SymbolTable,
425
3468
    sig: ClosureSigId,
426
3468
    func_idx: u32,
427
3468
    env_layout: Option<&EnvStructLayout>,
428
3468
) -> Result<()> {
429
3468
    let closure_type_idx = ctx.closure_sig(sig).closure_type_idx;
430
3468
    emit.ref_func(func_idx);
431
3468
    match env_layout {
432
340
        Some(layout) => emit_env_struct_construction(emit, symbols, layout)?,
433
3128
        None => emit.ref_null_any(),
434
    }
435
3468
    emit.struct_new(closure_type_idx);
436
3468
    Ok(())
437
3468
}
438

            
439
340
fn emit_env_struct_construction(
440
340
    emit: &mut FunctionEmitter,
441
340
    symbols: &SymbolTable,
442
340
    layout: &EnvStructLayout,
443
340
) -> Result<()> {
444
340
    for field in &layout.fields {
445
340
        let sym = symbols.lookup(&field.name).ok_or_else(|| {
446
            Error::Compile(format!(
447
                "captured name {:?} disappeared from symbol table before env-struct \
448
                 construction",
449
                field.name
450
            ))
451
        })?;
452
340
        let Some(Expr::WasmLocal(idx, _)) = sym.value() else {
453
            return Err(Error::Compile(format!(
454
                "captured name {:?} no longer resolves to a local at env-struct \
455
                 construction time",
456
                field.name
457
            )));
458
        };
459
340
        emit.local_get(*idx);
460
    }
461
340
    emit.struct_new(layout.type_idx);
462
340
    Ok(())
463
340
}