1
//! `APPEND` — concatenate lists left to right. Constant-folds when every
2
//! argument is a compile-time list; otherwise lowers the two-list runtime
3
//! case by reversing the prefix onto an accumulator seeded with the
4
//! suffix chain (so the suffix is shared, not copied).
5

            
6
use crate::ast::{Expr, PairElement, WasmType};
7
use crate::compiler::context::CompileContext;
8
use crate::compiler::emit::FunctionEmitter;
9
use crate::compiler::expr::{
10
    compile_expr, compile_for_stack, eval_value, format_expr, serialize_stack_to_output,
11
};
12
use crate::error::{Error, Result};
13
use crate::runtime::SymbolTable;
14

            
15
use super::datum::{compile_folded_to_stack, is_datum_result};
16
use super::map::extract_list_elements;
17

            
18
1020
pub(super) fn append(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
19
1632
    if args.iter().any(|a| {
20
1632
        eval_value(symbols, a)
21
1632
            .map(|r| matches!(r.wasm_type(), Some(WasmType::PairRef(_))))
22
1632
            .unwrap_or(false)
23
1632
    }) {
24
680
        return Ok(Expr::WasmRuntime(WasmType::PairRef(result_element(
25
680
            symbols, args,
26
        )?)));
27
340
    }
28
340
    let mut out = Vec::new();
29
680
    for arg in args {
30
680
        let resolved = eval_value(symbols, arg)?;
31
680
        out.extend(extract_list_elements(&resolved).map_err(|_| {
32
            Error::Compile(format!(
33
                "APPEND expects lists, got {}",
34
                format_expr(&resolved)
35
            ))
36
        })?);
37
    }
38
340
    Ok(Expr::Quote(Box::new(Expr::List(out))))
39
1020
}
40

            
41
/// The element type of a runtime APPEND result: the widening of every
42
/// argument's element type across both runtime `PairRef` chains and constant
43
/// lists. A constant list's element is the widening of its members' literal
44
/// element types (`AnyRef` if any member isn't a plain literal). An empty
45
/// constant list / nil doesn't constrain the result.
46
1020
fn result_element(symbols: &mut SymbolTable, args: &[Expr]) -> Result<PairElement> {
47
1020
    let mut elem: Option<PairElement> = None;
48
2652
    let mut widen = |e: PairElement| {
49
2652
        elem = Some(match elem {
50
1632
            Some(prev) => prev.widen(e),
51
1020
            None => e,
52
        });
53
2652
    };
54
2040
    for arg in args {
55
2040
        let resolved = eval_value(symbols, arg)?;
56
2040
        match resolved.wasm_type() {
57
1020
            Some(WasmType::PairRef(e)) => widen(e),
58
            _ => {
59
1632
                for member in extract_list_elements(&resolved).map_err(|_| {
60
                    Error::Compile(format!(
61
                        "APPEND expects lists, got {}",
62
                        format_expr(&resolved)
63
                    ))
64
1632
                })? {
65
1632
                    widen(
66
1632
                        super::infer::literal_pair_element(&member).unwrap_or(PairElement::AnyRef),
67
1632
                    );
68
1632
                }
69
            }
70
        }
71
    }
72
    // All-empty / all-nil args: the result is an empty chain; element type is
73
    // irrelevant (no cells), so any concrete slot works.
74
1020
    Ok(elem.unwrap_or(PairElement::AnyRef))
75
1020
}
76

            
77
544
pub(super) fn compile_append(
78
544
    ctx: &mut CompileContext,
79
544
    emit: &mut FunctionEmitter,
80
544
    symbols: &mut SymbolTable,
81
544
    args: &[Expr],
82
544
) -> Result<()> {
83
544
    let folded = append(symbols, args)?;
84
544
    if folded.is_wasm_runtime() {
85
340
        let ty = compile_append_to_stack(ctx, emit, symbols, args)?;
86
272
        return serialize_stack_to_output(ctx, emit, ty);
87
204
    }
88
204
    compile_expr(ctx, emit, symbols, &folded)
89
544
}
90

            
91
476
pub(super) fn compile_append_to_stack(
92
476
    ctx: &mut CompileContext,
93
476
    emit: &mut FunctionEmitter,
94
476
    symbols: &mut SymbolTable,
95
476
    args: &[Expr],
96
476
) -> Result<WasmType> {
97
    // All-constant APPEND folds to a single quoted list — render it as a datum
98
    // (matching CAR/CDR/REVERSE/CONS and the effect path) instead of forcing it
99
    // through the runtime materialization below, which can't represent symbols.
100
476
    let folded = append(symbols, args)?;
101
476
    if !folded.is_wasm_runtime() && is_datum_result(&folded) {
102
136
        return compile_folded_to_stack(ctx, emit, symbols, folded);
103
340
    }
104
340
    if args.len() != 2 {
105
        return Err(Error::Compile(
106
            "APPEND with a runtime list argument requires exactly 2 lists".to_string(),
107
        ));
108
340
    }
109
340
    let elem = result_element(symbols, args)?;
110
340
    let pair_idx = ctx.ids.ty_pair;
111
340
    let prefix_local = ctx.alloc_local(WasmType::PairRef(elem))?;
112
340
    let acc_local = ctx.alloc_local(WasmType::PairRef(elem))?;
113

            
114
    // acc ← suffix (shared tail); then prepend the prefix in reverse so
115
    // the final chain is prefix ++ suffix in original order.
116
340
    push_list_arg(ctx, emit, symbols, &args[1])?;
117
340
    emit.local_set(acc_local);
118
340
    push_list_arg(ctx, emit, symbols, &args[0])?;
119
272
    emit.local_set(prefix_local);
120

            
121
272
    let reversed_local = ctx.alloc_local(WasmType::PairRef(elem))?;
122
272
    reverse_into(ctx, emit, pair_idx, prefix_local, reversed_local);
123

            
124
    // Walk the reversed prefix, prepending each car onto acc (= suffix).
125
272
    emit.block_start();
126
272
    emit.loop_start();
127
272
    emit.local_get(reversed_local);
128
272
    emit.ref_is_null();
129
272
    emit.br_if(1);
130
272
    emit.local_get(reversed_local);
131
272
    emit.struct_get(pair_idx, 0);
132
272
    emit.local_get(acc_local);
133
272
    emit.call(ctx.ids.pair_new);
134
272
    emit.local_set(acc_local);
135
272
    emit.local_get(reversed_local);
136
272
    emit.struct_get(pair_idx, 1);
137
272
    emit.local_set(reversed_local);
138
272
    emit.br(0);
139
272
    emit.block_end();
140
272
    emit.block_end();
141

            
142
272
    emit.local_get(acc_local);
143
272
    Ok(WasmType::PairRef(elem))
144
476
}
145

            
146
/// Push an APPEND argument as a runtime `$pair` chain on the stack. A runtime
147
/// `PairRef` arg lowers directly; a constant list is materialized into a fresh
148
/// `$pair` chain via the cons builder; nil / empty list becomes a null pair.
149
/// The runtime `$pair` struct is monomorphic (anyref car), so a materialized
150
/// constant chain and a runtime chain are the same wasm type — only the
151
/// compile-time element label (computed by `result_element`) differs.
152
680
fn push_list_arg(
153
680
    ctx: &mut CompileContext,
154
680
    emit: &mut FunctionEmitter,
155
680
    symbols: &mut SymbolTable,
156
680
    arg: &Expr,
157
680
) -> Result<()> {
158
680
    let resolved = eval_value(symbols, arg)?;
159
680
    if matches!(resolved.wasm_type(), Some(WasmType::PairRef(_))) {
160
340
        compile_for_stack(ctx, emit, symbols, arg)?;
161
340
        return Ok(());
162
340
    }
163
340
    let elements = extract_list_elements(&resolved).map_err(|_| {
164
        Error::Compile(format!(
165
            "APPEND expects lists, got {}",
166
            format_expr(&resolved)
167
        ))
168
    })?;
169
340
    if elements.is_empty() {
170
68
        emit.ref_null(ctx.ids.ty_pair);
171
68
        return Ok(());
172
272
    }
173
    // The extracted members are already the list's DATA, not expressions to
174
    // evaluate. Self-evaluating literals (number / string / bool / nil) pass
175
    // through as-is; a member that the cons builder would otherwise RESOLVE as
176
    // a reference (a bare `Symbol("a")` → variable lookup → "Undefined symbol",
177
    // or a nested list → a call) is quoted so it stays literal. A quoted datum
178
    // with no runtime representation (e.g. a symbol) then surfaces the standard
179
    // "cannot compile to WASM stack value" error rather than a misleading
180
    // undefined-symbol one. Full symbol-list support is a separate slice.
181
272
    let members: Vec<Expr> = elements
182
272
        .into_iter()
183
544
        .map(|e| match e {
184
408
            Expr::Number(_) | Expr::String(_) | Expr::Bool(_) | Expr::Nil => e,
185
136
            other => Expr::Quote(Box::new(other)),
186
544
        })
187
272
        .collect();
188
272
    super::cons::compile_pair_chain(ctx, emit, symbols, &members)?;
189
204
    Ok(())
190
680
}
191

            
192
/// Reverses the chain in `src_local` into `dst_local` (a fresh null-seeded
193
/// accumulator), consuming `src_local`. Used so APPEND can prepend the
194
/// prefix onto the shared suffix in original order.
195
272
fn reverse_into(
196
272
    ctx: &mut CompileContext,
197
272
    emit: &mut FunctionEmitter,
198
272
    pair_idx: u32,
199
272
    src_local: u32,
200
272
    dst_local: u32,
201
272
) {
202
272
    emit.ref_null(pair_idx);
203
272
    emit.local_set(dst_local);
204
272
    emit.block_start();
205
272
    emit.loop_start();
206
272
    emit.local_get(src_local);
207
272
    emit.ref_is_null();
208
272
    emit.br_if(1);
209
272
    emit.local_get(src_local);
210
272
    emit.struct_get(pair_idx, 0);
211
272
    emit.local_get(dst_local);
212
272
    emit.call(ctx.ids.pair_new);
213
272
    emit.local_set(dst_local);
214
272
    emit.local_get(src_local);
215
272
    emit.struct_get(pair_idx, 1);
216
272
    emit.local_set(src_local);
217
272
    emit.br(0);
218
272
    emit.block_end();
219
272
    emit.block_end();
220
272
}