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}