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
95624
pub(in crate::compiler) fn compile_for_effect(
20
95624
    ctx: &mut CompileContext,
21
95624
    emit: &mut FunctionEmitter,
22
95624
    symbols: &mut SymbolTable,
23
95624
    expr: &Expr,
24
95624
) -> Result<()> {
25
95624
    if let Expr::List(elems) = expr
26
80478
        && let Some(Expr::Symbol(name)) = elems.first()
27
    {
28
80478
        let args = &elems[1..];
29
        // Expand macros (e.g. WHEN → IF) and recurse
30
80478
        if let Some(sym) = symbols.lookup(name)
31
80478
            && sym.kind() == SymbolKind::Macro
32
9142
            && let Some(Expr::Lambda(params, body)) = sym.function().cloned()
33
        {
34
9142
            return expand_macro_then(symbols, &params, &body, args, |symbols, code| {
35
9142
                compile_for_effect(ctx, emit, symbols, &code)
36
9142
            });
37
71336
        }
38
71336
        match name.as_str() {
39
71336
            "DEBUG" => {
40
2788
                return crate::compiler::native::compile_debug_effect(ctx, emit, symbols, args);
41
            }
42
68548
            "PRINT" | "DISPLAY" => {
43
476
                return crate::compiler::native::compile_print_effect(ctx, emit, symbols, args);
44
            }
45
68072
            "NEWLINE" => {
46
                return crate::compiler::native::compile_newline_effect(ctx, emit, symbols, args);
47
            }
48
68072
            "SETF" => {
49
8388
                return compile_setf_for_effect(ctx, emit, symbols, args);
50
            }
51
59684
            "DOLIST" | "DO" | "DO*" | "TAGBODY" | "GO" | "BLOCK" | "RETURN-FROM"
52
53620
            | "HANDLER-CASE" | "UNWIND-PROTECT" => {
53
7084
                return crate::compiler::special::compile_for_effect(
54
7084
                    ctx, emit, symbols, name, args,
55
                );
56
            }
57
52600
            "CREATE-TAG" | "DELETE-ENTITY" => {
58
1778
                return crate::compiler::native::compile(ctx, emit, symbols, name, args);
59
            }
60
50822
            "IF" => {
61
5130
                return compile_if_for_effect(ctx, emit, symbols, args);
62
            }
63
45692
            "BEGIN" => {
64
4586
                for arg in args {
65
4586
                    compile_for_effect(ctx, emit, symbols, arg)?;
66
                }
67
4586
                return Ok(());
68
            }
69
41106
            "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
2048
                return crate::compiler::special::compile_for_effect(
78
2048
                    ctx, emit, symbols, name, args,
79
                );
80
            }
81
39058
            "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
1020
                return crate::compiler::special::compile_for_effect(
90
1020
                    ctx, emit, symbols, name, args,
91
                );
92
            }
93
            _ => {
94
38038
                if let Some(sym) = symbols.lookup(name)
95
38038
                    && let Some(Expr::Lambda(params, body)) = sym.function().cloned()
96
                {
97
616
                    let val = eval_value(symbols, expr)?;
98
616
                    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
68
                        if let Some(_ty) = try_compile_runtime_call(
106
68
                            ctx, emit, symbols, name, &params, &body, args,
107
                        )? {
108
68
                            emit.drop_value();
109
68
                            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
548
                    }
119
37422
                }
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
37970
        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
2792
            if ctx.lookup_host_fn(name).is_some() {
136
408
                return crate::compiler::native::compile(ctx, emit, symbols, name, args);
137
2384
            }
138
2384
            compile_for_stack(ctx, emit, symbols, expr)?;
139
2384
            emit.drop_value();
140
2384
            return Ok(());
141
35178
        }
142
15146
    }
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
50324
    eval_value(symbols, expr)?;
147
50324
    Ok(())
148
95624
}
149

            
150
5130
fn compile_if_for_effect(
151
5130
    ctx: &mut CompileContext,
152
5130
    emit: &mut FunctionEmitter,
153
5130
    symbols: &mut SymbolTable,
154
5130
    args: &[Expr],
155
5130
) -> Result<()> {
156
5130
    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
5130
    }
161
    // Classify on a CLONE — the test's compile-time effects reach the live
162
    // table only via the single emit below.
163
5130
    let test = eval_value(&mut symbols.clone(), &args[0])?;
164
5130
    let test_diverges =
165
5130
        crate::compiler::special::form_diverges_for_test(&mut symbols.clone(), &args[0])?;
166
5130
    crate::compiler::special::reject_non_boolean_runtime_test(&test, test_diverges)?;
167
5130
    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
68
        return compile_for_effect(ctx, emit, symbols, &args[0]);
171
5062
    }
172
5062
    if crate::compiler::special::is_runtime_test(&test) {
173
4926
        compile_for_stack(ctx, emit, symbols, &args[0])?;
174
4926
        emit.if_block(wasm_encoder::BlockType::Empty);
175
4926
        compile_for_effect(ctx, emit, symbols, &args[1])?;
176
4926
        if args.len() == 3 {
177
4858
            emit.else_block();
178
4858
            compile_for_effect(ctx, emit, symbols, &args[2])?;
179
68
        }
180
4926
        emit.block_end();
181
4926
        return Ok(());
182
136
    }
183
    // Const test: apply its effects to the live table once, then the live arm.
184
136
    let test = eval_value(symbols, &args[0])?;
185
136
    if crate::compiler::special::is_truthy(&test) {
186
68
        compile_for_effect(ctx, emit, symbols, &args[1])
187
68
    } else if args.len() == 3 {
188
68
        compile_for_effect(ctx, emit, symbols, &args[2])
189
    } else {
190
        Ok(())
191
    }
192
5130
}
193

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