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/// Spawn a quotation or closure as a new strand (green thread)
273///
274/// Pops a quotation or closure from the stack and spawns it as a new strand.
275/// - For Quotations: The quotation executes concurrently with an empty initial stack
276/// - For Closures: The closure executes with its captured environment
277///
278/// Returns the strand ID.
279///
280/// Stack effect: ( ..a quot -- ..a strand_id )
281/// Spawns a quotation or closure as a new strand (green thread).
282///
283/// The child strand receives a COPY of the parent's stack (after popping the quotation).
284/// This enables CSP/Actor patterns where actors receive arguments via the stack.
285///
286/// Stack effect: ( ...args quotation -- ...args strand-id )
287/// - Parent: keeps original stack with quotation removed, plus strand-id
288/// - Child: gets a clone of the stack (without quotation)
289///
290/// # Safety
291/// - Stack must have at least 1 value
292/// - Top must be Quotation or Closure
293/// - Function must be safe to execute on any thread
294#[unsafe(no_mangle)]
295pub unsafe extern "C" fn patch_seq_spawn(stack: Stack) -> Stack {
296 use crate::scheduler::patch_seq_strand_spawn_with_base;
297 use crate::stack::clone_stack_with_base;
298
299 unsafe {
300 // Pop quotation or closure
301 let (stack, value) = pop(stack);
302
303 match value {
304 Value::Quotation { wrapper, .. } => {
305 // Validate function pointer is not null
306 if wrapper == 0 {
307 panic!("spawn: quotation wrapper function pointer is null");
308 }
309
310 // SAFETY: wrapper was created by the compiler's codegen and stored via push_quotation.
311 // The compiler guarantees that quotation wrapper functions use C calling convention.
312 // We've verified wrapper is non-null above.
313 let fn_ref: extern "C" fn(Stack) -> Stack = std::mem::transmute(wrapper);
314
315 // Clone the parent's stack for the child, getting both sp and base
316 // The child gets a copy of the stack (after the quotation was popped)
317 let (child_stack, child_base) = clone_stack_with_base(stack);
318
319 // Spawn the strand with the cloned stack and its base
320 // The scheduler will set STACK_BASE for the child strand
321 let strand_id = patch_seq_strand_spawn_with_base(fn_ref, child_stack, child_base);
322
323 // Push strand ID back onto the parent's stack
324 push(stack, Value::Int(strand_id))
325 }
326 Value::Closure { fn_ptr, env } => {
327 // Validate function pointer is not null
328 if fn_ptr == 0 {
329 panic!("spawn: closure function pointer is null");
330 }
331
332 // We need to pass the closure data to the spawned strand.
333 // We use a registry with a unique ID (separate from strand_id).
334 use std::sync::atomic::{AtomicI64, Ordering};
335 static NEXT_CLOSURE_SPAWN_ID: AtomicI64 = AtomicI64::new(1);
336 let closure_spawn_id = NEXT_CLOSURE_SPAWN_ID.fetch_add(1, Ordering::Relaxed);
337
338 // Store closure data in registry
339 // Clone the Arc contents to Box - this ensures:
340 // 1. Arena-allocated strings are copied to global memory
341 // 2. The spawned strand gets independent ownership
342 {
343 let env_box: Box<[Value]> = env.iter().cloned().collect();
344 let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
345 registry.insert(closure_spawn_id, (fn_ptr, env_box));
346 }
347
348 // Create a guard to cleanup registry on failure
349 // If spawn fails or the strand panics before retrieving the closure,
350 // the guard's Drop impl will remove the registry entry
351 let mut guard = SpawnRegistryGuard::new(closure_spawn_id);
352
353 // Create initial stack with the closure_spawn_id
354 // The base is the freshly allocated stack pointer
355 let stack_base = crate::stack::alloc_stack();
356 let initial_stack = push(stack_base, Value::Int(closure_spawn_id));
357
358 // Spawn strand with trampoline, passing the stack base
359 let strand_id = patch_seq_strand_spawn_with_base(
360 closure_spawn_trampoline,
361 initial_stack,
362 stack_base,
363 );
364
365 // Spawn succeeded - disarm the guard so it won't cleanup
366 // The trampoline will retrieve and remove the closure data from the registry
367 guard.disarm();
368
369 // Push strand ID back onto stack
370 push(stack, Value::Int(strand_id))
371 }
372 _ => panic!("spawn: expected Quotation or Closure, got {:?}", value),
373 }
374 }
375}
376
377// Public re-exports with short names for internal use
378pub use patch_seq_call as call;
379pub use patch_seq_push_quotation as push_quotation;
380pub use patch_seq_spawn as spawn;
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::arithmetic::push_int;
386 use crate::value::Value;
387
388 #[test]
389 fn test_spawn_registry_guard_cleanup() {
390 // Test that the RAII guard cleans up the registry on drop
391 let closure_id = 12345;
392
393 // Create a test closure environment
394 let env: Box<[Value]> = vec![Value::Int(42), Value::Int(99)].into_boxed_slice();
395 let fn_ptr: usize = 0x1234;
396
397 // Insert into registry
398 {
399 let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
400 registry.insert(closure_id, (fn_ptr, env));
401 }
402
403 // Verify it's in the registry
404 {
405 let registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
406 assert!(registry.contains_key(&closure_id));
407 }
408
409 // Create a guard (without disarming) and let it drop
410 {
411 let _guard = SpawnRegistryGuard::new(closure_id);
412 // Guard drops here, should clean up the registry
413 }
414
415 // Verify the registry was cleaned up
416 {
417 let registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
418 assert!(
419 !registry.contains_key(&closure_id),
420 "Guard should have cleaned up registry entry on drop"
421 );
422 }
423 }
424
425 #[test]
426 fn test_spawn_registry_guard_disarm() {
427 // Test that disarming the guard prevents cleanup
428 let closure_id = 54321;
429
430 // Create a test closure environment
431 let env: Box<[Value]> = vec![Value::Int(10), Value::Int(20)].into_boxed_slice();
432 let fn_ptr: usize = 0x5678;
433
434 // Insert into registry
435 {
436 let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
437 registry.insert(closure_id, (fn_ptr, env));
438 }
439
440 // Create a guard, disarm it, and let it drop
441 {
442 let mut guard = SpawnRegistryGuard::new(closure_id);
443 guard.disarm();
444 // Guard drops here, but should NOT clean up because it's disarmed
445 }
446
447 // Verify the registry entry is still there
448 {
449 let registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
450 assert!(
451 registry.contains_key(&closure_id),
452 "Disarmed guard should not clean up registry entry"
453 );
454
455 // Manual cleanup for this test
456 drop(registry);
457 let mut registry = SPAWN_CLOSURE_REGISTRY.lock().unwrap();
458 registry.remove(&closure_id);
459 }
460 }
461
462 // Helper function for testing: a quotation that adds 1
463 unsafe extern "C" fn add_one_quot(stack: Stack) -> Stack {
464 unsafe {
465 let stack = push_int(stack, 1);
466 crate::arithmetic::add(stack)
467 }
468 }
469
470 #[test]
471 fn test_push_quotation() {
472 unsafe {
473 let stack: Stack = crate::stack::alloc_test_stack();
474
475 // Push a quotation (for tests, wrapper and impl are the same C function)
476 let fn_ptr = add_one_quot as usize;
477 let stack = push_quotation(stack, fn_ptr, fn_ptr);
478
479 // Verify it's on the stack
480 let (_stack, value) = pop(stack);
481 assert!(matches!(value, Value::Quotation { .. }));
482 }
483 }
484
485 #[test]
486 fn test_call_quotation() {
487 unsafe {
488 let stack: Stack = crate::stack::alloc_test_stack();
489
490 // Push 5, then a quotation that adds 1
491 let stack = push_int(stack, 5);
492 let fn_ptr = add_one_quot as usize;
493 let stack = push_quotation(stack, fn_ptr, fn_ptr);
494
495 // Call the quotation
496 let stack = call(stack);
497
498 // Result should be 6
499 let (_stack, result) = pop(stack);
500 assert_eq!(result, Value::Int(6));
501 }
502 }
503
504 // Helper quotation for spawn test: does nothing, just completes
505 unsafe extern "C" fn noop_quot(stack: Stack) -> Stack {
506 stack
507 }
508
509 #[test]
510 fn test_spawn_quotation() {
511 unsafe {
512 // Initialize scheduler
513 crate::scheduler::scheduler_init();
514
515 let stack: Stack = crate::stack::alloc_test_stack();
516
517 // Push a quotation
518 let fn_ptr = noop_quot as usize;
519 let stack = push_quotation(stack, fn_ptr, fn_ptr);
520
521 // Spawn it
522 let stack = spawn(stack);
523
524 // Should have strand ID on stack
525 let (_stack, result) = pop(stack);
526 match result {
527 Value::Int(strand_id) => {
528 assert!(strand_id > 0, "Strand ID should be positive");
529 }
530 _ => panic!("Expected Int (strand ID), got {:?}", result),
531 }
532
533 // Wait for strand to complete
534 crate::scheduler::wait_all_strands();
535 }
536 }
537}