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
99613
pub(in crate::compiler) fn compile_for_effect(
20
99613
    ctx: &mut CompileContext,
21
99613
    emit: &mut FunctionEmitter,
22
99613
    symbols: &mut SymbolTable,
23
99613
    expr: &Expr,
24
99613
) -> Result<()> {
25
99613
    if let Expr::List(elems) = expr
26
83851
        && let Some(Expr::Symbol(name)) = elems.first()
27
    {
28
83851
        let args = &elems[1..];
29
        // Expand macros (e.g. WHEN → IF) and recurse
30
83851
        if let Some(sym) = symbols.lookup(name)
31
83851
            && sym.kind() == SymbolKind::Macro
32
9514
            && let Some(Expr::Lambda(params, body)) = sym.function().cloned()
33
        {
34
9514
            return expand_macro_then(symbols, &params, &body, args, |symbols, code| {
35
9514
                compile_for_effect(ctx, emit, symbols, &code)
36
9514
            });
37
74337
        }
38
74337
        match name.as_str() {
39
74337
            "DEBUG" => {
40
2911
                return crate::compiler::native::compile_debug_effect(ctx, emit, symbols, args);
41
            }
42
71426
            "PRINT" | "DISPLAY" => {
43
497
                return crate::compiler::native::compile_print_effect(ctx, emit, symbols, args);
44
            }
45
70929
            "NEWLINE" => {
46
                return crate::compiler::native::compile_newline_effect(ctx, emit, symbols, args);
47
            }
48
70929
            "SETF" => {
49
8733
                return compile_setf_for_effect(ctx, emit, symbols, args);
50
            }
51
62196
            "DOLIST" | "DO" | "DO*" | "TAGBODY" | "GO" | "BLOCK" | "RETURN-FROM"
52
55877
            | "HANDLER-CASE" | "UNWIND-PROTECT" => {
53
7384
                return crate::compiler::special::compile_for_effect(
54
7384
                    ctx, emit, symbols, name, args,
55
                );
56
            }
57
54812
            "CREATE-TAG" | "DELETE-ENTITY" => {
58
1846
                return crate::compiler::native::compile(ctx, emit, symbols, name, args);
59
            }
60
52966
            "IF" => {
61
5325
                return compile_if_for_effect(ctx, emit, symbols, args);
62
            }
63
47641
            "BEGIN" => {
64
4757
                for arg in args {
65
4757
                    compile_for_effect(ctx, emit, symbols, arg)?;
66
                }
67
4757
                return Ok(());
68
            }
69
42884
            "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
2130
                return crate::compiler::special::compile_for_effect(
78
2130
                    ctx, emit, symbols, name, args,
79
                );
80
            }
81
40754
            "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
1065
                return crate::compiler::special::compile_for_effect(
90
1065
                    ctx, emit, symbols, name, args,
91
                );
92
            }
93
            _ => {
94
39689
                if let Some(sym) = symbols.lookup(name)
95
39689
                    && let Some(Expr::Lambda(params, body)) = sym.function().cloned()
96
                {
97
639
                    let val = eval_value(symbols, expr)?;
98
639
                    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
71
                        if let Some(_ty) = try_compile_runtime_call(
106
71
                            ctx, emit, symbols, name, &params, &body, args,
107
                        )? {
108
71
                            emit.drop_value();
109
71
                            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
568
                    }
119
39050
                }
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
39618
        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
2911
            if ctx.lookup_host_fn(name).is_some() {
136
426
                return crate::compiler::native::compile(ctx, emit, symbols, name, args);
137
2485
            }
138
2485
            compile_for_stack(ctx, emit, symbols, expr)?;
139
2485
            emit.drop_value();
140
2485
            return Ok(());
141
36707
        }
142
15762
    }
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
52469
    eval_value(symbols, expr)?;
147
52469
    Ok(())
148
99613
}
149

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

            
194
8733
fn compile_setf_for_effect(
195
8733
    ctx: &mut CompileContext,
196
8733
    emit: &mut FunctionEmitter,
197
8733
    symbols: &mut SymbolTable,
198
8733
    args: &[Expr],
199
8733
) -> Result<()> {
200
8733
    if !args.len().is_multiple_of(2) {
201
        return Err(Error::Compile(
202
            "SETF requires an even number of arguments".to_string(),
203
        ));
204
8733
    }
205
8733
    for pair in args.chunks(2) {
206
8733
        let place = &pair[0];
207
8733
        let value_expr = &pair[1];
208
8733
        if let Expr::Symbol(name) = place {
209
8733
            let wasm_local = symbols
210
8733
                .lookup(name)
211
8733
                .and_then(|s| s.value())
212
8733
                .and_then(|v| match v {
213
8733
                    Expr::WasmLocal(idx, ty) => Some((*idx, *ty)),
214
                    _ => None,
215
8733
                });
216
8733
            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
8733
                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
8733
                if matches!(ty, WasmType::Closure(_)) {
227
142
                    ctx.forget_closure_body(idx);
228
8591
                }
229
8733
                emit.local_set(idx);
230
8733
                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
8733
    Ok(())
246
8733
}