1
//! `FOLD` — left fold over a list. `(fold f init list)` returns the
2
//! accumulator after applying `f(acc, item)` left-to-right.
3
//!
4
//! Three paths matching MAP / FILTER:
5
//! - **Constant-fold**: function + list both resolve at compile time;
6
//!   walk element-by-element calling `f` via the eval `call` pipeline,
7
//!   threading `init` through.
8
//! - **Runtime closure, literal list**: emit FUNCALL per element,
9
//!   threading the accumulator through a fresh local each iteration.
10
//! - **Runtime closure or runtime `PairRef` list**: walk the input
11
//!   chain at runtime, calling `f` on each car with the accumulator.
12

            
13
use crate::ast::{Expr, PairElement, WasmType};
14
use crate::compiler::context::CompileContext;
15
use crate::compiler::emit::FunctionEmitter;
16
use crate::compiler::expr::{
17
    as_literal_arg, call, compile_expr, compile_for_effect, compile_for_stack,
18
    compile_for_stack_as, emit_nil_default, eval_value, format_expr,
19
};
20
use crate::error::{Error, Result};
21
use crate::runtime::SymbolTable;
22

            
23
1909
pub(super) fn fold(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
24
1909
    if args.len() != 3 {
25
        return Err(Error::Arity {
26
            name: "FOLD".to_string(),
27
            expected: 3,
28
            actual: args.len(),
29
        });
30
1909
    }
31
1909
    let fn_resolved = eval_value(symbols, &args[0])?;
32
1909
    let init_resolved = eval_value(symbols, &args[1])?;
33
1909
    let list_resolved = eval_value(symbols, &args[2])?;
34
1909
    let runtime_closure = matches!(fn_resolved.wasm_type(), Some(WasmType::Closure(_)));
35
1909
    let runtime_list = matches!(list_resolved.wasm_type(), Some(WasmType::PairRef(_)));
36
1909
    if runtime_closure || runtime_list {
37
1909
        let acc_ty = eval_fold_result_type(
38
1909
            symbols,
39
1909
            &fn_resolved,
40
1909
            &args[0],
41
1909
            &init_resolved,
42
1909
            &list_resolved,
43
        )?;
44
1909
        return Ok(Expr::WasmRuntime(acc_ty));
45
    }
46
    // Static fn + constant list: try to fully reduce. If the body produces a
47
    // runtime value (a side-effecting native like `print`, or any host-fn
48
    // result), the fold ISN'T constant-foldable — surface a runtime placeholder
49
    // so the compile path takes the per-element FUNCALL runtime lowering
50
    // instead of baking a bare `WasmRuntime` placeholder into the result.
51
    let folded = constant_fold_fold(symbols, &args[0], &init_resolved, &list_resolved)?;
52
    if folded.is_wasm_runtime() {
53
        let acc_ty = eval_fold_result_type(
54
            symbols,
55
            &fn_resolved,
56
            &args[0],
57
            &init_resolved,
58
            &list_resolved,
59
        )?;
60
        return Ok(Expr::WasmRuntime(acc_ty));
61
    }
62
    Ok(folded)
63
1909
}
64

            
65
/// Eval-path fold result type, kept in lockstep with `compile_fold_to_stack`:
66
/// a runtime closure's accumulator type IS its signature's result type. The
67
/// eval surface has no `CompileContext`, so it reads the result the closure-emit
68
/// site recorded into the symbol table (`SymbolTable::closure_result`); without
69
/// it (closure not recorded) it falls back to the body/seed probe. This stops
70
/// the eval mirror sizing a fold accumulator from the seed (e.g. `0` → Index)
71
/// while codegen emits the closure's real (e.g. Ratio) result.
72
1909
fn eval_fold_result_type(
73
1909
    symbols: &mut SymbolTable,
74
1909
    fn_resolved: &Expr,
75
1909
    fn_arg: &Expr,
76
1909
    init_resolved: &Expr,
77
1909
    list_resolved: &Expr,
78
1909
) -> Result<WasmType> {
79
1909
    if let Some(WasmType::Closure(sig)) = fn_resolved.wasm_type()
80
1743
        && let Some(result) = symbols.closure_result(sig)
81
    {
82
1743
        return Ok(result);
83
166
    }
84
166
    accumulator_type(symbols, fn_arg, init_resolved, list_resolved)
85
1909
}
86

            
87
/// The accumulator/result `WasmType` of a fold. A concrete (non-nil) init
88
/// fixes it. A `nil` init is POLYMORPHIC — its `Bool` type is just "empty" —
89
/// so the real accumulator type is whatever the fold body returns; probe it by
90
/// classifying one application `f(init, <sample-elem>)` on a clone (no
91
/// live-table mutation). Falls back to the init's own type when the body can't
92
/// be probed. Shared by the eval mirror and the codegen path so they agree.
93
1660
fn accumulator_type(
94
1660
    symbols: &mut SymbolTable,
95
1660
    fn_arg: &Expr,
96
1660
    init_resolved: &Expr,
97
1660
    list_resolved: &Expr,
98
1660
) -> Result<WasmType> {
99
    // A concrete runtime-typed init (a `WasmLocal`/`WasmRuntime`, NOT nil)
100
    // fixes the accumulator type directly.
101
1660
    if let Some(ty) = init_resolved.wasm_type()
102
        && !matches!(init_resolved, Expr::Nil)
103
    {
104
        return Ok(ty);
105
1660
    }
106
    // Otherwise (a `nil` polymorphic seed, or a literal init like `0` whose
107
    // own classified type may still be refined by the body) the accumulator
108
    // type is what the body returns — probe one application.
109
1660
    if let Some(elem) = sample_element(list_resolved)?
110
1660
        && let Some(ty) = probe_body_type(symbols, fn_arg, init_resolved, &elem)?
111
    {
112
1660
        return widen_with_seed(ty, init_resolved);
113
    }
114
    // Empty list or un-probeable body: fall back to the init's own stack type
115
    // (a literal → its classified type, e.g. `0` → Ratio; nil → Bool).
116
    Ok(crate::compiler::expr::classify_stack_type(init_resolved).unwrap_or(WasmType::Ratio))
117
1660
}
118

            
119
/// A constant-list seed's members share the accumulator chain with the body's
120
/// cells, so they widen a pair accumulator's element: `'(nil)` seeding an
121
/// index chain is heterogeneous, never a zero.
122
1660
fn widen_with_seed(ty: WasmType, init_resolved: &Expr) -> Result<WasmType> {
123
1660
    let (WasmType::PairRef(elem), Expr::Quote(seed)) = (ty, init_resolved) else {
124
913
        return Ok(ty);
125
    };
126
747
    let Expr::List(members) = seed.as_ref() else {
127
        return Ok(ty);
128
    };
129
747
    let mut cells = members.clone();
130
747
    cells.push(Expr::WasmRuntime(elem.as_wasm_type()));
131
747
    Ok(WasmType::PairRef(super::infer::chain_pair_element(&cells)?))
132
1660
}
133

            
134
/// A representative element value for probing the fold body's return type:
135
/// the runtime element's placeholder for a `PairRef` list, else the first
136
/// constant element. `None` for an empty list (nothing to probe).
137
1660
fn sample_element(list_resolved: &Expr) -> Result<Option<Expr>> {
138
1660
    if let Some(WasmType::PairRef(elem)) = list_resolved.wasm_type() {
139
1494
        return Ok(Some(Expr::WasmRuntime(elem.as_wasm_type())));
140
166
    }
141
166
    Ok(extract_list_elements(list_resolved)?.into_iter().next())
142
1660
}
143

            
144
/// Classify the type of one fold application `f(acc, elem)` without emitting —
145
/// evaluated on a CLONE so no live-table mutation, then run through the shared
146
/// stack-type classifier so the prediction matches codegen.
147
1660
fn probe_body_type(
148
1660
    symbols: &mut SymbolTable,
149
1660
    fn_arg: &Expr,
150
1660
    acc: &Expr,
151
1660
    elem: &Expr,
152
1660
) -> Result<Option<WasmType>> {
153
1660
    let call_args = vec![fn_arg.clone(), as_literal_arg(acc), as_literal_arg(elem)];
154
1660
    let mut probe = symbols.clone();
155
1660
    let Ok(result) = call(&mut probe, &call_args) else {
156
        return Ok(None);
157
    };
158
1660
    Ok(crate::compiler::expr::classify_stack_type(&result))
159
1660
}
160

            
161
249
fn constant_fold_fold(
162
249
    symbols: &mut SymbolTable,
163
249
    fn_arg: &Expr,
164
249
    init_resolved: &Expr,
165
249
    list_resolved: &Expr,
166
249
) -> Result<Expr> {
167
249
    let elements = extract_list_elements(list_resolved)?;
168
249
    let mut acc = init_resolved.clone();
169
581
    for elem in elements {
170
581
        let call_args = vec![fn_arg.clone(), as_literal_arg(&acc), as_literal_arg(&elem)];
171
581
        let result = call(symbols, &call_args)?;
172
581
        acc = eval_value(symbols, &result)?;
173
    }
174
249
    Ok(acc)
175
249
}
176

            
177
/// Seed the accumulator local from `init`, coercing the init to the resolved
178
/// accumulator type. For a `PairRef` accumulator a constant-list seed (`nil`,
179
/// or a quoted literal list) is materialized into a runtime `$pair` chain —
180
/// `nil` is the empty chain (`ref.null pair`). For any other `acc_ty` the init
181
/// must already match.
182
2490
fn emit_fold_init(
183
2490
    ctx: &mut CompileContext,
184
2490
    emit: &mut FunctionEmitter,
185
2490
    symbols: &mut SymbolTable,
186
2490
    init_arg: &Expr,
187
2490
    acc_ty: WasmType,
188
2490
) -> Result<()> {
189
    // Probe the seed's resolved value on a CLONE so a seed that mutates the
190
    // table (SETF/DEFVAR) isn't applied here AND again by the emission path.
191
2490
    let resolved = eval_value(&mut symbols.clone(), init_arg)?;
192
    // A nil seed (for ANY accumulator type, including PairRef where it is the
193
    // empty `$pair` chain) must first emit the effects of a non-literal seed
194
    // that merely *resolves* to nil, then push the typed nil default. Checked
195
    // before the PairRef constant-list materialization so a `(progn …effects… nil)`
196
    // seed isn't silently collapsed to `ref.null`.
197
2490
    if matches!(resolved, Expr::Nil) {
198
498
        if !matches!(init_arg, Expr::Nil) {
199
166
            compile_for_effect(ctx, emit, symbols, init_arg)?;
200
332
        }
201
498
        return emit_nil_default(ctx, emit, acc_ty);
202
1992
    }
203
1992
    if let WasmType::PairRef(elem) = acc_ty
204
581
        && !matches!(resolved.wasm_type(), Some(WasmType::PairRef(_)))
205
    {
206
        // A constant-list seed for a runtime pair accumulator: materialize it
207
        // with cells at the accumulator's element, so a fold forced onto the
208
        // runtime path by a side-effecting body can still seed a non-nil
209
        // literal-list accumulator. See `append::push_list_arg`.
210
581
        let elements = super::map::extract_list_elements(&resolved)?;
211
581
        return super::cons::emit_constant_chain(ctx, emit, symbols, elements, elem);
212
1411
    }
213
    // Scalar / Index / Money / nil seed: coerce to the accumulator type (an
214
    // integer/fractional literal crosses the sanctioned Index↔Scalar boundary).
215
1411
    compile_for_stack_as(ctx, emit, symbols, init_arg, acc_ty).map_err(|_| {
216
        Error::Compile(format!(
217
            "FOLD: init does not match accumulator type {acc_ty}"
218
        ))
219
    })
220
2490
}
221

            
222
1245
fn extract_list_elements(expr: &Expr) -> Result<Vec<Expr>> {
223
1245
    match expr {
224
        Expr::List(elems) => Ok(elems.clone()),
225
        Expr::Nil => Ok(vec![]),
226
1245
        Expr::Quote(inner) => match inner.as_ref() {
227
1245
            Expr::List(elems) => Ok(elems.clone()),
228
            Expr::Nil => Ok(vec![]),
229
            other => Err(Error::Compile(format!(
230
                "FOLD expects a list, got quoted {}",
231
                format_expr(other)
232
            ))),
233
        },
234
        other => Err(Error::Compile(format!(
235
            "FOLD expects a list, got {}",
236
            format_expr(other)
237
        ))),
238
    }
239
1245
}
240

            
241
1079
pub(super) fn compile_fold(
242
1079
    ctx: &mut CompileContext,
243
1079
    emit: &mut FunctionEmitter,
244
1079
    symbols: &mut SymbolTable,
245
1079
    args: &[Expr],
246
1079
) -> Result<()> {
247
1079
    if args.len() != 3 {
248
        return Err(Error::Arity {
249
            name: "FOLD".to_string(),
250
            expected: 3,
251
            actual: args.len(),
252
        });
253
1079
    }
254
1079
    let fn_resolved = eval_value(symbols, &args[0])?;
255
1079
    let init_resolved = eval_value(symbols, &args[1])?;
256
1079
    let list_resolved = eval_value(symbols, &args[2])?;
257
1079
    let runtime_closure = matches!(fn_resolved.wasm_type(), Some(WasmType::Closure(_)));
258
1079
    let runtime_list = matches!(list_resolved.wasm_type(), Some(WasmType::PairRef(_)));
259
1079
    if runtime_closure || runtime_list {
260
830
        let ty = compile_fold_to_stack(ctx, emit, symbols, args)?;
261
830
        return crate::compiler::expr::serialize_stack_to_output(ctx, emit, ty);
262
249
    }
263
249
    let result = constant_fold_fold(symbols, &args[0], &init_resolved, &list_resolved)?;
264
249
    if result.is_wasm_runtime() {
265
        // Body produced a runtime value — not constant-foldable. Lower the
266
        // static fn + constant list element-by-element via the runtime path.
267
166
        let ty = compile_fold_to_stack(ctx, emit, symbols, args)?;
268
166
        return crate::compiler::expr::serialize_stack_to_output(ctx, emit, ty);
269
83
    }
270
83
    compile_expr(ctx, emit, symbols, &result)
271
1079
}
272

            
273
2490
pub(super) fn compile_fold_to_stack(
274
2490
    ctx: &mut CompileContext,
275
2490
    emit: &mut FunctionEmitter,
276
2490
    symbols: &mut SymbolTable,
277
2490
    args: &[Expr],
278
2490
) -> Result<WasmType> {
279
2490
    if args.len() != 3 {
280
        return Err(Error::Compile(
281
            "FOLD: stack-position lowering needs (fn init list)".to_string(),
282
        ));
283
2490
    }
284
2490
    let fn_resolved = eval_value(symbols, &args[0])?;
285
2490
    let init_resolved = eval_value(symbols, &args[1])?;
286
2490
    let list_resolved = eval_value(symbols, &args[2])?;
287
    // A runtime closure declares its accumulator type in its signature (the
288
    // result type the body returns under its parameter types). Use that rather
289
    // than the eval-side body probe, which sees literal-typed sample args and
290
    // would mis-infer Index where the closure params are Scalar/Money.
291
2490
    let acc_ty = match fn_resolved.wasm_type() {
292
996
        Some(WasmType::Closure(sig)) => ctx.closure_sig(sig).result,
293
1494
        _ => accumulator_type(symbols, &args[0], &init_resolved, &list_resolved)?,
294
    };
295

            
296
    // A let-bound closure has a FIXED signature; applying it via `call_ref` over
297
    // a non-Ratio list mismatches its iteration param. If its source body is
298
    // recoverable, inline it per element instead — `compile_lambda_call` binds
299
    // the iteration param to the actual element type. The accumulator type stays
300
    // the closure's declared result (it threads through unchanged).
301
2490
    let call_fn = super::inline_closure_fn(ctx, symbols, &args[0])?;
302

            
303
2490
    if let Some(WasmType::PairRef(elem)) = list_resolved.wasm_type() {
304
1660
        return compile_fold_runtime_list(
305
1660
            ctx,
306
1660
            emit,
307
1660
            symbols,
308
1660
            FoldRuntimeArgs {
309
1660
                fn_arg: &call_fn,
310
1660
                init_arg: &args[1],
311
1660
                list_expr: &args[2],
312
1660
                elem,
313
1660
                acc_ty,
314
1660
            },
315
        );
316
830
    }
317
    // Any callable over a constant list — runtime closure OR a bare lambda /
318
    // defun whose body isn't constant-foldable (const-fold bailed here). Each
319
    // element is applied via FUNCALL, which serves both shapes.
320
830
    compile_fold_literal(
321
830
        ctx,
322
830
        emit,
323
830
        symbols,
324
830
        &call_fn,
325
830
        &args[1],
326
830
        &list_resolved,
327
830
        acc_ty,
328
    )
329
2490
}
330

            
331
/// Lower `(fold <fn> init '(e0 e1 ...))` element-by-element. Each per-element
332
/// call rides FUNCALL (→ `call_ref` for a closure, inline for a bare lambda)
333
/// and threads the accumulator through a fresh local.
334
830
fn compile_fold_literal(
335
830
    ctx: &mut CompileContext,
336
830
    emit: &mut FunctionEmitter,
337
830
    symbols: &mut SymbolTable,
338
830
    fn_arg: &Expr,
339
830
    init_arg: &Expr,
340
830
    list_resolved: &Expr,
341
830
    acc_ty: WasmType,
342
830
) -> Result<WasmType> {
343
830
    let elements = extract_list_elements(list_resolved)?;
344
830
    let acc_local = ctx.alloc_local(acc_ty)?;
345
830
    emit_fold_init(ctx, emit, symbols, init_arg, acc_ty)?;
346
830
    emit.local_set(acc_local);
347

            
348
1909
    for elem_expr in &elements {
349
1909
        let funcall = Expr::List(vec![
350
1909
            Expr::Symbol("FUNCALL".to_string()),
351
1909
            fn_arg.clone(),
352
1909
            Expr::WasmLocal(acc_local, acc_ty),
353
1909
            as_literal_arg(elem_expr),
354
1909
        ]);
355
1909
        let result_ty = compile_for_stack(ctx, emit, symbols, &funcall)?;
356
1909
        if result_ty != acc_ty {
357
            return Err(Error::Compile(format!(
358
                "FOLD: closure return type {result_ty} doesn't match accumulator type {acc_ty}"
359
            )));
360
1909
        }
361
1909
        emit.local_set(acc_local);
362
    }
363
830
    emit.local_get(acc_local);
364
830
    Ok(acc_ty)
365
830
}
366

            
367
struct FoldRuntimeArgs<'a> {
368
    fn_arg: &'a Expr,
369
    init_arg: &'a Expr,
370
    list_expr: &'a Expr,
371
    elem: PairElement,
372
    acc_ty: WasmType,
373
}
374

            
375
/// Walks a runtime `PairRef(elem)` input. Calls `f(acc, car)` per cell,
376
/// threading the accumulator through a fresh local.
377
1660
fn compile_fold_runtime_list(
378
1660
    ctx: &mut CompileContext,
379
1660
    emit: &mut FunctionEmitter,
380
1660
    symbols: &mut SymbolTable,
381
1660
    args: FoldRuntimeArgs<'_>,
382
1660
) -> Result<WasmType> {
383
    let FoldRuntimeArgs {
384
1660
        fn_arg,
385
1660
        init_arg,
386
1660
        list_expr,
387
1660
        elem,
388
1660
        acc_ty,
389
1660
    } = args;
390
1660
    let pair_idx = ctx.ids.ty_pair;
391
1660
    let pair_local = ctx.alloc_local(WasmType::PairRef(elem))?;
392
1660
    let acc_local = ctx.alloc_local(acc_ty)?;
393
1660
    let car_local = ctx.alloc_local(elem.as_wasm_type())?;
394

            
395
1660
    emit_fold_init(ctx, emit, symbols, init_arg, acc_ty)?;
396
1660
    emit.local_set(acc_local);
397

            
398
1660
    compile_for_stack(ctx, emit, symbols, list_expr)?;
399
1660
    emit.local_set(pair_local);
400

            
401
1660
    emit.block_start();
402
1660
    emit.loop_start();
403

            
404
1660
    emit.local_get(pair_local);
405
1660
    emit.ref_is_null();
406
1660
    emit.br_if(1);
407

            
408
1660
    emit.local_get(pair_local);
409
1660
    emit.struct_get(pair_idx, 0);
410
1660
    crate::compiler::native::list::emit_pair_car_downcast(ctx, emit, elem);
411
1660
    emit.local_set(car_local);
412

            
413
1660
    let funcall = Expr::List(vec![
414
1660
        Expr::Symbol("FUNCALL".to_string()),
415
1660
        fn_arg.clone(),
416
1660
        Expr::WasmLocal(acc_local, acc_ty),
417
1660
        Expr::WasmLocal(car_local, elem.as_wasm_type()),
418
1660
    ]);
419
1660
    let result_ty = compile_for_stack(ctx, emit, symbols, &funcall)?;
420
1660
    if result_ty != acc_ty {
421
        return Err(Error::Compile(format!(
422
            "FOLD: closure return type {result_ty} doesn't match accumulator type {acc_ty}"
423
        )));
424
1660
    }
425
1660
    emit.local_set(acc_local);
426

            
427
1660
    emit.local_get(pair_local);
428
1660
    emit.struct_get(pair_idx, 1);
429
1660
    emit.local_set(pair_local);
430

            
431
1660
    emit.br(0);
432
1660
    emit.block_end();
433
1660
    emit.block_end();
434

            
435
1660
    emit.local_get(acc_local);
436
1660
    Ok(acc_ty)
437
1660
}