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
1065
pub(super) fn append(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
19
1704
    if args.iter().any(|a| {
20
1704
        eval_value(symbols, a)
21
1704
            .map(|r| matches!(r.wasm_type(), Some(WasmType::PairRef(_))))
22
1704
            .unwrap_or(false)
23
1704
    }) {
24
710
        return Ok(Expr::WasmRuntime(WasmType::PairRef(result_element(
25
710
            symbols, args,
26
        )?)));
27
355
    }
28
355
    let mut out = Vec::new();
29
710
    for arg in args {
30
710
        let resolved = eval_value(symbols, arg)?;
31
710
        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
355
    Ok(Expr::Quote(Box::new(Expr::List(out))))
39
1065
}
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
1065
fn result_element(symbols: &mut SymbolTable, args: &[Expr]) -> Result<PairElement> {
47
1065
    let mut elem: Option<PairElement> = None;
48
2769
    let mut widen = |e: PairElement| {
49
2769
        elem = Some(match elem {
50
1704
            Some(prev) => prev.widen(e),
51
1065
            None => e,
52
        });
53
2769
    };
54
2130
    for arg in args {
55
2130
        let resolved = eval_value(symbols, arg)?;
56
2130
        match resolved.wasm_type() {
57
1065
            Some(WasmType::PairRef(e)) => widen(e),
58
            _ => {
59
1704
                for member in extract_list_elements(&resolved).map_err(|_| {
60
                    Error::Compile(format!(
61
                        "APPEND expects lists, got {}",
62
                        format_expr(&resolved)
63
                    ))
64
1704
                })? {
65
1704
                    widen(
66
1704
                        super::infer::literal_pair_element(&member).unwrap_or(PairElement::AnyRef),
67
1704
                    );
68
1704
                }
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
1065
    Ok(elem.unwrap_or(PairElement::AnyRef))
75
1065
}
76

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

            
91
497
pub(super) fn compile_append_to_stack(
92
497
    ctx: &mut CompileContext,
93
497
    emit: &mut FunctionEmitter,
94
497
    symbols: &mut SymbolTable,
95
497
    args: &[Expr],
96
497
) -> 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
497
    let folded = append(symbols, args)?;
101
497
    if !folded.is_wasm_runtime() && is_datum_result(&folded) {
102
142
        return compile_folded_to_stack(ctx, emit, symbols, folded);
103
355
    }
104
355
    if args.len() != 2 {
105
        return Err(Error::Compile(
106
            "APPEND with a runtime list argument requires exactly 2 lists".to_string(),
107
        ));
108
355
    }
109
355
    let elem = result_element(symbols, args)?;
110
355
    let pair_idx = ctx.ids.ty_pair;
111
355
    let prefix_local = ctx.alloc_local(WasmType::PairRef(elem))?;
112
355
    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
355
    push_list_arg(ctx, emit, symbols, &args[1])?;
117
355
    emit.local_set(acc_local);
118
355
    push_list_arg(ctx, emit, symbols, &args[0])?;
119
284
    emit.local_set(prefix_local);
120

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

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

            
142
284
    emit.local_get(acc_local);
143
284
    Ok(WasmType::PairRef(elem))
144
497
}
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
710
fn push_list_arg(
153
710
    ctx: &mut CompileContext,
154
710
    emit: &mut FunctionEmitter,
155
710
    symbols: &mut SymbolTable,
156
710
    arg: &Expr,
157
710
) -> Result<()> {
158
710
    let resolved = eval_value(symbols, arg)?;
159
710
    if matches!(resolved.wasm_type(), Some(WasmType::PairRef(_))) {
160
355
        compile_for_stack(ctx, emit, symbols, arg)?;
161
355
        return Ok(());
162
355
    }
163
355
    let elements = extract_list_elements(&resolved).map_err(|_| {
164
        Error::Compile(format!(
165
            "APPEND expects lists, got {}",
166
            format_expr(&resolved)
167
        ))
168
    })?;
169
355
    if elements.is_empty() {
170
71
        emit.ref_null(ctx.ids.ty_pair);
171
71
        return Ok(());
172
284
    }
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
284
    let members: Vec<Expr> = elements
182
284
        .into_iter()
183
568
        .map(|e| match e {
184
426
            Expr::Number(_) | Expr::String(_) | Expr::Bool(_) | Expr::Nil => e,
185
142
            other => Expr::Quote(Box::new(other)),
186
568
        })
187
284
        .collect();
188
284
    super::cons::compile_pair_chain(ctx, emit, symbols, &members)?;
189
213
    Ok(())
190
710
}
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
284
fn reverse_into(
196
284
    ctx: &mut CompileContext,
197
284
    emit: &mut FunctionEmitter,
198
284
    pair_idx: u32,
199
284
    src_local: u32,
200
284
    dst_local: u32,
201
284
) {
202
284
    emit.ref_null(pair_idx);
203
284
    emit.local_set(dst_local);
204
284
    emit.block_start();
205
284
    emit.loop_start();
206
284
    emit.local_get(src_local);
207
284
    emit.ref_is_null();
208
284
    emit.br_if(1);
209
284
    emit.local_get(src_local);
210
284
    emit.struct_get(pair_idx, 0);
211
284
    emit.local_get(dst_local);
212
284
    emit.call(ctx.ids.pair_new);
213
284
    emit.local_set(dst_local);
214
284
    emit.local_get(src_local);
215
284
    emit.struct_get(pair_idx, 1);
216
284
    emit.local_set(src_local);
217
284
    emit.br(0);
218
284
    emit.block_end();
219
284
    emit.block_end();
220
284
}