1
//! `AND` / `OR` short-circuit boolean special forms. Each provides
2
//! three compile paths (effect, stack, runtime short-circuit) plus
3
//! the constant-folding eval path. The `_runtime` helpers handle
4
//! the mixed compile-time-known + runtime-tail case where the prefix
5
//! folds but a later arg is a runtime value.
6

            
7
use wasm_encoder::BlockType;
8

            
9
use crate::ast::{Expr, WasmType};
10
use crate::compiler::context::CompileContext;
11
use crate::compiler::emit::FunctionEmitter;
12
use crate::compiler::expr::{
13
    compile_expr, compile_for_effect, compile_for_stack, compile_nil, eval_value,
14
    serialize_stack_to_output,
15
};
16
use crate::error::{Error, Result};
17
use crate::runtime::SymbolTable;
18

            
19
use super::is_truthy;
20

            
21
/// A runtime `and`/`or` short-circuit chain lowers each operand into an
22
/// `if (result i32)` whose other edge is a 0/1 filler, so every operand value
23
/// must be i32-shaped (`I32` or `Bool`). A ref-typed operand (Ratio / String /
24
/// Pair / …) can't ride that block, and forcing it through would emit a
25
/// malformed module. Reject it with a structured error instead — the strict
26
/// numeric/bool lattice (ADR-0014) does not implicitly truthify ref values.
27
9104
fn require_i32_shaped(op: &str, ty: WasmType) -> Result<()> {
28
9104
    match ty {
29
9036
        WasmType::I32 | WasmType::Bool => Ok(()),
30
68
        other => Err(error_ref_operand(op, other)),
31
    }
32
9104
}
33

            
34
/// Reject a *runtime* operand that isn't an i32-shaped truth value, mirroring
35
/// IF/COND's [`reject_non_boolean_runtime_test`]: nomiscript does NOT do
36
/// implicit truthiness for runtime ref values (a runtime ref may be null, so
37
/// "always truthy" would silently mis-branch). A ref must be tested explicitly
38
/// (e.g. `(null? x)`). Const-foldable values pass through untouched — they
39
/// fold via [`is_truthy`]. Returns the operand's i32-shaped type when it IS a
40
/// valid runtime truth value, so callers can keep routing through the chain.
41
5742
fn classify_runtime_operand(op: &str, value: &Expr) -> Result<Option<WasmType>> {
42
5062
    match value {
43
4926
        Expr::WasmRuntime(t @ (WasmType::I32 | WasmType::Bool))
44
5198
        | Expr::WasmLocal(_, t @ (WasmType::I32 | WasmType::Bool)) => Ok(Some(*t)),
45
272
        Expr::WasmRuntime(ty) | Expr::WasmLocal(_, ty) => Err(error_ref_operand(op, *ty)),
46
272
        _ => Ok(None),
47
    }
48
5742
}
49

            
50
340
fn error_ref_operand(op: &str, ty: WasmType) -> Error {
51
340
    Error::Compile(format!(
52
340
        "{op}: runtime operands must be boolean / i32 truth values, got {ty}; \
53
340
         wrap a ref-typed value in an explicit predicate (e.g. `(null? …)`)"
54
340
    ))
55
340
}
56

            
57
/// Result type of a unary runtime `(and x)` / `(or x)` ≡ `x`. The operand is
58
/// always i32-shaped here — ref operands are rejected upstream by
59
/// [`classify_runtime_operand`] — and is TRUTHIFIED to `Bool`: `and`/`or` are
60
/// truth-value producers, so `(and <count>)` answers "is the count truthy" and
61
/// must serialize as Nil/Bool, not Number (the `v != 0` the Bool serializer
62
/// computes is exactly that test). Codegen and the eval path share this so a
63
/// binder sizing the result agrees with the stack value.
64
408
fn unary_runtime_type(ty: WasmType) -> WasmType {
65
408
    match ty {
66
408
        WasmType::I32 | WasmType::Bool => WasmType::Bool,
67
        other => other,
68
    }
69
408
}
70

            
71
748
pub(super) fn compile_and(
72
748
    ctx: &mut CompileContext,
73
748
    emit: &mut FunctionEmitter,
74
748
    symbols: &mut SymbolTable,
75
748
    args: &[Expr],
76
748
) -> Result<()> {
77
748
    if args.is_empty() {
78
        return compile_expr(ctx, emit, symbols, &Expr::Bool(true));
79
748
    }
80

            
81
748
    let mut last = Expr::Bool(true);
82
884
    for (i, arg) in args.iter().enumerate() {
83
884
        let value = eval_value(symbols, arg)?;
84
884
        if classify_runtime_operand("AND", &value)?.is_some() {
85
            // Emit ONE merged `if (result T)` chain via the stack path and
86
            // serialize the single result. The old effect-only short-circuit
87
            // helper produced NO value on the early-exit branch (a top-level
88
            // `(and …)` / `(or …)` that short-circuited emitted no output
89
            // entity). The stack path yields a value on every path. Same
90
            // remedy as the IF/COND single-serialization fix.
91
544
            let ty = compile_and_for_stack(ctx, emit, symbols, &args[i..])?;
92
476
            return serialize_stack_to_output(ctx, emit, ty);
93
272
        }
94
272
        if !is_truthy(&value) {
95
            return compile_expr(ctx, emit, symbols, &value);
96
272
        }
97
272
        last = value;
98
    }
99
136
    compile_expr(ctx, emit, symbols, &last)
100
748
}
101

            
102
408
pub(super) fn compile_or(
103
408
    ctx: &mut CompileContext,
104
408
    emit: &mut FunctionEmitter,
105
408
    symbols: &mut SymbolTable,
106
408
    args: &[Expr],
107
408
) -> Result<()> {
108
408
    if args.is_empty() {
109
        compile_nil(ctx, emit);
110
        return Ok(());
111
408
    }
112

            
113
408
    let mut last = Expr::Nil;
114
408
    for (i, arg) in args.iter().enumerate() {
115
408
        let value = eval_value(symbols, arg)?;
116
408
        if classify_runtime_operand("OR", &value)?.is_some() {
117
            // See `compile_and`: route through the value-producing stack path
118
            // so the short-circuit branch still yields a serializable result.
119
340
            let ty = compile_or_for_stack(ctx, emit, symbols, &args[i..])?;
120
340
            return serialize_stack_to_output(ctx, emit, ty);
121
        }
122
        if is_truthy(&value) {
123
            return compile_expr(ctx, emit, symbols, &value);
124
        }
125
        last = value;
126
    }
127
    compile_expr(ctx, emit, symbols, &last)
128
408
}
129

            
130
4108
pub(super) fn compile_and_for_stack(
131
4108
    ctx: &mut CompileContext,
132
4108
    emit: &mut FunctionEmitter,
133
4108
    symbols: &mut SymbolTable,
134
4108
    args: &[Expr],
135
4108
) -> Result<WasmType> {
136
4108
    if args.is_empty() {
137
        emit.i32_const(1);
138
        return Ok(WasmType::Bool);
139
4108
    }
140
4108
    if args.len() == 1 {
141
136
        classify_runtime_operand("AND", &eval_value(symbols, &args[0])?)?;
142
136
        let ty = compile_for_stack(ctx, emit, symbols, &args[0])?;
143
136
        return Ok(unary_runtime_type(ty));
144
3972
    }
145
3972
    let first_ty = compile_for_stack(ctx, emit, symbols, &args[0])?;
146
3972
    require_i32_shaped("AND", first_ty)?;
147
4452
    for arg in &args[1..] {
148
4452
        emit.if_block(BlockType::Result(wasm_encoder::ValType::I32));
149
4452
        let arg_ty = compile_for_stack(ctx, emit, symbols, arg)?;
150
4452
        require_i32_shaped("AND", arg_ty)?;
151
4384
        emit.else_block();
152
4384
        emit.i32_const(0);
153
4384
        emit.block_end();
154
    }
155
3904
    Ok(WasmType::Bool)
156
4108
}
157

            
158
476
pub(super) fn compile_or_for_stack(
159
476
    ctx: &mut CompileContext,
160
476
    emit: &mut FunctionEmitter,
161
476
    symbols: &mut SymbolTable,
162
476
    args: &[Expr],
163
476
) -> Result<WasmType> {
164
476
    if args.is_empty() {
165
        emit.i32_const(0);
166
        return Ok(WasmType::Bool);
167
476
    }
168
476
    if args.len() == 1 {
169
136
        classify_runtime_operand("OR", &eval_value(symbols, &args[0])?)?;
170
136
        let ty = compile_for_stack(ctx, emit, symbols, &args[0])?;
171
136
        return Ok(unary_runtime_type(ty));
172
340
    }
173
340
    let first_ty = compile_for_stack(ctx, emit, symbols, &args[0])?;
174
340
    require_i32_shaped("OR", first_ty)?;
175
340
    for arg in &args[1..] {
176
340
        emit.i32_eqz();
177
340
        emit.if_block(BlockType::Result(wasm_encoder::ValType::I32));
178
340
        let arg_ty = compile_for_stack(ctx, emit, symbols, arg)?;
179
340
        require_i32_shaped("OR", arg_ty)?;
180
340
        emit.else_block();
181
340
        emit.i32_const(1);
182
340
        emit.block_end();
183
    }
184
340
    Ok(WasmType::Bool)
185
476
}
186

            
187
68
pub(super) fn compile_and_for_effect(
188
68
    ctx: &mut CompileContext,
189
68
    emit: &mut FunctionEmitter,
190
68
    symbols: &mut SymbolTable,
191
68
    args: &[Expr],
192
68
) -> Result<()> {
193
68
    for (i, arg) in args.iter().enumerate() {
194
68
        let value = eval_value(symbols, arg)?;
195
68
        if classify_runtime_operand("AND", &value)?.is_some() {
196
            // i32-shaped runtime condition: the rest runs only if it's truthy.
197
            compile_for_stack(ctx, emit, symbols, arg)?;
198
            emit.if_block(BlockType::Empty);
199
            for remaining in &args[i + 1..] {
200
                compile_for_effect(ctx, emit, symbols, remaining)?;
201
            }
202
            emit.block_end();
203
            return Ok(());
204
        }
205
        if !is_truthy(&value) {
206
            return Ok(());
207
        }
208
    }
209
    Ok(())
210
68
}
211

            
212
68
pub(super) fn compile_or_for_effect(
213
68
    ctx: &mut CompileContext,
214
68
    emit: &mut FunctionEmitter,
215
68
    symbols: &mut SymbolTable,
216
68
    args: &[Expr],
217
68
) -> Result<()> {
218
68
    for (i, arg) in args.iter().enumerate() {
219
68
        let value = eval_value(symbols, arg)?;
220
68
        if classify_runtime_operand("OR", &value)?.is_some() {
221
            // i32-shaped runtime condition: the rest runs only if it's falsy.
222
            compile_for_stack(ctx, emit, symbols, arg)?;
223
            emit.i32_eqz();
224
            emit.if_block(BlockType::Empty);
225
            for remaining in &args[i + 1..] {
226
                compile_for_effect(ctx, emit, symbols, remaining)?;
227
            }
228
            emit.block_end();
229
            return Ok(());
230
        }
231
        if is_truthy(&value) {
232
            return Ok(());
233
        }
234
    }
235
    Ok(())
236
68
}
237

            
238
3838
pub(super) fn and_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
239
3838
    if args.is_empty() {
240
        return Ok(Expr::Bool(true));
241
3838
    }
242

            
243
3838
    let mut last = Expr::Bool(true);
244
3838
    for (i, arg) in args.iter().enumerate() {
245
3838
        let value = eval_value(symbols, arg)?;
246
3838
        if let Some(t) = classify_runtime_operand("AND", &value)? {
247
3838
            return Ok(Expr::WasmRuntime(runtime_form_type(args.len() - i, t)));
248
        }
249
        if !is_truthy(&value) {
250
            return Ok(value);
251
        }
252
        last = value;
253
    }
254
    Ok(last)
255
3838
}
256

            
257
204
pub(super) fn or_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
258
204
    if args.is_empty() {
259
        return Ok(Expr::Nil);
260
204
    }
261

            
262
204
    let mut last = Expr::Nil;
263
204
    for (i, arg) in args.iter().enumerate() {
264
204
        let value = eval_value(symbols, arg)?;
265
204
        if let Some(t) = classify_runtime_operand("OR", &value)? {
266
204
            return Ok(Expr::WasmRuntime(runtime_form_type(args.len() - i, t)));
267
        }
268
        if is_truthy(&value) {
269
            return Ok(value);
270
        }
271
        last = value;
272
    }
273
    Ok(last)
274
204
}
275

            
276
/// Eval-time mirror of the codegen result type for a runtime `and`/`or`.
277
/// `compile_and`/`compile_or` route the suffix `&args[i..]` through the
278
/// for-stack path, so the type depends on the REMAINING arg count: a lone
279
/// runtime tail (`remaining == 1`) is `unary_runtime_type` (the value ≡ `x`),
280
/// a longer chain always coerces to `Bool` (the `if`-chain emits 0/1 fillers).
281
4042
fn runtime_form_type(remaining: usize, runtime_ty: WasmType) -> WasmType {
282
4042
    match remaining {
283
136
        1 => unary_runtime_type(runtime_ty),
284
3906
        _ => WasmType::Bool,
285
    }
286
4042
}