use crate::crash_snapshot::{CrashSnapshot, SnapshotSlot};
use crate::debug::{
DEBUG_FRAME_STACK_SLOTS, DEBUG_VALUE_STACK_SLOTS, DebugFrameEntry, DebugFrameStack,
DebugFrameStackHeader, DebugValueStack, DebugValueStackHeader,
};
use crate::gc::GcRef;
use crate::heap::Heap;
use crate::immortal::{Immortals, read_bool};
use crate::parse_detail::ParseDetail;
#[cfg(test)]
use crate::roots::RootSet;
use crate::shadow_stack::{SHADOW_STACK_SLOTS, ShadowStack, ShadowStackHeader};
use crate::{
collections::VecPayload,
descriptor::{BuiltinTypeId, TypeDescriptor, builtin_descriptor_addresses},
};
pub const FRAME_BYTES_BASE: u32 = 160;
pub const FRAME_BYTES_PER_SLOT: u32 = 2;
pub const MAX_RECURSION_DEPTH: u32 = 8000;
pub const REFERENCE_FRAME_SLOTS: u32 = 11;
pub const STACK_BUDGET_BYTES: u32 = MAX_RECURSION_DEPTH * FRAME_BYTES_BASE;
#[must_use]
pub const fn frame_cost(slots: u32) -> u32 {
let over = slots.saturating_sub(REFERENCE_FRAME_SLOTS);
FRAME_BYTES_BASE.saturating_add(FRAME_BYTES_PER_SLOT.saturating_mul(over))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StackBudget(u32);
impl StackBudget {
pub const DEFAULT: StackBudget = StackBudget(STACK_BUDGET_BYTES);
#[must_use]
pub const fn new(bytes: u32) -> Option<StackBudget> {
if bytes <= STACK_BUDGET_BYTES {
Some(StackBudget(bytes))
} else {
None
}
}
#[must_use]
pub const fn get(self) -> u32 {
self.0
}
}
impl Default for StackBudget {
fn default() -> Self {
StackBudget::DEFAULT
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FaultKind {
None = 0,
IntOverflow = 1,
DivByZero = 2,
IndexOutOfBounds = 3,
ParseFailed = 4,
EmptyCollection = 5,
StackOverflow = 6,
FloatToInt = 7,
InvalidChar = 8,
InvalidText = 9,
InvalidSize = 10,
TypeMismatch = 11,
Panic = 12,
AssertFailed = 13,
EmptyRange = 14,
NoAnswer = 15,
}
#[derive(Debug, Default)]
pub struct FaultMessage {
text: Option<String>,
}
impl FaultMessage {
#[must_use]
pub fn new() -> FaultMessage {
FaultMessage { text: None }
}
pub fn set(&mut self, text: String) {
self.text = Some(text);
}
#[must_use]
pub fn get(&self) -> Option<&str> {
self.text.as_deref()
}
pub fn clear(&mut self) {
self.text = None;
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct RaisedFault(FaultKind);
impl RaisedFault {
pub const INT_OVERFLOW: RaisedFault = RaisedFault(FaultKind::IntOverflow);
pub const DIV_BY_ZERO: RaisedFault = RaisedFault(FaultKind::DivByZero);
pub const INDEX_OUT_OF_BOUNDS: RaisedFault = RaisedFault(FaultKind::IndexOutOfBounds);
pub const PARSE_FAILED: RaisedFault = RaisedFault(FaultKind::ParseFailed);
pub const EMPTY_COLLECTION: RaisedFault = RaisedFault(FaultKind::EmptyCollection);
pub const STACK_OVERFLOW: RaisedFault = RaisedFault(FaultKind::StackOverflow);
pub const FLOAT_TO_INT: RaisedFault = RaisedFault(FaultKind::FloatToInt);
pub const INVALID_CHAR: RaisedFault = RaisedFault(FaultKind::InvalidChar);
pub const INVALID_TEXT: RaisedFault = RaisedFault(FaultKind::InvalidText);
pub const INVALID_SIZE: RaisedFault = RaisedFault(FaultKind::InvalidSize);
pub const TYPE_MISMATCH: RaisedFault = RaisedFault(FaultKind::TypeMismatch);
pub const PANIC: RaisedFault = RaisedFault(FaultKind::Panic);
pub const ASSERT_FAILED: RaisedFault = RaisedFault(FaultKind::AssertFailed);
pub const EMPTY_RANGE: RaisedFault = RaisedFault(FaultKind::EmptyRange);
pub const NO_ANSWER: RaisedFault = RaisedFault(FaultKind::NoAnswer);
#[must_use]
pub const fn new(kind: FaultKind) -> Option<RaisedFault> {
match kind {
FaultKind::None => None,
raisable => Some(RaisedFault(raisable)),
}
}
#[inline]
#[must_use]
pub const fn kind(self) -> FaultKind {
self.0
}
}
impl std::fmt::Display for FaultKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FaultKind::None => write!(f, "no fault"),
FaultKind::IntOverflow => write!(f, "integer overflow"),
FaultKind::DivByZero => write!(f, "division by zero"),
FaultKind::IndexOutOfBounds => write!(f, "index out of bounds"),
FaultKind::ParseFailed => write!(f, "input parse mismatch"),
FaultKind::EmptyCollection => write!(f, "empty collection"),
FaultKind::StackOverflow => write!(f, "stack overflow (recursion limit)"),
FaultKind::FloatToInt => write!(f, "float-to-int conversion out of range"),
FaultKind::InvalidChar => write!(f, "not a Unicode scalar value"),
FaultKind::InvalidText => write!(f, "invalid UTF-8 in Text"),
FaultKind::InvalidSize => write!(f, "size or extent out of range"),
FaultKind::TypeMismatch => write!(f, "value does not have the declared type"),
FaultKind::Panic => write!(f, "panic"),
FaultKind::AssertFailed => write!(f, "assertion failed"),
FaultKind::EmptyRange => write!(f, "empty range"),
FaultKind::NoAnswer => write!(f, "an argument this algorithm has no answer for"),
}
}
}
#[repr(C)]
pub struct Fault {
kind: FaultKind,
}
impl Fault {
pub const KIND_OFFSET: usize = core::mem::offset_of!(Fault, kind);
pub const KIND_SIZE: usize = core::mem::size_of::<FaultKind>();
pub fn clear() -> Self {
Fault {
kind: FaultKind::None,
}
}
pub fn set(&mut self, fault: RaisedFault) {
self.kind = fault.kind();
}
#[inline]
#[must_use]
pub fn kind(&self) -> FaultKind {
self.kind
}
pub fn is_pending(&self) -> bool {
self.kind != FaultKind::None
}
}
impl Default for Fault {
fn default() -> Self {
Self::clear()
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct DebugLocal {
pub source_name: *const u8,
pub name_len: u32,
pub symbol_id: u32,
pub descriptor: *const crate::TypeDescriptor,
pub value: Option<crate::debug::DebugValue>,
pub type_id: u32,
pub kind: u8,
pub span_start: u32,
pub span_end: u32,
pub callee_name: *const u8,
pub callee_name_len: u32,
}
#[repr(C)]
pub struct RuntimeContext {
pub heap: *mut Heap,
pub pending_fault: *mut Fault,
pub debug_frames: *mut DebugFrameStackHeader,
pub shadow: *mut ShadowStackHeader,
pub input_source: GcRef,
pub unit_ref: GcRef,
pub current_generation: u64,
pub stack_left: u32,
pub parse_detail: *mut crate::ParseDetail,
pub crash_snapshot: *mut crate::SnapshotSlot,
pub native_roots: *mut crate::roots::NativeRootStore,
pub true_ref: GcRef,
pub false_ref: GcRef,
pub fault_message: *mut FaultMessage,
pub small_ints: *const GcRef,
pub debug_values: *mut DebugValueStackHeader,
pub small_chars: *const GcRef,
pub descriptors: [*const TypeDescriptor; BuiltinTypeId::COUNT],
}
const _: () = assert!(
RuntimeContext::descriptor_offset(BuiltinTypeId::Unit)
== core::mem::offset_of!(RuntimeContext, small_chars)
+ core::mem::size_of::<*const GcRef>(),
"the descriptor table is appended after `small_chars`, not spliced in"
);
const _: () = assert!(
RuntimeContext::descriptor_offset(BuiltinTypeId::Range)
+ core::mem::size_of::<*const TypeDescriptor>()
== core::mem::size_of::<RuntimeContext>(),
"`Range` is the last built-in and its slot is the last word of the context"
);
impl RuntimeContext {
#[must_use]
pub const fn descriptor_offset(id: BuiltinTypeId) -> usize {
core::mem::offset_of!(RuntimeContext, descriptors)
+ (id as usize) * core::mem::size_of::<*const TypeDescriptor>()
}
pub unsafe fn placeholder(input_source: GcRef) -> RuntimeContext {
RuntimeContext {
heap: std::ptr::null_mut(),
pending_fault: std::ptr::null_mut(),
debug_frames: std::ptr::null_mut(),
shadow: std::ptr::null_mut(),
input_source,
unit_ref: input_source,
current_generation: 0,
stack_left: 0,
parse_detail: std::ptr::null_mut(),
crash_snapshot: std::ptr::null_mut(),
native_roots: std::ptr::null_mut(),
true_ref: input_source,
false_ref: input_source,
fault_message: std::ptr::null_mut(),
small_ints: std::ptr::null(),
debug_values: std::ptr::null_mut(),
small_chars: std::ptr::null(),
descriptors: builtin_descriptor_addresses(),
}
}
#[inline]
pub fn has_pending_fault(&self) -> bool {
if self.pending_fault.is_null() {
return false;
}
unsafe { (*self.pending_fault).is_pending() }
}
}
pub unsafe fn current_fault_kind(ctx: *mut RuntimeContext) -> FaultKind {
if ctx.is_null() || unsafe { (*ctx).pending_fault.is_null() } {
return FaultKind::None;
}
unsafe { (*(*ctx).pending_fault).kind }
}
pub struct Runtime {
heap: Heap,
immortals: Immortals,
fault: Fault,
parse_detail: ParseDetail,
crash_snapshot: SnapshotSlot,
fault_message: FaultMessage,
shadow_stack: ShadowStack,
native_roots: crate::roots::NativeRootStore,
debug_frames: DebugFrameStack,
debug_values: DebugValueStack,
stack_budget: StackBudget,
}
impl Runtime {
pub fn new() -> Self {
let heap = Heap::new();
let immortals = Immortals::new(&heap);
Runtime {
heap,
immortals,
fault: Fault::clear(),
parse_detail: ParseDetail::new(),
crash_snapshot: SnapshotSlot::new(),
fault_message: FaultMessage::new(),
shadow_stack: ShadowStack::new(SHADOW_STACK_SLOTS, std::ptr::null_mut()),
native_roots: crate::roots::NativeRootStore::new(),
debug_frames: DebugFrameStack::new(DEBUG_FRAME_STACK_SLOTS, DebugFrameEntry::empty()),
debug_values: DebugValueStack::new(DEBUG_VALUE_STACK_SLOTS, None),
stack_budget: StackBudget::DEFAULT,
}
}
pub fn set_stack_budget(&mut self, budget: StackBudget) {
self.stack_budget = budget;
}
#[must_use]
pub fn stack_budget(&self) -> StackBudget {
self.stack_budget
}
#[inline]
pub fn heap(&self) -> &Heap {
&self.heap
}
#[inline]
pub fn immortals(&self) -> &Immortals {
&self.immortals
}
pub fn collect_now(&mut self) {
let mut ctx = self.context();
let roots = unsafe { crate::roots::RuntimeRoots::from_context(&mut ctx) };
self.heap.collect(&roots);
}
#[cfg(test)]
pub fn collect_with(&self, roots: &dyn RootSet) {
self.heap.collect_with(roots);
}
pub fn context(&mut self) -> RuntimeContext {
RuntimeContext {
heap: &mut self.heap as *mut Heap,
pending_fault: &mut self.fault as *mut Fault,
debug_frames: self.debug_frames.header_ptr(),
shadow: self.shadow_stack.header_ptr(),
input_source: self.immortals.unit(),
unit_ref: self.immortals.unit(),
current_generation: 0,
stack_left: self.stack_budget.get(),
parse_detail: &mut self.parse_detail as *mut ParseDetail,
crash_snapshot: &mut self.crash_snapshot as *mut SnapshotSlot,
native_roots: &mut self.native_roots as *mut crate::roots::NativeRootStore,
true_ref: self.immortals.true_(),
false_ref: self.immortals.false_(),
fault_message: &mut self.fault_message as *mut FaultMessage,
small_ints: self.immortals.small_ints_ptr(),
debug_values: self.debug_values.header_ptr(),
small_chars: self.immortals.small_chars_ptr(),
descriptors: builtin_descriptor_addresses(),
}
}
pub fn fault(&self) -> FaultKind {
self.fault.kind()
}
pub fn has_pending_fault(&self) -> bool {
self.fault.is_pending()
}
pub fn take_fault(&mut self) -> Option<FaultKind> {
let kind = self.fault.kind();
if self.fault.is_pending() {
self.fault = Fault::clear();
self.fault_message.clear();
Some(kind)
} else {
None
}
}
#[must_use]
pub fn fault_message(&self) -> Option<&str> {
self.fault_message.get()
}
#[must_use]
pub fn parse_detail(&self) -> &ParseDetail {
&self.parse_detail
}
pub fn parse_detail_mut(&mut self) -> &mut ParseDetail {
&mut self.parse_detail
}
#[must_use]
pub fn crash_snapshot(&self) -> Option<&CrashSnapshot> {
self.crash_snapshot.get()
}
pub fn take_crash_snapshot(&mut self) -> Option<CrashSnapshot> {
self.crash_snapshot.take()
}
pub fn clear_for_rerun(&mut self) {
self.fault = Fault::clear();
self.crash_snapshot.clear();
self.parse_detail.clear();
self.fault_message.clear();
debug_assert!(
self.shadow_stack.is_empty(),
"the shadow stack is {} slots deep between runs; some prologue was \
not balanced by an epilogue",
self.shadow_stack.len()
);
debug_assert!(
self.debug_frames.is_empty() && self.debug_values.is_empty(),
"the debug stacks are {} frames / {} values deep between runs; some \
prologue was not balanced by an epilogue",
self.debug_frames.len(),
self.debug_values.len()
);
debug_assert!(
self.native_roots.is_empty(),
"the native root store holds {} roots between runs; some \
`NativeScope` was not dropped",
self.native_roots.len()
);
self.shadow_stack.reset();
self.native_roots.reset();
self.debug_frames.reset();
self.debug_values.reset();
}
#[must_use]
pub fn shadow_stack(&self) -> &ShadowStack {
&self.shadow_stack
}
#[must_use]
pub fn native_root_store(&self) -> &crate::roots::NativeRootStore {
&self.native_roots
}
#[must_use]
pub fn debug_frame_stack(&self) -> &DebugFrameStack {
&self.debug_frames
}
#[must_use]
pub fn debug_value_stack(&self) -> &DebugValueStack {
&self.debug_values
}
#[must_use]
pub fn teardown(self) -> crate::teardown::HeapDrained {
drop(self);
crate::teardown::HeapDrained::new()
}
}
impl Default for Runtime {
fn default() -> Self {
Self::new()
}
}
impl Runtime {
pub fn alloc_int(&self, value: i64) -> GcRef {
match self.immortals.small_int(value) {
Some(interned) => interned,
None => self.heap.alloc_unpaced(crate::scalars::INT_PAYLOAD, value),
}
}
pub fn alloc_bool(&self, value: bool) -> GcRef {
self.immortals.bool_(value)
}
pub fn alloc_byte(&self, value: u8) -> GcRef {
self.heap.alloc_unpaced(crate::scalars::BYTE_PAYLOAD, value)
}
pub fn alloc_char(&self, value: u32) -> GcRef {
assert!(
crate::scalars::is_valid_char(value),
"{value:#x} is not a valid Unicode scalar"
);
match self.immortals.small_char(value) {
Some(interned) => interned,
None => self.heap.alloc_unpaced(crate::scalars::CHAR_PAYLOAD, value),
}
}
pub fn alloc_float(&self, value: f64) -> GcRef {
self.heap
.alloc_unpaced(crate::scalars::FLOAT_PAYLOAD, value)
}
pub fn alloc_unit(&self) -> GcRef {
self.immortals.unit()
}
pub fn alloc_text(&self, value: &str) -> GcRef {
unsafe {
self.heap
.alloc_payload_unpaced(&crate::text::TEXT, crate::text::TextPayload::owned(value))
}
}
#[must_use]
pub unsafe fn alloc_text_slice(&self, owner: GcRef, start: usize, len: usize) -> Option<GcRef> {
let slice = unsafe { crate::text::SourceSlice::new(owner, start, len) }?;
let payload = crate::text::TextPayload::Slice(slice);
Some(unsafe { self.heap.alloc_payload_unpaced(&crate::text::TEXT, payload) })
}
pub fn alloc_vec(
&self,
element_descriptor: &'static TypeDescriptor,
items: Vec<GcRef>,
) -> GcRef {
unsafe {
self.heap.alloc_payload_unpaced(
&crate::collections::VEC,
VecPayload {
element_descriptor,
items: items.into(),
},
)
}
}
pub fn alloc_grid(
&self,
element_descriptor: &'static TypeDescriptor,
items: Vec<GcRef>,
width: usize,
) -> GcRef {
debug_assert!(
width == 0 || items.len().is_multiple_of(width),
"grid items ({}) not a multiple of width ({})",
items.len(),
width
);
unsafe {
self.heap.alloc_payload_unpaced(
&crate::collections::GRID,
crate::collections::GridPayload {
element_descriptor,
items,
width,
},
)
}
}
pub fn alloc_record(
&self,
schema: &'static crate::records::RecordSchema,
items: Vec<GcRef>,
) -> GcRef {
debug_assert_eq!(
items.len(),
schema.arity(),
"record field count ({}) != schema arity ({})",
items.len(),
schema.arity()
);
unsafe {
self.heap.alloc_payload_unpaced(
&crate::records::RECORD,
crate::records::RecordPayload { schema, items },
)
}
}
}
impl GcRef {
pub fn as_int(&self) -> i64 {
assert_eq!(
self.descriptor().id(),
crate::scalars::INT.id(),
"not an Int"
);
unsafe { *self.payload::<i64>() }
}
pub fn as_bool(&self) -> bool {
assert_eq!(
self.descriptor().id(),
crate::scalars::BOOL.id(),
"not a Bool"
);
unsafe { read_bool(*self) }
}
pub fn as_byte(&self) -> u8 {
assert_eq!(
self.descriptor().id(),
crate::scalars::BYTE.id(),
"not a Byte"
);
unsafe { *self.payload::<u8>() }
}
pub fn as_char(&self) -> char {
assert_eq!(
self.descriptor().id(),
crate::scalars::CHAR.id(),
"not a Char"
);
let raw = unsafe { *self.payload::<u32>() };
char::from_u32(raw).expect("Char payload was not a valid scalar; memory corrupted")
}
pub fn as_float(&self) -> f64 {
assert_eq!(
self.descriptor().id(),
crate::scalars::FLOAT.id(),
"not a Float"
);
unsafe { *self.payload::<f64>() }
}
pub fn as_text(&self) -> &str {
assert_eq!(self.descriptor().id(), crate::text::TEXT.id(), "not Text");
let payload = self.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload;
unsafe { crate::text::text_str(payload) }
}
pub fn as_vec(&self) -> &[GcRef] {
assert_eq!(
self.descriptor().id(),
crate::collections::VEC.id(),
"not a Vec"
);
let p: &VecPayload = unsafe { &*self.payload::<VecPayload>() };
&p.items
}
pub fn format(&self, out: &mut dyn std::fmt::Write) {
self.format_styled(&mut crate::FormatSink::display(out));
}
pub fn format_debug(&self, out: &mut dyn std::fmt::Write) {
self.format_styled(&mut crate::FormatSink::debug(out));
}
pub fn format_styled(&self, out: &mut crate::FormatSink<'_>) {
let desc = self.descriptor();
unsafe { (desc.format)(self.payload::<u8>() as *const u8, out) };
}
pub fn equals(&self, other: &GcRef) -> bool {
let a = self.descriptor();
let b = other.descriptor();
if a.id() != b.id() {
return false;
}
let Some(eq) = a.equals else {
return false;
};
unsafe {
eq(
self.payload::<u8>() as *const u8,
other.payload::<u8>() as *const u8,
)
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_runtimes_immortals_belong_to_its_own_live_heap() {
let mut rt = Runtime::new();
let ctx = rt.context();
for cached in [ctx.unit_ref, ctx.true_ref, ctx.false_ref] {
assert!(
rt.heap().owns(cached),
"a cached immortal must be live storage in this runtime's heap"
);
}
assert_eq!(ctx.unit_ref.as_ptr(), rt.immortals().unit().as_ptr());
assert_eq!(ctx.true_ref.as_ptr(), rt.immortals().true_().as_ptr());
assert_eq!(ctx.false_ref.as_ptr(), rt.immortals().false_().as_ptr());
}
use super::*;
use crate::gc::GcHeader;
use crate::roots::RootScope;
use std::ptr::NonNull;
#[test]
fn the_fault_record_is_one_kind_at_offset_zero() {
assert_eq!(Fault::KIND_OFFSET, 0);
assert_eq!(
Fault::KIND_SIZE,
4,
"a `#[repr(C)]` fieldless enum is a C `int`, and the backend loads \
this width"
);
assert_eq!(
std::mem::size_of::<Fault>(),
Fault::KIND_SIZE,
"the kind is the whole record; a second field would make the \
inline load read half of it"
);
assert_eq!(FaultKind::None as u32, 0, "the zero word means no fault");
let mut fault = Fault::clear();
let word = |f: &Fault| {
let base = f as *const Fault as *const u8;
unsafe { base.add(Fault::KIND_OFFSET).cast::<u32>().read() }
};
assert_eq!(word(&fault), 0, "a clear record loads as zero");
fault.set(RaisedFault::INT_OVERFLOW);
assert_ne!(word(&fault), 0, "a raised record loads as non-zero");
assert!(fault.is_pending());
}
#[test]
fn a_wired_context_has_a_fault_slot() {
let mut rt = Runtime::new();
let ctx = rt.context();
assert!(
!ctx.pending_fault.is_null(),
"`Runtime::context` is the only producer of a context generated code \
sees, and generated code dereferences this without testing it"
);
assert!(!unsafe { (*ctx.pending_fault).is_pending() });
}
#[test]
fn every_descriptor_slot_holds_the_builtin_whose_id_indexes_it() {
let mut rt = Runtime::new();
let ctx = rt.context();
let base = &ctx as *const RuntimeContext as *const u8;
for index in 0..BuiltinTypeId::COUNT {
let id = BuiltinTypeId::from_u32(index as u32).expect("index is in range");
let read = unsafe {
base.add(RuntimeContext::descriptor_offset(id))
.cast::<*const TypeDescriptor>()
.read()
};
assert!(
std::ptr::eq(read, id.descriptor()),
"the slot generated code reads for {id:?} holds `{}`",
unsafe { (*read).name }
);
}
}
#[test]
fn two_runtimes_agree_on_every_descriptor_address() {
let mut first = Runtime::new();
let mut second = Runtime::new();
assert_eq!(first.context().descriptors, second.context().descriptors);
}
#[test]
fn a_placeholder_context_still_knows_every_builtin_descriptor() {
let mut header = GcHeader::detached();
let nn = NonNull::from(&mut header);
let gcref = unsafe { GcRef::from_non_null(nn) };
let ctx = unsafe { RuntimeContext::placeholder(gcref) };
assert!(std::ptr::eq(
ctx.descriptors[BuiltinTypeId::Int as usize],
&crate::scalars::INT
));
assert!(ctx.descriptors.iter().all(|d| !d.is_null()));
}
#[test]
fn placeholder_reports_no_fault() {
let mut header = GcHeader::detached();
let nn = NonNull::from(&mut header);
let gcref = unsafe { GcRef::from_non_null(nn) };
let ctx = unsafe { RuntimeContext::placeholder(gcref) };
assert!(!ctx.has_pending_fault());
assert_eq!(ctx.current_generation, 0);
}
#[test]
fn has_pending_fault_flips_with_non_null_pointer() {
let mut header = GcHeader::detached();
let nn = NonNull::from(&mut header);
let gcref = unsafe { GcRef::from_non_null(nn) };
let mut ctx = unsafe { RuntimeContext::placeholder(gcref) };
assert!(!ctx.has_pending_fault());
let mut fault = Fault::clear();
fault.set(RaisedFault::INT_OVERFLOW);
ctx.pending_fault = &mut fault;
assert!(ctx.has_pending_fault());
}
#[test]
fn setting_none_cannot_create_a_pending_fault() {
assert!(
RaisedFault::new(FaultKind::None).is_none(),
"FaultKind::None represents the absence of a fault and cannot be raised"
);
let mut fault = Fault::clear();
assert!(!fault.is_pending());
assert_eq!(fault.kind(), FaultKind::None);
for kind in [
FaultKind::IntOverflow,
FaultKind::DivByZero,
FaultKind::IndexOutOfBounds,
FaultKind::ParseFailed,
FaultKind::EmptyCollection,
FaultKind::StackOverflow,
FaultKind::FloatToInt,
FaultKind::InvalidChar,
FaultKind::InvalidText,
] {
let raised = RaisedFault::new(kind).expect("every non-None kind is raisable");
assert_eq!(raised.kind(), kind);
fault.set(raised);
assert!(fault.is_pending(), "{kind} must be pending once raised");
assert_eq!(fault.kind(), kind);
}
}
#[test]
fn runtime_allocates_and_reads_scalars() {
let rt = Runtime::new();
let i = rt.alloc_int(-123);
assert_eq!(i.as_int(), -123);
let b = rt.alloc_bool(true);
assert!(b.as_bool());
let by = rt.alloc_byte(200);
assert_eq!(by.as_byte(), 200);
let c = rt.alloc_char('€' as u32);
assert_eq!(c.as_char(), '€');
let t = rt.alloc_text("héllo");
assert_eq!(t.as_text(), "héllo");
assert_eq!(rt.alloc_unit().as_ptr(), rt.immortals().unit().as_ptr());
}
#[test]
fn runtime_formats_and_compares() {
let rt = Runtime::new();
let a = rt.alloc_int(42);
let b = rt.alloc_int(42);
let c = rt.alloc_int(43);
assert!(a.equals(&b));
assert!(!a.equals(&c));
let mut out = String::new();
a.format(&mut out);
assert_eq!(out, "42");
}
#[test]
fn runtime_vec_allocates_and_reads() {
let rt = Runtime::new();
let e0 = rt.alloc_int(1);
let e1 = rt.alloc_int(2);
let v = rt.alloc_vec(&crate::scalars::INT, vec![e0, e1]);
assert_eq!(v.descriptor().name, "Vec");
assert_eq!(v.as_vec().len(), 2);
let mut out = String::new();
v.format(&mut out);
assert_eq!(out, "[1, 2]");
}
#[test]
fn runtime_collect_keeps_immortals_alive_unrooted() {
let rt = Runtime::new();
let unit_before = rt.immortals().unit().as_ptr();
let true_before = rt.immortals().true_().as_ptr();
let false_before = rt.immortals().false_().as_ptr();
let roots = RootScope::new();
rt.collect_with(&roots);
assert_eq!(rt.immortals().unit().as_ptr(), unit_before);
assert_eq!(rt.immortals().true_().as_ptr(), true_before);
assert_eq!(rt.immortals().false_().as_ptr(), false_before);
}
#[test]
#[should_panic(expected = "not an Int")]
fn as_int_rejects_wrong_descriptor() {
let rt = Runtime::new();
let b = rt.alloc_bool(false);
let _ = b.as_int();
}
#[test]
fn a_rerun_starts_from_an_empty_shadow_stack() {
let mut rt = Runtime::new();
let mut ctx = rt.context();
let guard = unsafe {
crate::shadow_stack::push_frame(
&mut ctx as *mut RuntimeContext,
crate::shadow_stack::SlotCount::new(5).unwrap(),
)
};
assert_eq!(rt.shadow_stack().len(), 5);
drop(guard);
assert!(rt.shadow_stack().is_empty());
rt.clear_for_rerun();
assert!(rt.shadow_stack().is_empty());
}
}