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}