Skip to main content

seq_runtime/
cond.rs

1//! Conditional combinator for multi-way branching
2//!
3//! Provides `cond` - a concatenative alternative to match/case statements.
4//! Uses quotation pairs (predicate + body) evaluated in order until one matches.
5
6use crate::stack::{Stack, pop};
7use crate::value::Value;
8
9/// Multi-way conditional combinator
10///
11/// # Stack Effect
12///
13/// `( value [pred1] [body1] ... [predN] [bodyN] N -- result )`
14///
15/// # How It Works
16///
17/// 1. Takes a value and N predicate/body quotation pairs from the stack
18/// 2. Tries each predicate in order (first pair = first tried)
19/// 3. When a predicate returns true, executes its body and returns
20/// 4. Panics if no predicate matches (always include a default case)
21///
22/// # Quotation Contracts
23///
24/// - **Predicate**: `( value -- value Bool )` - keeps value on stack, pushes true or false
25/// - **Body**: `( value -- result )` - consumes value, produces result
26///
27/// # Default Case Pattern
28///
29/// Use `[ true ]` as the last predicate to create an "otherwise" case that always matches:
30///
31/// ```text
32/// [ true ] [ drop "default result" ]
33/// ```
34///
35/// # Example: Classify a Number
36///
37/// ```text
38/// : classify ( Int -- String )
39///   [ dup 0 i.< ]  [ drop "negative" ]
40///   [ dup 0 i.= ]  [ drop "zero" ]
41///   [ true ]       [ drop "positive" ]
42///   3 cond
43/// ;
44///
45/// -5 classify   # "negative"
46/// 0 classify    # "zero"
47/// 42 classify   # "positive"
48/// ```
49///
50/// # Example: FizzBuzz Logic
51///
52/// ```text
53/// : fizzbuzz ( Int -- String )
54///   [ dup 15 i.% 0 i.= ]  [ drop "FizzBuzz" ]
55///   [ dup 3 i.% 0 i.= ]   [ drop "Fizz" ]
56///   [ dup 5 i.% 0 i.= ]   [ drop "Buzz" ]
57///   [ true ]              [ int->string ]
58///   4 cond
59/// ;
60/// ```
61///
62/// # Safety
63///
64/// - Stack must have at least (2*N + 1) values (value + N pairs)
65/// - All predicate/body values must be Quotations
66/// - Predicates must return Bool
67#[unsafe(no_mangle)]
68pub unsafe extern "C" fn patch_seq_cond(mut stack: Stack) -> Stack {
69    unsafe {
70        // Pop count
71        let (stack_temp, count_val) = pop(stack);
72        let count = match count_val {
73            Value::Int(n) if n >= 0 => n as usize,
74            Value::Int(n) => panic!("cond: count must be non-negative, got {}", n),
75            _ => panic!("cond: expected Int count, got {:?}", count_val),
76        };
77
78        if count == 0 {
79            panic!("cond: need at least one predicate/body pair");
80        }
81
82        // Pop all predicate/body pairs into a vector
83        // Stack is [ value pred1 body1 pred2 body2 ... predN bodyN ]
84        // We pop from top (bodyN) down to bottom (pred1)
85        let mut pairs = Vec::with_capacity(count);
86        stack = stack_temp;
87
88        for _ in 0..count {
89            // Pop body quotation
90            let (stack_temp, body_val) = pop(stack);
91            let body_wrapper = match body_val {
92                Value::Quotation { wrapper, .. } => wrapper,
93                _ => panic!("cond: expected body Quotation, got {:?}", body_val),
94            };
95
96            // Pop predicate quotation
97            let (stack_temp2, pred_val) = pop(stack_temp);
98            let pred_wrapper = match pred_val {
99                Value::Quotation { wrapper, .. } => wrapper,
100                _ => panic!("cond: expected predicate Quotation, got {:?}", pred_val),
101            };
102
103            stack = stack_temp2;
104            pairs.push((pred_wrapper, body_wrapper));
105        }
106
107        // Now pairs is in reverse order (last pair at index 0)
108        // Reverse it so we try first pair first
109        pairs.reverse();
110
111        // Value is now on top of stack
112        // For each pair, dup value, run predicate, check result
113        for (pred_ptr, body_ptr) in pairs {
114            // Cast function pointers
115            let pred_fn: unsafe extern "C" fn(Stack) -> Stack = std::mem::transmute(pred_ptr);
116            let body_fn: unsafe extern "C" fn(Stack) -> Stack = std::mem::transmute(body_ptr);
117
118            // Execute predicate (keeps value on stack, adds boolean result)
119            stack = pred_fn(stack);
120
121            // Pop predicate result
122            let (stack_after_pred, pred_result) = pop(stack);
123
124            let matches = match pred_result {
125                Value::Bool(b) => b,
126                _ => panic!("cond: predicate must return Bool, got {:?}", pred_result),
127            };
128
129            if matches {
130                // Predicate matched! Execute body and return
131                stack = body_fn(stack_after_pred);
132                return stack;
133            }
134
135            // Predicate didn't match, try next pair
136            stack = stack_after_pred;
137        }
138
139        // No predicate matched!
140        panic!("cond: no predicate matched");
141    }
142}
143
144// Public re-export with short name for internal use
145pub use patch_seq_cond as cond;
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::stack::push;
151
152    // Helper: predicate that always returns true (keeps value, pushes true)
153    // Stack effect: ( value -- value Bool )
154    unsafe extern "C" fn pred_always_true(stack: Stack) -> Stack {
155        unsafe { push(stack, Value::Bool(true)) }
156    }
157
158    // Helper: predicate that always returns false (keeps value, pushes false)
159    // Stack effect: ( value -- value Bool )
160    unsafe extern "C" fn pred_always_false(stack: Stack) -> Stack {
161        unsafe { push(stack, Value::Bool(false)) }
162    }
163
164    // Helper: predicate that checks if top value is zero
165    // Stack effect: ( Int -- Int Bool )
166    unsafe extern "C" fn pred_is_zero(stack: Stack) -> Stack {
167        unsafe {
168            let val = crate::stack::peek(stack);
169            match val {
170                Value::Int(n) => push(stack, Value::Bool(n == 0)),
171                _ => panic!("pred_is_zero: expected Int"),
172            }
173        }
174    }
175
176    // Helper: predicate that checks if value is negative
177    // Stack effect: ( Int -- Int Bool )
178    unsafe extern "C" fn pred_is_negative(stack: Stack) -> Stack {
179        unsafe {
180            let val = crate::stack::peek(stack);
181            match val {
182                Value::Int(n) => push(stack, Value::Bool(n < 0)),
183                _ => panic!("pred_is_negative: expected Int"),
184            }
185        }
186    }
187
188    // Helper: body that drops value and pushes "matched"
189    // Stack effect: ( value -- String )
190    unsafe extern "C" fn body_matched(stack: Stack) -> Stack {
191        unsafe {
192            let (stack, _) = pop(stack);
193            push(
194                stack,
195                Value::String(crate::seqstring::global_string("matched".to_string())),
196            )
197        }
198    }
199
200    // Helper: body that drops value and pushes "zero"
201    // Stack effect: ( value -- String )
202    unsafe extern "C" fn body_zero(stack: Stack) -> Stack {
203        unsafe {
204            let (stack, _) = pop(stack);
205            push(
206                stack,
207                Value::String(crate::seqstring::global_string("zero".to_string())),
208            )
209        }
210    }
211
212    // Helper: body that drops value and pushes "positive"
213    // Stack effect: ( value -- String )
214    unsafe extern "C" fn body_positive(stack: Stack) -> Stack {
215        unsafe {
216            let (stack, _) = pop(stack);
217            push(
218                stack,
219                Value::String(crate::seqstring::global_string("positive".to_string())),
220            )
221        }
222    }
223
224    // Helper: body that drops value and pushes "negative"
225    // Stack effect: ( value -- String )
226    unsafe extern "C" fn body_negative(stack: Stack) -> Stack {
227        unsafe {
228            let (stack, _) = pop(stack);
229            push(
230                stack,
231                Value::String(crate::seqstring::global_string("negative".to_string())),
232            )
233        }
234    }
235
236    // Helper: body that drops value and pushes "default"
237    // Stack effect: ( value -- String )
238    unsafe extern "C" fn body_default(stack: Stack) -> Stack {
239        unsafe {
240            let (stack, _) = pop(stack);
241            push(
242                stack,
243                Value::String(crate::seqstring::global_string("default".to_string())),
244            )
245        }
246    }
247
248    // Helper to create a quotation value from a function pointer
249    fn make_quotation(f: unsafe extern "C" fn(Stack) -> Stack) -> Value {
250        let ptr = f as usize;
251        Value::Quotation {
252            wrapper: ptr,
253            impl_: ptr,
254        }
255    }
256
257    #[test]
258    fn test_cond_single_match() {
259        // Test: single predicate that always matches
260        // Stack: value [pred_always_true] [body_matched] 1
261        unsafe {
262            let stack = crate::stack::alloc_test_stack();
263            let stack = push(stack, Value::Int(42)); // value
264            let stack = push(stack, make_quotation(pred_always_true));
265            let stack = push(stack, make_quotation(body_matched));
266            let stack = push(stack, Value::Int(1)); // count
267
268            let stack = cond(stack);
269
270            let (_, result) = pop(stack);
271            match result {
272                Value::String(s) => assert_eq!(s.as_str(), "matched"),
273                _ => panic!("Expected String, got {:?}", result),
274            }
275        }
276    }
277
278    #[test]
279    fn test_cond_first_match_wins() {
280        // Test: multiple predicates, first one that matches should win
281        // Both predicates would match, but first one should be used
282        // Stack: value [pred_always_true] [body_matched] [pred_always_true] [body_default] 2
283        unsafe {
284            let stack = crate::stack::alloc_test_stack();
285            let stack = push(stack, Value::Int(42)); // value
286            let stack = push(stack, make_quotation(pred_always_true));
287            let stack = push(stack, make_quotation(body_matched)); // first pair
288            let stack = push(stack, make_quotation(pred_always_true));
289            let stack = push(stack, make_quotation(body_default)); // second pair
290            let stack = push(stack, Value::Int(2)); // count
291
292            let stack = cond(stack);
293
294            let (_, result) = pop(stack);
295            match result {
296                Value::String(s) => assert_eq!(s.as_str(), "matched"), // first body wins
297                _ => panic!("Expected String, got {:?}", result),
298            }
299        }
300    }
301
302    #[test]
303    fn test_cond_second_match() {
304        // Test: first predicate fails, second matches
305        // Stack: value [pred_always_false] [body_matched] [pred_always_true] [body_default] 2
306        unsafe {
307            let stack = crate::stack::alloc_test_stack();
308            let stack = push(stack, Value::Int(42)); // value
309            let stack = push(stack, make_quotation(pred_always_false));
310            let stack = push(stack, make_quotation(body_matched)); // first pair - won't match
311            let stack = push(stack, make_quotation(pred_always_true));
312            let stack = push(stack, make_quotation(body_default)); // second pair - will match
313            let stack = push(stack, Value::Int(2)); // count
314
315            let stack = cond(stack);
316
317            let (_, result) = pop(stack);
318            match result {
319                Value::String(s) => assert_eq!(s.as_str(), "default"), // second body wins
320                _ => panic!("Expected String, got {:?}", result),
321            }
322        }
323    }
324
325    #[test]
326    fn test_cond_classify_number() {
327        // Test: classify numbers as negative, zero, or positive
328        // This mimics the example from the docs
329        unsafe {
330            // Test negative number
331            let stack = crate::stack::alloc_test_stack();
332            let stack = push(stack, Value::Int(-5)); // value
333            let stack = push(stack, make_quotation(pred_is_negative));
334            let stack = push(stack, make_quotation(body_negative));
335            let stack = push(stack, make_quotation(pred_is_zero));
336            let stack = push(stack, make_quotation(body_zero));
337            let stack = push(stack, make_quotation(pred_always_true)); // default
338            let stack = push(stack, make_quotation(body_positive));
339            let stack = push(stack, Value::Int(3)); // count
340
341            let stack = cond(stack);
342            let (_, result) = pop(stack);
343            match result {
344                Value::String(s) => assert_eq!(s.as_str(), "negative"),
345                _ => panic!("Expected String"),
346            }
347
348            // Test zero
349            let stack = crate::stack::alloc_test_stack();
350            let stack = push(stack, Value::Int(0)); // value
351            let stack = push(stack, make_quotation(pred_is_negative));
352            let stack = push(stack, make_quotation(body_negative));
353            let stack = push(stack, make_quotation(pred_is_zero));
354            let stack = push(stack, make_quotation(body_zero));
355            let stack = push(stack, make_quotation(pred_always_true)); // default
356            let stack = push(stack, make_quotation(body_positive));
357            let stack = push(stack, Value::Int(3)); // count
358
359            let stack = cond(stack);
360            let (_, result) = pop(stack);
361            match result {
362                Value::String(s) => assert_eq!(s.as_str(), "zero"),
363                _ => panic!("Expected String"),
364            }
365
366            // Test positive
367            let stack = crate::stack::alloc_test_stack();
368            let stack = push(stack, Value::Int(42)); // value
369            let stack = push(stack, make_quotation(pred_is_negative));
370            let stack = push(stack, make_quotation(body_negative));
371            let stack = push(stack, make_quotation(pred_is_zero));
372            let stack = push(stack, make_quotation(body_zero));
373            let stack = push(stack, make_quotation(pred_always_true)); // default
374            let stack = push(stack, make_quotation(body_positive));
375            let stack = push(stack, Value::Int(3)); // count
376
377            let stack = cond(stack);
378            let (_, result) = pop(stack);
379            match result {
380                Value::String(s) => assert_eq!(s.as_str(), "positive"),
381                _ => panic!("Expected String"),
382            }
383        }
384    }
385
386    // Note: #[should_panic] tests don't work with extern "C" functions because
387    // they can't unwind. The following panic conditions are documented in the
388    // function's doc comments and verified by the compiler's type system:
389    //
390    // - "cond: no predicate matched" - when all predicates return false
391    // - "cond: need at least one predicate/body pair" - when count is 0
392    // - "cond: count must be non-negative" - when count is negative
393    // - "cond: expected body Quotation" - when body is not a Quotation
394    // - "cond: expected predicate Quotation" - when predicate is not a Quotation
395    // - "cond: predicate must return Bool" - when predicate returns non-Bool
396}