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
9443
fn require_i32_shaped(op: &str, ty: WasmType) -> Result<()> {
28
9443
    match ty {
29
9372
        WasmType::I32 | WasmType::Bool => Ok(()),
30
71
        other => Err(error_ref_operand(op, other)),
31
    }
32
9443
}
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
5964
fn classify_runtime_operand(op: &str, value: &Expr) -> Result<Option<WasmType>> {
42
5254
    match value {
43
5112
        Expr::WasmRuntime(t @ (WasmType::I32 | WasmType::Bool))
44
5396
        | Expr::WasmLocal(_, t @ (WasmType::I32 | WasmType::Bool)) => Ok(Some(*t)),
45
284
        Expr::WasmRuntime(ty) | Expr::WasmLocal(_, ty) => Err(error_ref_operand(op, *ty)),
46
284
        _ => Ok(None),
47
    }
48
5964
}
49

            
50
355
fn error_ref_operand(op: &str, ty: WasmType) -> Error {
51
355
    Error::Compile(format!(
52
355
        "{op}: runtime operands must be boolean / i32 truth values, got {ty}; \
53
355
         wrap a ref-typed value in an explicit predicate (e.g. `(null? …)`)"
54
355
    ))
55
355
}
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
426
fn unary_runtime_type(ty: WasmType) -> WasmType {
65
426
    match ty {
66
426
        WasmType::I32 | WasmType::Bool => WasmType::Bool,
67
        other => other,
68
    }
69
426
}
70

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

            
81
781
    let mut last = Expr::Bool(true);
82
923
    for (i, arg) in args.iter().enumerate() {
83
923
        let value = eval_value(symbols, arg)?;
84
923
        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
568
            let ty = compile_and_for_stack(ctx, emit, symbols, &args[i..])?;
92
497
            return serialize_stack_to_output(ctx, emit, ty);
93
284
        }
94
284
        if !is_truthy(&value) {
95
            return compile_expr(ctx, emit, symbols, &value);
96
284
        }
97
284
        last = value;
98
    }
99
142
    compile_expr(ctx, emit, symbols, &last)
100
781
}
101

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

            
113
426
    let mut last = Expr::Nil;
114
426
    for (i, arg) in args.iter().enumerate() {
115
426
        let value = eval_value(symbols, arg)?;
116
426
        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
355
            let ty = compile_or_for_stack(ctx, emit, symbols, &args[i..])?;
120
355
            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
426
}
129

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

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

            
187
71
pub(super) fn compile_and_for_effect(
188
71
    ctx: &mut CompileContext,
189
71
    emit: &mut FunctionEmitter,
190
71
    symbols: &mut SymbolTable,
191
71
    args: &[Expr],
192
71
) -> Result<()> {
193
71
    for (i, arg) in args.iter().enumerate() {
194
71
        let value = eval_value(symbols, arg)?;
195
71
        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
71
}
211

            
212
71
pub(super) fn compile_or_for_effect(
213
71
    ctx: &mut CompileContext,
214
71
    emit: &mut FunctionEmitter,
215
71
    symbols: &mut SymbolTable,
216
71
    args: &[Expr],
217
71
) -> Result<()> {
218
71
    for (i, arg) in args.iter().enumerate() {
219
71
        let value = eval_value(symbols, arg)?;
220
71
        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
71
}
237

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

            
243
3976
    let mut last = Expr::Bool(true);
244
3976
    for (i, arg) in args.iter().enumerate() {
245
3976
        let value = eval_value(symbols, arg)?;
246
3976
        if let Some(t) = classify_runtime_operand("AND", &value)? {
247
3976
            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
3976
}
256

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

            
262
213
    let mut last = Expr::Nil;
263
213
    for (i, arg) in args.iter().enumerate() {
264
213
        let value = eval_value(symbols, arg)?;
265
213
        if let Some(t) = classify_runtime_operand("OR", &value)? {
266
213
            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
213
}
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
4189
fn runtime_form_type(remaining: usize, runtime_ty: WasmType) -> WasmType {
282
4189
    match remaining {
283
142
        1 => unary_runtime_type(runtime_ty),
284
4047
        _ => WasmType::Bool,
285
    }
286
4189
}