Skip to main content

otter/types/
mod.rs

1mod binarybuf;
2mod ops;
3pub mod terms;
4mod typed;
5
6pub use binarybuf::BinaryBuf;
7pub use ops::{deserialize, port_command, serialize};
8pub use terms::*;
9pub use typed::TypedTerm;
10
11// enif types surfaced by the `Env` methods below — `term_type` returns
12// `TermType`, `hash` takes `Hash`, `make_unique_integer` takes `UniqueInteger`.
13// Re-exported here so callers can name them without the `raw` feature.
14pub use enif_ffi::{Hash, TermType, UniqueInteger};
15
16// `BigInt` (the `bigint` feature) re-exported as `otter::types::BigInt` so NIF
17// authors share otter's exact `num-bigint` version — the `Encoder`/`Decoder`
18// impls are tied to this crate's `BigInt`, so a semver-incompatible copy would
19// not satisfy them. Only the type is exposed, not the whole `num_bigint` crate.
20#[cfg(feature = "bigint")]
21#[cfg_attr(docsrs, doc(cfg(feature = "bigint")))]
22pub use num_bigint::BigInt;
23
24
25use core::marker::PhantomData;
26use core::sync::atomic::{AtomicU64, Ordering};
27
28mod sealed {
29    pub trait Sealed {}
30}
31
32/// The brand marker carried by every env and term: a zero-sized type that is
33/// *invariant* over the lifetime `'id`. Invariance is what makes a brand
34/// generative — two brands minted by different `for<'id>` entry points can never
35/// be unified, so a term cannot leak from the env that produced it.
36pub type Invariant<'id> = PhantomData<*mut &'id ()>;
37
38// --- Env ---
39
40pub(crate) type RawEnv = *mut enif_ffi::Env;
41
42/// A handle to a BEAM environment, tagged with the generative brand `'id`.
43///
44/// `Env` is the central lifetime-safety mechanism. It is a sealed, `Copy` trait
45/// implemented by each env *kind* ([`CallEnv`], [`InitEnv`], [`CallbackEnv`],
46/// [`DeinitEnv`], [`OwnedEnv`], and the kind-erased [`AnyEnv`]). The brand `'id`
47/// is minted fresh and non-escaping for each entry point, so terms branded by
48/// one env are rejected at compile time when used with another — see the
49/// [crate-level overview](crate#the-mental-model-branded-envs-and-lazy-terms).
50///
51/// The methods here are the *generic verbs* valid on any env kind. Context-
52/// specific verbs live inherent on the kind that carries the context
53/// ([`CallEnv::raise`], [`InitEnv::set_option_delay_halt`], …).
54pub trait Env<'id>: Copy + sealed::Sealed {
55    /// The raw `*mut enif_ffi::Env` pointer this handle wraps.
56    fn raw_env(&self) -> RawEnv;
57
58    /// The kind-erased handle terms are built against. Every env kind downcasts
59    /// to the same `AnyEnv` for a given brand.
60    fn as_any_env(self) -> AnyEnv<'id> {
61        AnyEnv { raw_env: self.raw_env(), _id: PhantomData }
62    }
63
64    /// The dynamic type of `term` (`enif_term_type`). `None` for a type code
65    /// this otter build does not recognize (a newer-OTP type).
66    fn term_type(self, term: impl Term<'id>) -> Option<enif_ffi::TermType> {
67        let code = unsafe { enif_ffi::term_type(self.raw_env(), term.raw_term()) };
68        enif_ffi::TermType::from_raw(code)
69    }
70
71    /// Hash a term (`enif_hash`). `algorithm` is `Phash2` (portable) or
72    /// `InternalHash` (node-local, faster).
73    fn hash(self, algorithm: enif_ffi::Hash, term: impl Term<'id>, salt: u64) -> u64 {
74        unsafe { enif_ffi::hash(algorithm, term.raw_term(), salt) }
75    }
76
77    /// Tell the scheduler how much of the timeslice this NIF used
78    /// (`enif_consume_timeslice`). `true` if the timeslice is exhausted.
79    fn consume_timeslice(self, percent: i32) -> bool {
80        unsafe { enif_ffi::consume_timeslice(self.raw_env(), percent) != 0 }
81    }
82
83    /// Whether the calling process is still alive
84    /// (`enif_is_current_process_alive`).
85    fn is_current_process_alive(self) -> bool {
86        unsafe { enif_ffi::is_current_process_alive(self.raw_env()) != 0 }
87    }
88
89    /// Create a unique integer (`enif_make_unique_integer`). `properties` is a
90    /// bitmask of `UniqueInteger::POSITIVE` / `MONOTONIC`.
91    fn make_unique_integer(self, properties: enif_ffi::UniqueInteger) -> Integer<'id> {
92        let raw = unsafe { enif_ffi::make_unique_integer(self.raw_env(), properties) };
93        Integer::from_raw(raw)
94    }
95}
96
97/// The kind-erased environment handle terms are built against. Every concrete
98/// env kind ([`CallEnv`], [`InitEnv`], …) downcasts to this via
99/// [`Env::as_any_env`].
100#[derive(Clone, Copy)]
101pub struct AnyEnv<'id> {
102    raw_env: RawEnv,
103    _id: Invariant<'id>,
104}
105
106impl<'id> sealed::Sealed for AnyEnv<'id> {}
107
108impl<'id> Env<'id> for AnyEnv<'id> {
109    fn raw_env(&self) -> RawEnv {
110        self.raw_env
111    }
112}
113
114// --- VM-provided env kinds ---
115//
116// The VM hands a NIF call or callback a raw environment pointer; codegen wraps
117// it in the matching concrete kind so context-specific verbs are typed — wrong
118// kind is a compile error, no runtime tag (this is the lifted `EnvKind`). Each
119// kind is structurally an `AnyEnv`; they differ only as types.
120//
121// Each `with_*` entry mints the brand through a `for<'id>` closure, so every
122// call gets a unique, non-escaping brand (the GhostCell construction); `R` is
123// brand-free by construction — exactly the raw `Term`/status word the C ABI
124// returns.
125//
126// # Safety (all `with_*` entries)
127// `raw` must be the live environment pointer the VM supplied for this callback,
128// used only for the duration of `f`.
129
130/// The process-bound environment handed to a NIF call.
131#[derive(Clone, Copy)]
132pub struct CallEnv<'id> {
133    raw_env: RawEnv,
134    _id: Invariant<'id>,
135}
136
137impl<'id> sealed::Sealed for CallEnv<'id> {}
138
139impl<'id> Env<'id> for CallEnv<'id> {
140    fn raw_env(&self) -> RawEnv {
141        self.raw_env
142    }
143}
144
145impl CallEnv<'_> {
146    /// Enter a NIF call with a freshly branded [`CallEnv`]. See the env-kind
147    /// note above for the brand guarantee.
148    ///
149    /// # Safety
150    /// `raw` must be the live env pointer the VM supplied for this call.
151    pub unsafe fn with_raw<R>(raw: RawEnv, f: impl for<'id> FnOnce(CallEnv<'id>) -> R) -> R {
152        f(CallEnv { raw_env: raw, _id: PhantomData })
153    }
154}
155
156/// The environment handed to the `load` and `upgrade` callbacks.
157#[derive(Clone, Copy)]
158pub struct InitEnv<'id> {
159    raw_env: RawEnv,
160    _id: Invariant<'id>,
161}
162
163impl<'id> sealed::Sealed for InitEnv<'id> {}
164
165impl<'id> Env<'id> for InitEnv<'id> {
166    fn raw_env(&self) -> RawEnv {
167        self.raw_env
168    }
169}
170
171impl InitEnv<'_> {
172    /// Enter a `load`/`upgrade` callback with a freshly branded [`InitEnv`].
173    ///
174    /// # Safety
175    /// `raw` must be the live env pointer the VM supplied for this callback.
176    pub unsafe fn with_raw<R>(raw: RawEnv, f: impl for<'id> FnOnce(InitEnv<'id>) -> R) -> R {
177        f(InitEnv { raw_env: raw, _id: PhantomData })
178    }
179}
180
181/// The environment handed to resource callbacks (destructor, monitor-down, …).
182#[derive(Clone, Copy)]
183pub struct CallbackEnv<'id> {
184    raw_env: RawEnv,
185    _id: Invariant<'id>,
186}
187
188impl<'id> sealed::Sealed for CallbackEnv<'id> {}
189
190impl<'id> Env<'id> for CallbackEnv<'id> {
191    fn raw_env(&self) -> RawEnv {
192        self.raw_env
193    }
194}
195
196impl CallbackEnv<'_> {
197    /// Enter a resource callback with a freshly branded [`CallbackEnv`].
198    ///
199    /// # Safety
200    /// `raw` must be the live env pointer the VM supplied for this callback.
201    pub unsafe fn with_raw<R>(raw: RawEnv, f: impl for<'id> FnOnce(CallbackEnv<'id>) -> R) -> R {
202        f(CallbackEnv { raw_env: raw, _id: PhantomData })
203    }
204}
205
206/// The environment handed to the `unload` callback.
207#[derive(Clone, Copy)]
208pub struct DeinitEnv<'id> {
209    raw_env: RawEnv,
210    _id: Invariant<'id>,
211}
212
213impl<'id> sealed::Sealed for DeinitEnv<'id> {}
214
215impl<'id> Env<'id> for DeinitEnv<'id> {
216    fn raw_env(&self) -> RawEnv {
217        self.raw_env
218    }
219}
220
221impl DeinitEnv<'_> {
222    /// Enter the `unload` callback with a freshly branded [`DeinitEnv`].
223    ///
224    /// # Safety
225    /// `raw` must be the live env pointer the VM supplied for this callback.
226    pub unsafe fn with_raw<R>(raw: RawEnv, f: impl for<'id> FnOnce(DeinitEnv<'id>) -> R) -> R {
227        f(DeinitEnv { raw_env: raw, _id: PhantomData })
228    }
229}
230
231fn send_move_(env: RawEnv, pid: &LocalPid, msg_env: &mut OwnedEnvArena, msg: OwnedEnvTerm) -> bool {
232    assert!(!msg_env.is_dirty);
233    let msg = msg_env.unwrap_term(msg);
234    let ok = unsafe { enif_ffi::send(env, &pid.pid, msg_env.env, msg) != 0 };
235    msg_env.is_dirty = ok;
236    ok
237}
238
239fn send_copy_<'a>(env: RawEnv, pid: &LocalPid, msg: impl Term<'a>) -> bool {
240    unsafe { enif_ffi::send(env, &pid.pid, std::ptr::null_mut(), msg.raw_term()) != 0 }
241}
242
243// Sending (`enif_send`) is a 2×2 of independent choices, exposed as four free
244// verbs. They are verbs, not methods: the env and pid are ingredients of the
245// operation, not its owner.
246//
247// * **`_copy` vs `_move`** — how the payload reaches the recipient. `_copy`
248//   copies a live term into the recipient's mailbox (`enif_send` with a NULL
249//   `msg_env`). `_move` transplants (steals) an entire [`OwnedEnvArena`] heap
250//   into the message (`enif_send` with the arena as `msg_env`) — O(1), no copy;
251//   the arena is left dirty and must be [`clear`](OwnedEnvArena::clear)ed before
252//   reuse.
253// * **plain vs `_from`** — who the message is attributed to. The plain verbs
254//   send with a NULL `caller_env`, for use from a non-scheduler thread that has
255//   no process context. The `_from` verbs take a [`CallingEnv`] (a live NIF call
256//   or callback) as the `caller_env`, so the BEAM attributes the message to the
257//   calling process (send-trace, seq-trace, reductions, process-aware enqueue).
258//
259// All four return `true` if the message was delivered (the target was alive),
260// `false` otherwise, mirroring `enif_send`.
261
262/// Steal-send `msg` to `pid` from a non-scheduler thread (NULL caller).
263///
264/// Transplants `msg_env`'s entire heap into the message in O(1) — no copy — then
265/// leaves the arena dirty; [`clear`](OwnedEnvArena::clear) it before reusing it.
266/// `msg` must have been [`export`](OwnedEnv::export)ed from *this* arena.
267///
268/// The off-thread counterpart to [`send_move_from`]. Use that instead from
269/// inside a NIF to attribute the message to the calling process.
270pub fn send_move(pid: &LocalPid, msg_env: &mut OwnedEnvArena, msg: OwnedEnvTerm) -> bool {
271    send_move_(std::ptr::null_mut(), pid, msg_env, msg)
272}
273
274/// Copy-send a live term `msg` to `pid` from a non-scheduler thread (NULL
275/// caller).
276///
277/// Copies `msg` into the recipient's mailbox (`enif_send`, NULL `msg_env`). The
278/// off-thread counterpart to [`send_copy_from`]; use that from inside a NIF to
279/// attribute the message to the calling process. To steal an owned-env heap
280/// instead of copying, use [`send_move`].
281pub fn send_copy<'a>(pid: &LocalPid, msg: impl Term<'a>) -> bool {
282    send_copy_(std::ptr::null_mut(), pid, msg)
283}
284
285/// Steal-send `msg` to `pid` from inside a NIF, attributed to the calling
286/// process.
287///
288/// Like [`send_move`] (transplants `msg_env`'s heap in O(1), leaving the arena
289/// dirty), but passes `calling_env` as the `caller_env`, so the BEAM attributes
290/// the message to the calling process. `calling_env` must be a live
291/// [`CallEnv`]/[`CallbackEnv`]; `msg` must have been
292/// [`export`](OwnedEnv::export)ed from *this* arena.
293pub fn send_move_from<'id>(calling_env: impl CallingEnv<'id>, pid: &LocalPid, msg_env: &mut OwnedEnvArena, msg: OwnedEnvTerm) -> bool {
294    send_move_(calling_env.raw_env(), pid, msg_env, msg)
295}
296
297/// Copy-send a live term `msg` to `pid` from inside a NIF, attributed to the
298/// calling process.
299///
300/// Like [`send_copy`] (copies `msg` into the recipient's mailbox), but passes
301/// `calling_env` as the `caller_env`, so the BEAM attributes the message to the
302/// calling process. `calling_env` must be a live [`CallEnv`]/[`CallbackEnv`];
303/// `msg` is a term of any brand. This is the common in-NIF send.
304pub fn send_copy_from<'id, 'a>(calling_env: impl CallingEnv<'id>, pid: &LocalPid, msg: impl Term<'a>) -> bool {
305    send_copy_(calling_env.raw_env(), pid, msg)
306}
307
308/// The env kinds that carry a live process/scheduler context — those you can
309/// send a message or issue a port command *from*, with caller attribution
310/// (`enif_send`/`enif_port_command` with a non-NULL `caller_env`). Grouping
311/// trait for verbs that accept any such env.
312///
313/// Implemented by [`CallEnv`] and [`CallbackEnv`]. Not [`InitEnv`]/[`DeinitEnv`]
314/// (module load/unload — no caller), and not [`OwnedEnv`] (sends with a NULL
315/// caller instead). Sealed transitively through [`Env`].
316pub trait CallingEnv<'id>: Env<'id> {}
317
318impl<'id> CallingEnv<'id> for CallEnv<'id> {}
319impl<'id> CallingEnv<'id> for CallbackEnv<'id> {}
320
321// --- Exceptions ---
322
323/// Proof that an exception is pending on a [`CallEnv`].
324///
325/// A term-less typestate token: it can only be produced by an operation that
326/// raises or detects a pending exception, so holding one means the env is in the
327/// pending-exception state in which no further env operation is valid. Propagate
328/// it straight out of the NIF with `?`; the generated wrapper returns the
329/// non-value word and the BEAM raises the pending exception. (No term is kept —
330/// detection via `has_pending_exception` yields no reason term, and the returned
331/// word is ignored once an exception is pending.)
332pub struct Raised<'id> {
333    _id: Invariant<'id>,
334}
335
336impl<'id> CallEnv<'id> {
337    /// Raise an exception with `reason` (`enif_raise_exception`). Always `Err`,
338    /// generic over the success type so it fits any position:
339    /// `return env.raise(reason)`.
340    pub fn raise<T>(self, reason: impl Term<'id>) -> Result<T, Raised<'id>> {
341        unsafe { enif_ffi::raise_exception(self.raw_env(), reason.raw_term()) };
342        Err(Raised { _id: PhantomData })
343    }
344
345    /// Raise a `badarg` error (`enif_make_badarg`). Always `Err`.
346    pub fn badarg<T>(self) -> Result<T, Raised<'id>> {
347        unsafe { enif_ffi::make_badarg(self.raw_env()) };
348        Err(Raised { _id: PhantomData })
349    }
350
351    /// If the env has a pending exception, return `Err(Raised)`; otherwise
352    /// `Ok(term)` (`enif_has_pending_exception`). The safe way to call a `raw`
353    /// enif function that may raise: pass its result straight through.
354    pub fn check_raised(self, term: AnyTerm<'id>) -> Result<AnyTerm<'id>, Raised<'id>> {
355        if unsafe { enif_ffi::has_pending_exception(self.raw_env(), std::ptr::null_mut()) } != 0 {
356            Err(Raised { _id: PhantomData })
357        } else {
358            Ok(term)
359        }
360    }
361}
362
363// --- Term ---
364
365pub(crate) type RawTerm = enif_ffi::Term;
366
367/// The BEAM's non-value marker (`THE_NON_VALUE`). Returned from a NIF whose
368/// `Result` raised: the word is ignored once an exception is pending, and the
369/// BEAM raises the pending exception on return.
370pub(crate) const THE_NON_VALUE: RawTerm = 0;
371
372/// A BEAM term branded to the env `'id` that produced it.
373///
374/// `Term` is the universal term-input trait: every otter term type implements it,
375/// and functions that accept a term take `impl Term<'id>`, so you pass concrete
376/// types directly (no `.encode()`) and a term of the wrong brand fails to
377/// compile. Sealed — it cannot be implemented outside the crate.
378///
379/// Env-portable types ([`Atom`], [`LocalPid`], [`LocalPort`]) implement
380/// [`FreeTerm`] and so satisfy an `impl Term<'id>` slot for *every* brand;
381/// env-bound types carry only their own brand. The brand constraint is
382/// load-bearing: the BEAM treats a cross-env term as undefined behavior.
383pub trait Term<'id>: sealed::Sealed {
384    /// The raw machine word backing this term.
385    fn raw_term(self) -> RawTerm;
386
387    /// Copy this term into another environment (`enif_make_copy`), producing a
388    /// term branded to the destination. The general cross-env copy — distinct
389    /// from same-brand [`Encoder`](crate::codec::Encoder) (which wraps for free)
390    /// and from [`OwnedEnvArena`] `copy_out` (the arena exit).
391    fn copy_to<'dst>(self, env: impl Env<'dst>) -> AnyTerm<'dst>
392    where
393        Self: Sized,
394    {
395        let raw = unsafe { enif_ffi::make_copy(env.raw_env(), self.raw_term()) };
396        AnyTerm::wrap(raw, env)
397    }
398}
399
400/// Marker for env-portable terms: those valid in *any* env, hence branded for
401/// every `'id` at once. Implemented by [`Atom`], [`LocalPid`], and [`LocalPort`]
402/// — tagged immediates and locality-validated handles with no heap data to
403/// outlive an env. A `FreeTerm` satisfies an `impl Term<'id>` argument for any
404/// brand.
405pub trait FreeTerm: for<'id> Term<'id> {}
406
407/// The bare term word, carrying only the brand `'id` — the env is *not* stored.
408///
409/// The fastest term representation: no `enif_term_type` call has been made and no
410/// data has been read off the BEAM heap. Resolve it to a known type with
411/// `resolve(env)` (yielding a [`TypedTerm`]), or decode it directly.
412/// `#[repr(transparent)]` over the raw word (the brand is a ZST), so a
413/// `&[RawTerm]` can be viewed in place as `&[AnyTerm<'id>]` (used by
414/// [`TupleView`]).
415#[derive(Clone, Copy)]
416#[repr(transparent)]
417pub struct AnyTerm<'id> {
418    raw_term: RawTerm,
419    _id: Invariant<'id>,
420}
421
422impl<'id> AnyTerm<'id> {
423    #[crate::raw]
424    pub(crate) fn wrap(raw_term: RawTerm, _env: impl Env<'id>) -> Self {
425        Self { raw_term, _id: PhantomData }
426    }
427}
428
429impl sealed::Sealed for AnyTerm<'_> {}
430
431impl<'id> Term<'id> for AnyTerm<'id> {
432    fn raw_term(self) -> RawTerm {
433        self.raw_term
434    }
435}
436
437
438/// Process-global source of arena generation stamps. Handed out monotonically,
439/// so every `(arena, post-clear state)` gets a value no other arena ever holds.
440/// Wrap is unreachable at 2^64 stamps.
441static GENERATION: AtomicU64 = AtomicU64::new(1);
442
443fn next_generation() -> u64 {
444    GENERATION.fetch_add(1, Ordering::Relaxed)
445}
446
447/// A portable handle to a term stored in an [`OwnedEnvArena`]. `Copy` and
448/// unbranded so it can cross the `run` closure / be carried to a worker thread;
449/// its `version` is a globally-unique generation stamp, so it is only valid
450/// against the exact arena-generation that produced it.
451#[derive(Clone, Copy)]
452pub struct OwnedEnvTerm {
453    version: u64,
454    term: RawTerm,
455}
456
457/// A reusable, process-independent environment for building messages outside a
458/// NIF call (e.g. on a spawned OS thread).
459///
460/// Wraps an `enif_alloc_env` heap. Build terms inside [`run`](Self::run) — the
461/// closure's branded [`OwnedEnv`] keeps them from escaping — and
462/// [`export`](OwnedEnv::export) the one you want to a portable [`OwnedEnvTerm`].
463/// Then either copy-send it ([`send_copy`]) or steal-send the whole arena heap in
464/// O(1) ([`send_move`], which leaves the arena dirty until you [`clear`](Self::clear)
465/// it). The arena is reusable: `clear` wipes the heap and bumps the generation
466/// stamp, invalidating every term exported before it.
467pub struct OwnedEnvArena {
468    env: RawEnv,
469    version: u64,
470    is_dirty: bool,
471}
472
473impl Default for OwnedEnvArena {
474    fn default() -> Self {
475        Self::new()
476    }
477}
478
479impl OwnedEnvArena {
480    /// Allocate a fresh arena (`enif_alloc_env`). Panics if the VM returns null.
481    /// Also available as [`Default`].
482    pub fn new() -> Self {
483        let env = unsafe { enif_ffi::alloc_env() };
484        assert!(!env.is_null(), "enif_alloc_env returned null");
485        OwnedEnvArena { env, version: next_generation(), is_dirty: false }
486    }
487
488    /// Drop all stored terms and wipe the env heap, in lockstep, for reuse.
489    /// Takes a fresh generation stamp so every term stored before the clear is
490    /// invalidated.
491    pub fn clear(&mut self) {
492        unsafe { enif_ffi::clear_env(self.env) };
493        self.version = next_generation();
494        self.is_dirty = false;
495    }
496
497    /// Copy a term from any env into this arena (`enif_make_copy`), returning a
498    /// portable [`OwnedEnvTerm`]. The standalone counterpart to
499    /// [`OwnedEnv::export`] when you are not inside [`run`](Self::run).
500    pub fn copy_in<'a>(&mut self, term: impl Term<'a>) -> OwnedEnvTerm {
501        assert!(!self.is_dirty);
502        self.wrap_term(unsafe { enif_ffi::make_copy(self.env, term.raw_term()) })
503    }
504
505    /// Copy a stored [`OwnedEnvTerm`] out into `env` (`enif_make_copy`),
506    /// re-branding it to `env`'s `'id`. Panics if `oterm` does not belong to this
507    /// arena-generation.
508    pub fn copy_out<'a>(&self, oterm: OwnedEnvTerm, env: impl Env<'a>) -> AnyTerm<'a> {
509        assert!(!self.is_dirty);
510        let remote_term = unsafe { enif_ffi::make_copy(env.raw_env(), self.unwrap_term(oterm)) };
511        AnyTerm { raw_term: remote_term, _id: PhantomData }
512    }
513
514    fn wrap_term(&self, raw_term: RawTerm) -> OwnedEnvTerm {
515        assert!(!self.is_dirty);
516        OwnedEnvTerm { version: self.version, term: raw_term }
517    }
518
519    fn unwrap_term(&self, oterm: OwnedEnvTerm) -> RawTerm {
520        assert!(!self.is_dirty);
521        // The generation stamp is globally unique, so a matching version
522        // identifies this exact arena-generation — and since an arena's env
523        // pointer is fixed for its life, equal versions imply the same env. The
524        // version check alone is therefore sufficient (no env-pointer compare,
525        // which could otherwise alias a freed-then-reused env).
526        assert!(self.version == oterm.version, "OwnedEnvTerm used with a different arena or after clear");
527        oterm.term
528    }
529
530    /// Run `f` with a freshly branded [`OwnedEnv`] over this arena. Build terms
531    /// inside the closure and [`export`](OwnedEnv::export) any you need to keep;
532    /// the brand prevents a live term from escaping in `R`. Panics if the arena
533    /// is dirty (steal-sent but not yet [`clear`](Self::clear)ed).
534    pub fn run<R>(&mut self, f: impl for<'id> FnOnce(OwnedEnv<'_, 'id>) -> R) -> R {
535        assert!(!self.is_dirty);
536        f(OwnedEnv { owner: self, _id: PhantomData })
537    }
538}
539
540impl Drop for OwnedEnvArena {
541    fn drop(&mut self) {
542        unsafe { enif_ffi::free_env(self.env) };
543    }
544}
545
546
547/// A branded handle to a process-independent environment, handed to the closure
548/// by [`OwnedEnvArena::run`]. It is an [`Env`] like any other kind — generic verbs
549/// take `impl Env<'id>` — and additionally carries the owned-env-specific verbs
550/// `export`/`import` for moving terms across the closure boundary.
551#[derive(Clone, Copy)]
552pub struct OwnedEnv<'a, 'id> {
553    owner: &'a OwnedEnvArena,
554    _id: Invariant<'id>,
555}
556
557impl<'a, 'id> OwnedEnv<'a, 'id> {
558    /// Save a term built in this env to a portable [`OwnedEnvTerm`] that can
559    /// outlive the [`run`](OwnedEnvArena::run) closure (e.g. to steal-send later).
560    /// The term must already live in this arena's heap.
561    pub fn export(self, term: impl Term<'id>) -> OwnedEnvTerm {
562        self.owner.wrap_term(term.raw_term())
563    }
564
565    /// Recover a branded [`AnyTerm`] from an [`OwnedEnvTerm`] previously
566    /// [`export`](Self::export)ed from this same arena-generation. Panics if it
567    /// belongs to a different arena or a pre-[`clear`](OwnedEnvArena::clear) state.
568    pub fn import(self, oterm: OwnedEnvTerm) -> AnyTerm<'id> {
569        AnyTerm { raw_term: self.owner.unwrap_term(oterm), _id: PhantomData }
570    }
571}
572
573impl<'a, 'id> sealed::Sealed for OwnedEnv<'a, 'id> {}
574
575impl<'a, 'id> Env<'id> for OwnedEnv<'a, 'id> {
576    fn raw_env(&self) -> RawEnv {
577        self.owner.env
578    }
579}
580
581#[cfg(test)]
582mod brand_tests {
583    use super::*;
584
585    // Ties an env and a term to ONE brand: only a term from this env type-checks.
586    fn use_in<'id>(_env: impl Env<'id>, t: AnyTerm<'id>) -> RawTerm {
587        t.raw_term()
588    }
589
590    // Two independently branded envs are distinct: a term from env 1 used with
591    // env 2's brand is rejected at compile time — the 1:1 brand↔identity
592    // guarantee. The same-brand call compiles; uncomment the cross-brand line to
593    // re-verify rejection ("borrowed data escapes outside of closure": the two
594    // invariant brands from the nested `for<'id>` closures cannot be unified).
595    #[allow(dead_code)]
596    fn brands_are_distinct() {
597        unsafe {
598            CallEnv::with_raw(std::ptr::null_mut(), |e1| {
599                CallEnv::with_raw(std::ptr::null_mut(), |e2| {
600                    let t1 = AnyTerm::wrap(0, e1);
601                    let _ = use_in(e1, t1); // same brand: compiles
602                    let _ = e2;
603                    // let _ = use_in(e2, t1); // cross brand: rejected
604                });
605            });
606        }
607    }
608}
609
610#[cfg(test)]
611mod owned_env_tests {
612    use super::*;
613
614    // Positive: build a term in the scope, save it, return the index. Compiles.
615    #[allow(dead_code)]
616    fn positive(b: &mut OwnedEnvArena) -> OwnedEnvTerm {
617        b.run(|env| {
618            let t = AnyTerm::wrap(0, env);
619            env.export(t)
620        })
621    }
622
623    // Escape test — verified to FAIL with "lifetime may not live long enough"
624    // (`AnyTerm<'id>` is invariant over `'id`, so the rigid brand can't leak
625    // into `run`'s return type `R`). Kept commented so the crate builds; uncomment
626    // to re-verify.
627    //
628    // fn escape(b: &mut OwnedEnvArena) {
629    //     let _leaked = b.run(|env| env.import(OwnedEnvTerm { version: b.version, term: 0 }));
630    // }
631}