use crate::abi::abi_guard;
use crate::context::DebugLocal;
use crate::debug::DebugFrameEntry;
use crate::gc::GcRef;
use crate::roots::RootSet;
#[derive(Debug)]
pub struct SnapshotFrame {
pub parent: usize,
pub func_name: *const u8,
pub func_name_len: u32,
pub locals: Vec<DebugLocal>,
pub source_span: (u32, u32),
}
#[derive(Debug)]
pub struct CrashSnapshot {
pub frames: Vec<SnapshotFrame>,
pub fault_kind: crate::FaultKind,
}
impl Default for CrashSnapshot {
fn default() -> Self {
CrashSnapshot {
frames: Vec::new(),
fault_kind: crate::FaultKind::None,
}
}
}
impl CrashSnapshot {
pub fn new() -> Self {
CrashSnapshot::default()
}
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
pub fn len(&self) -> usize {
self.frames.len()
}
pub unsafe fn frame_name(&self, i: usize) -> &str {
let f = &self.frames[i];
if f.func_name.is_null() || f.func_name_len == 0 {
return "<unknown>";
}
unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
f.func_name,
f.func_name_len as usize,
))
}
}
}
impl RootSet for CrashSnapshot {
fn push_roots(&self, out: &mut Vec<GcRef>) {
for frame in &self.frames {
out.extend(
frame
.locals
.iter()
.filter_map(|l| l.value.and_then(crate::debug::DebugValue::reference)),
);
}
}
}
#[derive(Debug, Default)]
pub struct SnapshotSlot {
snapshot: Option<CrashSnapshot>,
}
impl SnapshotSlot {
pub fn new() -> Self {
SnapshotSlot::default()
}
pub fn clear(&mut self) {
self.snapshot = None;
}
#[must_use]
pub fn get(&self) -> Option<&CrashSnapshot> {
self.snapshot.as_ref()
}
pub fn take(&mut self) -> Option<CrashSnapshot> {
self.snapshot.take()
}
pub fn is_set(&self) -> bool {
self.snapshot.is_some()
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn praxis_snapshot_debug_chain(ctx: *mut crate::RuntimeContext) {
abi_guard!("praxis_snapshot_debug_chain", ctx, {
if ctx.is_null() {
return;
}
let slot_ptr = unsafe { (*ctx).crash_snapshot };
if slot_ptr.is_null() {
return;
}
if unsafe { (*slot_ptr).is_set() } {
return;
}
let frames = unsafe { (*ctx).debug_frames };
if frames.is_null() {
return;
}
let entries = unsafe { (*frames).claimed() };
if entries.is_empty() {
return;
}
let snapshot = unsafe { copy_stack(entries) };
let kind = unsafe { crate::context::current_fault_kind(ctx) };
let mut s = CrashSnapshot::new();
s.fault_kind = kind;
s.frames = snapshot;
unsafe { (*slot_ptr).snapshot = Some(s) };
})
}
pub(crate) unsafe fn copy_live_chain(ctx: *mut crate::RuntimeContext) -> CrashSnapshot {
let mut snapshot = CrashSnapshot::new();
if ctx.is_null() {
return snapshot;
}
let frames = unsafe { (*ctx).debug_frames };
if frames.is_null() {
return snapshot;
}
let entries = unsafe { (*frames).claimed() };
snapshot.frames = unsafe { copy_stack(entries) };
snapshot
}
unsafe fn copy_stack(entries: &[DebugFrameEntry]) -> Vec<SnapshotFrame> {
let mut out = Vec::with_capacity(entries.len());
for entry in entries.iter().rev() {
let Some(meta) = (unsafe { entry.meta.as_ref() }) else {
continue;
};
let count = meta.local_count as usize;
let locals: Vec<DebugLocal> = if count == 0 {
Vec::new()
} else {
let metas = unsafe { std::slice::from_raw_parts(meta.locals, count) };
let values = unsafe { std::slice::from_raw_parts(entry.values, count) };
metas
.iter()
.zip(values)
.map(|(m, &word)| DebugLocal {
source_name: m.source_name,
name_len: m.name_len,
symbol_id: m.symbol_id,
descriptor: m.descriptor,
value: unsafe { m.read(word) },
type_id: m.type_id,
kind: m.kind,
span_start: m.span_start,
span_end: m.span_end,
callee_name: m.callee_name,
callee_name_len: m.callee_name_len,
})
.collect()
};
out.push(SnapshotFrame {
parent: usize::MAX,
func_name: meta.func_name,
func_name_len: meta.func_name_len,
source_span: (meta.span_start, meta.span_end),
locals,
});
}
for i in 0..out.len().saturating_sub(1) {
out[i].parent = i + 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scalars::{INT, INT_PAYLOAD};
use crate::{LOCAL_KIND_USER, Runtime};
#[test]
fn empty_snapshot_roots_nothing() {
let s = CrashSnapshot::new();
let mut out = Vec::new();
s.push_roots(&mut out);
assert!(out.is_empty());
assert!(s.is_empty());
}
#[test]
fn snapshot_slot_clear_resets() {
let mut slot = SnapshotSlot::new();
assert!(!slot.is_set());
slot.snapshot = Some(CrashSnapshot::new());
assert!(slot.is_set());
slot.clear();
assert!(!slot.is_set());
}
#[test]
fn explicit_collection_preserves_values_held_by_a_crash_snapshot() {
let runtime = Runtime::new();
let value = runtime.heap().alloc_unpaced(INT_PAYLOAD, 42_i64);
let snapshot = CrashSnapshot {
frames: vec![SnapshotFrame {
parent: usize::MAX,
func_name: std::ptr::null(),
func_name_len: 0,
locals: vec![DebugLocal {
callee_name: std::ptr::null(),
callee_name_len: 0,
source_name: std::ptr::null(),
name_len: 0,
symbol_id: 0,
descriptor: &INT as *const _,
value: Some(crate::debug::DebugValue::Reference(value)),
type_id: 0,
kind: LOCAL_KIND_USER,
span_start: 0,
span_end: 0,
}],
source_span: (0, 0),
}],
fault_kind: crate::FaultKind::None,
};
runtime.collect_with(&snapshot);
assert_eq!(runtime.heap().stats().live_count, 1);
assert_eq!(value.as_int(), 42);
}
#[test]
fn a_snapshot_may_be_dropped_after_the_runtime_it_names() {
let snapshot = {
let runtime = Runtime::new();
let value = runtime.heap().alloc_unpaced(INT_PAYLOAD, 42_i64);
CrashSnapshot {
frames: vec![SnapshotFrame {
parent: usize::MAX,
func_name: std::ptr::null(),
func_name_len: 0,
locals: vec![DebugLocal {
callee_name: std::ptr::null(),
callee_name_len: 0,
source_name: std::ptr::null(),
name_len: 0,
symbol_id: 0,
descriptor: &INT as *const _,
value: Some(crate::debug::DebugValue::Reference(value)),
type_id: 0,
kind: LOCAL_KIND_USER,
span_start: 0,
span_end: 0,
}],
source_span: (0, 0),
}],
fault_kind: crate::FaultKind::None,
}
};
assert_eq!(snapshot.len(), 1);
drop(snapshot);
}
#[test]
fn a_reissued_block_is_not_rendered_under_the_dead_locals_name() {
use crate::scalars::FLOAT_PAYLOAD;
let mut rt = Runtime::new();
let dead = rt.heap().alloc_unpaced(INT_PAYLOAD, 9_999_i64);
let address = dead.as_ptr();
let mut ctx = Box::new(rt.context());
let name = b"xs";
let locals = [crate::DebugLocalMeta {
callee_name: std::ptr::null(),
callee_name_len: 0,
source_name: name.as_ptr(),
name_len: 2,
symbol_id: 1,
descriptor: &INT,
type_id: 1,
kind: LOCAL_KIND_USER,
span_start: 0,
span_end: 0,
slot_kind: crate::debug::DebugSlotKind::Reference,
}];
let meta = crate::FunctionDebugMeta {
func_name: b"main".as_ptr(),
func_name_len: 4,
local_count: 1,
locals: locals.as_ptr(),
span_start: 0,
span_end: 0,
};
let mut guard = unsafe { crate::debug::push_frame(&mut *ctx, &meta) };
guard.set(0, dead);
rt.collect_now();
let reissued = rt.heap().alloc_unpaced(FLOAT_PAYLOAD, 2.5_f64);
assert_eq!(
reissued.as_ptr(),
address,
"this test only says anything if the dead local's block came back"
);
unsafe { praxis_snapshot_debug_chain(&mut *ctx) };
drop(guard);
let snapshot = rt.take_crash_snapshot().expect("a frame was claimed");
assert_eq!(snapshot.len(), 1);
let local = &snapshot.frames[0].locals[0];
assert_eq!(
local.value,
Some(crate::debug::DebugValue::Reclaimed),
"the snapshot copied the reissued block under `xs`, whose static \
descriptor is Int — a `Float` rendered as an `Int`, and a strong \
root to it out of `CrashSnapshot::push_roots`"
);
assert_ne!(local.value, None, "a written slot never reads as unwritten");
let mut out = Vec::new();
snapshot.push_roots(&mut out);
assert!(
out.is_empty(),
"an absence must root nothing; a dangling entry would have made \
the snapshot a strong root set for storage it does not own"
);
assert_eq!(reissued.descriptor().name, "Float");
}
}