Skip to main content

seq_runtime/
closures.rs

1//! Closure support for Seq
2//!
3//! Provides runtime functions for creating and managing closures (quotations with captured environments).
4//!
5//! A closure consists of:
6//! - Function pointer (the compiled quotation code)
7//! - Environment (Arc-shared array of captured values for TCO support)
8//!
9//! Note: These extern "C" functions use Value and slice pointers, which aren't technically FFI-safe,
10//! but they work correctly when called from LLVM-generated code (not actual C interop).
11//!
12//! ## Type Support Status
13//!
14//! Currently supported capture types:
15//! - **Int** (via `env_get_int`)
16//! - **Bool** (via `env_get_bool`) - returns i64 (0/1)
17//! - **Float** (via `env_get_float`) - returns f64
18//! - **String** (via `env_get_string`)
19//! - **Quotation** (via `env_get_quotation`) - returns function pointer as i64
20//!
21//! Types to be added in future PR:
22//! - Closure (nested closures with their own environments)
23//! - Variant (tagged unions)
24//!
25//! See <https://github.com/navicore/patch-seq> for roadmap.
26
27use crate::stack::{Stack, pop, push};
28use crate::value::Value;
29use std::sync::Arc;
30
31/// Maximum number of captured values allowed in a closure environment.
32/// This prevents unbounded memory allocation and potential resource exhaustion.
33pub const MAX_CAPTURES: usize = 1024;
34
35/// Create a closure environment (array of captured values)
36///
37/// Called from generated LLVM code to allocate space for captured values.
38/// Returns a raw pointer to a boxed slice that will be filled with values.
39///
40/// # Safety
41/// - Caller must populate the environment with `env_set` before using
42/// - Caller must eventually pass ownership to a Closure value (via `make_closure`)
43// Allow improper_ctypes_definitions: Called from LLVM IR (not C), both sides understand layout
44#[allow(improper_ctypes_definitions)]
45#[unsafe(no_mangle)]
46pub extern "C" fn patch_seq_create_env(size: i32) -> *mut [Value] {
47    if size < 0 {
48        panic!("create_env: size cannot be negative: {}", size);
49    }
50
51    let size_usize = size as usize;
52    if size_usize > MAX_CAPTURES {
53        panic!(
54            "create_env: size {} exceeds MAX_CAPTURES ({})",
55            size_usize, MAX_CAPTURES
56        );
57    }
58
59    let mut vec: Vec<Value> = Vec::with_capacity(size_usize);
60
61    // Fill with placeholder values (will be replaced by env_set)
62    for _ in 0..size {
63        vec.push(Value::Int(0));
64    }
65
66    Box::into_raw(vec.into_boxed_slice())
67}
68
69/// Set a value in the closure environment
70///
71/// Called from generated LLVM code to populate captured values.
72///
73/// # Safety
74/// - env must be a valid pointer from `create_env`
75/// - index must be in bounds [0, size)
76/// - env must not have been passed to `make_closure` yet
77// Allow improper_ctypes_definitions: Called from LLVM IR (not C), both sides understand layout
78#[allow(improper_ctypes_definitions)]
79#[unsafe(no_mangle)]
80pub unsafe extern "C" fn patch_seq_env_set(env: *mut [Value], index: i32, value: Value) {
81    if env.is_null() {
82        panic!("env_set: null environment pointer");
83    }
84
85    if index < 0 {
86        panic!("env_set: index cannot be negative: {}", index);
87    }
88
89    let env_slice = unsafe { &mut *env };
90    let idx = index as usize;
91
92    if idx >= env_slice.len() {
93        panic!(
94            "env_set: index {} out of bounds for environment of size {}",
95            index,
96            env_slice.len()
97        );
98    }
99
100    env_slice[idx] = value;
101}
102
103/// Get a value from the closure environment
104///
105/// Called from generated closure function code to access captured values.
106/// Takes environment as separate data pointer and length (since LLVM can't handle fat pointers).
107///
108/// # Safety
109/// - env_data must be a valid pointer to an array of Values
110/// - env_len must match the actual array length
111/// - index must be in bounds [0, env_len)
112// Allow improper_ctypes_definitions: Called from LLVM IR (not C), both sides understand layout
113#[allow(improper_ctypes_definitions)]
114#[unsafe(no_mangle)]
115pub unsafe extern "C" fn patch_seq_env_get(
116    env_data: *const Value,
117    env_len: usize,
118    index: i32,
119) -> Value {
120    if env_data.is_null() {
121        panic!("env_get: null environment pointer");
122    }
123
124    if index < 0 {
125        panic!("env_get: index cannot be negative: {}", index);
126    }
127
128    let idx = index as usize;
129
130    if idx >= env_len {
131        panic!(
132            "env_get: index {} out of bounds for environment of size {}",
133            index, env_len
134        );
135    }
136
137    // Clone the value from the environment
138    unsafe { (*env_data.add(idx)).clone() }
139}
140
141/// Get an Int value from the closure environment
142///
143/// This is a type-specific helper that avoids passing large Value enums through LLVM IR.
144/// Returns primitive i64 instead of Value to avoid FFI issues with by-value enum passing.
145///
146/// # Safety
147/// - env_data must be a valid pointer to an array of Values
148/// - env_len must match the actual array length
149/// - index must be in bounds [0, env_len)
150/// - The value at index must be Value::Int
151///
152/// # FFI Notes
153/// This function is ONLY called from LLVM-generated code, not from external C code.
154/// The signature is safe for LLVM IR but would be undefined behavior if called from C
155/// with incorrect assumptions about type layout.
156#[unsafe(no_mangle)]
157pub unsafe extern "C" fn patch_seq_env_get_int(
158    env_data: *const Value,
159    env_len: usize,
160    index: i32,
161) -> i64 {
162    if env_data.is_null() {
163        panic!("env_get_int: null environment pointer");
164    }
165
166    if index < 0 {
167        panic!("env_get_int: index cannot be negative: {}", index);
168    }
169
170    let idx = index as usize;
171
172    if idx >= env_len {
173        panic!(
174            "env_get_int: index {} out of bounds for environment of size {}",
175            index, env_len
176        );
177    }
178
179    // Access the value at the index
180    let value = unsafe { &*env_data.add(idx) };
181
182    match value {
183        Value::Int(n) => *n,
184        _ => panic!(
185            "env_get_int: expected Int at index {}, got {:?}",
186            index, value
187        ),
188    }
189}
190
191/// Get a String value from the environment at the given index
192///
193/// # Safety
194/// - env_data must be a valid pointer to an array of Values
195/// - env_len must be the actual length of that array
196/// - index must be within bounds
197/// - The value at index must be a String
198///
199/// This function returns a SeqString by-value.
200/// This is safe for FFI because it's only called from LLVM-generated code, not actual C code.
201#[allow(improper_ctypes_definitions)]
202#[unsafe(no_mangle)]
203pub unsafe extern "C" fn patch_seq_env_get_string(
204    env_data: *const Value,
205    env_len: usize,
206    index: i32,
207) -> crate::seqstring::SeqString {
208    if env_data.is_null() {
209        panic!("env_get_string: null environment pointer");
210    }
211
212    if index < 0 {
213        panic!("env_get_string: index cannot be negative: {}", index);
214    }
215
216    let idx = index as usize;
217
218    if idx >= env_len {
219        panic!(
220            "env_get_string: index {} out of bounds for environment of size {}",
221            index, env_len
222        );
223    }
224
225    // Access the value at the index
226    let value = unsafe { &*env_data.add(idx) };
227
228    match value {
229        Value::String(s) => s.clone(),
230        _ => panic!(
231            "env_get_string: expected String at index {}, got {:?}",
232            index, value
233        ),
234    }
235}
236
237/// Push a String from the closure environment directly onto the stack
238///
239/// This combines getting and pushing in one operation to avoid returning
240/// SeqString by value through FFI, which has calling convention issues on Linux.
241///
242/// # Safety
243/// - Stack pointer must be valid
244/// - env_data must be a valid pointer to an array of Values
245/// - env_len must match the actual array length
246/// - index must be in bounds [0, env_len)
247/// - The value at index must be Value::String
248#[unsafe(no_mangle)]
249pub unsafe extern "C" fn patch_seq_env_push_string(
250    stack: Stack,
251    env_data: *const Value,
252    env_len: usize,
253    index: i32,
254) -> Stack {
255    if env_data.is_null() {
256        panic!("env_push_string: null environment pointer");
257    }
258
259    if index < 0 {
260        panic!("env_push_string: index cannot be negative: {}", index);
261    }
262
263    let idx = index as usize;
264
265    if idx >= env_len {
266        panic!(
267            "env_push_string: index {} out of bounds for environment of size {}",
268            index, env_len
269        );
270    }
271
272    // Access the value at the index
273    let value = unsafe { &*env_data.add(idx) };
274
275    match value {
276        Value::String(s) => unsafe { push(stack, Value::String(s.clone())) },
277        _ => panic!(
278            "env_push_string: expected String at index {}, got {:?}",
279            index, value
280        ),
281    }
282}
283
284/// Push any value from the closure environment onto the stack.
285///
286/// This is the generic capture-push function for types that don't have
287/// specialized getters (Variant, Map, Union, Symbol, Channel). It clones
288/// the Value from the env and pushes it directly, avoiding passing Value
289/// by value through the FFI boundary (which crashes on Linux for some types).
290///
291/// # Safety
292/// - `stack` must be a valid stack pointer
293/// - `env_data` must be a valid pointer to a Value array
294/// - `env_len` must match the actual array length
295/// - `index` must be in bounds [0, env_len)
296#[unsafe(no_mangle)]
297pub unsafe extern "C" fn patch_seq_env_push_value(
298    stack: Stack,
299    env_data: *const Value,
300    env_len: usize,
301    index: i32,
302) -> Stack {
303    if env_data.is_null() {
304        panic!("env_push_value: null environment pointer");
305    }
306
307    if index < 0 {
308        panic!("env_push_value: index cannot be negative: {}", index);
309    }
310
311    let idx = index as usize;
312
313    if idx >= env_len {
314        panic!(
315            "env_push_value: index {} out of bounds for environment of size {}",
316            index, env_len
317        );
318    }
319
320    // Clone the value from the environment and push onto the stack.
321    // This works for any Value variant (Variant, Map, Symbol, Channel, etc.)
322    // The clone is O(1) for Arc-wrapped types (Variant, Map) — just a refcount bump.
323    //
324    // Primitive types (Int, Bool, Float) should use their specialized getters
325    // (env_get_int, etc.) for efficiency. This generic path is for types that
326    // don't have specialized LLVM IR representations.
327    let value = unsafe { (*env_data.add(idx)).clone() };
328    debug_assert!(
329        !matches!(value, Value::Int(_) | Value::Bool(_) | Value::Float(_)),
330        "env_push_value called for primitive type {:?} — use the specialized getter",
331        value
332    );
333    unsafe { push(stack, value) }
334}
335
336/// Get a Bool value from the closure environment
337///
338/// Returns i64 (0 for false, 1 for true) to match LLVM IR representation.
339/// Bools are stored as i64 in the generated code for simplicity.
340///
341/// # Safety
342/// - env_data must be a valid pointer to an array of Values
343/// - env_len must match the actual array length
344/// - index must be in bounds [0, env_len)
345/// - The value at index must be Value::Bool
346#[unsafe(no_mangle)]
347pub unsafe extern "C" fn patch_seq_env_get_bool(
348    env_data: *const Value,
349    env_len: usize,
350    index: i32,
351) -> i64 {
352    if env_data.is_null() {
353        panic!("env_get_bool: null environment pointer");
354    }
355
356    if index < 0 {
357        panic!("env_get_bool: index cannot be negative: {}", index);
358    }
359
360    let idx = index as usize;
361
362    if idx >= env_len {
363        panic!(
364            "env_get_bool: index {} out of bounds for environment of size {}",
365            index, env_len
366        );
367    }
368
369    let value = unsafe { &*env_data.add(idx) };
370
371    match value {
372        Value::Bool(b) => {
373            if *b {
374                1
375            } else {
376                0
377            }
378        }
379        _ => panic!(
380            "env_get_bool: expected Bool at index {}, got {:?}",
381            index, value
382        ),
383    }
384}
385
386/// Get a Float value from the closure environment
387///
388/// Returns f64 directly for efficient LLVM IR integration.
389///
390/// # Safety
391/// - env_data must be a valid pointer to an array of Values
392/// - env_len must match the actual array length
393/// - index must be in bounds [0, env_len)
394/// - The value at index must be Value::Float
395#[unsafe(no_mangle)]
396pub unsafe extern "C" fn patch_seq_env_get_float(
397    env_data: *const Value,
398    env_len: usize,
399    index: i32,
400) -> f64 {
401    if env_data.is_null() {
402        panic!("env_get_float: null environment pointer");
403    }
404
405    if index < 0 {
406        panic!("env_get_float: index cannot be negative: {}", index);
407    }
408
409    let idx = index as usize;
410
411    if idx >= env_len {
412        panic!(
413            "env_get_float: index {} out of bounds for environment of size {}",
414            index, env_len
415        );
416    }
417
418    let value = unsafe { &*env_data.add(idx) };
419
420    match value {
421        Value::Float(f) => *f,
422        _ => panic!(
423            "env_get_float: expected Float at index {}, got {:?}",
424            index, value
425        ),
426    }
427}
428
429/// Get a Quotation impl_ function pointer from the closure environment
430///
431/// Returns i64 (the impl_ function pointer as usize) for LLVM IR.
432/// Returns the tailcc impl_ pointer for TCO when called from compiled code.
433/// Quotations are stateless, so only the function pointer is needed.
434///
435/// # Safety
436/// - env_data must be a valid pointer to an array of Values
437/// - env_len must match the actual array length
438/// - index must be in bounds [0, env_len)
439/// - The value at index must be Value::Quotation
440#[unsafe(no_mangle)]
441pub unsafe extern "C" fn patch_seq_env_get_quotation(
442    env_data: *const Value,
443    env_len: usize,
444    index: i32,
445) -> i64 {
446    if env_data.is_null() {
447        panic!("env_get_quotation: null environment pointer");
448    }
449
450    if index < 0 {
451        panic!("env_get_quotation: index cannot be negative: {}", index);
452    }
453
454    let idx = index as usize;
455
456    if idx >= env_len {
457        panic!(
458            "env_get_quotation: index {} out of bounds for environment of size {}",
459            index, env_len
460        );
461    }
462
463    let value = unsafe { &*env_data.add(idx) };
464
465    match value {
466        Value::Quotation { impl_, .. } => *impl_ as i64,
467        _ => panic!(
468            "env_get_quotation: expected Quotation at index {}, got {:?}",
469            index, value
470        ),
471    }
472}
473
474/// Create a closure value from a function pointer and environment
475///
476/// Takes ownership of the environment (converts raw pointer to Arc).
477/// Arc enables TCO: no cleanup needed after tail calls.
478///
479/// # Safety
480/// - fn_ptr must be a valid function pointer (will be transmuted when called)
481/// - env must be a valid pointer from `create_env`, fully populated via `env_set`
482/// - env ownership is transferred to the Closure value
483// Allow improper_ctypes_definitions: Called from LLVM IR (not C), both sides understand layout
484#[allow(improper_ctypes_definitions)]
485#[unsafe(no_mangle)]
486pub unsafe extern "C" fn patch_seq_make_closure(fn_ptr: u64, env: *mut [Value]) -> Value {
487    if fn_ptr == 0 {
488        panic!("make_closure: null function pointer");
489    }
490
491    if env.is_null() {
492        panic!("make_closure: null environment pointer");
493    }
494
495    // Take ownership of the environment and convert to Arc for TCO support
496    let env_box = unsafe { Box::from_raw(env) };
497    let env_arc: Arc<[Value]> = Arc::from(env_box);
498
499    Value::Closure {
500        fn_ptr: fn_ptr as usize,
501        env: env_arc,
502    }
503}
504
505/// Create closure from function pointer and stack values (all-in-one helper)
506///
507/// Pops `capture_count` values from stack (top-down order), creates environment,
508/// makes closure, and pushes it onto the stack.
509///
510/// This is a convenience function for LLVM codegen that handles the entire
511/// closure creation process in one call. Uses Arc for TCO support.
512///
513/// # Safety
514/// - fn_ptr must be a valid function pointer
515/// - stack must have at least `capture_count` values
516#[unsafe(no_mangle)]
517pub unsafe extern "C" fn patch_seq_push_closure(
518    mut stack: Stack,
519    fn_ptr: u64,
520    capture_count: i32,
521) -> Stack {
522    if fn_ptr == 0 {
523        panic!("push_closure: null function pointer");
524    }
525
526    if capture_count < 0 {
527        panic!(
528            "push_closure: capture_count cannot be negative: {}",
529            capture_count
530        );
531    }
532
533    let count = capture_count as usize;
534
535    // Pop values from stack (captures are in top-down order)
536    let mut captures: Vec<Value> = Vec::with_capacity(count);
537    for _ in 0..count {
538        let (new_stack, value) = unsafe { pop(stack) };
539        captures.push(value);
540        stack = new_stack;
541    }
542
543    // Create closure value with Arc for TCO support
544    let closure = Value::Closure {
545        fn_ptr: fn_ptr as usize,
546        env: Arc::from(captures.into_boxed_slice()),
547    };
548
549    // Push onto stack
550    unsafe { push(stack, closure) }
551}
552
553// Public re-exports with short names for internal use
554pub use patch_seq_create_env as create_env;
555pub use patch_seq_env_get as env_get;
556pub use patch_seq_env_get_bool as env_get_bool;
557pub use patch_seq_env_get_float as env_get_float;
558pub use patch_seq_env_get_int as env_get_int;
559pub use patch_seq_env_get_quotation as env_get_quotation;
560pub use patch_seq_env_get_string as env_get_string;
561pub use patch_seq_env_push_string as env_push_string;
562pub use patch_seq_env_push_value as env_push_value;
563pub use patch_seq_env_set as env_set;
564pub use patch_seq_make_closure as make_closure;
565pub use patch_seq_push_closure as push_closure;
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570
571    #[test]
572    fn test_create_env() {
573        let env = create_env(3);
574        assert!(!env.is_null());
575
576        // Clean up
577        unsafe {
578            let _ = Box::from_raw(env);
579        }
580    }
581
582    #[test]
583    fn test_env_set_and_get() {
584        let env = create_env(3);
585
586        // Set values
587        unsafe {
588            env_set(env, 0, Value::Int(42));
589            env_set(env, 1, Value::Bool(true));
590            env_set(env, 2, Value::Int(99));
591        }
592
593        // Get values (convert to data pointer + length)
594        unsafe {
595            let env_slice = &*env;
596            let env_data = env_slice.as_ptr();
597            let env_len = env_slice.len();
598            assert_eq!(env_get(env_data, env_len, 0), Value::Int(42));
599            assert_eq!(env_get(env_data, env_len, 1), Value::Bool(true));
600            assert_eq!(env_get(env_data, env_len, 2), Value::Int(99));
601        }
602
603        // Clean up
604        unsafe {
605            let _ = Box::from_raw(env);
606        }
607    }
608
609    #[test]
610    fn test_make_closure() {
611        let env = create_env(2);
612
613        unsafe {
614            env_set(env, 0, Value::Int(5));
615            env_set(env, 1, Value::Int(10));
616
617            let closure = make_closure(0x1234, env);
618
619            match closure {
620                Value::Closure { fn_ptr, env } => {
621                    assert_eq!(fn_ptr, 0x1234);
622                    assert_eq!(env.len(), 2);
623                    assert_eq!(env[0], Value::Int(5));
624                    assert_eq!(env[1], Value::Int(10));
625                }
626                _ => panic!("Expected Closure value"),
627            }
628        }
629    }
630
631    // Note: We don't test panic behavior for FFI functions as they use
632    // extern "C" which cannot unwind. The functions will still panic at runtime
633    // if called incorrectly, but we can't test that behavior with #[should_panic].
634
635    #[test]
636    fn test_push_closure() {
637        use crate::stack::{pop, push};
638        use crate::value::Value;
639
640        // Create a stack with some values
641        let mut stack = crate::stack::alloc_test_stack();
642        stack = unsafe { push(stack, Value::Int(10)) };
643        stack = unsafe { push(stack, Value::Int(5)) };
644
645        // Create a closure that captures both values
646        let fn_ptr = 0x1234;
647        stack = unsafe { push_closure(stack, fn_ptr, 2) };
648
649        // Pop the closure
650        let (_stack, closure_value) = unsafe { pop(stack) };
651
652        // Verify it's a closure with correct captures
653        match closure_value {
654            Value::Closure { fn_ptr: fp, env } => {
655                assert_eq!(fp, fn_ptr as usize);
656                assert_eq!(env.len(), 2);
657                assert_eq!(env[0], Value::Int(5)); // Top of stack
658                assert_eq!(env[1], Value::Int(10)); // Second from top
659            }
660            _ => panic!("Expected Closure value, got {:?}", closure_value),
661        }
662
663        // Stack should be empty now
664    }
665
666    #[test]
667    fn test_push_closure_zero_captures() {
668        use crate::stack::pop;
669        use crate::value::Value;
670
671        // Create empty stack
672        let stack = crate::stack::alloc_test_stack();
673
674        // Create a closure with no captures
675        let fn_ptr = 0x5678;
676        let stack = unsafe { push_closure(stack, fn_ptr, 0) };
677
678        // Pop the closure
679        let (_stack, closure_value) = unsafe { pop(stack) };
680
681        // Verify it's a closure with no captures
682        match closure_value {
683            Value::Closure { fn_ptr: fp, env } => {
684                assert_eq!(fp, fn_ptr as usize);
685                assert_eq!(env.len(), 0);
686            }
687            _ => panic!("Expected Closure value, got {:?}", closure_value),
688        }
689
690        // Stack should be empty
691    }
692
693    #[test]
694    fn test_env_get_bool() {
695        let env = create_env(2);
696
697        unsafe {
698            env_set(env, 0, Value::Bool(true));
699            env_set(env, 1, Value::Bool(false));
700
701            let env_slice = &*env;
702            let env_data = env_slice.as_ptr();
703            let env_len = env_slice.len();
704
705            assert_eq!(env_get_bool(env_data, env_len, 0), 1);
706            assert_eq!(env_get_bool(env_data, env_len, 1), 0);
707
708            let _ = Box::from_raw(env);
709        }
710    }
711
712    #[test]
713    fn test_env_get_float() {
714        let env = create_env(2);
715
716        unsafe {
717            env_set(env, 0, Value::Float(1.234));
718            env_set(env, 1, Value::Float(-5.678));
719
720            let env_slice = &*env;
721            let env_data = env_slice.as_ptr();
722            let env_len = env_slice.len();
723
724            assert!((env_get_float(env_data, env_len, 0) - 1.234).abs() < 0.0001);
725            assert!((env_get_float(env_data, env_len, 1) - (-5.678)).abs() < 0.0001);
726
727            let _ = Box::from_raw(env);
728        }
729    }
730
731    #[test]
732    fn test_env_get_quotation() {
733        let env = create_env(1);
734        let wrapper: usize = 0xDEADBEEF;
735        let impl_: usize = 0xCAFEBABE;
736
737        unsafe {
738            env_set(env, 0, Value::Quotation { wrapper, impl_ });
739
740            let env_slice = &*env;
741            let env_data = env_slice.as_ptr();
742            let env_len = env_slice.len();
743
744            // env_get_quotation returns the impl_ pointer for TCO
745            assert_eq!(env_get_quotation(env_data, env_len, 0), impl_ as i64);
746
747            let _ = Box::from_raw(env);
748        }
749    }
750}