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