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}