1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! Owned capture of process exit values.
//!
//! A process heap is torn down shortly after the process exits, but exit
//! results and exceptions are read later — by `run_until_exit` callers, the
//! CLI formatter, and supervision diagnostics. Storing raw heap terms at the
//! exit boundary therefore produces dangling pointers. This module deep-copies
//! exit values into self-owned allocations while the source heap is still
//! alive, so consumers can hold them indefinitely.
use crate::atom::Atom;
use crate::ets::copy::{OwnedTerm, copy_term_to_ets};
use crate::process::Exception;
use crate::term::Term;
/// Deep-copies `term` into self-owned storage.
///
/// Falls back to the `undefined` atom for terms that cannot be represented
/// outside their source heap (for example an exotic boxed kind the copier
/// does not support); exit reporting must never tear down the scheduler.
pub(super) fn capture_term(term: Term) -> OwnedTerm {
copy_term_to_ets(term).unwrap_or_else(|_| OwnedTerm::immediate(Term::atom(Atom::UNDEFINED)))
}
/// One stacktrace frame pre-resolved to names at exit time.
///
/// The raw frames recorded by `raise_exception` reference pinned modules and
/// instruction pointers that die with the process; resolving them into plain
/// strings at the exit boundary lets diagnostics outlive the heap.
#[derive(Debug)]
pub struct CapturedFrame {
/// Module name.
pub module: String,
/// Function name.
pub function: String,
/// Function arity.
pub arity: u8,
/// Source line, when line info is present in the module.
pub line: Option<u32>,
}
/// An exception whose class, reason, and stacktrace are owned by the capture
/// rather than borrowed from a process heap.
#[derive(Debug)]
pub struct OwnedException {
class: OwnedTerm,
reason: OwnedTerm,
stacktrace: OwnedTerm,
frames: Vec<CapturedFrame>,
}
impl OwnedException {
/// Captures `exception` together with pre-resolved raise-time frames used
/// when the exception itself carries no stacktrace term.
///
/// Only the `threads`-gated scheduler `execution` path captures frame-bearing
/// exceptions; the cooperative build never reaches this constructor.
#[cfg(feature = "threads")]
pub(super) fn capture_with_frames(exception: Exception, frames: Vec<CapturedFrame>) -> Self {
Self {
class: capture_term(exception.class),
reason: capture_term(exception.reason),
stacktrace: capture_term(exception.stacktrace),
frames,
}
}
/// Borrowed [`Exception`] view over the owned terms.
///
/// The returned terms are valid for as long as this capture is alive.
#[must_use]
pub fn view(&self) -> Exception {
Exception {
class: self.class.root(),
reason: self.reason.root(),
stacktrace: self.stacktrace.root(),
}
}
/// Stacktrace frames resolved at the exit boundary.
#[must_use]
pub fn frames(&self) -> &[CapturedFrame] {
&self.frames
}
/// Formats exception details for user-facing diagnostics.
#[must_use]
pub fn format_with_atoms(&self, atom_table: &crate::atom::AtomTable) -> String {
let mut output = self.view().format_with_atoms(atom_table);
if self.view().stacktrace.is_nil() && !self.frames.is_empty() {
output.push_str("\n stacktrace (captured at raise):");
for frame in &self.frames {
output.push_str(&format!(
"\n {}:{}/{}",
frame.module, frame.function, frame.arity
));
if let Some(line) = frame.line {
output.push_str(&format!(" line {line}"));
}
}
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::process::Process;
#[test]
fn captured_list_survives_source_heap_teardown() {
let mut process = Process::new(1, 64);
let heap = process.heap_mut();
let block = heap.alloc_slice(2).expect("heap alloc");
let cons = crate::term::boxed::write_cons(block, Term::small_int(7), Term::NIL)
.expect("cons write");
let captured = capture_term(cons);
drop(process);
let copied = crate::term::boxed::Cons::new(captured.root()).expect("owned cons");
assert_eq!(copied.head().as_small_int(), Some(7));
assert!(copied.tail().is_nil());
}
#[test]
fn captured_exception_views_owned_terms() {
let mut process = Process::new(1, 64);
let heap = process.heap_mut();
let block = heap.alloc_slice(2).expect("heap alloc");
let reason = crate::term::boxed::write_cons(block, Term::small_int(1), Term::NIL)
.expect("cons write");
let exception = Exception {
class: Term::atom(Atom::ERROR),
reason,
stacktrace: Term::NIL,
};
let captured = OwnedException::capture_with_frames(exception, Vec::new());
drop(process);
let view = captured.view();
assert_eq!(view.class, Term::atom(Atom::ERROR));
let copied = crate::term::boxed::Cons::new(view.reason).expect("owned reason");
assert_eq!(copied.head().as_small_int(), Some(1));
}
}