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

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

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

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

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

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

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

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

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

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

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

            
267
3621
    ctx.restore_local_pool(snapshot);
268

            
269
3621
    let result_vt = ctx.wasm_val_type(result_ty);
270
3621
    let helper_name = ctx.next_lambda_helper_name()?;
271
3621
    let func_idx = ctx.register_function(&helper_name, &helper_param_vts, &[result_vt])?;
272
3621
    ctx.declare_funcref(func_idx);
273
3621
    ctx.queue_helper(helper_fn);
274
3621
    Ok((result_ty, func_idx, env_layout))
275
4118
}
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
4118
fn bind_user_params(
283
4118
    ctx: &mut CompileContext,
284
4118
    helper: &mut FunctionEmitter,
285
4118
    local_symbols: &mut SymbolTable,
286
4118
    param_names: &[String],
287
4118
    user_param_types: &[WasmType],
288
4118
    cc: CallingConvention,
289
4118
) -> Result<()> {
290
5183
    for (idx, (name, ty)) in param_names.iter().zip(user_param_types.iter()).enumerate() {
291
5183
        let wire_local_idx = u32::try_from(idx + 1)
292
5183
            .map_err(|_| Error::Compile("lambda parameter count exceeds u32 range".to_string()))?;
293
5183
        let bound_idx = match cc {
294
4047
            CallingConvention::Typed => wire_local_idx,
295
            CallingConvention::HostAnyref => {
296
1136
                let typed_local = ctx.alloc_local(*ty)?;
297
1136
                helper.local_get(wire_local_idx);
298
1136
                emit_anyref_to_typed_downcast(ctx, helper, *ty)?;
299
994
                helper.local_set(typed_local);
300
994
                typed_local
301
            }
302
        };
303
5041
        local_symbols.define(
304
5041
            Symbol::new(name, SymbolKind::Variable).with_value(Expr::WasmLocal(bound_idx, *ty)),
305
        );
306
    }
307
3976
    Ok(())
308
4118
}
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
1136
fn emit_anyref_to_typed_downcast(
318
1136
    ctx: &CompileContext,
319
1136
    helper: &mut FunctionEmitter,
320
1136
    ty: WasmType,
321
1136
) -> Result<()> {
322
1136
    match ty {
323
568
        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
142
        WasmType::EntityRef(kind) => helper.ref_cast(ctx.ids.entity_type(kind)),
328
284
        WasmType::AnyRef => {}
329
        WasmType::I32 | WasmType::Bool | WasmType::Closure(_) => {
330
142
            return Err(Error::Compile(format!(
331
142
                "host-anyref calling convention cannot downcast to {ty}; \
332
142
                 the iteration variable's element type must be a reference-typed \
333
142
                 value (Ratio / Commodity / Pair / String / Entity / AnyRef). \
334
142
                 Cross-type bridging (e.g. integer literal lists into a Ratio \
335
142
                 body) is forbidden — adjust the items list to the type the \
336
142
                 body expects, or rebox via an explicit conversion native"
337
142
            )));
338
        }
339
    }
340
994
    Ok(())
341
1136
}
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
639
fn promote_result_to_anyref(
349
639
    ctx: &CompileContext,
350
639
    helper: &mut FunctionEmitter,
351
639
    ty: WasmType,
352
639
) -> Result<()> {
353
639
    match ty {
354
142
        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
142
            let _ = ctx;
360
142
            helper.ref_i31();
361
142
            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
142
            let _ = ctx;
368
142
            let _ = helper;
369
142
            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
213
            let _ = ctx;
378
213
            let _ = helper;
379
213
            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
639
}
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
355
fn emit_env_unpack_prologue(
394
355
    ctx: &mut CompileContext,
395
355
    helper: &mut FunctionEmitter,
396
355
    local_symbols: &mut SymbolTable,
397
355
    layout: &EnvStructLayout,
398
355
) -> Result<()> {
399
355
    for (field_idx, field) in layout.fields.iter().enumerate() {
400
355
        let local_idx = ctx.alloc_local(field.ty)?;
401
355
        let field_idx_u32 = u32::try_from(field_idx)
402
355
            .map_err(|_| Error::Compile("env-struct field count exceeds u32 range".to_string()))?;
403
355
        helper.local_get(0);
404
355
        helper.ref_cast(layout.type_idx);
405
355
        helper.struct_get(layout.type_idx, field_idx_u32);
406
355
        helper.local_set(local_idx);
407
355
        local_symbols.define(
408
355
            Symbol::new(&field.name, SymbolKind::Variable)
409
355
                .with_value(Expr::WasmLocal(local_idx, field.ty)),
410
        );
411
    }
412
355
    Ok(())
413
355
}
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
3621
fn push_closure_value(
422
3621
    ctx: &CompileContext,
423
3621
    emit: &mut FunctionEmitter,
424
3621
    symbols: &SymbolTable,
425
3621
    sig: ClosureSigId,
426
3621
    func_idx: u32,
427
3621
    env_layout: Option<&EnvStructLayout>,
428
3621
) -> Result<()> {
429
3621
    let closure_type_idx = ctx.closure_sig(sig).closure_type_idx;
430
3621
    emit.ref_func(func_idx);
431
3621
    match env_layout {
432
355
        Some(layout) => emit_env_struct_construction(emit, symbols, layout)?,
433
3266
        None => emit.ref_null_any(),
434
    }
435
3621
    emit.struct_new(closure_type_idx);
436
3621
    Ok(())
437
3621
}
438

            
439
355
fn emit_env_struct_construction(
440
355
    emit: &mut FunctionEmitter,
441
355
    symbols: &SymbolTable,
442
355
    layout: &EnvStructLayout,
443
355
) -> Result<()> {
444
355
    for field in &layout.fields {
445
355
        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
355
        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
355
        emit.local_get(*idx);
460
    }
461
355
    emit.struct_new(layout.type_idx);
462
355
    Ok(())
463
355
}