seq_runtime/
quotations.rs

1//! Quotation operations for Seq
2//!
3//! Quotations are deferred code blocks (first-class functions).
4//! A quotation is represented as a function pointer stored as usize.
5
6use crate::stack::{Stack, pop, push};
7use crate::value::Value;
8use std::collections::HashMap;
9use std::sync::{LazyLock, Mutex};
10
11/// Type alias for closure registry entries
12/// Uses Box (not Arc) because cross-thread transfer needs owned data
13/// and cloning ensures arena strings become global strings
14type ClosureEntry = (usize, Box<[Value]>);
15
16/// Global registry for closure environments in spawned strands
17/// Maps closure_spawn_id -> (fn_ptr, env)
18/// Cleaned up when the trampoline retrieves and executes the closure
19static SPAWN_CLOSURE_REGISTRY: LazyLock<Mutex<HashMap<i64, ClosureEntry>>> =
20    LazyLock::new(|| Mutex::new(HashMap::new()));
21
22/// RAII guard for cleanup of spawn registry on failure
23///
24/// If the spawned strand fails to start or panics before retrieving
25/// the closure from the registry, this guard ensures the environment
26/// is cleaned up and not leaked.
27struct SpawnRegistryGuard {
28    closure_spawn_id: i64,
29    should_cleanup: bool,
30}
31
32impl SpawnRegistryGuard {
33    fn new(closure_spawn_id: i64) -> Self {
34        Self {
35            closure_spawn_id,
36            should_cleanup: true,
37        }
38    }
39
40    /// Disarm the guard - strand successfully started and will retrieve the closure
41    fn disarm(&mut self) {
42        self.should_cleanup = false;
43    }
44}
45
46impl Drop for SpawnRegistryGuard {
47    fn drop(&mut self) {
48        if self.should_cleanup {
49            let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
50            if let Some((_, env)) = registry.remove(&self.closure_spawn_id) {
51                // env (Box<[Value]>) will be dropped here, freeing memory
52                drop(env);
53            }
54        }
55    }
56}
57
58/// Trampoline function for spawning closures
59///
60/// This function is passed to strand_spawn when spawning a closure.
61/// It expects the closure_spawn_id on the stack, retrieves the closure data
62/// from the registry, and calls the closure function with the environment.
63///
64/// Stack effect: ( closure_spawn_id -- ... )
65/// The closure function determines the final stack state.
66///
67/// # Safety
68/// This function is safe to call, but internally uses unsafe operations
69/// to transmute function pointers and call the closure function.
70extern "C" fn closure_spawn_trampoline(stack: Stack) -> Stack {
71    unsafe {
72        // Pop closure_spawn_id from stack
73        let (stack, closure_spawn_id_val) = pop(stack);
74        let closure_spawn_id = match closure_spawn_id_val {
75            Value::Int(id) => id,
76            _ => panic!(
77                "closure_spawn_trampoline: expected Int (closure_spawn_id), got {:?}",
78                closure_spawn_id_val
79            ),
80        };
81
82        // Retrieve closure data from registry
83        let (fn_ptr, env) = {
84            let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
85            registry.remove(&closure_spawn_id).unwrap_or_else(|| {
86                panic!(
87                    "closure_spawn_trampoline: no data for closure_spawn_id {}",
88                    closure_spawn_id
89                )
90            })
91        };
92
93        // Call closure function with empty stack and environment
94        // Closure signature: fn(Stack, *const Value, usize) -> Stack
95        let env_ptr = env.as_ptr();
96        let env_len = env.len();
97
98        let fn_ref: unsafe extern "C" fn(Stack, *const Value, usize) -> Stack =
99            std::mem::transmute(fn_ptr);
100
101        // Call closure and return result (Arc ref count decremented after return)
102        fn_ref(stack, env_ptr, env_len)
103    }
104}
105
106/// Push a quotation onto the stack with both wrapper and impl pointers
107///
108/// Stack effect: ( -- quot )
109///
110/// # Arguments
111/// - `wrapper`: C-convention function pointer for runtime calls
112/// - `impl_`: tailcc function pointer for TCO tail calls
113///
114/// # Safety
115/// - Stack pointer must be valid (or null for empty stack)
116/// - Both function pointers must be valid (compiler guarantees this)
117#[unsafe(no_mangle)]
118pub unsafe extern "C" fn patch_seq_push_quotation(
119    stack: Stack,
120    wrapper: usize,
121    impl_: usize,
122) -> Stack {
123    // Debug-only validation - compiler guarantees non-null pointers
124    // Using debug_assert to avoid UB from panicking across FFI boundary
125    debug_assert!(
126        wrapper != 0,
127        "push_quotation: wrapper function pointer is null"
128    );
129    debug_assert!(impl_ != 0, "push_quotation: impl function pointer is null");
130    unsafe { push(stack, Value::Quotation { wrapper, impl_ }) }
131}
132
133/// Check if the top of stack is a quotation (not a closure)
134///
135/// Used by the compiler for tail call optimization of `call`.
136/// Returns 1 if the top value is a Quotation, 0 otherwise.
137///
138/// Stack effect: ( quot -- quot ) [non-consuming peek]
139///
140/// # Safety
141/// - Stack must not be null
142#[unsafe(no_mangle)]
143pub unsafe extern "C" fn patch_seq_peek_is_quotation(stack: Stack) -> i64 {
144    use crate::stack::peek;
145    unsafe {
146        let value = peek(stack);
147        match value {
148            Value::Quotation { .. } => 1,
149            _ => 0,
150        }
151    }
152}
153
154/// Get the impl_ function pointer from a quotation on top of stack
155///
156/// Used by the compiler for tail call optimization of `call`.
157/// Returns the tailcc impl_ pointer for musttail calls from compiled code.
158/// Caller must ensure the top value is a Quotation (use peek_is_quotation first).
159///
160/// Stack effect: ( quot -- quot ) [non-consuming peek]
161///
162/// # Safety
163/// - Stack must not be null
164/// - Top of stack must be a Quotation (panics otherwise)
165#[unsafe(no_mangle)]
166pub unsafe extern "C" fn patch_seq_peek_quotation_fn_ptr(stack: Stack) -> usize {
167    use crate::stack::peek;
168    unsafe {
169        let value = peek(stack);
170        match value {
171            Value::Quotation { impl_, .. } => {
172                // Debug-only validation - compiler guarantees non-null pointers
173                debug_assert!(
174                    impl_ != 0,
175                    "peek_quotation_fn_ptr: impl function pointer is null"
176                );
177                impl_
178            }
179            // This branch indicates a compiler bug - patch_seq_peek_is_quotation should
180            // have been called first to verify the value type. In release builds,
181            // returning 0 will cause a crash at the call site rather than here.
182            _ => {
183                debug_assert!(
184                    false,
185                    "peek_quotation_fn_ptr: expected Quotation, got {:?}",
186                    value
187                );
188                0
189            }
190        }
191    }
192}
193
194/// Call a quotation or closure
195///
196/// Pops a quotation or closure from the stack and executes it.
197/// For stateless quotations, calls the function with just the stack.
198/// For closures, calls the function with both the stack and captured environment.
199/// The function takes the current stack and returns a new stack.
200///
201/// Stack effect: ( ..a quot -- ..b )
202/// where the quotation has effect ( ..a -- ..b )
203///
204/// # TCO Considerations
205///
206/// With Arc-based closure environments, this function is tail-position friendly:
207/// no cleanup is needed after the call returns (Arc ref-counting handles it).
208///
209/// However, full `musttail` TCO across quotations and closures is limited by
210/// calling convention mismatches:
211/// - Quotations use `tailcc` with signature: `fn(Stack) -> Stack`
212/// - Closures use C convention with signature: `fn(Stack, *const Value, usize) -> Stack`
213///
214/// LLVM's `musttail` requires matching signatures, so the compiler can only
215/// guarantee TCO within the same category (quotation-to-quotation or closure-to-closure).
216/// Cross-category calls go through this function, which is still efficient but
217/// doesn't use `musttail`.
218///
219/// # Safety
220/// - Stack must not be null
221/// - Top of stack must be a Quotation or Closure value
222/// - Function pointer must be valid
223/// - Quotation signature: Stack -> Stack
224/// - Closure signature: Stack, *const [Value] -> Stack
225#[unsafe(no_mangle)]
226pub unsafe extern "C" fn patch_seq_call(stack: Stack) -> Stack {
227    unsafe {
228        let (stack, value) = pop(stack);
229
230        match value {
231            Value::Quotation { wrapper, .. } => {
232                // Validate function pointer is not null
233                if wrapper == 0 {
234                    panic!("call: quotation wrapper function pointer is null");
235                }
236
237                // SAFETY: wrapper was created by the compiler's codegen and stored via push_quotation.
238                // The compiler guarantees that quotation wrapper functions use C calling convention
239                // with the signature: unsafe extern "C" fn(Stack) -> Stack.
240                // We've verified wrapper is non-null above.
241                let fn_ref: unsafe extern "C" fn(Stack) -> Stack = std::mem::transmute(wrapper);
242                fn_ref(stack)
243            }
244            Value::Closure { fn_ptr, env } => {
245                // Validate function pointer is not null
246                if fn_ptr == 0 {
247                    panic!("call: closure function pointer is null");
248                }
249
250                // Get environment data pointer and length from Arc
251                // Arc enables TCO: no explicit cleanup needed, ref-count handles it
252                let env_data = env.as_ptr();
253                let env_len = env.len();
254
255                // SAFETY: fn_ptr was created by the compiler's codegen for a closure.
256                // The compiler guarantees that closure functions have the signature:
257                // unsafe extern "C" fn(Stack, *const Value, usize) -> Stack.
258                // We pass the environment as (data, len) since LLVM can't handle fat pointers.
259                // The Arc keeps the environment alive during the call and is dropped after.
260                let fn_ref: unsafe extern "C" fn(Stack, *const Value, usize) -> Stack =
261                    std::mem::transmute(fn_ptr);
262                fn_ref(stack, env_data, env_len)
263            }
264            _ => panic!(
265                "call: expected Quotation or Closure on stack, got {:?}",
266                value
267            ),
268        }
269    }
270}
271
272/// Execute a quotation n times
273///
274/// Pops a count (Int) and a quotation from the stack, then executes
275/// the quotation that many times.
276///
277/// Stack effect: ( ..a quot n -- ..a )
278/// where the quotation has effect ( ..a -- ..a )
279///
280/// # Safety
281/// - Stack must have at least 2 values
282/// - Top must be Int (the count)
283/// - Second must be Quotation
284/// - Quotation's effect must preserve stack shape
285#[unsafe(no_mangle)]
286pub unsafe extern "C" fn patch_seq_times(mut stack: Stack) -> Stack {
287    unsafe {
288        // Pop count
289        let (stack_temp, count_value) = pop(stack);
290        let count = match count_value {
291            Value::Int(n) => n,
292            _ => panic!("times: expected Int count, got {:?}", count_value),
293        };
294
295        // Pop quotation
296        let (stack_temp2, quot_value) = pop(stack_temp);
297        let wrapper = match quot_value {
298            Value::Quotation { wrapper, .. } => wrapper,
299            _ => panic!("times: expected Quotation, got {:?}", quot_value),
300        };
301
302        // Validate function pointer is not null
303        if wrapper == 0 {
304            panic!("times: quotation wrapper function pointer is null");
305        }
306
307        // SAFETY: wrapper was created by the compiler's codegen and stored via push_quotation.
308        // The compiler guarantees that quotation wrapper functions use C calling convention.
309        // We've verified wrapper is non-null above.
310        let fn_ref: unsafe extern "C" fn(Stack) -> Stack = std::mem::transmute(wrapper);
311
312        // Execute quotation n times
313        // IMPORTANT: Yield after each iteration to maintain cooperative scheduling
314        stack = stack_temp2;
315        for _ in 0..count {
316            stack = fn_ref(stack);
317            may::coroutine::yield_now();
318        }
319
320        stack
321    }
322}
323
324/// Loop while a condition is true
325///
326/// Pops a body quotation and a condition quotation from the stack.
327/// Repeatedly executes: condition quotation, check result (Int: 0=false, non-zero=true),
328/// if true then execute body quotation, repeat.
329///
330/// Stack effect: ( ..a cond-quot body-quot -- ..a )
331/// where cond-quot has effect ( ..a -- ..a Int )
332/// and body-quot has effect ( ..a -- ..a )
333///
334/// # Safety
335/// - Stack must have at least 2 values
336/// - Top must be Quotation (body)
337/// - Second must be Quotation (condition)
338/// - Condition quotation must push exactly one Int
339/// - Body quotation must preserve stack shape
340#[unsafe(no_mangle)]
341pub unsafe extern "C" fn patch_seq_while_loop(mut stack: Stack) -> Stack {
342    unsafe {
343        // Pop body quotation
344        let (stack_temp, body_value) = pop(stack);
345        let body_wrapper = match body_value {
346            Value::Quotation { wrapper, .. } => wrapper,
347            _ => panic!("while: expected body Quotation, got {:?}", body_value),
348        };
349
350        // Pop condition quotation
351        let (stack_temp2, cond_value) = pop(stack_temp);
352        let cond_wrapper = match cond_value {
353            Value::Quotation { wrapper, .. } => wrapper,
354            _ => panic!("while: expected condition Quotation, got {:?}", cond_value),
355        };
356
357        // Validate function pointers are not null
358        if cond_wrapper == 0 {
359            panic!("while: condition quotation wrapper function pointer is null");
360        }
361        if body_wrapper == 0 {
362            panic!("while: body quotation wrapper function pointer is null");
363        }
364
365        // SAFETY: Both wrappers were created by the compiler's codegen and stored via push_quotation.
366        // The compiler guarantees that quotation wrapper functions use C calling convention.
367        // We've verified both wrappers are non-null above.
368        let cond_fn: unsafe extern "C" fn(Stack) -> Stack = std::mem::transmute(cond_wrapper);
369        let body_fn: unsafe extern "C" fn(Stack) -> Stack = std::mem::transmute(body_wrapper);
370
371        // Loop while condition is true
372        // IMPORTANT: Yield after each iteration to maintain cooperative scheduling
373        stack = stack_temp2;
374        loop {
375            // Execute condition quotation
376            stack = cond_fn(stack);
377
378            // Pop the condition result
379            let (stack_after_cond, cond_result) = pop(stack);
380            let is_true = match cond_result {
381                Value::Bool(b) => b,
382                _ => panic!("while: condition must return Bool, got {:?}", cond_result),
383            };
384
385            if !is_true {
386                // Condition is false, exit loop
387                stack = stack_after_cond;
388                break;
389            }
390
391            // Condition is true, execute body
392            stack = body_fn(stack_after_cond);
393
394            // Yield to scheduler after each iteration
395            may::coroutine::yield_now();
396        }
397
398        stack
399    }
400}
401
402/// Loop until a condition is true
403///
404/// Pops a condition quotation and a body quotation from the stack.
405/// Repeatedly executes: body quotation, then condition quotation, check result (Int: 0=false, non-zero=true),
406/// if false then continue loop, if true then exit.
407///
408/// This is the inverse of `while`: executes body at least once, then checks condition.
409///
410/// Stack effect: ( ..a body-quot cond-quot -- ..a )
411/// where body-quot has effect ( ..a -- ..a )
412/// and cond-quot has effect ( ..a -- ..a Int )
413///
414/// # Safety
415/// - Stack must have at least 2 values
416/// - Top must be Quotation (condition)
417/// - Second must be Quotation (body)
418/// - Condition quotation must push exactly one Int
419/// - Body quotation must preserve stack shape
420#[unsafe(no_mangle)]
421pub unsafe extern "C" fn patch_seq_until_loop(mut stack: Stack) -> Stack {
422    unsafe {
423        // Pop condition quotation
424        let (stack_temp, cond_value) = pop(stack);
425        let cond_wrapper = match cond_value {
426            Value::Quotation { wrapper, .. } => wrapper,
427            _ => panic!("until: expected condition Quotation, got {:?}", cond_value),
428        };
429
430        // Pop body quotation
431        let (stack_temp2, body_value) = pop(stack_temp);
432        let body_wrapper = match body_value {
433            Value::Quotation { wrapper, .. } => wrapper,
434            _ => panic!("until: expected body Quotation, got {:?}", body_value),
435        };
436
437        // Validate function pointers are not null
438        if cond_wrapper == 0 {
439            panic!("until: condition quotation wrapper function pointer is null");
440        }
441        if body_wrapper == 0 {
442            panic!("until: body quotation wrapper function pointer is null");
443        }
444
445        // SAFETY: Both wrappers were created by the compiler's codegen and stored via push_quotation.
446        // The compiler guarantees that quotation wrapper functions use C calling convention.
447        // We've verified both wrappers are non-null above.
448        let cond_fn: unsafe extern "C" fn(Stack) -> Stack = std::mem::transmute(cond_wrapper);
449        let body_fn: unsafe extern "C" fn(Stack) -> Stack = std::mem::transmute(body_wrapper);
450
451        // Loop until condition is true (do-while style)
452        // IMPORTANT: Yield after each iteration to maintain cooperative scheduling
453        stack = stack_temp2;
454        loop {
455            // Execute body quotation
456            stack = body_fn(stack);
457
458            // Execute condition quotation
459            stack = cond_fn(stack);
460
461            // Pop the condition result
462            let (stack_after_cond, cond_result) = pop(stack);
463            let is_true = match cond_result {
464                Value::Bool(b) => b,
465                _ => panic!("until: condition must return Bool, got {:?}", cond_result),
466            };
467
468            if is_true {
469                // Condition is true, exit loop
470                stack = stack_after_cond;
471                break;
472            }
473
474            // Condition is false, continue loop
475            stack = stack_after_cond;
476
477            // Yield to scheduler after each iteration
478            may::coroutine::yield_now();
479        }
480
481        stack
482    }
483}
484
485/// Spawn a quotation or closure as a new strand (green thread)
486///
487/// Pops a quotation or closure from the stack and spawns it as a new strand.
488/// - For Quotations: The quotation executes concurrently with an empty initial stack
489/// - For Closures: The closure executes with its captured environment
490///
491/// Returns the strand ID.
492///
493/// Stack effect: ( ..a quot -- ..a strand_id )
494/// Spawns a quotation or closure as a new strand (green thread).
495///
496/// The child strand receives a COPY of the parent's stack (after popping the quotation).
497/// This enables CSP/Actor patterns where actors receive arguments via the stack.
498///
499/// Stack effect: ( ...args quotation -- ...args strand-id )
500/// - Parent: keeps original stack with quotation removed, plus strand-id
501/// - Child: gets a clone of the stack (without quotation)
502///
503/// # Safety
504/// - Stack must have at least 1 value
505/// - Top must be Quotation or Closure
506/// - Function must be safe to execute on any thread
507#[unsafe(no_mangle)]
508pub unsafe extern "C" fn patch_seq_spawn(stack: Stack) -> Stack {
509    use crate::scheduler::patch_seq_strand_spawn_with_base;
510    use crate::stack::clone_stack_with_base;
511
512    unsafe {
513        // Pop quotation or closure
514        let (stack, value) = pop(stack);
515
516        match value {
517            Value::Quotation { wrapper, .. } => {
518                // Validate function pointer is not null
519                if wrapper == 0 {
520                    panic!("spawn: quotation wrapper function pointer is null");
521                }
522
523                // SAFETY: wrapper was created by the compiler's codegen and stored via push_quotation.
524                // The compiler guarantees that quotation wrapper functions use C calling convention.
525                // We've verified wrapper is non-null above.
526                let fn_ref: extern "C" fn(Stack) -> Stack = std::mem::transmute(wrapper);
527
528                // Clone the parent's stack for the child, getting both sp and base
529                // The child gets a copy of the stack (after the quotation was popped)
530                let (child_stack, child_base) = clone_stack_with_base(stack);
531
532                // Spawn the strand with the cloned stack and its base
533                // The scheduler will set STACK_BASE for the child strand
534                let strand_id = patch_seq_strand_spawn_with_base(fn_ref, child_stack, child_base);
535
536                // Push strand ID back onto the parent's stack
537                push(stack, Value::Int(strand_id))
538            }
539            Value::Closure { fn_ptr, env } => {
540                // Validate function pointer is not null
541                if fn_ptr == 0 {
542                    panic!("spawn: closure function pointer is null");
543                }
544
545                // We need to pass the closure data to the spawned strand.
546                // We use a registry with a unique ID (separate from strand_id).
547                use std::sync::atomic::{AtomicI64, Ordering};
548                static NEXT_CLOSURE_SPAWN_ID: AtomicI64 = AtomicI64::new(1);
549                let closure_spawn_id = NEXT_CLOSURE_SPAWN_ID.fetch_add(1, Ordering::Relaxed);
550
551                // Store closure data in registry
552                // Clone the Arc contents to Box - this ensures:
553                // 1. Arena-allocated strings are copied to global memory
554                // 2. The spawned strand gets independent ownership
555                {
556                    let env_box: Box<[Value]> = env.iter().cloned().collect();
557                    let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
558                    registry.insert(closure_spawn_id, (fn_ptr, env_box));
559                }
560
561                // Create a guard to cleanup registry on failure
562                // If spawn fails or the strand panics before retrieving the closure,
563                // the guard's Drop impl will remove the registry entry
564                let mut guard = SpawnRegistryGuard::new(closure_spawn_id);
565
566                // Create initial stack with the closure_spawn_id
567                // The base is the freshly allocated stack pointer
568                let stack_base = crate::stack::alloc_stack();
569                let initial_stack = push(stack_base, Value::Int(closure_spawn_id));
570
571                // Spawn strand with trampoline, passing the stack base
572                let strand_id = patch_seq_strand_spawn_with_base(
573                    closure_spawn_trampoline,
574                    initial_stack,
575                    stack_base,
576                );
577
578                // Spawn succeeded - disarm the guard so it won't cleanup
579                // The trampoline will retrieve and remove the closure data from the registry
580                guard.disarm();
581
582                // Push strand ID back onto stack
583                push(stack, Value::Int(strand_id))
584            }
585            _ => panic!("spawn: expected Quotation or Closure, got {:?}", value),
586        }
587    }
588}
589
590// Public re-exports with short names for internal use
591pub use patch_seq_call as call;
592pub use patch_seq_push_quotation as push_quotation;
593pub use patch_seq_spawn as spawn;
594pub use patch_seq_times as times;
595pub use patch_seq_until_loop as until_loop;
596pub use patch_seq_while_loop as while_loop;
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601    use crate::arithmetic::push_int;
602    use crate::value::Value;
603
604    #[test]
605    fn test_spawn_registry_guard_cleanup() {
606        // Test that the RAII guard cleans up the registry on drop
607        let closure_id = 12345;
608
609        // Create a test closure environment
610        let env: Box<[Value]> = vec![Value::Int(42), Value::Int(99)].into_boxed_slice();
611        let fn_ptr: usize = 0x1234;
612
613        // Insert into registry
614        {
615            let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
616            registry.insert(closure_id, (fn_ptr, env));
617        }
618
619        // Verify it's in the registry
620        {
621            let registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
622            assert!(registry.contains_key(&closure_id));
623        }
624
625        // Create a guard (without disarming) and let it drop
626        {
627            let _guard = SpawnRegistryGuard::new(closure_id);
628            // Guard drops here, should clean up the registry
629        }
630
631        // Verify the registry was cleaned up
632        {
633            let registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
634            assert!(
635                !registry.contains_key(&closure_id),
636                "Guard should have cleaned up registry entry on drop"
637            );
638        }
639    }
640
641    #[test]
642    fn test_spawn_registry_guard_disarm() {
643        // Test that disarming the guard prevents cleanup
644        let closure_id = 54321;
645
646        // Create a test closure environment
647        let env: Box<[Value]> = vec![Value::Int(10), Value::Int(20)].into_boxed_slice();
648        let fn_ptr: usize = 0x5678;
649
650        // Insert into registry
651        {
652            let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
653            registry.insert(closure_id, (fn_ptr, env));
654        }
655
656        // Create a guard, disarm it, and let it drop
657        {
658            let mut guard = SpawnRegistryGuard::new(closure_id);
659            guard.disarm();
660            // Guard drops here, but should NOT clean up because it's disarmed
661        }
662
663        // Verify the registry entry is still there
664        {
665            let registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
666            assert!(
667                registry.contains_key(&closure_id),
668                "Disarmed guard should not clean up registry entry"
669            );
670
671            // Manual cleanup for this test
672            drop(registry);
673            let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
674            registry.remove(&closure_id);
675        }
676    }
677
678    // Helper function for testing: a quotation that adds 1
679    unsafe extern "C" fn add_one_quot(stack: Stack) -> Stack {
680        unsafe {
681            let stack = push_int(stack, 1);
682            crate::arithmetic::add(stack)
683        }
684    }
685
686    #[test]
687    fn test_push_quotation() {
688        unsafe {
689            let stack: Stack = crate::stack::alloc_test_stack();
690
691            // Push a quotation (for tests, wrapper and impl are the same C function)
692            let fn_ptr = add_one_quot as usize;
693            let stack = push_quotation(stack, fn_ptr, fn_ptr);
694
695            // Verify it's on the stack
696            let (_stack, value) = pop(stack);
697            assert!(matches!(value, Value::Quotation { .. }));
698        }
699    }
700
701    #[test]
702    fn test_call_quotation() {
703        unsafe {
704            let stack: Stack = crate::stack::alloc_test_stack();
705
706            // Push 5, then a quotation that adds 1
707            let stack = push_int(stack, 5);
708            let fn_ptr = add_one_quot as usize;
709            let stack = push_quotation(stack, fn_ptr, fn_ptr);
710
711            // Call the quotation
712            let stack = call(stack);
713
714            // Result should be 6
715            let (_stack, result) = pop(stack);
716            assert_eq!(result, Value::Int(6));
717        }
718    }
719
720    #[test]
721    fn test_times_combinator() {
722        unsafe {
723            let stack: Stack = crate::stack::alloc_test_stack();
724
725            // Push 0, then execute [ 1 add ] 5 times
726            let stack = push_int(stack, 0);
727            let fn_ptr = add_one_quot as usize;
728            let stack = push_quotation(stack, fn_ptr, fn_ptr);
729            let stack = push_int(stack, 5);
730
731            // Execute times
732            let stack = times(stack);
733
734            // Result should be 5 (0 + 1 + 1 + 1 + 1 + 1)
735            let (_stack, result) = pop(stack);
736            assert_eq!(result, Value::Int(5));
737        }
738    }
739
740    #[test]
741    fn test_times_zero() {
742        unsafe {
743            let stack: Stack = crate::stack::alloc_test_stack();
744
745            // Push 10, then execute quotation 0 times
746            let stack = push_int(stack, 10);
747            let fn_ptr = add_one_quot as usize;
748            let stack = push_quotation(stack, fn_ptr, fn_ptr);
749            let stack = push_int(stack, 0);
750
751            // Execute times
752            let stack = times(stack);
753
754            // Result should still be 10 (quotation not executed)
755            let (_stack, result) = pop(stack);
756            assert_eq!(result, Value::Int(10));
757        }
758    }
759
760    // Helper quotation: dup then check if top value > 0
761    // Corresponds to: [ dup 0 > ]
762    unsafe extern "C" fn dup_gt_zero_quot(stack: Stack) -> Stack {
763        unsafe {
764            let stack = crate::stack::dup(stack); // Duplicate the value
765            let stack = push_int(stack, 0);
766            crate::arithmetic::gt(stack)
767        }
768    }
769
770    // Helper quotation: subtract 1 from top value
771    // Corresponds to: [ 1 subtract ]
772    unsafe extern "C" fn subtract_one_quot(stack: Stack) -> Stack {
773        unsafe {
774            let stack = push_int(stack, 1);
775            crate::arithmetic::subtract(stack)
776        }
777    }
778
779    #[test]
780    fn test_while_countdown() {
781        unsafe {
782            let stack: Stack = crate::stack::alloc_test_stack();
783
784            // Countdown from 5 to 0 using while
785            // [ dup 0 > ] [ dup 1 - ] while
786            let stack = push_int(stack, 5);
787
788            // Push condition: dup 0 >
789            let cond_ptr = dup_gt_zero_quot as usize;
790            let stack = push_quotation(stack, cond_ptr, cond_ptr);
791
792            // Push body: 1 subtract
793            let body_ptr = subtract_one_quot as usize;
794            let stack = push_quotation(stack, body_ptr, body_ptr);
795
796            // Execute while
797            let stack = while_loop(stack);
798
799            // Result should be 0
800            let (_stack, result) = pop(stack);
801            assert_eq!(result, Value::Int(0));
802        }
803    }
804
805    #[test]
806    fn test_while_false_immediately() {
807        unsafe {
808            let stack: Stack = crate::stack::alloc_test_stack();
809
810            // Start with 0, so condition is immediately false
811            let stack = push_int(stack, 0);
812
813            let cond_ptr = dup_gt_zero_quot as usize;
814            let stack = push_quotation(stack, cond_ptr, cond_ptr);
815
816            let body_ptr = subtract_one_quot as usize;
817            let stack = push_quotation(stack, body_ptr, body_ptr);
818
819            // Execute while
820            let stack = while_loop(stack);
821
822            // Result should still be 0 (body never executed)
823            let (_stack, result) = pop(stack);
824            assert_eq!(result, Value::Int(0));
825        }
826    }
827
828    // Helper quotation: check if top value <= 0
829    // Corresponds to: [ dup 0 <= ]
830    unsafe extern "C" fn dup_lte_zero_quot(stack: Stack) -> Stack {
831        unsafe {
832            let stack = crate::stack::dup(stack);
833            let stack = push_int(stack, 0);
834            crate::arithmetic::lte(stack)
835        }
836    }
837
838    #[test]
839    fn test_until_countdown() {
840        unsafe {
841            let stack: Stack = crate::stack::alloc_test_stack();
842
843            // Countdown from 5 to 0 using until
844            // [ 1 subtract ] [ dup 0 <= ] until
845            let stack = push_int(stack, 5);
846
847            // Push body: subtract 1
848            let body_ptr = subtract_one_quot as usize;
849            let stack = push_quotation(stack, body_ptr, body_ptr);
850
851            // Push condition: dup 0 <=
852            let cond_ptr = dup_lte_zero_quot as usize;
853            let stack = push_quotation(stack, cond_ptr, cond_ptr);
854
855            // Execute until
856            let stack = until_loop(stack);
857
858            // Result should be 0
859            let (_stack, result) = pop(stack);
860            assert_eq!(result, Value::Int(0));
861        }
862    }
863
864    #[test]
865    fn test_until_executes_at_least_once() {
866        unsafe {
867            let stack: Stack = crate::stack::alloc_test_stack();
868
869            // Start with 0, so condition is immediately true, but body should execute once
870            let stack = push_int(stack, 0);
871
872            // Push body: subtract 1
873            let body_ptr = subtract_one_quot as usize;
874            let stack = push_quotation(stack, body_ptr, body_ptr);
875
876            // Push condition: dup 0 <=  (will be true after first iteration)
877            let cond_ptr = dup_lte_zero_quot as usize;
878            let stack = push_quotation(stack, cond_ptr, cond_ptr);
879
880            // Execute until
881            let stack = until_loop(stack);
882
883            // Result should be -1 (body executed once)
884            let (_stack, result) = pop(stack);
885            assert_eq!(result, Value::Int(-1));
886        }
887    }
888
889    // Helper quotation for spawn test: does nothing, just completes
890    unsafe extern "C" fn noop_quot(stack: Stack) -> Stack {
891        stack
892    }
893
894    #[test]
895    fn test_spawn_quotation() {
896        unsafe {
897            // Initialize scheduler
898            crate::scheduler::scheduler_init();
899
900            let stack: Stack = crate::stack::alloc_test_stack();
901
902            // Push a quotation
903            let fn_ptr = noop_quot as usize;
904            let stack = push_quotation(stack, fn_ptr, fn_ptr);
905
906            // Spawn it
907            let stack = spawn(stack);
908
909            // Should have strand ID on stack
910            let (_stack, result) = pop(stack);
911            match result {
912                Value::Int(strand_id) => {
913                    assert!(strand_id > 0, "Strand ID should be positive");
914                }
915                _ => panic!("Expected Int (strand ID), got {:?}", result),
916            }
917
918            // Wait for strand to complete
919            crate::scheduler::wait_all_strands();
920        }
921    }
922}