1
//! Effect-position codegen.
2
//!
3
//! `compile_for_effect` is the entry point — it inspects the leading
4
//! form and either dispatches into the specialised SETF / IF / BEGIN
5
//! handlers below, hands off to a special form's effect path, or
6
//! falls back to value-position emit when the form has a runtime
7
//! result that must be consumed.
8

            
9
use crate::ast::{Expr, WasmType};
10
use crate::compiler::context::CompileContext;
11
use crate::compiler::emit::FunctionEmitter;
12
use crate::error::{Error, Result};
13
use crate::runtime::{SymbolKind, SymbolTable};
14

            
15
use super::call::{compile_and_bind_lambda_params, try_compile_runtime_call};
16
use super::eval::{eval_value, expand_macro_then};
17
use super::stack::{compile_for_stack, compile_for_stack_as};
18

            
19
119271
pub(in crate::compiler) fn compile_for_effect(
20
119271
    ctx: &mut CompileContext,
21
119271
    emit: &mut FunctionEmitter,
22
119271
    symbols: &mut SymbolTable,
23
119271
    expr: &Expr,
24
119271
) -> Result<()> {
25
119271
    if let Expr::List(elems) = expr
26
100679
        && let Some(Expr::Symbol(name)) = elems.first()
27
    {
28
100679
        let args = &elems[1..];
29
        // Expand macros (e.g. WHEN → IF) and recurse
30
100679
        if let Some(sym) = symbols.lookup(name)
31
100679
            && sym.kind() == SymbolKind::Macro
32
11205
            && let Some(Expr::Lambda(params, body)) = sym.function().cloned()
33
        {
34
11205
            return expand_macro_then(symbols, &params, &body, args, |symbols, code| {
35
11205
                compile_for_effect(ctx, emit, symbols, &code)
36
11205
            });
37
89474
        }
38
89474
        match name.as_str() {
39
89474
            "DEBUG" => {
40
3403
                return crate::compiler::native::compile_debug_effect(ctx, emit, symbols, args);
41
            }
42
86071
            "PRINT" | "DISPLAY" => {
43
581
                return crate::compiler::native::compile_print_effect(ctx, emit, symbols, args);
44
            }
45
85490
            "NEWLINE" => {
46
                return crate::compiler::native::compile_newline_effect(ctx, emit, symbols, args);
47
            }
48
85490
            "SETF" => {
49
10707
                return compile_setf_for_effect(ctx, emit, symbols, args);
50
            }
51
74783
            "DOLIST" | "DO" | "DO*" | "TAGBODY" | "GO" | "BLOCK" | "RETURN-FROM"
52
67147
            | "HANDLER-CASE" | "UNWIND-PROTECT" => {
53
8964
                return crate::compiler::special::compile_for_effect(
54
8964
                    ctx, emit, symbols, name, args,
55
                );
56
            }
57
65819
            "CREATE-TAG" | "DELETE-ENTITY" => {
58
2158
                return crate::compiler::native::compile(ctx, emit, symbols, name, args);
59
            }
60
63661
            "IF" => {
61
6308
                return compile_if_for_effect(ctx, emit, symbols, args);
62
            }
63
57353
            "BEGIN" => {
64
6059
                for arg in args {
65
6059
                    compile_for_effect(ctx, emit, symbols, arg)?;
66
                }
67
6059
                return Ok(());
68
            }
69
51294
            "LET" | "LET*" | "COND" | "AND" | "OR" => {
70
                // Always compile for effect — never skip on an eval-fold to a
71
                // const. `eval_value` is the const-fold surface and does NOT
72
                // model side effects (`create-tag`, `debug`, host fns), so an
73
                // effectful body (or a runtime condition eval mis-resolves to a
74
                // const) would fold to `nil` and get silently dropped. Compiling
75
                // for effect emits the body's effects; a genuinely pure const
76
                // body lowers to a harmless no-op (same as `BEGIN`).
77
2490
                return crate::compiler::special::compile_for_effect(
78
2490
                    ctx, emit, symbols, name, args,
79
                );
80
            }
81
48804
            "DEFVAR" | "DEFPARAMETER" => {
82
                // Route definition forms through their compile-side
83
                // wrappers so the runtime-init promotion (allocating a
84
                // wasm local + emitting the init's wasm into it) fires.
85
                // Without this, the eval-only path stores the
86
                // `Expr::WasmRuntime(_)` placeholder on the symbol and
87
                // later uses resolve to a value that was never put on
88
                // the stack — emitted wasm fails validation.
89
1245
                return crate::compiler::special::compile_for_effect(
90
1245
                    ctx, emit, symbols, name, args,
91
                );
92
            }
93
            _ => {
94
47559
                if let Some(sym) = symbols.lookup(name)
95
47559
                    && let Some(Expr::Lambda(params, body)) = sym.function().cloned()
96
                {
97
747
                    let val = eval_value(symbols, expr)?;
98
747
                    if val.is_wasm_runtime() {
99
                        // A recursive runtime-arg call routes to the monomorph
100
                        // helper (its result is computed then dropped here);
101
                        // otherwise inline the body, depth-guarded so a
102
                        // non-terminating recursion is a structured error, not
103
                        // a native compiler-stack overflow — same contract as
104
                        // the value/stack call paths.
105
83
                        if let Some(_ty) = try_compile_runtime_call(
106
83
                            ctx, emit, symbols, name, &params, &body, args,
107
                        )? {
108
83
                            emit.drop_value();
109
83
                            return Ok(());
110
                        }
111
                        let mut local =
112
                            compile_and_bind_lambda_params(ctx, emit, symbols, &params, args)?;
113
                        ctx.push_inlining_frame(name)?;
114
                        let result = compile_for_effect(ctx, emit, &mut local, &body);
115
                        ctx.pop_inlining_frame(name);
116
                        result?;
117
                        return Ok(());
118
664
                    }
119
46812
                }
120
            }
121
        }
122
        // A value-producing native / host-fn call (LIST, CONS, +, an accessor,
123
        // …) whose result is discarded in effect position: COMPILE it and drop
124
        // the value, so effects in its ARGUMENTS (a nested `setf`, a promoted
125
        // accumulator update, …) actually emit. `eval_value` alone const-folds
126
        // without emitting — that silently dropped those arg effects. Definition
127
        // / non-value special forms (DEFUN, DEFSTRUCT, QUOTE, …) are NOT
128
        // compilable for stack and stay on the eval path below.
129
47476
        if !crate::compiler::special::is_special_form(name) {
130
            // A host fn — including a VOID one (`result: None`, e.g. `rpc-log`) —
131
            // routes through its effect compiler, which emits the import call and
132
            // drops any result. `compile_for_stack` would reject a void host fn
133
            // ("no return type"). A non-host value-producing native is compiled
134
            // and dropped.
135
3984
            if ctx.lookup_host_fn(name).is_some() {
136
498
                return crate::compiler::native::compile(ctx, emit, symbols, name, args);
137
3486
            }
138
3486
            compile_for_stack(ctx, emit, symbols, expr)?;
139
3486
            emit.drop_value();
140
3486
            return Ok(());
141
43492
        }
142
18592
    }
143
    // A bare atom, or a definition / non-value special form, in effect position:
144
    // const-fold it (no stack value to emit). Definition forms register into the
145
    // symbol table here.
146
62084
    eval_value(symbols, expr)?;
147
62084
    Ok(())
148
119271
}
149

            
150
6308
fn compile_if_for_effect(
151
6308
    ctx: &mut CompileContext,
152
6308
    emit: &mut FunctionEmitter,
153
6308
    symbols: &mut SymbolTable,
154
6308
    args: &[Expr],
155
6308
) -> Result<()> {
156
6308
    if args.len() < 2 || args.len() > 3 {
157
        return Err(Error::Compile(
158
            "IF requires a test, a then-form, and an optional else-form".to_string(),
159
        ));
160
6308
    }
161
    // Classify on a CLONE — the test's compile-time effects reach the live
162
    // table only via the single emit below.
163
6308
    let test = eval_value(&mut symbols.clone(), &args[0])?;
164
6308
    let test_diverges =
165
6308
        crate::compiler::special::form_diverges_for_test(&mut symbols.clone(), &args[0])?;
166
6308
    crate::compiler::special::reject_non_boolean_runtime_test(&test, test_diverges)?;
167
6308
    if test_diverges {
168
        // The test transfers control before producing a condition; compile it
169
        // for effect so the exit fires. Both branches are dead.
170
83
        return compile_for_effect(ctx, emit, symbols, &args[0]);
171
6225
    }
172
6225
    if crate::compiler::special::is_runtime_test(&test) {
173
6059
        compile_for_stack(ctx, emit, symbols, &args[0])?;
174
6059
        emit.if_block(wasm_encoder::BlockType::Empty);
175
6059
        compile_for_effect(ctx, emit, symbols, &args[1])?;
176
6059
        if args.len() == 3 {
177
5976
            emit.else_block();
178
5976
            compile_for_effect(ctx, emit, symbols, &args[2])?;
179
83
        }
180
6059
        emit.block_end();
181
6059
        return Ok(());
182
166
    }
183
166
    let test = crate::compiler::special::apply_const_test(ctx, emit, symbols, &args[0], &test)?;
184
166
    if !test.is_nil() {
185
83
        compile_for_effect(ctx, emit, symbols, &args[1])
186
83
    } else if args.len() == 3 {
187
83
        compile_for_effect(ctx, emit, symbols, &args[2])
188
    } else {
189
        Ok(())
190
    }
191
6308
}
192

            
193
10707
fn compile_setf_for_effect(
194
10707
    ctx: &mut CompileContext,
195
10707
    emit: &mut FunctionEmitter,
196
10707
    symbols: &mut SymbolTable,
197
10707
    args: &[Expr],
198
10707
) -> Result<()> {
199
10707
    if !args.len().is_multiple_of(2) {
200
        return Err(Error::Compile(
201
            "SETF requires an even number of arguments".to_string(),
202
        ));
203
10707
    }
204
10790
    for pair in args.chunks(2) {
205
10790
        let place = &pair[0];
206
10790
        let value_expr = &pair[1];
207
10790
        if let Expr::Symbol(name) = place {
208
10790
            let wasm_local = symbols
209
10790
                .lookup(name)
210
10790
                .and_then(|s| s.value())
211
10790
                .and_then(|v| match v {
212
10624
                    Expr::WasmLocal(idx, ty) => Some((*idx, *ty)),
213
166
                    _ => None,
214
10790
                });
215
10790
            if let Some((idx, ty)) = wasm_local {
216
                // Coerce the rhs to the local's declared type (literal Index↔Scalar
217
                // crossing, nil → typed default); a real clash is a clean compile
218
                // error, not an invalid `local.set`.
219
10624
                compile_for_stack_as(ctx, emit, symbols, value_expr, ty)?;
220
                // Reassigning a closure local invalidates the body recorded for it
221
                // at bind time — the local now holds a different closure, so a later
222
                // FOLD inlining the stale body would call the wrong fn. Forget AFTER
223
                // compiling the rhs (which may legitimately FOLD the OLD closure)
224
                // and before the store.
225
10624
                if matches!(ty, WasmType::Closure(_)) {
226
166
                    ctx.forget_closure_body(idx);
227
10458
                }
228
10624
                emit.local_set(idx);
229
10624
                continue;
230
166
            }
231
        }
232
        // const / struct-field place. Emit any nested runtime-local `setf` in the
233
        // value expr for effect (the eval-rebind path is a no-op for a WasmLocal
234
        // and would drop the store); take the place's value from a clone so the
235
        // live table isn't double-mutated.
236
166
        let value = if crate::compiler::special::rhs_has_runtime_store(value_expr, symbols) {
237
83
            compile_for_effect(ctx, emit, symbols, value_expr)?;
238
83
            eval_value(&mut symbols.clone(), value_expr)?
239
        } else {
240
83
            eval_value(symbols, value_expr)?
241
        };
242
166
        crate::compiler::special::setf_set_place(symbols, place, value)?;
243
    }
244
10707
    Ok(())
245
10707
}