Skip to main content

praxis_runtime/
closures.rs

1//! The `Closure` value descriptor (§4.10).
2//!
3//! A closure value carries a function pointer (the JIT'd entry point of the
4//! closure's synthetic function) plus a captured environment (the values it
5//! closes over, each a `GcRef`). A single `CLOSURE` descriptor serves every
6//! closure because the per-closure knowledge (arity, env size) lives in the
7//! payload.
8//!
9//! Per §5.5, function and closure values are **never equatable or hashable** —
10//! they have no structural identity. The `equals`/`hash` callbacks are `None`.
11//!
12//! ## Calling convention
13//!
14//! The closure's synthetic function takes the closure value itself as a hidden
15//! first explicit parameter (after the implicit `ctx`): its MIR signature is
16//! `fn(ctx, closure_self, params...)`. At entry, a prologue loads each captured
17//! value via [`praxis_closure_capture`](crate::abi::praxis_closure_capture) and
18//! binds it to a local. Calling a closure value is an indirect call: the call
19//! site reads `fn_ptr` via
20//! [`praxis_closure_fn_ptr`](crate::abi::praxis_closure_fn_ptr), then emits a
21//! native `call_indirect` passing `[ctx, closure, args...]`. Keeping the
22//! closure value intact at the call site (rather than spreading the env into
23//! trailing params) makes the indirect call uniform per-arity and keeps the
24//! closure self-contained for fault snapshots and future borrow/move semantics.
25
26use std::fmt::Write as _;
27
28use crate::GcRef;
29use crate::descriptor::{BuiltinTypeId, FormatSink, Tracer, TypeDescriptor};
30
31/// The runtime payload of a closure value: the function pointer plus the
32/// captured environment values (one `GcRef` per captured variable, in capture
33/// order established by the HIR capture analysis).
34#[repr(C)]
35pub struct ClosurePayload {
36    /// The JIT'd entry-point function pointer, called as
37    /// `fn(ctx: i64, closure_self: i64, params...) -> i64`. The captures are not
38    /// trailing parameters; the prologue reads them out of this payload.
39    pub fn_ptr: *const u8,
40    /// The captured values, in the order the capture analysis recorded them.
41    /// Each is a `GcRef` into the GC heap.
42    pub env: Vec<GcRef>,
43}
44
45unsafe fn closure_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
46    // SAFETY: caller guarantees `payload` points at an initialized ClosurePayload.
47    let p = unsafe { &*(payload as *const ClosurePayload) };
48    for captured in p.env.iter() {
49        tracer.trace(*captured);
50    }
51}
52
53unsafe fn closure_drop(payload: *mut u8) {
54    // SAFETY: caller guarantees `payload` points at an initialized ClosurePayload.
55    // `drop_in_place` frees the env Vec; the fn_ptr is not owned (it's JIT code).
56    unsafe { std::ptr::drop_in_place(payload as *mut ClosurePayload) };
57}
58
59unsafe fn closure_format(payload: *const u8, out: &mut FormatSink<'_>) {
60    // SAFETY: caller guarantees `payload` points at an initialized ClosurePayload.
61    let p = unsafe { &*(payload as *const ClosurePayload) };
62    // Closures have no source-level printable form; render as `<closure>` with
63    // the capture count for debugging.
64    let _ = write!(out, "<closure:{}>", p.env.len());
65}
66
67/// Descriptor for the `Closure` value type (§4.10). Closures are never
68/// equatable or hashable (§5.5: function values have no structural identity).
69pub static CLOSURE: TypeDescriptor = TypeDescriptor::builtin::<ClosurePayload>(
70    BuiltinTypeId::Closure,
71    "Closure",
72    closure_trace,
73    closure_drop,
74    closure_format,
75    None,
76    None,
77    // No container order: a closure is not even equatable, so it can never be a
78    // `Map` key or a `Set` member and nothing ever has to order one (ADR-138).
79    None,
80)
81.with_owned_bytes(closure_owned_bytes);
82
83/// The heap bytes a closure owns beyond its payload, for GC pacing.
84/// `capacity`, not `len`: the buffer's real footprint is what the collector is
85/// paced against.
86///
87/// # Safety
88/// `payload` must point at an initialized `ClosurePayload`.
89unsafe fn closure_owned_bytes(payload: *const u8) -> usize {
90    // SAFETY: caller guarantees `payload` points at an initialized ClosurePayload.
91    let p = unsafe { &*(payload as *const ClosurePayload) };
92    p.env.capacity() * std::mem::size_of::<GcRef>()
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn closure_descriptor_reports_capabilities() {
101        // Closures are never equatable/hashable (§5.5).
102        assert!(!CLOSURE.is_equatable());
103        assert!(!CLOSURE.is_hashable());
104        assert_eq!(CLOSURE.name, "Closure");
105        assert_eq!(CLOSURE.as_builtin(), Some(BuiltinTypeId::Closure));
106    }
107}