Skip to main content

praxis_runtime/
abi.rs

1//! Runtime ABI versioning (§11.6) and the `praxis_*` extern wrappers (§10.2,
2//! §10.4, §11.1) that JIT-generated code calls.
3//!
4//! The runtime ABI is private to one Praxis executable build: there is no
5//! cross-version compatibility promise, and no externally linkable surface for
6//! user programs. Even so, the compiler and runtime are built from the same
7//! workspace, and a single constant — checked at startup — catches accidental
8//! internal drift between the code that *generates* calls and the code that
9//! *implements* them.
10//!
11//! The `praxis_*` wrappers are the **only** functions generated code may call.
12//! Every argument and return value that represents a language value is a
13//! [`GcRef`]; scalars (`i64` payloads) cross the ABI only as transient values
14//! tied to a single non-safepointed computation (§10.3). Per §10.4, **no wrapper
15//! ever lets a Rust panic unwind across the ABI**: on overflow or division by
16//! zero the wrapper writes the fault into the context's fault slot and returns a
17//! defined sentinel.
18
19use crate::context::{RaisedFault, RuntimeContext};
20use crate::dynamic_key::DynamicKey;
21use crate::gc::GcRef;
22use crate::graph::GraphOracle;
23use crate::heap::{Heap, Safepoint};
24use crate::roots::{NativeScope, Rooted};
25use crate::scalars;
26use crate::{
27    collections::VecPayload,
28    descriptor::{Payload, TypeDescriptor},
29    repr_c_vec::ReprCVec,
30};
31pub use praxis_stdlib::abi::{AbiKind, AbiRet, AbiSig, Effect, RuntimeSymbol};
32
33/// The runtime ABI version for this build. Bump it whenever a program compiled
34/// against one version could be misled by a runtime of another.
35///
36/// Three classes of change owe a bump:
37///
38/// * **Layout, calling convention, or signature.** Any move of a field
39///   generated code reads; any change to a `praxis_*` wrapper's parameters or
40///   return type; and any change to the *size* of
41///   [`RuntimeContext`](crate::RuntimeContext), because a host that built a
42///   context of the previous size would have this runtime read past its end.
43/// * **Meaning, with the layout unchanged.** A field or wrapper whose bits stay
44///   where they were but stand for something else: a slot whose "absent" value
45///   moves from a sentinel to all-zero `None`; a counter that counted calls up
46///   to a limit now counting a native-stack budget down; a wrapper whose
47///   manifest [`Effect`] row changes, and with it whether the caller must emit a
48///   `CheckFault` after it.
49/// * **A new dependency of generated code.** Nothing moves, but the compiler
50///   starts reading a field it never read, so repacking that field becomes a
51///   generated-code change from then on.
52///
53/// All three are about what generated code or a host can observe, so a field
54/// with no reader outside `praxis-runtime` whose displacement and width do not
55/// move owes nothing, however much the thing it points at changes.
56/// `RuntimeContext.native_roots` is the one such field: its writers are
57/// `Runtime::context` and `NativeScope`, its only reader is
58/// `RuntimeRoots::from_context`, and no `offset_of!` in
59/// `crates/praxis-codegen-cranelift/src/lower.rs` names it (ADR-114).
60///
61/// What generated code reads today — and therefore what the third class now
62/// covers — is: [`RuntimeContext`](crate::RuntimeContext)'s own `shadow`,
63/// `stack_left`, `pending_fault`, `unit_ref`, `true_ref`, `false_ref`,
64/// `small_ints`, `small_chars`, `descriptors`, `debug_frames`, `debug_values`
65/// and `heap`; [`Fault::KIND_OFFSET`](crate::Fault::KIND_OFFSET) and
66/// [`GcHeader::DESCRIPTOR_OFFSET`](crate::GcHeader::DESCRIPTOR_OFFSET);
67/// `EnumPayload::tag`; the two pacing words at
68/// [`Heap::BYTES_SINCE_COLLECT_OFFSET`](crate::Heap::BYTES_SINCE_COLLECT_OFFSET)
69/// and [`Heap::COLLECT_THRESHOLD_OFFSET`](crate::Heap::COLLECT_THRESHOLD_OFFSET),
70/// plus `Heap::live_count`; `PageHeader`'s and `GcHeader`'s fields, which the
71/// inline claim path both reads and writes; and `VecPayload`'s and
72/// `BitSetPayload`'s leading words, reached through
73/// [`ReprCVec`](crate::ReprCVec). A debug value slot's word is an
74/// `Option<GcRef>` only when the `DebugLocalMeta` beside it says so: a temp
75/// whose box was elided stores its payload raw (ADR-120).
76///
77/// One numeral per build, not one per change — a version is a statement about a
78/// build, so several packages landing in the same round share a single bump,
79/// owned by one of them, rather than taking four bumps in four worktrees that a
80/// merge would silently reduce to the last.
81pub const RUNTIME_ABI_VERSION: u32 = 20;
82
83/// Assert that the compiler's expected ABI version matches this build's.
84///
85/// Called once at CLI / LSP startup. Today the compiler and runtime are the
86/// same binary, so the assertion is trivially satisfied; the point is to have
87/// the check in place before the runtime is split across build artifacts.
88///
89/// # Panics
90/// Panics if the versions disagree. A disagreement is always a build bug, never
91/// a user-facing condition.
92pub fn assert_abi_version() {
93    assert_eq!(
94        COMPILER_EXPECTED_ABI_VERSION, RUNTIME_ABI_VERSION,
95        "compiler/runtime ABI version mismatch: compiler expected \
96         {COMPILER_EXPECTED_ABI_VERSION}, runtime reports {RUNTIME_ABI_VERSION}. \
97         This is a build inconsistency; rebuild the workspace."
98    );
99}
100
101/// The ABI version the compiler front end assumes when generating code. Kept in
102/// lockstep with [`RUNTIME_ABI_VERSION`] within a single build.
103const COMPILER_EXPECTED_ABI_VERSION: u32 = 20;
104
105// ---------------------------------------------------------------------------
106// The runtime symbol table.
107// ---------------------------------------------------------------------------
108
109/// The address of a runtime wrapper, for the JIT to resolve an import to.
110///
111/// This match is the **only** symbol→address table in the workspace, and it is
112/// exhaustive over [`RuntimeSymbol`]: adding a row to the manifest without
113/// giving it an address here is a compile error. There is no fallback — the JIT
114/// never reaches `dlsym`, so a symbol the compiler failed to register cannot
115/// accidentally "work" because it happens to be linked in.
116#[must_use]
117pub fn address(symbol: RuntimeSymbol) -> *const u8 {
118    let ptr: *const () = match symbol {
119        RuntimeSymbol::AllocBool => praxis_alloc_bool as *const (),
120        RuntimeSymbol::AllocChar => praxis_alloc_char as *const (),
121        RuntimeSymbol::AllocClosure => praxis_alloc_closure as *const (),
122        RuntimeSymbol::AllocEnum => praxis_alloc_enum as *const (),
123        RuntimeSymbol::AllocFloat => praxis_alloc_float as *const (),
124        RuntimeSymbol::AllocInt => praxis_alloc_int as *const (),
125        RuntimeSymbol::AllocRecord => praxis_alloc_record as *const (),
126        RuntimeSymbol::AllocText => praxis_alloc_text as *const (),
127        RuntimeSymbol::AllocTuple => praxis_alloc_tuple as *const (),
128        RuntimeSymbol::AllocUnit => praxis_alloc_unit as *const (),
129        RuntimeSymbol::AllocVarCell => praxis_alloc_var_cell as *const (),
130        RuntimeSymbol::Assert => praxis_assert as *const (),
131        RuntimeSymbol::AStarDistance => praxis_a_star_distance as *const (),
132        RuntimeSymbol::AStarPath => praxis_a_star_path as *const (),
133        RuntimeSymbol::Bfs => praxis_bfs as *const (),
134        RuntimeSymbol::BfsDistance => praxis_bfs_distance as *const (),
135        RuntimeSymbol::BfsPath => praxis_bfs_path as *const (),
136        RuntimeSymbol::BitsetContains => praxis_bitset_contains as *const (),
137        RuntimeSymbol::BitsetInsert => praxis_bitset_insert as *const (),
138        RuntimeSymbol::BitsetIsEmpty => praxis_bitset_is_empty as *const (),
139        RuntimeSymbol::BitsetItems => praxis_bitset_items as *const (),
140        RuntimeSymbol::BitsetLen => praxis_bitset_len as *const (),
141        RuntimeSymbol::BitsetNew => praxis_bitset_new as *const (),
142        RuntimeSymbol::Breakpoint => praxis_breakpoint as *const (),
143        RuntimeSymbol::BitsetRemove => praxis_bitset_remove as *const (),
144        RuntimeSymbol::BoolLoad => praxis_bool_load as *const (),
145        RuntimeSymbol::CharLoad => praxis_char_load as *const (),
146        RuntimeSymbol::CharToInt => praxis_char_to_int as *const (),
147        RuntimeSymbol::CharToText => praxis_char_to_text as *const (),
148        RuntimeSymbol::CheckFault => praxis_check_fault as *const (),
149        RuntimeSymbol::ClosureCapture => praxis_closure_capture as *const (),
150        RuntimeSymbol::ClosureFnPtr => praxis_closure_fn_ptr as *const (),
151        RuntimeSymbol::ClosureSetCapture => praxis_closure_set_capture as *const (),
152        RuntimeSymbol::CounterGet => praxis_counter_get as *const (),
153        RuntimeSymbol::CounterInc => praxis_counter_inc as *const (),
154        RuntimeSymbol::CounterIsEmpty => praxis_counter_is_empty as *const (),
155        RuntimeSymbol::CounterLen => praxis_counter_len as *const (),
156        RuntimeSymbol::CounterKeys => praxis_counter_keys as *const (),
157        RuntimeSymbol::CounterNew => praxis_counter_new as *const (),
158        RuntimeSymbol::CounterSet => praxis_counter_set as *const (),
159        RuntimeSymbol::CounterValues => praxis_counter_values as *const (),
160        RuntimeSymbol::DequeGet => praxis_deque_get as *const (),
161        RuntimeSymbol::DequeSet => praxis_deque_set as *const (),
162        RuntimeSymbol::DequeUpdateMax => praxis_deque_update_max as *const (),
163        RuntimeSymbol::DequeUpdateMin => praxis_deque_update_min as *const (),
164        RuntimeSymbol::DequeIsEmpty => praxis_deque_is_empty as *const (),
165        RuntimeSymbol::DequeLen => praxis_deque_len as *const (),
166        RuntimeSymbol::DequeNew => praxis_deque_new as *const (),
167        RuntimeSymbol::DequePopBack => praxis_deque_pop_back as *const (),
168        RuntimeSymbol::DequePopFront => praxis_deque_pop_front as *const (),
169        RuntimeSymbol::DequePushBack => praxis_deque_push_back as *const (),
170        RuntimeSymbol::DequePushFront => praxis_deque_push_front as *const (),
171        RuntimeSymbol::Dbg => praxis_dbg as *const (),
172        RuntimeSymbol::Dfs => praxis_dfs as *const (),
173        RuntimeSymbol::DfsDistance => praxis_dfs_distance as *const (),
174        RuntimeSymbol::DfsPath => praxis_dfs_path as *const (),
175        RuntimeSymbol::Dijkstra => praxis_dijkstra as *const (),
176        RuntimeSymbol::DijkstraDistance => praxis_dijkstra_distance as *const (),
177        RuntimeSymbol::DijkstraPath => praxis_dijkstra_path as *const (),
178        RuntimeSymbol::EnumPayload => praxis_enum_payload as *const (),
179        RuntimeSymbol::EnumSetPayload => praxis_enum_set_payload as *const (),
180        RuntimeSymbol::EnumTag => praxis_enum_tag as *const (),
181        RuntimeSymbol::FloatAbs => praxis_float_abs as *const (),
182        RuntimeSymbol::FloatCeil => praxis_float_ceil as *const (),
183        RuntimeSymbol::FloatE => praxis_float_e as *const (),
184        RuntimeSymbol::FloatFloor => praxis_float_floor as *const (),
185        RuntimeSymbol::FloatIsInfinite => praxis_float_is_infinite as *const (),
186        RuntimeSymbol::FloatIsNan => praxis_float_is_nan as *const (),
187        RuntimeSymbol::FloatLoad => praxis_float_load as *const (),
188        RuntimeSymbol::FloatMax => praxis_float_max as *const (),
189        RuntimeSymbol::FloatMin => praxis_float_min as *const (),
190        RuntimeSymbol::FloatPi => praxis_float_pi as *const (),
191        RuntimeSymbol::FloatRound => praxis_float_round as *const (),
192        RuntimeSymbol::FloatSign => praxis_float_sign as *const (),
193        RuntimeSymbol::FloatSqrt => praxis_float_sqrt as *const (),
194        RuntimeSymbol::FloatToInt => praxis_float_to_int as *const (),
195        RuntimeSymbol::FloatToText => praxis_float_to_text as *const (),
196        RuntimeSymbol::FloodFill => praxis_flood_fill as *const (),
197        RuntimeSymbol::GetInput => praxis_get_input as *const (),
198        RuntimeSymbol::GridAround4 => praxis_grid_around4 as *const (),
199        RuntimeSymbol::GridAround8 => praxis_grid_around8 as *const (),
200        RuntimeSymbol::GridCells => praxis_grid_cells as *const (),
201        RuntimeSymbol::GridColumn => praxis_grid_column as *const (),
202        RuntimeSymbol::GridContains => praxis_grid_contains as *const (),
203        RuntimeSymbol::GridCount4 => praxis_grid_count4 as *const (),
204        RuntimeSymbol::GridCount4Where => praxis_grid_count4_where as *const (),
205        RuntimeSymbol::GridCount8 => praxis_grid_count8 as *const (),
206        RuntimeSymbol::GridCount8Where => praxis_grid_count8_where as *const (),
207        RuntimeSymbol::GridFind => praxis_grid_find as *const (),
208        RuntimeSymbol::GridFindAll => praxis_grid_find_all as *const (),
209        RuntimeSymbol::GridGet => praxis_grid_get as *const (),
210        RuntimeSymbol::GridHeight => praxis_grid_height as *const (),
211        RuntimeSymbol::GridNeighbors4 => praxis_grid_neighbors4 as *const (),
212        RuntimeSymbol::GridNeighbors8 => praxis_grid_neighbors8 as *const (),
213        RuntimeSymbol::GridFilled => praxis_grid_filled as *const (),
214        RuntimeSymbol::GridNew => praxis_grid_new as *const (),
215        RuntimeSymbol::GridPositions => praxis_grid_positions as *const (),
216        RuntimeSymbol::GridRotateLeft => praxis_grid_rotate_left as *const (),
217        RuntimeSymbol::GridRotateRight => praxis_grid_rotate_right as *const (),
218        RuntimeSymbol::GridRow => praxis_grid_row as *const (),
219        RuntimeSymbol::GridSet => praxis_grid_set as *const (),
220        RuntimeSymbol::GridTranspose => praxis_grid_transpose as *const (),
221        RuntimeSymbol::GridUpdateMax => praxis_grid_update_max as *const (),
222        RuntimeSymbol::GridUpdateMin => praxis_grid_update_min as *const (),
223        RuntimeSymbol::GridWidth => praxis_grid_width as *const (),
224        RuntimeSymbol::IntAbs => praxis_int_abs as *const (),
225        RuntimeSymbol::IntAdd => praxis_int_add as *const (),
226        RuntimeSymbol::IntCheckedAdd => praxis_int_checked_add as *const (),
227        RuntimeSymbol::IntCheckedMul => praxis_int_checked_mul as *const (),
228        RuntimeSymbol::IntCheckedSub => praxis_int_checked_sub as *const (),
229        RuntimeSymbol::IntClamp => praxis_int_clamp as *const (),
230        RuntimeSymbol::IntDiv => praxis_int_div as *const (),
231        RuntimeSymbol::IntEq => praxis_int_eq as *const (),
232        RuntimeSymbol::IntGcd => praxis_int_gcd as *const (),
233        RuntimeSymbol::IntGe => praxis_int_ge as *const (),
234        RuntimeSymbol::IntGt => praxis_int_gt as *const (),
235        RuntimeSymbol::IntLcm => praxis_int_lcm as *const (),
236        RuntimeSymbol::IntLe => praxis_int_le as *const (),
237        RuntimeSymbol::IntLoad => praxis_int_load as *const (),
238        RuntimeSymbol::IntLt => praxis_int_lt as *const (),
239        RuntimeSymbol::IntMax => praxis_int_max as *const (),
240        RuntimeSymbol::IntMin => praxis_int_min as *const (),
241        RuntimeSymbol::IntMul => praxis_int_mul as *const (),
242        RuntimeSymbol::IntNe => praxis_int_ne as *const (),
243        RuntimeSymbol::IntNeg => praxis_int_neg as *const (),
244        RuntimeSymbol::IntRem => praxis_int_rem as *const (),
245        RuntimeSymbol::IntSaturatingAdd => praxis_int_saturating_add as *const (),
246        RuntimeSymbol::IntSaturatingMul => praxis_int_saturating_mul as *const (),
247        RuntimeSymbol::IntSaturatingSub => praxis_int_saturating_sub as *const (),
248        RuntimeSymbol::IntSign => praxis_int_sign as *const (),
249        RuntimeSymbol::IntSub => praxis_int_sub as *const (),
250        RuntimeSymbol::IntToChar => praxis_int_to_char as *const (),
251        RuntimeSymbol::IntToFloat => praxis_int_to_float as *const (),
252        RuntimeSymbol::IntToText => praxis_int_to_text as *const (),
253        RuntimeSymbol::IntWrappingAdd => praxis_int_wrapping_add as *const (),
254        RuntimeSymbol::IntWrappingMul => praxis_int_wrapping_mul as *const (),
255        RuntimeSymbol::IntWrappingSub => praxis_int_wrapping_sub as *const (),
256        RuntimeSymbol::MapContains => praxis_map_contains as *const (),
257        RuntimeSymbol::RangeGet => praxis_range_get as *const (),
258        RuntimeSymbol::RangeLen => praxis_range_len as *const (),
259        RuntimeSymbol::RangeNew => praxis_range_new as *const (),
260        RuntimeSymbol::RangeNewInclusive => praxis_range_new_inclusive as *const (),
261        RuntimeSymbol::MapGet => praxis_map_get as *const (),
262        RuntimeSymbol::MapIndex => praxis_map_index as *const (),
263        RuntimeSymbol::MapInsert => praxis_map_insert as *const (),
264        RuntimeSymbol::MapIsEmpty => praxis_map_is_empty as *const (),
265        RuntimeSymbol::MapKeys => praxis_map_keys as *const (),
266        RuntimeSymbol::MapLen => praxis_map_len as *const (),
267        RuntimeSymbol::MapNew => praxis_map_new as *const (),
268        RuntimeSymbol::MapRemove => praxis_map_remove as *const (),
269        RuntimeSymbol::MapUpdateMax => praxis_map_update_max as *const (),
270        RuntimeSymbol::MapUpdateMin => praxis_map_update_min as *const (),
271        RuntimeSymbol::MapValues => praxis_map_values as *const (),
272        RuntimeSymbol::MaxHeapIsEmpty => praxis_max_heap_is_empty as *const (),
273        RuntimeSymbol::MaxHeapItems => praxis_max_heap_items as *const (),
274        RuntimeSymbol::MaxHeapLen => praxis_max_heap_len as *const (),
275        RuntimeSymbol::MaxHeapNew => praxis_max_heap_new as *const (),
276        RuntimeSymbol::MaxHeapPeek => praxis_max_heap_peek as *const (),
277        RuntimeSymbol::MaxHeapPop => praxis_max_heap_pop as *const (),
278        RuntimeSymbol::MaxHeapPush => praxis_max_heap_push as *const (),
279        RuntimeSymbol::MinHeapIsEmpty => praxis_min_heap_is_empty as *const (),
280        RuntimeSymbol::MinHeapItems => praxis_min_heap_items as *const (),
281        RuntimeSymbol::MinHeapLen => praxis_min_heap_len as *const (),
282        RuntimeSymbol::MinHeapNew => praxis_min_heap_new as *const (),
283        RuntimeSymbol::MinHeapPeek => praxis_min_heap_peek as *const (),
284        RuntimeSymbol::MinHeapPop => praxis_min_heap_pop as *const (),
285        RuntimeSymbol::MinHeapPush => praxis_min_heap_push as *const (),
286        RuntimeSymbol::Panic => praxis_panic as *const (),
287        RuntimeSymbol::RaiseDivByZeroIf => praxis_raise_div_by_zero_if as *const (),
288        RuntimeSymbol::RaiseEmptyCollection => praxis_raise_empty_collection as *const (),
289        RuntimeSymbol::RaiseIntOverflowIf => praxis_raise_int_overflow_if as *const (),
290        RuntimeSymbol::RaiseStackOverflow => praxis_raise_stack_overflow as *const (),
291        RuntimeSymbol::RecordField => praxis_record_field as *const (),
292        RuntimeSymbol::RecordSetField => praxis_record_set_field as *const (),
293        RuntimeSymbol::RunParser => praxis_run_parser as *const (),
294        RuntimeSymbol::SetContains => praxis_set_contains as *const (),
295        RuntimeSymbol::SetInsert => praxis_set_insert as *const (),
296        RuntimeSymbol::SetIsEmpty => praxis_set_is_empty as *const (),
297        RuntimeSymbol::SetItems => praxis_set_items as *const (),
298        RuntimeSymbol::SetLen => praxis_set_len as *const (),
299        RuntimeSymbol::SetNew => praxis_set_new as *const (),
300        RuntimeSymbol::SetRemove => praxis_set_remove as *const (),
301        RuntimeSymbol::SnapshotDebugChain => {
302            crate::crash_snapshot::praxis_snapshot_debug_chain as *const ()
303        }
304        RuntimeSymbol::StructEq => praxis_struct_eq as *const (),
305        RuntimeSymbol::TextConcat => praxis_text_concat as *const (),
306        RuntimeSymbol::TextGet => praxis_text_get as *const (),
307        RuntimeSymbol::TextFloat => praxis_text_float as *const (),
308        RuntimeSymbol::TextInt => praxis_text_int as *const (),
309        RuntimeSymbol::TextIsEmpty => praxis_text_is_empty as *const (),
310        RuntimeSymbol::TextLen => praxis_text_len as *const (),
311        RuntimeSymbol::TupleGet => praxis_tuple_get as *const (),
312        RuntimeSymbol::TupleSet => praxis_tuple_set as *const (),
313        RuntimeSymbol::ValueCmp => praxis_value_cmp as *const (),
314        RuntimeSymbol::ValueKeepMax => praxis_value_keep_max as *const (),
315        RuntimeSymbol::ValueKeepMin => praxis_value_keep_min as *const (),
316        RuntimeSymbol::ValueToText => praxis_value_to_text as *const (),
317        RuntimeSymbol::VarCellGet => praxis_var_cell_get as *const (),
318        RuntimeSymbol::VarCellSet => praxis_var_cell_set as *const (),
319        RuntimeSymbol::VecFrequencies => praxis_vec_frequencies as *const (),
320        RuntimeSymbol::VecGet => praxis_vec_get as *const (),
321        RuntimeSymbol::VecSet => praxis_vec_set as *const (),
322        RuntimeSymbol::VecIsEmpty => praxis_vec_is_empty as *const (),
323        RuntimeSymbol::VecJoin => praxis_vec_join as *const (),
324        RuntimeSymbol::VecLen => praxis_vec_len as *const (),
325        RuntimeSymbol::VecChunks => praxis_vec_chunks as *const (),
326        RuntimeSymbol::VecFilled => praxis_vec_filled as *const (),
327        RuntimeSymbol::VecNew => praxis_vec_new as *const (),
328        RuntimeSymbol::VecPush => praxis_vec_push as *const (),
329        RuntimeSymbol::VecReversed => praxis_vec_reversed as *const (),
330        RuntimeSymbol::VecSorted => praxis_vec_sorted as *const (),
331        RuntimeSymbol::VecSortedByKey => praxis_vec_sorted_by_key as *const (),
332        RuntimeSymbol::VecToText => praxis_vec_to_text as *const (),
333        RuntimeSymbol::VecUnique => praxis_vec_unique as *const (),
334        RuntimeSymbol::VecUpdateMax => praxis_vec_update_max as *const (),
335        RuntimeSymbol::VecUpdateMin => praxis_vec_update_min as *const (),
336        RuntimeSymbol::VecWindows => praxis_vec_windows as *const (),
337        RuntimeSymbol::WriteStdout => praxis_write_stdout as *const (),
338    };
339    ptr as *const u8
340}
341
342// ---------------------------------------------------------------------------
343// The panic backstop (§9.2, §10.4)
344// ---------------------------------------------------------------------------
345
346/// The defined dummy a wrapper returns when it has raised a fault and has no
347/// real answer (§10.4).
348///
349/// A wrapper's return type is part of the ABI, so "return nothing" is not
350/// available; every type generated code can receive needs a value that is safe
351/// to hold and never read. `GcRef` is `NonNull`, so its dummy is the context's
352/// `Unit` — the same sentinel the fault epilogue returns — and integer zero
353/// would be an invalid reference, not a dummy.
354pub(crate) trait AbiSentinel {
355    /// # Safety
356    /// `ctx` must be null or point at a live, wired `RuntimeContext`.
357    unsafe fn sentinel(ctx: *mut RuntimeContext) -> Self;
358}
359
360impl AbiSentinel for () {
361    unsafe fn sentinel(_ctx: *mut RuntimeContext) {}
362}
363
364impl AbiSentinel for i64 {
365    unsafe fn sentinel(_ctx: *mut RuntimeContext) -> i64 {
366        0
367    }
368}
369
370impl AbiSentinel for GcRef {
371    unsafe fn sentinel(ctx: *mut RuntimeContext) -> GcRef {
372        // SAFETY: the caller guarantees a live, wired context. A null one
373        // cannot produce a `GcRef` at all, and `abi_panic_escaped` refuses to
374        // reach here with one.
375        unsafe { unit_sentinel(ctx) }
376    }
377}
378
379impl<T> AbiSentinel for *mut T {
380    unsafe fn sentinel(_ctx: *mut RuntimeContext) -> *mut T {
381        std::ptr::null_mut()
382    }
383}
384
385impl<T> AbiSentinel for *const T {
386    unsafe fn sentinel(_ctx: *mut RuntimeContext) -> *const T {
387        std::ptr::null()
388    }
389}
390
391/// Translate a panic that reached an `extern "C"` boundary into a fault, and
392/// return the boundary's defined dummy.
393///
394/// **This must never fire.** Totality is the contract — a wrapper validates its
395/// arguments and reports a bad one as a fault — and this exists because a
396/// contract that cannot be checked is a hope. A Rust panic unwinding out of
397/// `extern "C"` into Cranelift frames is undefined behaviour; the guard turns
398/// the one outcome nobody can reason about into the one §10.4 already
399/// specifies, and does it uniformly so no future wrapper has to remember.
400///
401/// The kind is [`FaultKind::Panic`](crate::FaultKind::Panic), with a message
402/// naming the wrapper. It is deliberately not a new `FaultKind::Internal`: a
403/// new kind is a `#[repr(C)]` layout change that costs an ABI bump (ADR-075),
404/// and `Panic` plus a message that names the function carries strictly more
405/// information for the crash report (§9.4) than a bare kind would.
406///
407/// # Safety
408/// `ctx` must be null or point at a live, wired `RuntimeContext`.
409#[cold]
410#[inline(never)]
411pub(crate) unsafe fn abi_panic_escaped<T: AbiSentinel>(
412    ctx: *mut RuntimeContext,
413    wrapper: &'static str,
414) -> T {
415    if ctx.is_null() {
416        // There is no fault slot to write and no `Unit` to return. Aborting is
417        // the only defined answer left, and it is still better than unwinding
418        // into generated frames.
419        std::process::abort();
420    }
421    // SAFETY: the caller guarantees a live, wired context.
422    unsafe { set_fault(ctx, RaisedFault::PANIC) };
423    let message = format!("internal error: a panic escaped the runtime wrapper `{wrapper}`");
424    // SAFETY: as above.
425    unsafe { set_fault_message(ctx, message.clone()) };
426    if !panic_fault_is_observable(wrapper) {
427        // **The dummy has to be unreachable where nobody will look at the
428        // fault.** Generated code tests the fault slot only where MIR emitted
429        // a `CheckFault`, and MIR emits one only after a call it classifies as
430        // faultable — so for a wrapper the manifest declares non-faulting there
431        // is *no* check by construction, and returning `unit_sentinel` would
432        // hand a `Unit` into a slot generated code believes holds a Record, a
433        // Tuple or a closure. Aborting with the message is the only answer that
434        // does not introduce a descriptor/payload confusion.
435        eprintln!("{message}");
436        std::process::abort();
437    }
438    // SAFETY: as above.
439    unsafe { T::sentinel(ctx) }
440}
441
442/// Whether generated code can be expected to observe a `Panic` fault raised by
443/// `wrapper` — i.e. whether the wrapper's defined dummy is ever consumed under
444/// a fault check rather than as a value.
445///
446/// The manifest is the authority: a symbol declared [`Effect::Pure`] or
447/// [`Effect::Allocates`] cannot be followed by a `CheckFault`. **That is
448/// `praxis_mir::verify`'s rule, not a claim restated here** — its
449/// `RedundantFaultCheck` variant rejects a check after an instruction that
450/// cannot fault, so this function's premise is enforced rather than assumed
451/// (ADR-088). A wrapper the manifest does not name at all is in the same
452/// position and is treated the same way.
453///
454/// The converse is *not* claimed here: a declared-faulting wrapper's call sites
455/// are MIR's business. What this rules out is the class where the check is
456/// impossible.
457fn panic_fault_is_observable(wrapper: &str) -> bool {
458    praxis_stdlib::abi::RuntimeSymbol::from_name(wrapper).is_some_and(|s| s.faults())
459}
460
461/// Wrap an `extern "C"` wrapper's body so a panic becomes a fault.
462///
463/// Every `#[unsafe(no_mangle)] extern "C" fn` in this crate has its body inside
464/// these, and `every_no_mangle_wrapper_is_behind_the_panic_guard` is the test
465/// that keeps it that way — a new wrapper that forgets is a failing test rather
466/// than a latent abort.
467macro_rules! abi_guard {
468    ($wrapper:expr_2021, $ctx:expr_2021, $body:block) => {{
469        // `AssertUnwindSafe`: the body's captures are the wrapper's own
470        // arguments, which are `Copy` C types, and the fault protocol is how
471        // the runtime already communicates a half-finished operation.
472        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || $body)) {
473            Ok(value) => value,
474            // SAFETY: `ctx` is the wrapper's own context argument, whose
475            // validity every wrapper's `# Safety` section already requires.
476            Err(_) => unsafe { crate::abi::abi_panic_escaped($ctx, $wrapper) },
477        }
478    }};
479}
480
481pub(crate) use abi_guard;
482
483// ---------------------------------------------------------------------------
484// Internals the wrappers share.
485// ---------------------------------------------------------------------------
486
487/// Raise `fault` on `ctx`'s fault slot (§10.4). Does nothing if the context's
488/// fault pointer is null (a misuse, but never panics across the ABI).
489///
490/// Takes a [`RaisedFault`], not a `FaultKind`: every raise names a kind that
491/// describes it, and "no fault" is not spellable here.
492unsafe fn set_fault(ctx: *mut RuntimeContext, fault: RaisedFault) {
493    if let Some(slot) = unsafe { (*ctx).pending_fault.as_mut() } {
494        slot.set(fault);
495    }
496}
497
498/// Record `text` as the message the fault about to be raised carries (§9.1).
499/// Does nothing if the context's message slot is null (a host that wired no
500/// runtime), so a `panic` still faults even where nothing can render its words.
501unsafe fn set_fault_message(ctx: *mut RuntimeContext, text: String) {
502    if let Some(slot) = unsafe { (*ctx).fault_message.as_mut() } {
503        slot.set(text);
504    }
505}
506
507/// The heap pointer out of a context, or null.
508#[inline]
509unsafe fn heap<'a>(ctx: *mut RuntimeContext) -> &'a Heap {
510    // SAFETY: the caller guarantees `ctx` points at a live, wired context whose
511    // `heap` field references a valid `Heap` for the duration of the call.
512    unsafe { &*(*ctx).heap }
513}
514
515#[inline]
516/// Charge the pacer for a collection's buffer growing (ADR-121).
517///
518/// `before` and `after` are the same payload's `owned_bytes()`, read either
519/// side of a mutation that may reallocate. Nothing is charged when the buffer
520/// did not grow, which is the overwhelmingly common case: amortized doubling
521/// means a `push` reallocates once every *n* pushes, so this is a compare and a
522/// not-taken branch on the hot path.
523///
524/// # Why every growing wrapper has to call this
525///
526/// `Heap::alloc_raw` charges `stride + owned_bytes_of(payload)` once, at
527/// construction. Leaving later growth uncharged relies on the elements
528/// themselves being paced allocations, so that the residual under-count is only
529/// the spine — and scalar promotion deletes exactly those element allocations,
530/// so an allocation-light program that grows a large buffer paces nothing.
531/// Uncharged, `bfs` runs 6 collections instead of 41 and reaches a peak
532/// resident set of 224 MiB against 61 (ADR-121).
533///
534/// So the rule is: **a wrapper that can grow a buffer charges the growth.** The
535/// `growth_charging_tests` module below has one case per such wrapper, because
536/// the failure mode is silent — a program that simply stops collecting, which
537/// reads as a leak nobody connects to the wrapper that was added.
538fn charge_growth(ctx: *mut RuntimeContext, before: usize, after: usize) {
539    let Some(grown) = after.checked_sub(before).filter(|g| *g != 0) else {
540        return;
541    };
542    if ctx.is_null() {
543        return;
544    }
545    // SAFETY: every caller is inside `abi_guard!`, which established that `ctx`
546    // is live and wired; the null check above covers the guard's own edge.
547    unsafe { heap(ctx).charge_owned_growth(grown) };
548}
549
550/// Trigger a collection on allocation pressure, rooting from the context
551/// (§12.4, ADR-019, ADR-101). Called by every allocating `praxis_*` wrapper.
552/// Safe to call with a null/unwired context (no-op).
553///
554/// The roots are every **strong** arm of
555/// [`RuntimeRoots`](crate::roots::RuntimeRoots) — the shadow stack, the ambient
556/// input buffer, a parse failure's partial value, a runtime-owned crash
557/// snapshot, and the native root store. All five, not the shadow stack alone:
558/// host-driven allocation and the parser interpreter push no shadow frame, and
559/// the other four owners are reachable regardless. The debug arm is
560/// deliberately *weak* (ADR-106): it names storage without keeping it alive, so
561/// it is cleared after the sweep rather than traced here.
562unsafe fn maybe_collect(ctx: *mut RuntimeContext) {
563    if ctx.is_null() {
564        return;
565    }
566    // SAFETY: ctx is live and wired.
567    let roots = unsafe { crate::roots::RuntimeRoots::from_context(ctx) };
568    unsafe { heap(ctx).maybe_collect(&roots) };
569}
570
571/// Pace the collector and mint the token one allocation needs.
572///
573/// Every `praxis_*` wrapper reaches the heap through [`gc_alloc`] or
574/// [`gc_alloc_owned`], which call this — and even a wrapper that reached
575/// `Heap::alloc` directly would have to come through here, because the token
576/// has no other producer. That is the whole point: an allocation that skipped
577/// the pacer would let a program whose pressure comes from `Text`, `.len()` or
578/// checked arithmetic run arbitrarily long without the collector ever being
579/// offered a turn.
580///
581/// # Safety
582/// `ctx` must point at a live, wired `RuntimeContext`. Every allocating
583/// wrapper already requires this to reach the heap at all.
584#[inline]
585unsafe fn safepoint<'a>(ctx: *mut RuntimeContext) -> (&'a Heap, Safepoint<'a>) {
586    // SAFETY: caller upholds ctx validity.
587    let h = unsafe { heap(ctx) };
588    // SAFETY: as above; the roots are read out of the same live context.
589    let roots = unsafe { crate::roots::RuntimeRoots::from_context(ctx) };
590    let sp = h.pace(&roots);
591    (h, sp)
592}
593
594/// Pace the collector, then allocate a `Copy` payload (§12.4).
595///
596/// The descriptor arrives as a [`Payload<T>`] — `scalars::INT_PAYLOAD`, not
597/// `&scalars::INT` — so `value`'s Rust type is checked against it here, at the
598/// call: a value of the wrong type is an `E0308`, and an *untyped* literal
599/// infers as the payload type instead of defaulting to `i32`. A bare descriptor
600/// reference with `T` free would let a width mismatch reach the heap and abort
601/// the process from inside `extern "C"`, which §10.4 forbids.
602///
603/// # Safety
604/// `ctx` must be live and wired.
605#[inline]
606unsafe fn gc_alloc<T: Copy>(ctx: *mut RuntimeContext, payload: Payload<T>, value: T) -> GcRef {
607    // SAFETY: caller upholds ctx validity.
608    let (h, sp) = unsafe { safepoint(ctx) };
609    h.alloc(sp, payload, value)
610}
611
612/// Pace the collector, then allocate a payload that owns Rust resources.
613///
614/// [`gc_alloc`]'s counterpart for the payloads no [`Payload<T>`] can describe.
615/// The type is named **once**, as `P`: [`Heap::alloc_payload`] derives the size,
616/// the alignment and the write from it, so no wrapper restates all three and
617/// keeps them in agreement by hand.
618///
619/// The payload arrives as a producer rather than a value, and that is
620/// load-bearing: `init` runs *after* [`safepoint`] has given the collector its
621/// turn, so a payload built out of bare `GcRef`s — `vec![fill; n]` in
622/// [`praxis_vec_filled`] and [`praxis_grid_filled`] — is never a `Vec<GcRef>`
623/// live across a collection with no root set able to see it.
624///
625/// # Safety
626/// `ctx` must be live and wired, and `descriptor` must be `P`'s own descriptor
627/// ([`Heap::alloc_payload`]'s contract).
628#[inline]
629unsafe fn gc_alloc_owned<P>(
630    ctx: *mut RuntimeContext,
631    descriptor: &'static TypeDescriptor,
632    init: impl FnOnce() -> P,
633) -> GcRef {
634    // SAFETY: caller upholds ctx validity.
635    let (h, sp) = unsafe { safepoint(ctx) };
636    // `init()` is evaluated here, downstream of the safepoint above — see the
637    // ordering note in this function's doc.
638    // SAFETY: forwarded from this function's contract.
639    unsafe { h.alloc_payload(sp, descriptor, init()) }
640}
641
642/// The immortal `Bool` for `value`, off the context's cached singletons.
643///
644/// Never an allocation: there are exactly two `Bool` values and the runtime
645/// minted both at startup, so every comparison, `contains` and `is_empty` a
646/// program evaluates answers with a singleton rather than consuming arena
647/// storage permanently. It is also what makes those manifest rows honestly
648/// `Effect::Pure`: nothing here can collect, so the call site is not a
649/// safepoint.
650///
651/// # Safety
652/// `ctx` must point at a live, wired `RuntimeContext`.
653#[inline]
654unsafe fn bool_ref(ctx: *mut RuntimeContext, value: bool) -> GcRef {
655    // SAFETY: caller upholds ctx validity.
656    let c = unsafe { &*ctx };
657    if value { c.true_ref } else { c.false_ref }
658}
659
660/// The `Int` for `value`: the interned immortal when it is small
661/// ([`crate::small_int`]), a fresh allocation otherwise.
662///
663/// [`bool_ref`]'s shape, one step less absolute: `Bool` has two values so it is
664/// always the singleton, while `Int` has a *range* that is interned and an
665/// unbounded remainder that is not. Every wrapper that answers an `Int` reaches
666/// the heap through here, so the interning covers not just literals but
667/// `Vec.len()`, a `Counter` bump, an enum tag, a comparison's index and the
668/// result of arithmetic — which is where most of a real program's small `Int`s
669/// come from.
670///
671/// # It paces even when it does not allocate, and that is deliberate
672///
673/// The manifest declares `VecLen`, `MapLen`, `EnumTag`, `TextLen`, `CounterGet`
674/// and two dozen more `Effect::Allocates`, which is generated code's contract
675/// that the call site is a GC safepoint. If this returned before [`safepoint`],
676/// a loop whose only allocations were small `Int`s would never offer the
677/// collector a turn — the collector's *only* trigger is the pacing counter, and
678/// nothing else in such a loop touches it. So the token is minted and then
679/// dropped: [`Safepoint`] is `#[must_use]`, so `drop(sp)` is the honest spelling
680/// of "the collector got its turn and we allocated nothing", and it is a
681/// compile error to forget which of the two happened.
682///
683/// The interned path therefore costs a threshold compare and a range test rather
684/// than an allocation. `Inst::ConstGc` is what removes even that, but only for a
685/// *literal*, where the compiler knows the value and no manifest row applies.
686///
687/// # Safety
688/// `ctx` must point at a live, wired `RuntimeContext`.
689#[inline]
690unsafe fn int_ref(ctx: *mut RuntimeContext, value: i64) -> GcRef {
691    // SAFETY: caller upholds ctx validity.
692    let (h, sp) = unsafe { safepoint(ctx) };
693    match crate::small_int::index_of(value) {
694        Some(i) => {
695            drop(sp);
696            // SAFETY: `index_of` bounds `i` by `SMALL_INT_COUNT`, and
697            // `Runtime::context` points `small_ints` at a table of exactly that
698            // length whose slot `i` holds `SMALL_INT_MIN + i`.
699            unsafe { *(*ctx).small_ints.add(i) }
700        }
701        None => h.alloc(sp, scalars::INT_PAYLOAD, value),
702    }
703}
704
705/// The `Char` for `code`: the interned immortal when it is ASCII
706/// ([`crate::small_char`]), a fresh allocation otherwise.
707///
708/// [`int_ref`]'s shape and its argument (ADR-107). Every wrapper that answers a
709/// `Char` reaches the heap through here — [`checked_alloc_char`], which is both
710/// `praxis_alloc_char` and `praxis_int_to_char`; [`praxis_text_get`], which is
711/// both `t[i]` and every step of `for c in t`; and [`default_cell`], which is a
712/// `Grid[Char]`'s fill. `praxis_text_get` is the one that matters for real code:
713/// an AoC-shaped program that walks a line of text would otherwise box a fresh
714/// object per character read, and every character of such a line is ASCII.
715///
716/// # It paces even when it does not allocate
717///
718/// The manifest declares `TextGet`, `AllocChar` and `IntToChar`
719/// `Effect::AllocatesAndFaults`, which is generated code's contract that the call
720/// site is a GC safepoint. If this returned before [`safepoint`], `for c in text`
721/// over an ASCII line would never offer the collector a turn — the collector's
722/// *only* trigger is the pacing counter, and a loop that reads characters and
723/// compares them touches nothing else that would bump it. So the token is minted
724/// and then dropped: [`Safepoint`] is `#[must_use]`, so `drop(sp)` is the honest
725/// spelling of "the collector got its turn and we allocated nothing", and it is a
726/// compile error to forget which of the two happened. Pinned by
727/// `char_ref_paces_the_collector_even_when_it_answers_from_the_table`.
728///
729/// Unlike `int_ref` there is no `Inst::ConstGc` that removes even the pacing
730/// check: that instruction exists for a *literal*, and the language has no
731/// character literal (ADR-107 Decision 2).
732///
733/// # Safety
734/// `ctx` must point at a live, wired `RuntimeContext`, and `code` must be a valid
735/// Unicode scalar value — every caller has already established this, either by
736/// [`checked_alloc_char`]'s range check or by starting from a Rust `char`.
737#[inline]
738unsafe fn char_ref(ctx: *mut RuntimeContext, code: u32) -> GcRef {
739    debug_assert!(
740        crate::scalars::is_valid_char(code),
741        "char_ref's callers validate first"
742    );
743    // SAFETY: caller upholds ctx validity.
744    let (h, sp) = unsafe { safepoint(ctx) };
745    match crate::small_char::index_of(code) {
746        Some(i) => {
747            drop(sp);
748            // SAFETY: `index_of` bounds `i` by `SMALL_CHAR_COUNT`, and
749            // `Runtime::context` points `small_chars` at a table of exactly that
750            // length whose slot `i` holds code point `i`.
751            unsafe { *(*ctx).small_chars.add(i) }
752        }
753        None => h.alloc(sp, scalars::CHAR_PAYLOAD, code),
754    }
755}
756
757/// A fresh owned `Text` holding `s`.
758///
759/// [`bool_ref`]/[`int_ref`]/[`char_ref`]'s place in the file but not their
760/// shape: there is nothing interned to answer from — every `Text` is a distinct
761/// object — so this always allocates, and always paces.
762///
763/// `impl Into<Box<str>>` is what lets every text-producing wrapper share this
764/// one allocation without a defensive `.clone()`: a `String` from a renderer, a
765/// `Box<str>` the caller already built ([`praxis_alloc_text`]), a `&str`.
766///
767/// # Safety
768/// `ctx` must point at a live, wired `RuntimeContext`.
769#[inline]
770unsafe fn text_ref(ctx: *mut RuntimeContext, s: impl Into<Box<str>>) -> GcRef {
771    // SAFETY: caller upholds ctx validity; `TextPayload` is `TEXT`'s payload type.
772    unsafe {
773        gc_alloc_owned(ctx, &crate::text::TEXT, || {
774            crate::text::TextPayload::owned(s)
775        })
776    }
777}
778
779/// Read `r`'s payload through a [`Payload`] handle, first checking that `r`
780/// really is that handle's type.
781///
782/// This is the reader to reach for whenever a wrapper receives a `GcRef` it did
783/// not itself allocate — a value handed back by a program's closure, most of
784/// all. Two mistakes are impossible through it: reading a value of the wrong
785/// *type* (the identity check answers `None`, and the caller decides whether
786/// that is a `TypeMismatch` fault), and reading the right type at the wrong
787/// *width* (the width is `size_of::<T>()`, which [`Payload::new`] proved is the
788/// descriptor's width when the handle was declared). Reading a one-byte `Bool`
789/// payload with an eight-byte `int_payload` is the class it rules out.
790///
791/// # Safety
792/// `r` must be a valid `GcRef` into a live heap.
793#[inline]
794unsafe fn read_scalar<T: Copy>(r: GcRef, handle: crate::descriptor::Payload<T>) -> Option<T> {
795    if !std::ptr::eq(r.descriptor(), handle.descriptor()) {
796        return None;
797    }
798    // SAFETY: the identity check proves `r`'s payload is this handle's type, and
799    // the handle's own construction proved `T` is that type's layout.
800    Some(unsafe { handle.read(r.payload::<u8>()) })
801}
802
803/// Read the `i64` payload of an `Int` `GcRef`. Used by every arithmetic wrapper.
804///
805/// Prefer [`read_scalar`] for any value whose type is not already established:
806/// this reads eight bytes, and the descriptor check is all that stands between
807/// it and a narrower payload.
808///
809/// # The width check is a branch, not a `debug_assert`
810///
811/// A `debug_assert` is not a bound — it compiles out of a release build, leaving
812/// an eight-byte read against a descriptor that may be narrower or zero bytes
813/// wide, so the two profiles would answer differently and the wrong one is the
814/// one users get. As an ordinary branch it holds in every profile: the read
815/// cannot happen. What happens instead is ADR-080's defined panic path —
816/// `abi_guard` catches it, raises `RaisedFault::PANIC` with a message naming
817/// the wrapper, and either faults into the crash debugger (a wrapper the
818/// manifest declares faultable) or prints that message and aborts (one it does
819/// not, which is `praxis_int_load`'s case). The guard is a memory-safety check
820/// on a raw read, not a stand-in for a type system.
821///
822/// This stays `-> i64` rather than becoming fallible: sixty-odd wrappers read
823/// through it, and a `ctx`-threading signature change is a larger edit than the
824/// one memory safety needs.
825#[inline]
826unsafe fn int_payload(r: GcRef) -> i64 {
827    // SAFETY: `read_scalar` proves `r`'s descriptor *is* `INT` before reading,
828    // so the eight bytes are in bounds and are an `i64`. The compiler only emits
829    // these calls with Int-typed operands, and a fault that would feed a
830    // non-`Int` (the Unit sentinel, say) into an arithmetic wrapper is diverted
831    // by `Inst::CheckFault` before it gets here (§10.4).
832    unsafe { read_scalar(r, scalars::INT_PAYLOAD) }
833        .unwrap_or_else(|| scalar_type_mismatch("int_payload", "Int", r.descriptor().name))
834}
835
836/// The refusal every scalar reader shares, out of line so the check costs a
837/// never-taken branch on the hot path.
838///
839/// `#[cold]` and `#[inline(never)]` are what let the check be unconditional, and
840/// it must be unconditional: a `debug_assert` compiles out, so a release build
841/// would do an out-of-bounds heap read where a debug build aborted.
842///
843/// A panic here is ADR-080's defined path: `abi_guard!` catches it, raises
844/// `RaisedFault::PANIC` naming the wrapper, and either faults into the crash
845/// debugger or prints the message and aborts. That is the backstop, not the
846/// primary defence — a raw scalar read must prove its own width whatever the
847/// type system believes.
848#[cold]
849#[inline(never)]
850fn scalar_type_mismatch(what: &'static str, want: &'static str, found: &'static str) -> ! {
851    panic!("{what} wants a `{want}` payload; this value is a `{found}` (REP-56)");
852}
853
854/// [`praxis_alloc_text`]'s refusal when its buffer is not UTF-8 — a violated
855/// precondition, not a runtime condition (ADR-111).
856///
857/// **Why this is a panic and not a fault.** The precondition is the one that is
858/// actually true: the compiler's bytes are a Rust `&str` unbroken from
859/// `Lit::Text(String)` through `AllocKind::Text { value: String }` to
860/// `Generation::alloc_str`, and the one caller in this crate that holds raw
861/// *host* bytes — [`praxis_get_input`] — validates them itself and raises
862/// `InvalidText` there, where the `read` can observe it. Spelling it as a fault
863/// instead would cost a `CheckFault` after every text literal for a fault no
864/// generated call site can raise.
865///
866/// **Why the `from_utf8` call above stays, in every profile.** This is
867/// [`scalar_type_mismatch`]'s argument verbatim and it is why that function is
868/// the neighbour: a `debug_assert` is not a bound, because it compiles out of a
869/// release build. What would be left in release is a `Box<str>` built from bytes
870/// that are not UTF-8, which [`crate::text::text_str`] later hands out as a
871/// `&str` — so the two profiles would answer differently and the wrong one is
872/// the one users get. `from_utf8_unchecked` is the same hole with the check
873/// deleted rather than compiled out. The unconditional branch costs a
874/// never-taken jump to this cold callee, which is the price ADR-102 §1 already
875/// established for the inline scalar loads.
876///
877/// **It must not reach `set_fault`, and that is enforced.**
878/// `a_wrapper_that_can_raise_a_fault_declares_that_it_faults` computes a textual
879/// fixed point over this file: a body that can reach `set_fault`, directly or
880/// through a helper defined here, must belong to a symbol whose manifest row
881/// says it faults. `AllocText`'s row is `Effect::Allocates`, so a refusal
882/// spelled as a fault would fail that test.
883///
884/// The end-to-end path on a violation is ADR-080's: panic → `abi_guard!`
885/// catches → `panic_fault_is_observable("praxis_alloc_text")` reads the
886/// `Allocates` row and answers `false` → the message is printed and the process
887/// aborts. That is the same outcome `praxis_int_load` gives a wrong descriptor,
888/// and it falls out of the row change with no code of its own.
889#[cold]
890#[inline(never)]
891fn text_bytes_are_not_utf8(len: usize) -> ! {
892    panic!(
893        "praxis_alloc_text was handed {len} bytes that are not valid UTF-8; its \
894         `# Safety` contract requires them to be (ADR-111). A host with untrusted \
895         bytes must validate them first, as `praxis_get_input` does."
896    );
897}
898
899/// The Unit `GcRef` returned on fault paths as the "defined dummy" (§10.4).
900/// Reads the cached immortal `unit_ref` from the context, which is stable for
901/// the program's lifetime.
902#[inline]
903unsafe fn unit_sentinel(ctx: *mut RuntimeContext) -> GcRef {
904    unsafe { (*ctx).unit_ref }
905}
906
907/// The slot a source index `i` names in a container of `len` elements, or
908/// `None` when it is outside `0..len` (§9.2, §11.1).
909///
910/// **The one place a source index becomes a `usize`.** The `< 0` test has to
911/// run before the cast: a negative `i64` casts to a value near `usize::MAX` and
912/// would sail straight past a bare length comparison. Every accessor shares this
913/// one copy, so no single site can drop the guard invisibly. The construction
914/// side guards the same hazard with a named
915/// [`GridExtent`](crate::collections::GridExtent).
916///
917/// `Vec` and `Deque` indexing is one language rule applied to two containers,
918/// so they share this rather than each spelling it out.
919fn linear_index(i: i64, len: usize) -> Option<usize> {
920    if i < 0 {
921        return None;
922    }
923    // Non-negative above, so the cast is exact.
924    let i = i as usize;
925    (i < len).then_some(i)
926}
927
928/// The row-major slot `(x, y)` names in a `width × height` grid, or `None` when
929/// either axis falls outside it.
930///
931/// Both axes go through [`linear_index`], so the 2-D rule is the 1-D rule twice
932/// and the signed-to-`usize` cast still exists in exactly one place. The
933/// product cannot overflow: `y < height` and `x < width` with
934/// `height = items.len() / width`, so the result is below `items.len()`.
935fn cell_index(x: i64, y: i64, width: usize, height: usize) -> Option<usize> {
936    let x = linear_index(x, width)?;
937    let y = linear_index(y, height)?;
938    Some(y * width + x)
939}
940
941/// [`linear_index`], raising `IndexOutOfBounds` when the index is out of range.
942/// `None` means the fault is already set and the caller owes only its sentinel
943/// return.
944///
945/// The raising wrapper is deliberately separate from the pure predicate,
946/// because not every bounds question may fault. [`praxis_grid_contains`] is
947/// declared `Pure` in the ABI manifest while `GridGet` is `Faults`, and MIR's
948/// `RedundantFaultCheck` emits no `CheckFault` after a `Pure` call — so a fault
949/// raised on every legitimate `false` would sit pending until some later check
950/// mistook it for its own. Bounds-testing sites take [`cell_index`] /
951/// [`linear_index`]; only sites the manifest says can fault take these.
952///
953/// # Safety
954/// `ctx` must be live and wired.
955unsafe fn checked_index(ctx: *mut RuntimeContext, i: i64, len: usize) -> Option<usize> {
956    let idx = linear_index(i, len);
957    if idx.is_none() {
958        unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
959    }
960    idx
961}
962
963/// [`cell_index`], raising `IndexOutOfBounds` when `(x, y)` is off the grid.
964/// See [`checked_index`] for why the raising and the predicate are two
965/// functions.
966///
967/// # Safety
968/// `ctx` must be live and wired.
969unsafe fn checked_cell(
970    ctx: *mut RuntimeContext,
971    x: i64,
972    y: i64,
973    width: usize,
974    height: usize,
975) -> Option<usize> {
976    let idx = cell_index(x, y, width, height);
977    if idx.is_none() {
978        unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
979    }
980    idx
981}
982
983// ---------------------------------------------------------------------------
984// Allocation wrappers.
985// ---------------------------------------------------------------------------
986
987/// The `Int` for `value` (§4.3, §11.1) — the interned immortal when it is small
988/// ([`crate::small_int`]), a fresh box otherwise.
989///
990/// The row stays `Effect::Allocates`, not `Pure` as `AllocBool`'s is: this
991/// wrapper still allocates for an out-of-range value, so the call site is still
992/// a GC safepoint and generated code must still spill its roots across it. The
993/// interning is invisible to the caller by design.
994///
995/// # Safety
996/// `ctx` must point at a live, wired `RuntimeContext` whose `heap` is valid.
997#[unsafe(no_mangle)]
998pub unsafe extern "C" fn praxis_alloc_int(ctx: *mut RuntimeContext, value: i64) -> GcRef {
999    abi_guard!("praxis_alloc_int", ctx, {
1000        // `int_ref` paces first, rooted at the whole `RuntimeRoots`. The new object
1001        // is not yet a root, but it is returned by value to the caller, which spills
1002        // it — so it is safe across this collection (the *previous* allocation's
1003        // result was already spilled by the backend before this wrapper was called).
1004        // SAFETY: caller upholds the ctx/heap validity.
1005        unsafe { int_ref(ctx, value) }
1006    })
1007}
1008
1009/// Allocate a boxed `Bool` from a 0/1 value (§4.3). Returns the immortal
1010/// singleton, never a fresh allocation.
1011///
1012/// # Safety
1013/// `ctx` must point at a live, wired `RuntimeContext`.
1014#[unsafe(no_mangle)]
1015pub unsafe extern "C" fn praxis_alloc_bool(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1016    abi_guard!("praxis_alloc_bool", ctx, {
1017        // There are two `Bool` values, and the runtime allocated both at startup.
1018        // `value != 0` is true; `0` is false.
1019        // SAFETY: caller upholds ctx validity.
1020        let c = unsafe { &*ctx };
1021        if value != 0 { c.true_ref } else { c.false_ref }
1022    })
1023}
1024
1025/// Allocate the `Unit` singleton (§4.3).
1026///
1027/// # Safety
1028/// `ctx` must point at a live, wired `RuntimeContext`.
1029#[unsafe(no_mangle)]
1030pub unsafe extern "C" fn praxis_alloc_unit(ctx: *mut RuntimeContext) -> GcRef {
1031    abi_guard!("praxis_alloc_unit", ctx, {
1032        // The one `Unit` value, cached on the context for the fault path.
1033        // SAFETY: caller upholds ctx validity.
1034        unsafe { (*ctx).unit_ref }
1035    })
1036}
1037
1038/// Allocate a boxed `Char` from a Unicode scalar value (§4.3). The `value`
1039/// is the `u32` code point carried as `i64` (the uniform scalar ABI width). If
1040/// the code point is not a valid scalar, the fault is set and the Unit sentinel
1041/// is returned (no panic crosses the ABI).
1042///
1043/// # Safety
1044/// `ctx` must point at a live, wired `RuntimeContext`.
1045#[unsafe(no_mangle)]
1046pub unsafe extern "C" fn praxis_alloc_char(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1047    abi_guard!("praxis_alloc_char", ctx, {
1048        // SAFETY: caller upholds ctx/heap validity.
1049        unsafe { checked_alloc_char(ctx, value) }
1050    })
1051}
1052
1053/// Box an `i64` as a `Char`, or raise `InvalidChar` and answer the Unit sentinel.
1054///
1055/// The one place the `i64`-to-code-point rule is enforced, because there are two
1056/// doors into it — `praxis_alloc_char` (the parser and codegen's `AllocKind::Char`)
1057/// and `praxis_int_to_char` (`Int.to_char()`, ADR-086) — and a rule stated at both
1058/// goes stale at one.
1059///
1060/// **Range-check before narrowing.** `value as u32` truncates, so
1061/// `0x1_0000_0041` would silently become `'A'`. The scalar ABI is 64 bits wide;
1062/// a code point is not, and the conversion has to say so rather than wrap. The
1063/// surrogate range is rejected for the same reason: `char::from_u32` is what
1064/// decides, not a width.
1065///
1066/// # Safety
1067/// `ctx` must point at a live, wired `RuntimeContext`.
1068unsafe fn checked_alloc_char(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1069    let Ok(code) = u32::try_from(value) else {
1070        unsafe { set_fault(ctx, RaisedFault::INVALID_CHAR) };
1071        return unsafe { unit_sentinel(ctx) };
1072    };
1073    if !crate::scalars::is_valid_char(code) {
1074        unsafe { set_fault(ctx, RaisedFault::INVALID_CHAR) };
1075        return unsafe { unit_sentinel(ctx) };
1076    }
1077    // SAFETY: caller upholds ctx/heap validity; code is a validated scalar.
1078    unsafe { char_ref(ctx, code) }
1079}
1080
1081/// Allocate an owned `Text` from a UTF-8 byte buffer (§4.3, ADR-013).
1082///
1083/// **UTF-8 is the caller's precondition, and this wrapper cannot fault**
1084/// (ADR-111). Its row is `Effect::Allocates`, so `Inst::Alloc { AllocKind::Text }`
1085/// is followed by no `CheckFault` — `praxis_mir::verify` rejects one — and a
1086/// `Text` literal in a loop is hoisted into the preheader like a `Float` one
1087/// (ADR-108 §3). Handing this bytes that are not UTF-8 is a violated contract,
1088/// not a runtime condition, and it aborts through `text_bytes_are_not_utf8`
1089/// (whose doc carries the argument) rather than raising `InvalidText`.
1090///
1091/// A host that holds *untrusted* bytes validates them before calling. There is
1092/// exactly one such caller in this crate — [`praxis_get_input`], whose row is
1093/// `AllocatesAndFaults` — and it raises `InvalidText` itself, so the fault a
1094/// `read` can observe still lands at the `read` (§4.3, §7.10).
1095///
1096/// # Safety
1097/// `ctx` must point at a live, wired `RuntimeContext`; `bytes` must point at
1098/// `len` valid UTF-8 bytes that remain valid for the duration of the call.
1099#[unsafe(no_mangle)]
1100pub unsafe extern "C" fn praxis_alloc_text(
1101    ctx: *mut RuntimeContext,
1102    bytes: *const u8,
1103    len: usize,
1104) -> GcRef {
1105    abi_guard!("praxis_alloc_text", ctx, {
1106        let slice = if bytes.is_null() || len == 0 {
1107            &[]
1108        } else {
1109            // SAFETY: caller guarantees `bytes..bytes+len` is a valid, UTF-8 buffer.
1110            unsafe { std::slice::from_raw_parts(bytes, len) }
1111        };
1112        // The check is unconditional in every profile: it is the backstop on a
1113        // raw read, the same standing `read_scalar` has, and its argument is
1114        // written out at `text_bytes_are_not_utf8`. A violation refuses rather
1115        // than recovering lossily behind a fault nobody at a generated call site
1116        // could observe (ADR-111).
1117        let owned: Box<str> = match std::str::from_utf8(slice) {
1118            Ok(s) => s.into(),
1119            Err(_) => text_bytes_are_not_utf8(len),
1120        };
1121        // SAFETY: ctx/heap valid.
1122        unsafe { text_ref(ctx, owned) }
1123    })
1124}
1125
1126// ---------------------------------------------------------------------------
1127// Scalar extraction / materialization.
1128// ---------------------------------------------------------------------------
1129
1130/// Read the `i64` payload of an `Int` `GcRef` (§10.3 transient scalar).
1131///
1132/// # Safety
1133/// `r` must be a valid `Int` `GcRef`.
1134#[unsafe(no_mangle)]
1135pub unsafe extern "C" fn praxis_int_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1136    abi_guard!("praxis_int_load", _ctx, {
1137        // SAFETY: caller guarantees `r` is an Int.
1138        unsafe { int_payload(r) }
1139    })
1140}
1141
1142/// Read a `Bool` payload as 0/1 (§10.3 transient scalar).
1143///
1144/// # Safety
1145/// `r` must be a valid `Bool` `GcRef`.
1146#[unsafe(no_mangle)]
1147pub unsafe extern "C" fn praxis_bool_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1148    abi_guard!("praxis_bool_load", _ctx, {
1149        // Read the byte, then decide — never `*r.payload::<bool>()`. A Rust
1150        // `bool` whose byte is not 0 or 1 is an *invalid value*, and
1151        // materializing one is undefined behaviour whatever the read's bounds
1152        // are; `BoolPayload` is a `u8` precisely so the runtime never has to.
1153        // SAFETY: `read_scalar` bounds the read against `r`'s own descriptor.
1154        let byte = unsafe { read_scalar(r, scalars::BOOL_PAYLOAD) }.unwrap_or_else(|| {
1155            scalar_type_mismatch("praxis_bool_load", "Bool", r.descriptor().name)
1156        });
1157        i64::from(byte != 0)
1158    })
1159}
1160
1161/// Read a `Char` payload as its `u32` code point widened to `i64` (§4.3).
1162///
1163/// # Safety
1164/// `r` must be a valid `Char` `GcRef`.
1165#[unsafe(no_mangle)]
1166pub unsafe extern "C" fn praxis_char_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1167    abi_guard!("praxis_char_load", _ctx, {
1168        // SAFETY: `read_scalar` bounds the read against `r`'s own descriptor.
1169        let code = unsafe { read_scalar(r, scalars::CHAR_PAYLOAD) }.unwrap_or_else(|| {
1170            scalar_type_mismatch("praxis_char_load", "Char", r.descriptor().name)
1171        });
1172        i64::from(code)
1173    })
1174}
1175
1176/// Allocate a boxed `Float` from an `i64` carrying the IEEE-754 binary64 bit
1177/// pattern (§4.3, §4.12). The uniform scalar ABI carries every payload as
1178/// `i64`; a float is transported as `f64::to_bits()` and reassembled here.
1179///
1180/// # Safety
1181/// `ctx` must point at a live, wired `RuntimeContext`.
1182#[unsafe(no_mangle)]
1183pub unsafe extern "C" fn praxis_alloc_float(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1184    abi_guard!("praxis_alloc_float", ctx, {
1185        let f = f64::from_bits(value as u64);
1186        // SAFETY: caller upholds ctx/heap validity; all f64 values are valid Floats.
1187        unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, f) }
1188    })
1189}
1190
1191/// Read a `Float` payload as its IEEE-754 bit pattern widened to `i64`
1192/// (§10.3 transient scalar). Generated code keeps floats in the uniform `i64`
1193/// scalar channel; the bit pattern is reassembled into an `f64` only at the
1194/// point of an arithmetic/comparison instruction.
1195///
1196/// # Safety
1197/// `r` must be a valid `Float` `GcRef`.
1198#[unsafe(no_mangle)]
1199pub unsafe extern "C" fn praxis_float_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1200    abi_guard!("praxis_float_load", _ctx, {
1201        // Through `float_payload`, which goes through `read_scalar`: the read
1202        // proves its own width rather than taking the caller's word for it.
1203        //
1204        // It matters more since ADR-102: generated code reads a `Float`
1205        // payload inline behind a descriptor check, and this wrapper is the
1206        // cold path that check branches to. If it read unchecked, the two
1207        // would disagree about what a wrong descriptor means — the inline
1208        // path would refuse and the fallback would read anyway.
1209        //
1210        // SAFETY: caller guarantees `r` is a valid `GcRef`; `float_payload`
1211        // proves it is a `Float` before reading.
1212        unsafe { float_payload(r) }.to_bits() as i64
1213    })
1214}
1215
1216// ---------------------------------------------------------------------------
1217// Float conversion & methods (§4.12). Float arithmetic never faults (IEEE-754
1218// produces inf/nan); only the narrowing `to_int` conversion does.
1219// ---------------------------------------------------------------------------
1220
1221/// Read a `Float` payload as an `f64` (private helper).
1222///
1223/// # Safety
1224/// `r` must be a valid `Float` `GcRef`.
1225unsafe fn float_payload(r: GcRef) -> f64 {
1226    // SAFETY: `read_scalar` proves `r`'s descriptor is `FLOAT` before reading.
1227    unsafe { read_scalar(r, scalars::FLOAT_PAYLOAD) }
1228        .unwrap_or_else(|| scalar_type_mismatch("float_payload", "Float", r.descriptor().name))
1229}
1230
1231/// Widen an `Int` to a `Float` (§4.12). Never faults — every `i64` is exactly
1232/// representable as an `f64`? No: integers above 2^53 lose precision, but the
1233/// conversion is still total and well-defined (rounds to nearest). This is the
1234/// explicit widening method `Int.to_float()`.
1235///
1236/// # Safety
1237/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1238#[unsafe(no_mangle)]
1239pub unsafe extern "C" fn praxis_int_to_float(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1240    abi_guard!("praxis_int_to_float", ctx, {
1241        let i = unsafe { int_payload(r) };
1242        // SAFETY: ctx/heap valid; every widened int is a valid Float payload.
1243        unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, i as f64) }
1244    })
1245}
1246
1247/// `Char.to_int()` — the Unicode scalar value, as an `Int` (ADR-086). Never
1248/// faults: every valid scalar fits an `i64`.
1249///
1250/// This reads through [`read_scalar`] with the `Char` handle rather than
1251/// `int_payload`, because a `Char` payload is **four** bytes and an `i64` read
1252/// would take eight of them.
1253///
1254/// # Safety
1255/// `ctx` must be live and wired; `r` must be a valid `Char` `GcRef`.
1256#[unsafe(no_mangle)]
1257pub unsafe extern "C" fn praxis_char_to_int(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1258    abi_guard!("praxis_char_to_int", ctx, {
1259        // SAFETY: caller guarantees `r` is a valid `GcRef`; `read_scalar` proves
1260        // the descriptor is `CHAR` before reading its four bytes.
1261        let code = unsafe { read_scalar(r, scalars::CHAR_PAYLOAD) }.unwrap_or_else(|| {
1262            scalar_type_mismatch("praxis_char_to_int", "Char", r.descriptor().name)
1263        });
1264        // SAFETY: ctx/heap valid; every scalar value is a valid Int payload.
1265        unsafe { int_ref(ctx, i64::from(code)) }
1266    })
1267}
1268
1269/// `Int.to_char()` — the `Char` with this Unicode scalar value (ADR-086).
1270/// Faults (`InvalidChar`) on a negative value, one above `0x10FFFF`, or one in
1271/// the surrogate range: those are not scalar values and have no `Char`.
1272///
1273/// It is `Char.to_int()`'s partial half exactly as `Float.to_int()` is
1274/// `Int.to_float()`'s — the narrowing direction is the one that can fail. The
1275/// check lives in [`checked_alloc_char`] and is not restated here.
1276///
1277/// # Safety
1278/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1279#[unsafe(no_mangle)]
1280pub unsafe extern "C" fn praxis_int_to_char(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1281    abi_guard!("praxis_int_to_char", ctx, {
1282        let value = unsafe { int_payload(r) };
1283        // SAFETY: caller upholds ctx/heap validity.
1284        unsafe { checked_alloc_char(ctx, value) }
1285    })
1286}
1287
1288/// Narrow a `Float` to an `Int` by truncating toward zero (§4.12). Faults
1289/// (`FloatToInt`) on NaN, ±infinity, or a finite value outside the signed
1290/// 64-bit range — these have no exact `Int` representation. On fault, sets
1291/// `pending_fault` and returns the Unit sentinel (no panic crosses the ABI).
1292///
1293/// # Safety
1294/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1295#[unsafe(no_mangle)]
1296pub unsafe extern "C" fn praxis_float_to_int(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1297    abi_guard!("praxis_float_to_int", ctx, {
1298        let f = unsafe { float_payload(r) };
1299        // NaN, infinities, and out-of-range finite values are not exactly
1300        // representable as i64. Rust's `as i64` saturates (inf→i64::MAX,
1301        // -inf→i64::MIN, nan→0), which would silently produce a plausible-but-wrong
1302        // value; per §4.12 these cases fault instead.
1303        if f.is_nan() || f.is_infinite() || f < i64::MIN as f64 || f >= i64::MAX as f64 {
1304            unsafe { set_fault(ctx, RaisedFault::FLOAT_TO_INT) };
1305            return unsafe { unit_sentinel(ctx) };
1306        }
1307        // The range check above bounds f to (-2^63, 2^63); truncation toward zero is
1308        // then exact for every representable integer and inexact-but-safe for the
1309        // fractional part (which is discarded).
1310        // SAFETY: ctx/heap valid; the value is in i64 range.
1311        unsafe { int_ref(ctx, f as i64) }
1312    })
1313}
1314
1315/// Re-box a `Float` after a pure transform (no fault possible). Used by
1316/// `abs`/`sqrt`/`floor`/`ceil`/`round`/`sign`.
1317unsafe fn rebox_float(ctx: *mut RuntimeContext, out: f64) -> GcRef {
1318    // SAFETY: ctx/heap valid; every f64 is a valid Float payload.
1319    unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, out) }
1320}
1321
1322/// `Float.abs()` — absolute value (§4.12).
1323///
1324/// # Safety
1325/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1326#[unsafe(no_mangle)]
1327pub unsafe extern "C" fn praxis_float_abs(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1328    abi_guard!("praxis_float_abs", ctx, {
1329        let f = unsafe { float_payload(r) };
1330        unsafe { rebox_float(ctx, f.abs()) }
1331    })
1332}
1333
1334/// `Float.sqrt()` — square root (§4.12). Negative inputs yield NaN (IEEE-754);
1335/// this never faults.
1336///
1337/// # Safety
1338/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1339#[unsafe(no_mangle)]
1340pub unsafe extern "C" fn praxis_float_sqrt(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1341    abi_guard!("praxis_float_sqrt", ctx, {
1342        let f = unsafe { float_payload(r) };
1343        unsafe { rebox_float(ctx, f.sqrt()) }
1344    })
1345}
1346
1347/// `Float.floor()` — round toward negative infinity (§4.12).
1348///
1349/// # Safety
1350/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1351#[unsafe(no_mangle)]
1352pub unsafe extern "C" fn praxis_float_floor(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1353    abi_guard!("praxis_float_floor", ctx, {
1354        let f = unsafe { float_payload(r) };
1355        unsafe { rebox_float(ctx, f.floor()) }
1356    })
1357}
1358
1359/// `Float.ceil()` — round toward positive infinity (§4.12).
1360///
1361/// # Safety
1362/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1363#[unsafe(no_mangle)]
1364pub unsafe extern "C" fn praxis_float_ceil(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1365    abi_guard!("praxis_float_ceil", ctx, {
1366        let f = unsafe { float_payload(r) };
1367        unsafe { rebox_float(ctx, f.ceil()) }
1368    })
1369}
1370
1371/// `Float.round()` — round half away from zero (§4.12, matches Rust's `f64::round`).
1372///
1373/// # Safety
1374/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1375#[unsafe(no_mangle)]
1376pub unsafe extern "C" fn praxis_float_round(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1377    abi_guard!("praxis_float_round", ctx, {
1378        let f = unsafe { float_payload(r) };
1379        unsafe { rebox_float(ctx, f.round()) }
1380    })
1381}
1382
1383/// `Float.sign()` — sign as -1.0 / 0.0 / 1.0 (§4.12). NaN yields NaN.
1384///
1385/// Not `f64::signum`: that returns `1.0` for `+0.0` and `-1.0` for `-0.0`,
1386/// because it reports the IEEE *sign bit*, not the sign of the value. Zero has
1387/// no sign in the sense `sign()` documents, so both zeros yield `0.0`.
1388///
1389/// # Safety
1390/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1391#[unsafe(no_mangle)]
1392pub unsafe extern "C" fn praxis_float_sign(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1393    abi_guard!("praxis_float_sign", ctx, {
1394        let f = unsafe { float_payload(r) };
1395        let sign = if f.is_nan() || f == 0.0 {
1396            // `f == 0.0` is true for both `+0.0` and `-0.0`; NaN falls through as
1397            // itself, which is what §4.12 specifies.
1398            f
1399        } else if f > 0.0 {
1400            1.0
1401        } else {
1402            -1.0
1403        };
1404        unsafe { rebox_float(ctx, sign) }
1405    })
1406}
1407
1408/// `Float.is_nan()` — true iff NaN (§4.12).
1409///
1410/// # Safety
1411/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1412#[unsafe(no_mangle)]
1413pub unsafe extern "C" fn praxis_float_is_nan(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1414    abi_guard!("praxis_float_is_nan", ctx, {
1415        let result = unsafe { float_payload(r) }.is_nan();
1416        // SAFETY: ctx valid; Bool immortal path.
1417        unsafe { bool_ref(ctx, result) }
1418    })
1419}
1420
1421/// `Float.is_infinite()` — true iff ±infinity (§4.12).
1422///
1423/// # Safety
1424/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1425#[unsafe(no_mangle)]
1426pub unsafe extern "C" fn praxis_float_is_infinite(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1427    abi_guard!("praxis_float_is_infinite", ctx, {
1428        let result = unsafe { float_payload(r) }.is_infinite();
1429        // SAFETY: ctx valid; Bool immortal path.
1430        unsafe { bool_ref(ctx, result) }
1431    })
1432}
1433
1434/// `Float.min(other)` — the smaller of two floats (§4.12). Per IEEE-754 /
1435/// Rust's `f64::min`: if either operand is NaN, returns the other (NaN only
1436/// propagates when both are NaN). `-0.0` is less than `+0.0`.
1437///
1438/// # Safety
1439/// `ctx` must be live and wired; both operands must be valid `Float` `GcRef`s.
1440#[unsafe(no_mangle)]
1441pub unsafe extern "C" fn praxis_float_min(
1442    ctx: *mut RuntimeContext,
1443    lhs: GcRef,
1444    rhs: GcRef,
1445) -> GcRef {
1446    abi_guard!("praxis_float_min", ctx, {
1447        let a = unsafe { float_payload(lhs) };
1448        let b = unsafe { float_payload(rhs) };
1449        unsafe { rebox_float(ctx, a.min(b)) }
1450    })
1451}
1452
1453/// `Float.max(other)` — the larger of two floats (§4.12). See `praxis_float_min`
1454/// for NaN handling.
1455///
1456/// # Safety
1457/// `ctx` must be live and wired; both operands must be valid `Float` `GcRef`s.
1458#[unsafe(no_mangle)]
1459pub unsafe extern "C" fn praxis_float_max(
1460    ctx: *mut RuntimeContext,
1461    lhs: GcRef,
1462    rhs: GcRef,
1463) -> GcRef {
1464    abi_guard!("praxis_float_max", ctx, {
1465        let a = unsafe { float_payload(lhs) };
1466        let b = unsafe { float_payload(rhs) };
1467        unsafe { rebox_float(ctx, a.max(b)) }
1468    })
1469}
1470
1471/// `Float.to_text()` — the same text `out()` writes, which is the shortest form
1472/// that reads back as the same Praxis `Float` (§4.12, ADR-083).
1473///
1474/// It goes through `scalars::write_float` rather than restating the rule,
1475/// because `to_text()` and `out()` disagreeing is a defect in itself: a program
1476/// that prints a value and a program that builds a string from it must produce
1477/// the same characters.
1478///
1479/// # Safety
1480/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1481#[unsafe(no_mangle)]
1482pub unsafe extern "C" fn praxis_float_to_text(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1483    abi_guard!("praxis_float_to_text", ctx, {
1484        let f = unsafe { float_payload(r) };
1485        let mut s = String::new();
1486        scalars::write_float(&mut s, f);
1487        // SAFETY: `s` is valid UTF-8 for the duration of the call; ctx/heap valid.
1488        unsafe { text_ref(ctx, s) }
1489    })
1490}
1491
1492/// `Int.to_text()` — the same digits `out()` writes (ADR-143).
1493///
1494/// It goes through `scalars::write_int` rather than restating the rendering,
1495/// because `to_text()` and `out()` disagreeing is a defect in itself: a program
1496/// that prints a value and a program that builds a string from it must produce
1497/// the same characters. That is the guarantee, and the shared writer is what
1498/// makes it structural rather than a thing a test happens to check.
1499///
1500/// Never faults: every `i64` renders, `i64::MIN` included.
1501///
1502/// # Safety
1503/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1504#[unsafe(no_mangle)]
1505pub unsafe extern "C" fn praxis_int_to_text(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1506    abi_guard!("praxis_int_to_text", ctx, {
1507        let v = unsafe { int_payload(r) };
1508        let mut s = String::new();
1509        scalars::write_int(&mut s, v);
1510        // SAFETY: `s` is valid UTF-8; ctx/heap valid.
1511        unsafe { text_ref(ctx, s) }
1512    })
1513}
1514
1515/// `Char.to_text()` — the one-character `Text` holding this scalar, which is the
1516/// same character `out()` writes (ADR-143).
1517///
1518/// Shares `scalars::write_char` with the descriptor's `format` callback for
1519/// [`praxis_int_to_text`]'s reason. Never faults: a `CharPayload` is a validated
1520/// Unicode scalar value by construction (ADR-086).
1521///
1522/// Reads through [`read_scalar`] with the `Char` handle rather than
1523/// `int_payload`, because a `Char` payload is **four** bytes and an `i64` read
1524/// would take eight of them — the same care [`praxis_char_to_int`] takes.
1525///
1526/// # Safety
1527/// `ctx` must be live and wired; `r` must be a valid `Char` `GcRef`.
1528#[unsafe(no_mangle)]
1529pub unsafe extern "C" fn praxis_char_to_text(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1530    abi_guard!("praxis_char_to_text", ctx, {
1531        // SAFETY: caller guarantees `r` is a valid `GcRef`; `read_scalar` proves
1532        // the descriptor is `CHAR` before reading its four bytes.
1533        let code = unsafe { read_scalar(r, scalars::CHAR_PAYLOAD) }.unwrap_or_else(|| {
1534            scalar_type_mismatch("praxis_char_to_text", "Char", r.descriptor().name)
1535        });
1536        let mut s = String::new();
1537        scalars::write_char(&mut s, code);
1538        // SAFETY: `s` is valid UTF-8; ctx/heap valid.
1539        unsafe { text_ref(ctx, s) }
1540    })
1541}
1542
1543/// `pi()` — the constant π as a `Float` (§4.12 prelude free function).
1544///
1545/// # Safety
1546/// `ctx` must be live and wired.
1547#[unsafe(no_mangle)]
1548pub unsafe extern "C" fn praxis_float_pi(ctx: *mut RuntimeContext) -> GcRef {
1549    abi_guard!("praxis_float_pi", ctx, {
1550        // SAFETY: ctx/heap valid.
1551        unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, core::f64::consts::PI) }
1552    })
1553}
1554
1555/// `e()` — Euler's number as a `Float` (§4.12 prelude free function).
1556///
1557/// # Safety
1558/// `ctx` must be live and wired.
1559#[unsafe(no_mangle)]
1560pub unsafe extern "C" fn praxis_float_e(ctx: *mut RuntimeContext) -> GcRef {
1561    abi_guard!("praxis_float_e", ctx, {
1562        // SAFETY: ctx/heap valid.
1563        unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, core::f64::consts::E) }
1564    })
1565}
1566
1567// ---------------------------------------------------------------------------
1568// Checked arithmetic (§4.12). All fault rather than panic (§10.4).
1569// ---------------------------------------------------------------------------
1570
1571macro_rules! checked_int_binop {
1572    ($name:ident, $op:tt, $fault:expr_2021) => {
1573        #[doc = concat!("Checked `Int ", stringify!($op), "` (§4.12). On fault sets `pending_fault` and returns Unit.")]
1574        ///
1575        /// # Safety
1576        /// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1577        #[unsafe(no_mangle)]
1578        pub unsafe extern "C" fn $name(
1579            ctx: *mut RuntimeContext,
1580            lhs: GcRef,
1581            rhs: GcRef,
1582        ) -> GcRef {
1583            abi_guard!(stringify!($name), ctx, {
1584                let a = unsafe { int_payload(lhs) };
1585                let b = unsafe { int_payload(rhs) };
1586                match a.$op(b) {
1587                    Some(result) => unsafe { int_ref(ctx, result) },
1588                    None => {
1589                        unsafe { set_fault(ctx, $fault) };
1590                        unsafe { unit_sentinel(ctx) }
1591                    }
1592                }
1593            })
1594        }
1595    };
1596}
1597
1598checked_int_binop!(praxis_int_add, checked_add, RaisedFault::INT_OVERFLOW);
1599checked_int_binop!(praxis_int_sub, checked_sub, RaisedFault::INT_OVERFLOW);
1600checked_int_binop!(praxis_int_mul, checked_mul, RaisedFault::INT_OVERFLOW);
1601
1602/// Checked `Int` division (§4.12). Faults on division by zero, and on overflow
1603/// (`Int::MIN / -1`, the one signed-division case that overflows §4.12).
1604///
1605/// # Safety
1606/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1607#[unsafe(no_mangle)]
1608pub unsafe extern "C" fn praxis_int_div(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
1609    abi_guard!("praxis_int_div", ctx, {
1610        let a = unsafe { int_payload(lhs) };
1611        let b = unsafe { int_payload(rhs) };
1612        if b == 0 {
1613            unsafe { set_fault(ctx, RaisedFault::DIV_BY_ZERO) };
1614            return unsafe { unit_sentinel(ctx) };
1615        }
1616        // `i64::MIN / -1` is the sole overflowing signed division: the mathematical
1617        // result (+2^63) is not representable, and the raw `/` panics on overflow in
1618        // debug builds (violating the no-panic-across-the-ABI rule, §10.4). Treat it
1619        // as checked-arithmetic overflow per §4.12.
1620        if a == i64::MIN && b == -1 {
1621            unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1622            return unsafe { unit_sentinel(ctx) };
1623        }
1624        // Division truncates toward zero (Rust's `i64::div_euclid` rounds differently;
1625        // Praxis follows C/Rust integer division semantics toward zero).
1626        unsafe { int_ref(ctx, a / b) }
1627    })
1628}
1629
1630/// Checked `Int` remainder (§4.12). Faults on division by zero, and on overflow
1631/// (`Int::MIN % -1`, whose result is not representable under the §4.12 rule).
1632///
1633/// # Safety
1634/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1635#[unsafe(no_mangle)]
1636pub unsafe extern "C" fn praxis_int_rem(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
1637    abi_guard!("praxis_int_rem", ctx, {
1638        let a = unsafe { int_payload(lhs) };
1639        let b = unsafe { int_payload(rhs) };
1640        if b == 0 {
1641            unsafe { set_fault(ctx, RaisedFault::DIV_BY_ZERO) };
1642            return unsafe { unit_sentinel(ctx) };
1643        }
1644        // `i64::MIN % -1`: the remainder is 0 mathematically, but the raw `%` traps
1645        // on this exact case in debug builds because the corresponding quotient
1646        // overflows. Guard it for the same no-panic reason as `praxis_int_div`.
1647        if a == i64::MIN && b == -1 {
1648            unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1649            return unsafe { unit_sentinel(ctx) };
1650        }
1651        unsafe { int_ref(ctx, a % b) }
1652    })
1653}
1654
1655/// Negate an `Int` (§4.12). Faults on overflow (`Int::MIN`).
1656///
1657/// # Safety
1658/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1659#[unsafe(no_mangle)]
1660pub unsafe extern "C" fn praxis_int_neg(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1661    abi_guard!("praxis_int_neg", ctx, {
1662        let a = unsafe { int_payload(r) };
1663        match a.checked_neg() {
1664            Some(result) => unsafe { int_ref(ctx, result) },
1665            None => {
1666                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1667                unsafe { unit_sentinel(ctx) }
1668            }
1669        }
1670    })
1671}
1672
1673// ---------------------------------------------------------------------------
1674// §4.12's explicit overflow alternatives: three modes —
1675// `wrapping_`, `saturating_`, `checked_` — over `add`, `sub` and `mul`.
1676//
1677// §4.12 states the family and its two closures (no `_div`/`_rem`, no
1678// `_neg`/`_abs`) and is the only place that rule is written; the catalog test
1679// `the_overflow_alternative_family_is_three_modes_over_three_operators` is what
1680// enforces it. Do not restate it here.
1681//
1682// **None of the nine can fault, and that is the whole point of them** — their
1683// manifest rows are `Allocates`, so ADR-088's verifier rule means no
1684// `CheckFault` follows the call. They allocate, like every other wrapper that
1685// answers a fresh number.
1686// ---------------------------------------------------------------------------
1687
1688/// `a.wrapping_add(b)` (§4.12): two's-complement wraparound instead of a fault.
1689///
1690/// # Safety
1691/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1692#[unsafe(no_mangle)]
1693pub unsafe extern "C" fn praxis_int_wrapping_add(
1694    ctx: *mut RuntimeContext,
1695    a: GcRef,
1696    b: GcRef,
1697) -> GcRef {
1698    abi_guard!("praxis_int_wrapping_add", ctx, {
1699        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1700        unsafe { int_ref(ctx, x.wrapping_add(y)) }
1701    })
1702}
1703
1704/// `a.saturating_add(b)` (§4.12): clamp to `Int`'s ends instead of faulting.
1705///
1706/// # Safety
1707/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1708#[unsafe(no_mangle)]
1709pub unsafe extern "C" fn praxis_int_saturating_add(
1710    ctx: *mut RuntimeContext,
1711    a: GcRef,
1712    b: GcRef,
1713) -> GcRef {
1714    abi_guard!("praxis_int_saturating_add", ctx, {
1715        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1716        unsafe { int_ref(ctx, x.saturating_add(y)) }
1717    })
1718}
1719
1720/// `a.checked_add(b)` (§4.12): `Option[Int]` — `None` where the checked `+`
1721/// would fault.
1722///
1723/// It answers a real `Option` (ADR-076): the absence is the *answer* here, not
1724/// an error channel, which is exactly §4.7's distinction.
1725///
1726/// # Safety
1727/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1728#[unsafe(no_mangle)]
1729pub unsafe extern "C" fn praxis_int_checked_add(
1730    ctx: *mut RuntimeContext,
1731    a: GcRef,
1732    b: GcRef,
1733) -> GcRef {
1734    abi_guard!("praxis_int_checked_add", ctx, {
1735        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1736        match x.checked_add(y) {
1737            Some(sum) => unsafe {
1738                let scope = NativeScope::new(ctx);
1739                let boxed = int_ref(ctx, sum);
1740                let rooted = scope.root(boxed);
1741                option_some(ctx, rooted.get())
1742            },
1743            None => unsafe { option_none(ctx) },
1744        }
1745    })
1746}
1747
1748/// `a.wrapping_sub(b)` (§4.12): two's-complement wraparound instead of a fault.
1749///
1750/// # Safety
1751/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1752#[unsafe(no_mangle)]
1753pub unsafe extern "C" fn praxis_int_wrapping_sub(
1754    ctx: *mut RuntimeContext,
1755    a: GcRef,
1756    b: GcRef,
1757) -> GcRef {
1758    abi_guard!("praxis_int_wrapping_sub", ctx, {
1759        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1760        unsafe { int_ref(ctx, x.wrapping_sub(y)) }
1761    })
1762}
1763
1764/// `a.saturating_sub(b)` (§4.12): clamp to `Int`'s ends instead of faulting.
1765///
1766/// # Safety
1767/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1768#[unsafe(no_mangle)]
1769pub unsafe extern "C" fn praxis_int_saturating_sub(
1770    ctx: *mut RuntimeContext,
1771    a: GcRef,
1772    b: GcRef,
1773) -> GcRef {
1774    abi_guard!("praxis_int_saturating_sub", ctx, {
1775        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1776        unsafe { int_ref(ctx, x.saturating_sub(y)) }
1777    })
1778}
1779
1780/// `a.checked_sub(b)` (§4.12): `Option[Int]` — `None` where the checked `-`
1781/// would fault.
1782///
1783/// # Safety
1784/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1785#[unsafe(no_mangle)]
1786pub unsafe extern "C" fn praxis_int_checked_sub(
1787    ctx: *mut RuntimeContext,
1788    a: GcRef,
1789    b: GcRef,
1790) -> GcRef {
1791    abi_guard!("praxis_int_checked_sub", ctx, {
1792        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1793        match x.checked_sub(y) {
1794            Some(difference) => unsafe {
1795                let scope = NativeScope::new(ctx);
1796                let boxed = int_ref(ctx, difference);
1797                let rooted = scope.root(boxed);
1798                option_some(ctx, rooted.get())
1799            },
1800            None => unsafe { option_none(ctx) },
1801        }
1802    })
1803}
1804
1805/// `a.wrapping_mul(b)` (§4.12): two's-complement wraparound instead of a fault.
1806///
1807/// This is the one of the nine a program could not write for itself: with every
1808/// arithmetic operator checked and no bitwise operators in the language, there
1809/// is no in-language spelling of modular multiplication (§4.12).
1810///
1811/// # Safety
1812/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1813#[unsafe(no_mangle)]
1814pub unsafe extern "C" fn praxis_int_wrapping_mul(
1815    ctx: *mut RuntimeContext,
1816    a: GcRef,
1817    b: GcRef,
1818) -> GcRef {
1819    abi_guard!("praxis_int_wrapping_mul", ctx, {
1820        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1821        unsafe { int_ref(ctx, x.wrapping_mul(y)) }
1822    })
1823}
1824
1825/// `a.saturating_mul(b)` (§4.12): clamp to `Int`'s ends instead of faulting.
1826///
1827/// # Safety
1828/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1829#[unsafe(no_mangle)]
1830pub unsafe extern "C" fn praxis_int_saturating_mul(
1831    ctx: *mut RuntimeContext,
1832    a: GcRef,
1833    b: GcRef,
1834) -> GcRef {
1835    abi_guard!("praxis_int_saturating_mul", ctx, {
1836        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1837        unsafe { int_ref(ctx, x.saturating_mul(y)) }
1838    })
1839}
1840
1841/// `a.checked_mul(b)` (§4.12): `Option[Int]` — `None` where the checked `*`
1842/// would fault.
1843///
1844/// # Safety
1845/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1846#[unsafe(no_mangle)]
1847pub unsafe extern "C" fn praxis_int_checked_mul(
1848    ctx: *mut RuntimeContext,
1849    a: GcRef,
1850    b: GcRef,
1851) -> GcRef {
1852    abi_guard!("praxis_int_checked_mul", ctx, {
1853        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1854        match x.checked_mul(y) {
1855            Some(product) => unsafe {
1856                let scope = NativeScope::new(ctx);
1857                let boxed = int_ref(ctx, product);
1858                let rooted = scope.root(boxed);
1859                option_some(ctx, rooted.get())
1860            },
1861            None => unsafe { option_none(ctx) },
1862        }
1863    })
1864}
1865
1866// ---------------------------------------------------------------------------
1867// The §16.1 numeric prelude helpers: `abs`, `sign`, `min`, `max`, `clamp`,
1868// `gcd`, `lcm`.
1869//
1870// All seven are monomorphic on `Int` (ADR-058), so every payload read here is
1871// an `Int` payload and no descriptor check is needed. `min`/`max`/`clamp` hand
1872// back one of the references they were given rather than allocating a copy:
1873// an `Int` object is immutable, so sharing it is what "the smaller of the two"
1874// means. The four that compute a *new* number allocate one, and the three that
1875// can leave the `Int` range fault rather than wrapping — `abs(Int::MIN)` has no
1876// positive counterpart, and `gcd`/`lcm` reach the same edge through it.
1877// ---------------------------------------------------------------------------
1878
1879/// `abs(n)` (§16.1). Faults on overflow: `Int::MIN` has no positive
1880/// counterpart, exactly as `praxis_int_neg` faults on it.
1881///
1882/// # Safety
1883/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1884#[unsafe(no_mangle)]
1885pub unsafe extern "C" fn praxis_int_abs(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1886    abi_guard!("praxis_int_abs", ctx, {
1887        let a = unsafe { int_payload(r) };
1888        match a.checked_abs() {
1889            Some(result) => unsafe { int_ref(ctx, result) },
1890            None => {
1891                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1892                unsafe { unit_sentinel(ctx) }
1893            }
1894        }
1895    })
1896}
1897
1898/// `sign(n)` (§16.1): `-1`, `0` or `1`. Total — every `Int`, `Int::MIN`
1899/// included, has a sign in range.
1900///
1901/// # Safety
1902/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1903#[unsafe(no_mangle)]
1904pub unsafe extern "C" fn praxis_int_sign(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1905    abi_guard!("praxis_int_sign", ctx, {
1906        let a = unsafe { int_payload(r) };
1907        unsafe { int_ref(ctx, a.signum()) }
1908    })
1909}
1910
1911/// `min(a, b)` (§16.1): the smaller operand, returned as **the reference that
1912/// was passed in**. Allocates nothing.
1913///
1914/// # Safety
1915/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1916#[unsafe(no_mangle)]
1917pub unsafe extern "C" fn praxis_int_min(
1918    _ctx: *mut RuntimeContext,
1919    lhs: GcRef,
1920    rhs: GcRef,
1921) -> GcRef {
1922    abi_guard!("praxis_int_min", _ctx, {
1923        let a = unsafe { int_payload(lhs) };
1924        let b = unsafe { int_payload(rhs) };
1925        if b < a { rhs } else { lhs }
1926    })
1927}
1928
1929/// `max(a, b)` (§16.1): the larger operand, returned as **the reference that
1930/// was passed in**. Allocates nothing.
1931///
1932/// # Safety
1933/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1934#[unsafe(no_mangle)]
1935pub unsafe extern "C" fn praxis_int_max(
1936    _ctx: *mut RuntimeContext,
1937    lhs: GcRef,
1938    rhs: GcRef,
1939) -> GcRef {
1940    abi_guard!("praxis_int_max", _ctx, {
1941        let a = unsafe { int_payload(lhs) };
1942        let b = unsafe { int_payload(rhs) };
1943        if b > a { rhs } else { lhs }
1944    })
1945}
1946
1947/// `clamp(value, low, high)` (§16.1): `value` confined to the inclusive range
1948/// `low..=high`, returned as one of the three references passed in.
1949///
1950/// **Faults when `low > high`.** The range is empty, so there is no value to
1951/// return and no answer that is not a guess — clamping to an empty range is a
1952/// mistake in the program, not in the data, and a mistake is reported rather
1953/// than answered with an invented number. (Rust's `Ord::clamp` panics on the
1954/// same input; a panic across `extern "C"` is what §10.4 forbids, so it is a
1955/// fault.) The kind is `EmptyRange` (ADR-058).
1956///
1957/// # Safety
1958/// `ctx` must be live and wired; all three operands must be valid `Int`
1959/// `GcRef`s.
1960#[unsafe(no_mangle)]
1961pub unsafe extern "C" fn praxis_int_clamp(
1962    ctx: *mut RuntimeContext,
1963    value: GcRef,
1964    low: GcRef,
1965    high: GcRef,
1966) -> GcRef {
1967    abi_guard!("praxis_int_clamp", ctx, {
1968        let v = unsafe { int_payload(value) };
1969        let lo = unsafe { int_payload(low) };
1970        let hi = unsafe { int_payload(high) };
1971        if lo > hi {
1972            unsafe { set_fault(ctx, RaisedFault::EMPTY_RANGE) };
1973            return unsafe { unit_sentinel(ctx) };
1974        }
1975        if v < lo {
1976            low
1977        } else if v > hi {
1978            high
1979        } else {
1980            value
1981        }
1982    })
1983}
1984
1985/// The non-negative greatest common divisor of two `i64`s, computed by
1986/// Euclid's algorithm **in `i128`** so that `Int::MIN`'s absolute value needs no
1987/// special case. Returns `None` only when the mathematical result is outside the
1988/// `Int` range, which happens for exactly one input pair:
1989/// `gcd(Int::MIN, Int::MIN)` is `2^63`.
1990///
1991/// `gcd(0, 0)` is `0` — the conventional answer, and the identity `gcd(n, 0) ==
1992/// abs(n)` extended to `n == 0`.
1993fn checked_gcd(a: i64, b: i64) -> Option<i64> {
1994    let mut x = (a as i128).abs();
1995    let mut y = (b as i128).abs();
1996    while y != 0 {
1997        let t = x % y;
1998        x = y;
1999        y = t;
2000    }
2001    i64::try_from(x).ok()
2002}
2003
2004/// `gcd(a, b)` (§16.1): the non-negative greatest common divisor. Faults on the
2005/// one pair whose result is out of range (`gcd(Int::MIN, Int::MIN)`).
2006///
2007/// # Safety
2008/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
2009#[unsafe(no_mangle)]
2010pub unsafe extern "C" fn praxis_int_gcd(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
2011    abi_guard!("praxis_int_gcd", ctx, {
2012        let a = unsafe { int_payload(lhs) };
2013        let b = unsafe { int_payload(rhs) };
2014        match checked_gcd(a, b) {
2015            Some(result) => unsafe { int_ref(ctx, result) },
2016            None => {
2017                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
2018                unsafe { unit_sentinel(ctx) }
2019            }
2020        }
2021    })
2022}
2023
2024/// `lcm(a, b)` (§16.1): the non-negative least common multiple, `0` when either
2025/// operand is `0`. Faults when the result does not fit an `Int` — which it
2026/// often does not, since the product of two large operands overflows long
2027/// before their multiple does.
2028///
2029/// # Safety
2030/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
2031#[unsafe(no_mangle)]
2032pub unsafe extern "C" fn praxis_int_lcm(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
2033    abi_guard!("praxis_int_lcm", ctx, {
2034        let a = unsafe { int_payload(lhs) };
2035        let b = unsafe { int_payload(rhs) };
2036        // `lcm(n, 0)` is 0 for every n: 0 is a multiple of everything, and dividing
2037        // by the gcd below would divide by zero when both are 0.
2038        if a == 0 || b == 0 {
2039            return unsafe { int_ref(ctx, 0i64) };
2040        }
2041        // |a / gcd * b| in i128, which cannot overflow: both operands fit i64, so
2042        // the product fits i128 with room to spare. The range check is the only
2043        // thing that can refuse.
2044        let result = checked_gcd(a, b)
2045            .map(|g| ((a as i128) / (g as i128) * (b as i128)).abs())
2046            .and_then(|m| i64::try_from(m).ok());
2047        match result {
2048            Some(result) => unsafe { int_ref(ctx, result) },
2049            None => {
2050                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
2051                unsafe { unit_sentinel(ctx) }
2052            }
2053        }
2054    })
2055}
2056
2057// ---------------------------------------------------------------------------
2058// Comparisons (yield a Bool GcRef).
2059// ---------------------------------------------------------------------------
2060
2061macro_rules! int_cmp {
2062    ($name:ident, $op:tt) => {
2063        #[doc = concat!(" `Int ", stringify!($op), "` comparison; returns a Bool GcRef (§4.12).")]
2064        ///
2065        /// # Safety
2066        /// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
2067        #[unsafe(no_mangle)]
2068        pub unsafe extern "C" fn $name(
2069            ctx: *mut RuntimeContext,
2070            lhs: GcRef,
2071            rhs: GcRef,
2072        ) -> GcRef {
2073            abi_guard!(stringify!($name), ctx, {
2074                let a = unsafe { int_payload(lhs) };
2075                let b = unsafe { int_payload(rhs) };
2076                let result = a $op b;
2077                // SAFETY: ctx/heap valid; Bool immortal path.
2078                unsafe { bool_ref(ctx, result) }
2079            })
2080        }
2081    };
2082}
2083
2084int_cmp!(praxis_int_eq, ==);
2085int_cmp!(praxis_int_ne, !=);
2086int_cmp!(praxis_int_lt, <);
2087int_cmp!(praxis_int_gt, >);
2088int_cmp!(praxis_int_le, <=);
2089int_cmp!(praxis_int_ge, >=);
2090
2091// ---------------------------------------------------------------------------
2092// Fault check.
2093// ---------------------------------------------------------------------------
2094
2095/// Return 1 if a fault is pending on `ctx`, else 0 (§10.4).
2096///
2097/// **Generated code does not call this.** An `Inst::CheckFault` is a load of
2098/// `ctx.pending_fault`, a load of the kind at
2099/// [`Fault::KIND_OFFSET`](crate::Fault::KIND_OFFSET) and a `brif` (ADR-102) —
2100/// the same question, without the call, the `catch_unwind` region and the
2101/// `Result` discriminant, on a path that runs once per faultable instruction.
2102///
2103/// The wrapper stays: it is the named ABI entry point for a host asking the
2104/// question from Rust (the JIT test harness does), it keeps its manifest row and
2105/// its address-table arm so `RuntimeSymbol` stays a bijection onto real
2106/// addresses, and deleting it would churn ADR-080's source-reading test for no
2107/// gain. Its two null tests are the difference between it and the inline form,
2108/// and they are why *this* is what a host with a possibly-unwired context calls.
2109///
2110/// # Safety
2111/// `ctx` must point at a live `RuntimeContext` (a null/unwired context reports
2112/// no fault rather than panicking).
2113#[unsafe(no_mangle)]
2114pub unsafe extern "C" fn praxis_check_fault(ctx: *mut RuntimeContext) -> i64 {
2115    abi_guard!("praxis_check_fault", ctx, {
2116        if ctx.is_null() {
2117            return 0;
2118        }
2119        if let Some(fault) = unsafe { (*ctx).pending_fault.as_ref() } {
2120            return fault.is_pending().into();
2121        }
2122        0
2123    })
2124}
2125
2126/// Stop the program at a `:bp` marker and show the host its frame chain (§9.8).
2127///
2128/// `span_start`/`span_end` are the marker's own source span, passed as
2129/// immediates: this is a call with no operands from the program, and boxing a
2130/// span so it could ride a `GcRef` argument would put an allocation at the one
2131/// site whose cost has to stay a single call.
2132///
2133/// Everything that makes a stop *not* a fault lives in
2134/// [`crate::breakpoint::stop`]: the host handler is given a snapshot and no
2135/// context, so it cannot allocate, cannot collect and cannot raise. That is what
2136/// lets this be declared [`Effect::Pure`](praxis_stdlib::abi::Effect::Pure), and
2137/// therefore what lets generated code emit no root spill before it and no fault
2138/// check after.
2139///
2140/// A program with no handler installed — every JIT test, every embedder that
2141/// wants none — finds nothing to call and returns.
2142///
2143/// # Safety
2144/// `ctx` must point at a live, wired `RuntimeContext` whose claimed debug frames
2145/// satisfy `copy_stack`'s contract, which every generated prologue establishes.
2146#[unsafe(no_mangle)]
2147pub unsafe extern "C" fn praxis_breakpoint(
2148    ctx: *mut RuntimeContext,
2149    span_start: u32,
2150    span_end: u32,
2151) {
2152    abi_guard!("praxis_breakpoint", ctx, {
2153        if ctx.is_null() {
2154            return;
2155        }
2156        // SAFETY: `ctx` is non-null and the caller guarantees it is live and
2157        // wired; the debug frames are the ones its prologue chain claimed.
2158        unsafe { crate::breakpoint::stop(ctx, (span_start, span_end)) };
2159    })
2160}
2161
2162/// Raise a [`FaultKind::StackOverflow`] fault on `ctx` (§9.2, §17.4). Called by
2163/// the generated prologue guard when `ctx.stack_left` is less than this frame's
2164/// [`frame_cost`](crate::frame_cost), so the host survives deep recursion
2165/// instead of overflowing the native stack. The prologue then unwinds to its
2166/// fault epilogue (pop frame + return Unit) — same path as any other fault.
2167///
2168/// # Safety
2169/// `ctx` must point at a live, wired `RuntimeContext`.
2170#[unsafe(no_mangle)]
2171pub unsafe extern "C" fn praxis_raise_stack_overflow(ctx: *mut RuntimeContext) {
2172    abi_guard!("praxis_raise_stack_overflow", ctx, {
2173        unsafe { set_fault(ctx, RaisedFault::STACK_OVERFLOW) };
2174    })
2175}
2176
2177/// Raise a [`FaultKind::EmptyCollection`] fault on `ctx` (§9.2).
2178///
2179/// `reduce`, `min_by` and `max_by` have no answer for an empty sequence: they
2180/// seed their accumulator from the first element, and there is no first
2181/// element. Handing back an unwritten accumulator slot would materialize
2182/// whatever the register held as a `GcRef` that is `NonNull` by type and
2183/// arbitrary in fact; this is the defined failure instead, and a fault is what
2184/// the other empty-collection accessors (`Deque.pop_front`, heap `pop`/`peek`)
2185/// already raise for the same reason.
2186///
2187/// Unconditional, unlike the two arithmetic raise wrappers: the emptiness test
2188/// is a branch generated code has to make anyway (the seen-flag gates the whole
2189/// sink), so there is no predicate worth passing. It returns the Unit sentinel
2190/// rather than nothing, so the MIR `Call` that emits it has an ordinary `Gc`
2191/// destination — a `Void` row would put the context pointer in a rootable slot.
2192///
2193/// # Safety
2194/// `ctx` must point at a live, wired `RuntimeContext`.
2195#[unsafe(no_mangle)]
2196pub unsafe extern "C" fn praxis_raise_empty_collection(ctx: *mut RuntimeContext) -> GcRef {
2197    abi_guard!("praxis_raise_empty_collection", ctx, {
2198        unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
2199        unsafe { unit_sentinel(ctx) }
2200    })
2201}
2202
2203/// Raise a [`FaultKind::IntOverflow`] fault on `ctx` iff `condition` is
2204/// non-zero (§4.12).
2205///
2206/// Generated code lowers `Int` arithmetic natively — `iadd`/`isub`/`imul` on
2207/// the raw scalar channel — and computes the overflow predicate inline. This is
2208/// how it reports one. It allocates nothing, so an arithmetic site is not a GC
2209/// safepoint and spills no roots.
2210///
2211/// **The call site branches; this is the cold path.** Calling unconditionally
2212/// and letting `condition` decide would keep arithmetic to a single basic
2213/// block, but a branch does not clobber registers and a call does, so it would
2214/// force a spill and reload of every live value around an arithmetic op that
2215/// never faults. The site is a `brif` to a cold block (ADR-102);
2216/// `raise_on_cold_path` in the backend carries the full argument.
2217///
2218/// The cold block passes a constant `1` — honest, since it is reached only when
2219/// the predicate held, and it keeps the test below a true statement rather than
2220/// dead code.
2221///
2222/// # Safety
2223/// `ctx` must point at a live, wired `RuntimeContext`.
2224#[unsafe(no_mangle)]
2225pub unsafe extern "C" fn praxis_raise_int_overflow_if(ctx: *mut RuntimeContext, condition: i64) {
2226    abi_guard!("praxis_raise_int_overflow_if", ctx, {
2227        if condition != 0 {
2228            unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
2229        }
2230    })
2231}
2232
2233/// Raise a [`FaultKind::DivByZero`] fault on `ctx` iff `condition` is non-zero
2234/// (§4.12). The division counterpart of [`praxis_raise_int_overflow_if`].
2235///
2236/// # Safety
2237/// `ctx` must point at a live, wired `RuntimeContext`.
2238#[unsafe(no_mangle)]
2239pub unsafe extern "C" fn praxis_raise_div_by_zero_if(ctx: *mut RuntimeContext, condition: i64) {
2240    abi_guard!("praxis_raise_div_by_zero_if", ctx, {
2241        if condition != 0 {
2242            unsafe { set_fault(ctx, RaisedFault::DIV_BY_ZERO) };
2243        }
2244    })
2245}
2246
2247// ---------------------------------------------------------------------------
2248// Collection payload accessors (§11.1, §11.4).
2249//
2250// Every collection below reaches its payload through a named accessor —
2251// `vec_payload`, `map_payload_mut`, … — and the name is the descriptor
2252// assertion the wrapper is making, which is why the nine kinds keep nine
2253// (shared, mut) pairs rather than calling `payload::<P>()` inline. The two
2254// casts underneath live here once, and each named accessor is the one line
2255// that spells its payload type.
2256// ---------------------------------------------------------------------------
2257
2258/// Read a `P` payload out of a `GcRef` as a shared ref.
2259///
2260/// # Safety
2261/// `r` must be a valid `GcRef` whose descriptor's payload type is `P`.
2262unsafe fn payload_ref<P>(r: GcRef) -> &'static P {
2263    // SAFETY: caller guarantees `r`'s payload is a `P`; the non-moving GC
2264    // (ADR-011) keeps the payload address stable for the object's lifetime. The
2265    // `'static` is unbounded because the raw FFI boundary has no lifetime to
2266    // carry; the caller (a wrapper that holds `ctx`) ensures the object outlives
2267    // the use.
2268    unsafe { &*r.payload::<P>() }
2269}
2270
2271/// Read a `P` payload out of a rooted `GcRef` as a mutable ref — the accessor
2272/// the wrappers that mutate in place go through (§11.1).
2273///
2274/// # Safety
2275/// `r` must be a valid `GcRef` whose descriptor's payload type is `P`, rooted
2276/// for `'s`.
2277unsafe fn payload_mut<'s, P>(r: Rooted<'s>) -> &'s mut P {
2278    // SAFETY: caller guarantees `r`'s payload is a `P`; the non-moving GC
2279    // (ADR-011) keeps the payload address stable for the object's lifetime, and
2280    // `Rooted` proves the object is in the collector's root set for `'s`, so a
2281    // collection triggered while this reference is held cannot reclaim what it
2282    // points at.
2283    unsafe { &mut *r.get().payload::<P>() }
2284}
2285
2286// ---------------------------------------------------------------------------
2287// Vec[T] collection methods (§11.1, §11.2, §11.5).
2288//
2289// `VecPayload` stores a growable [`ReprCVec<GcRef>`](crate::ReprCVec), so
2290// `push` mutates the existing payload in place and the receiver's `GcRef` stays
2291// valid across it. Per §11.5 reallocation safety, no interior pointer into that
2292// buffer is retained across a capacity-mutating op.
2293// ---------------------------------------------------------------------------
2294
2295/// Read the `VecPayload` out of a `GcRef` as a shared ref, asserting it is a Vec.
2296///
2297/// # Safety
2298/// `r` must be a valid `Vec` `GcRef`.
2299unsafe fn vec_payload(r: GcRef) -> &'static VecPayload {
2300    // SAFETY: caller guarantees `r` is a Vec; see `payload_ref`.
2301    unsafe { payload_ref::<VecPayload>(r) }
2302}
2303
2304/// Read the `VecPayload` out of a `GcRef` as a mutable ref, asserting it is a
2305/// Vec. Used by `push` to mutate the vector in place (§11.1).
2306///
2307/// # Safety
2308/// `r` must be a valid `Vec` `GcRef`, rooted for `'s`.
2309unsafe fn vec_payload_mut<'s>(r: Rooted<'s>) -> &'s mut VecPayload {
2310    // SAFETY: caller guarantees `r` is a Vec; see `payload_mut`.
2311    unsafe { payload_mut::<VecPayload>(r) }
2312}
2313
2314/// Build a `Vec[T]` holding `items`, with `element_descriptor` as its element
2315/// type — the shape every wrapper that answers with a collection needs.
2316///
2317/// The `Vec` is rooted across the pushes, which is the part worth having in one
2318/// place: `praxis_vec_new` allocates, and so may the caller's own iteration, so a
2319/// collection between the allocation and the last push would reclaim it.
2320///
2321/// `element_descriptor` may be **null**: the source collection's label is what
2322/// its own construction site knew, and that may have been nothing. A
2323/// `Vec`'s null means "empty" — `vec_format` reads it that way — so a null label
2324/// with members present would answer `[]`. The first member's own descriptor is
2325/// what the `Vec` adopts instead, which is exactly what `praxis_vec_push` does.
2326///
2327/// # Safety
2328/// `ctx` must be live and wired; `element_descriptor` must be a valid
2329/// `'static TypeDescriptor` or null; every item must be a valid `GcRef` whose
2330/// payload matches its own header.
2331unsafe fn vec_of(
2332    ctx: *mut RuntimeContext,
2333    element_descriptor: *const TypeDescriptor,
2334    items: impl Iterator<Item = GcRef>,
2335) -> GcRef {
2336    let items: Vec<GcRef> = items.collect();
2337    let element_descriptor = if element_descriptor.is_null() {
2338        items
2339            .first()
2340            .map_or(std::ptr::null(), |first| first.descriptor() as *const _)
2341    } else {
2342        element_descriptor
2343    };
2344    let result = unsafe { praxis_vec_new(ctx, element_descriptor) };
2345    let scope = unsafe { NativeScope::new(ctx) };
2346    let rp = unsafe { vec_payload_mut(scope.root(result)) };
2347    rp.items.extend(items);
2348    result
2349}
2350
2351/// Allocate a new empty `Vec[T]` with the given element descriptor (§11.2).
2352/// Returns a `GcRef` to a zero-length vector.
2353///
2354/// # Safety
2355/// `ctx` must be live and wired. `element_descriptor` must be a valid pointer to
2356/// a `'static TypeDescriptor`.
2357#[unsafe(no_mangle)]
2358pub unsafe extern "C" fn praxis_vec_new(
2359    ctx: *mut RuntimeContext,
2360    element_descriptor: *const TypeDescriptor,
2361) -> GcRef {
2362    abi_guard!("praxis_vec_new", ctx, {
2363        // A null descriptor is kept null: it means "the caller has no static
2364        // element type", which is a thing this payload can hold. Spelling it
2365        // `INT` instead would make an empty `Vec[Float]` claim to hold `Int`s.
2366        // SAFETY: VecPayload is VEC's payload type.
2367        unsafe {
2368            gc_alloc_owned(ctx, &crate::collections::VEC, || VecPayload {
2369                element_descriptor,
2370                items: ReprCVec::new(),
2371            })
2372        }
2373    })
2374}
2375
2376/// Allocate a `Vec[T]` of `count` slots, every one holding `fill` (ADR-146's
2377/// `Vec(n, fill)`).
2378///
2379/// Faults `InvalidSize` if `count` is negative or exceeds
2380/// [`VecExtent::MAX_ITEMS`](crate::collections::VecExtent::MAX_ITEMS): the count
2381/// arrives from source and would otherwise become a `usize` cast, where a
2382/// negative value lands near `usize::MAX` (ADR-041 decision 1).
2383///
2384/// Faults `TypeMismatch` if the caller declared an element type that the fill is
2385/// not, through the same [`adopt_or_reject`] `push` uses — a `Vec[Int]` filled
2386/// with a `Float` is a mislabelled element descriptor, and every later
2387/// `equals`/`hash`/`format` would read the payloads as the wrong type. A null
2388/// static descriptor adopts the fill's, which is what "the caller has no static
2389/// element type" already means here.
2390///
2391/// **`fill` is stored `count` times, not copied `count` times.** Every slot is
2392/// the same `GcRef`, so `Vec(3, Vec())` is three names for one inner `Vec`.
2393/// That is the language's existing reference semantics — `outer.push(a)` twice
2394/// aliases too — stated at a new site rather than a new rule (ADR-146 decision
2395/// 4).
2396///
2397/// `count` arrives boxed rather than as a `RawI64` like [`praxis_grid_new`]'s
2398/// extents: MIR lowers an argument expression to a `Gc` local, and unboxing it
2399/// there would cost an `ExtractScalar` and a second shape in the codegen's
2400/// allocation arm. `praxis_grid_new`'s two are `iconst` immediates with no local
2401/// to unbox, which is why the two wrappers differ.
2402///
2403/// # Safety
2404/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
2405/// a `'static TypeDescriptor` (or null); `count` must be a valid `Int` `GcRef`;
2406/// `fill` must be a valid `GcRef`.
2407#[unsafe(no_mangle)]
2408pub unsafe extern "C" fn praxis_vec_filled(
2409    ctx: *mut RuntimeContext,
2410    element_descriptor: *const TypeDescriptor,
2411    count: GcRef,
2412    fill: GcRef,
2413) -> GcRef {
2414    abi_guard!("praxis_vec_filled", ctx, {
2415        // SAFETY: caller guarantees `count` is a valid Int.
2416        let n = unsafe { int_payload(count) };
2417        let Some(extent) = crate::collections::VecExtent::new(n) else {
2418            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
2419            return unsafe { unit_sentinel(ctx) };
2420        };
2421        let mut descriptor = element_descriptor;
2422        if !unsafe { adopt_or_reject(ctx, &mut descriptor, fill) } {
2423            return unsafe { unit_sentinel(ctx) };
2424        }
2425        // `fill` is a bare `GcRef` argument, and `gc_alloc_owned` may collect.
2426        // Rooting it in a native scope is what keeps it addressable across the
2427        // allocation — the caller's shadow frame roots it up to the call, and
2428        // this roots it through it.
2429        let scope = unsafe { NativeScope::new(ctx) };
2430        let fill = scope.root(fill).get();
2431        // The items are built inside the initializer, which `gc_alloc_owned`
2432        // runs *after* the safepoint: no untraced `Vec<GcRef>` is ever live
2433        // across a collection.
2434        // SAFETY: VecPayload is VEC's payload type.
2435        unsafe {
2436            gc_alloc_owned(ctx, &crate::collections::VEC, || VecPayload {
2437                element_descriptor: descriptor,
2438                items: ReprCVec::from_vec(vec![fill; extent.len()]),
2439            })
2440        }
2441    })
2442}
2443
2444/// Allocate a nominal record (§4.5) with all fields initialized to Unit.
2445/// The `schema_ptr` points at a `'static RecordSchema` (built and leaked by the
2446/// codegen from the record def). Fields are filled in declaration order via
2447/// [`praxis_record_set_field`] after allocation. Returns the record `GcRef`.
2448///
2449/// # Safety
2450/// `ctx` must be live and wired; `schema_ptr` must be a valid `'static` pointer.
2451#[unsafe(no_mangle)]
2452pub unsafe extern "C" fn praxis_alloc_record(
2453    ctx: *mut RuntimeContext,
2454    schema_ptr: *const crate::records::RecordSchema,
2455) -> GcRef {
2456    abi_guard!("praxis_alloc_record", ctx, {
2457        if schema_ptr.is_null() {
2458            // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2459            // caller already guarantees.
2460            return unsafe { unit_sentinel(ctx) };
2461        }
2462        // SAFETY: caller guarantees schema_ptr is a valid 'static pointer.
2463        let schema = unsafe { &*schema_ptr };
2464        let arity = schema.fields.len();
2465        // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2466        // caller already guarantees.
2467        let unit = unsafe { unit_sentinel(ctx) };
2468        // SAFETY: RecordPayload is RECORD's payload type.
2469        // Every field slot starts as Unit (a valid GcRef), keeping the GC sound
2470        // before the caller fills them in via praxis_record_set_field.
2471        unsafe {
2472            gc_alloc_owned(ctx, &crate::records::RECORD, || {
2473                crate::records::RecordPayload {
2474                    schema: schema_ptr,
2475                    items: vec![unit; arity],
2476                }
2477            })
2478        }
2479    })
2480}
2481
2482/// Set field `idx` of `record` to `value` (§4.5). Used by the codegen to
2483/// fill in fields after [`praxis_alloc_record`]. Returns the record (the
2484/// receiver is mutated in place).
2485///
2486/// # Safety
2487/// `ctx` must be live; `record` must be a valid record `GcRef`; `idx` must be
2488/// in bounds.
2489#[unsafe(no_mangle)]
2490pub unsafe extern "C" fn praxis_record_set_field(
2491    ctx: *mut RuntimeContext,
2492    record: GcRef,
2493    idx: u32,
2494    value: GcRef,
2495) -> GcRef {
2496    abi_guard!("praxis_record_set_field", ctx, {
2497        let _ = ctx;
2498        // SAFETY: caller guarantees record is a valid record GcRef.
2499        let payload = record.payload::<u8>() as *mut crate::records::RecordPayload;
2500        // SAFETY: the payload is a RecordPayload for any RECORD-descriptor object.
2501        let rp = unsafe { &mut *payload };
2502        if let Some(slot) = rp.items.get_mut(idx as usize) {
2503            *slot = value;
2504        }
2505        record
2506    })
2507}
2508
2509/// Read field `idx` out of a record `GcRef` (§4.5). Returns the field's
2510/// `GcRef` value. Returns Unit if the record is malformed or the index is out
2511/// of bounds (defensive; the type checker prevents this in well-typed code).
2512///
2513/// # Safety
2514/// `ctx` must be live; `record` must be a valid record `GcRef`.
2515#[unsafe(no_mangle)]
2516pub unsafe extern "C" fn praxis_record_field(
2517    ctx: *mut RuntimeContext,
2518    record: GcRef,
2519    idx: u32,
2520) -> GcRef {
2521    abi_guard!("praxis_record_field", ctx, {
2522        // SAFETY: caller guarantees record is a valid record GcRef; the payload is
2523        // a RecordPayload for any RECORD-descriptor object.
2524        let payload = record.payload::<u8>() as *const crate::records::RecordPayload;
2525        let rp = unsafe { &*payload };
2526        rp.items
2527            .get(idx as usize)
2528            .copied()
2529            .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2530    })
2531}
2532
2533/// Allocate an enum value (§4.6) of the type `schema_ptr` describes, with
2534/// variant `tag` and every payload slot initialized to Unit. Payload values are
2535/// filled via [`praxis_enum_set_payload`] after allocation. Returns the enum
2536/// `GcRef`.
2537///
2538/// The arity is **read from the schema** rather than passed alongside it, as
2539/// [`praxis_alloc_tuple`] already does: a schema and an arity that disagree is
2540/// a state no caller can now reach. A null schema, or a tag the schema has no
2541/// variant for, allocates nothing and answers the Unit sentinel — the same
2542/// answer `praxis_alloc_tuple` gives a null schema.
2543///
2544/// # Safety
2545/// `ctx` must be live and wired; `schema_ptr` must be null or a valid
2546/// `'static` pointer.
2547#[unsafe(no_mangle)]
2548pub unsafe extern "C" fn praxis_alloc_enum(
2549    ctx: *mut RuntimeContext,
2550    schema_ptr: *const crate::enums::EnumSchema,
2551    tag: i64,
2552) -> GcRef {
2553    abi_guard!("praxis_alloc_enum", ctx, {
2554        if schema_ptr.is_null() || tag < 0 {
2555            return unsafe { unit_sentinel(ctx) };
2556        }
2557        // SAFETY: caller guarantees schema_ptr is a valid 'static pointer.
2558        let schema = unsafe { &*schema_ptr };
2559        if schema.variant_at(tag as usize).is_none() {
2560            return unsafe { unit_sentinel(ctx) };
2561        }
2562        let arity = schema.arity_of(tag as usize);
2563        let unit = unsafe { unit_sentinel(ctx) };
2564        let items = vec![unit; arity];
2565        // SAFETY: EnumPayload is ENUM's payload type.
2566        unsafe {
2567            gc_alloc_owned(ctx, &crate::enums::ENUM, || crate::enums::EnumPayload {
2568                schema: schema_ptr,
2569                tag: tag as u32,
2570                items,
2571            })
2572        }
2573    })
2574}
2575
2576/// Allocate `Some(value)` under the runtime's own [`option_schema`].
2577///
2578/// `value` is rooted across the enum allocation: the allocation is a safepoint,
2579/// and a bare `GcRef` argument is not in anyone's root set.
2580///
2581/// [`option_schema`]: crate::enums::option_schema
2582///
2583/// # Safety
2584/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
2585pub(crate) unsafe fn option_some(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
2586    // SAFETY: the caller upholds ctx/value validity.
2587    unsafe {
2588        let scope = NativeScope::new(ctx);
2589        let rooted = scope.root(value);
2590        let some = praxis_alloc_enum(
2591            ctx,
2592            crate::enums::option_schema(),
2593            crate::enums::OPTION_SOME_TAG,
2594        );
2595        praxis_enum_set_payload(ctx, some, 0, rooted.get());
2596        some
2597    }
2598}
2599
2600/// Allocate `None` under the runtime's own `option_schema`.
2601///
2602/// # Safety
2603/// `ctx` must be live and wired.
2604pub(crate) unsafe fn option_none(ctx: *mut RuntimeContext) -> GcRef {
2605    // SAFETY: the caller upholds ctx validity.
2606    unsafe {
2607        praxis_alloc_enum(
2608            ctx,
2609            crate::enums::option_schema(),
2610            crate::enums::OPTION_NONE_TAG,
2611        )
2612    }
2613}
2614
2615/// Set payload slot `idx` of `enum_value` to `value` (§4.6). Returns the
2616/// enum value (mutated in place).
2617///
2618/// # Safety
2619/// `ctx` must be live; `enum_value` must be a valid enum `GcRef`; `idx` in bounds.
2620#[unsafe(no_mangle)]
2621pub unsafe extern "C" fn praxis_enum_set_payload(
2622    ctx: *mut RuntimeContext,
2623    enum_value: GcRef,
2624    idx: i64,
2625    value: GcRef,
2626) -> GcRef {
2627    abi_guard!("praxis_enum_set_payload", ctx, {
2628        let _ = ctx;
2629        // SAFETY: caller guarantees enum_value is a valid enum GcRef.
2630        let payload = enum_value.payload::<u8>() as *mut crate::enums::EnumPayload;
2631        let ep = unsafe { &mut *payload };
2632        if let Some(slot) = ep.items.get_mut(idx as usize) {
2633            *slot = value;
2634        }
2635        enum_value
2636    })
2637}
2638
2639/// Read the variant tag (discriminant) of an enum value (§4.6). Returns the
2640/// tag as a boxed `Int` `GcRef` (the uniform ABI convention), so the `match`
2641/// lowering can extract the scalar and compare. Used by `match` to branch.
2642///
2643/// # Safety
2644/// `ctx` must be live; `enum_value` must be a valid enum `GcRef`.
2645#[unsafe(no_mangle)]
2646pub unsafe extern "C" fn praxis_enum_tag(ctx: *mut RuntimeContext, enum_value: GcRef) -> GcRef {
2647    abi_guard!("praxis_enum_tag", ctx, {
2648        // SAFETY: caller guarantees enum_value is a valid enum GcRef.
2649        // Read the tag BEFORE allocating — the alloc below may trigger GC, and
2650        // enum_value is not explicitly rooted (it's only in a Cranelift local).
2651        let payload = enum_value.payload::<u8>() as *const crate::enums::EnumPayload;
2652        let tag = unsafe { (*payload).tag as i64 };
2653        // SAFETY: alloc boxes the i64 into a fresh Int object. The tag value is
2654        // already in a register, so GC collecting enum_value here is safe.
2655        unsafe { int_ref(ctx, tag) }
2656    })
2657}
2658
2659/// Read payload slot `idx` of an enum value (§4.6). Returns the slot's
2660/// `GcRef`. Used by `match` to bind variant payload variables.
2661///
2662/// # Safety
2663/// `ctx` must be live; `enum_value` must be a valid enum `GcRef`; `idx` in bounds.
2664#[unsafe(no_mangle)]
2665pub unsafe extern "C" fn praxis_enum_payload(
2666    ctx: *mut RuntimeContext,
2667    enum_value: GcRef,
2668    idx: i64,
2669) -> GcRef {
2670    abi_guard!("praxis_enum_payload", ctx, {
2671        // SAFETY: caller guarantees enum_value is a valid enum GcRef.
2672        let payload = enum_value.payload::<u8>() as *const crate::enums::EnumPayload;
2673        let ep = unsafe { &*payload };
2674        ep.items
2675            .get(idx as usize)
2676            .copied()
2677            .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2678    })
2679}
2680
2681/// Allocate a tuple (§4.5 structural tuples) with all element slots
2682/// initialized to Unit. The `schema_ptr` points at a `'static TupleSchema`
2683/// (built and leaked by the codegen from the tuple's element-type sequence).
2684/// Elements are filled in positional order via [`praxis_tuple_set`] after
2685/// allocation. Returns the tuple `GcRef`.
2686///
2687/// # Safety
2688/// `ctx` must be live and wired; `schema_ptr` must be a valid `'static` pointer.
2689#[unsafe(no_mangle)]
2690pub unsafe extern "C" fn praxis_alloc_tuple(
2691    ctx: *mut RuntimeContext,
2692    schema_ptr: *const crate::tuples::TupleSchema,
2693) -> GcRef {
2694    abi_guard!("praxis_alloc_tuple", ctx, {
2695        if schema_ptr.is_null() {
2696            // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2697            // caller already guarantees.
2698            return unsafe { unit_sentinel(ctx) };
2699        }
2700        // SAFETY: caller guarantees schema_ptr is a valid 'static pointer.
2701        let schema = unsafe { &*schema_ptr };
2702        let arity = schema.descriptors.len();
2703        // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2704        // caller already guarantees.
2705        let unit = unsafe { unit_sentinel(ctx) };
2706        // SAFETY: TuplePayload is TUPLE's payload type.
2707        // Every element slot starts as Unit (a valid GcRef), keeping the GC sound
2708        // before the caller fills them in via praxis_tuple_set.
2709        unsafe {
2710            gc_alloc_owned(ctx, &crate::tuples::TUPLE, || crate::tuples::TuplePayload {
2711                schema: schema_ptr,
2712                items: vec![unit; arity],
2713            })
2714        }
2715    })
2716}
2717
2718/// Set element `idx` of `tuple` to `value` (§4.5). Used by the codegen to
2719/// fill in elements after [`praxis_alloc_tuple`]. Returns the tuple (the
2720/// receiver is mutated in place).
2721///
2722/// # Safety
2723/// `ctx` must be live; `tuple` must be a valid tuple `GcRef`; `idx` in bounds.
2724#[unsafe(no_mangle)]
2725pub unsafe extern "C" fn praxis_tuple_set(
2726    ctx: *mut RuntimeContext,
2727    tuple: GcRef,
2728    idx: i64,
2729    value: GcRef,
2730) -> GcRef {
2731    abi_guard!("praxis_tuple_set", ctx, {
2732        let _ = ctx;
2733        // SAFETY: caller guarantees tuple is a valid tuple GcRef.
2734        let payload = tuple.payload::<u8>() as *mut crate::tuples::TuplePayload;
2735        // SAFETY: the payload is a TuplePayload for any TUPLE-descriptor object.
2736        let tp = unsafe { &mut *payload };
2737        if let Some(slot) = tp.items.get_mut(idx as usize) {
2738            *slot = value;
2739        }
2740        tuple
2741    })
2742}
2743
2744/// Read element `idx` out of a tuple `GcRef` (§4.5). Returns the element's
2745/// `GcRef` value. Returns Unit if the tuple is malformed or the index is out of
2746/// bounds (defensive; the type checker prevents this in well-typed code).
2747///
2748/// # Safety
2749/// `ctx` must be live; `tuple` must be a valid tuple `GcRef`.
2750#[unsafe(no_mangle)]
2751pub unsafe extern "C" fn praxis_tuple_get(
2752    ctx: *mut RuntimeContext,
2753    tuple: GcRef,
2754    idx: i64,
2755) -> GcRef {
2756    abi_guard!("praxis_tuple_get", ctx, {
2757        // SAFETY: caller guarantees tuple is a valid tuple GcRef; the payload is a
2758        // TuplePayload for any TUPLE-descriptor object.
2759        let payload = tuple.payload::<u8>() as *const crate::tuples::TuplePayload;
2760        let tp = unsafe { &*payload };
2761        tp.items
2762            .get(idx as usize)
2763            .copied()
2764            .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2765    })
2766}
2767
2768/// Structural equality between two GC values (§5.5). Reads the descriptor
2769/// from `a` and dispatches to its `equals` callback, which recurses element/field
2770/// wise for composite types (records, tuples, enums, collections). Returns 1 for
2771/// equal, 0 for not equal. Returns 0 if `a`'s type is not equatable (functions
2772/// are never equatable, §5.5) — the type checker rejects this in well-typed code,
2773/// so this is defensive.
2774///
2775/// # Safety
2776/// `ctx` must be live and wired; `a` and `b` must be valid `GcRef`s of the same
2777/// type (the caller has already unified their types at compile time).
2778#[unsafe(no_mangle)]
2779pub unsafe extern "C" fn praxis_struct_eq(ctx: *mut RuntimeContext, a: GcRef, b: GcRef) -> i64 {
2780    abi_guard!("praxis_struct_eq", ctx, {
2781        let _ = ctx;
2782        // SAFETY: caller guarantees a is a valid GcRef; the descriptor header is
2783        // always present and its `equals` (if Some) is safe to call with a/b.
2784        let desc = a.descriptor();
2785        // Both operands must be the same runtime type before any callback runs
2786        // (ADR-045 decision 3). Well-typed code has unified them, so this is the
2787        // miscompile case — and a callback dispatched on a foreign layout is how a
2788        // type confusion becomes a wild read rather than a wrong answer.
2789        if !std::ptr::eq(desc, b.descriptor()) {
2790            return 0;
2791        }
2792        match desc.equals {
2793            // SAFETY: both a and b are values of desc's type (caller has type-checked
2794            // them equal); the equals callback is safe under that invariant.
2795            Some(eq) => {
2796                let pa = a.payload::<u8>() as *const u8;
2797                let pb = b.payload::<u8>() as *const u8;
2798                if unsafe { eq(pa, pb) } { 1 } else { 0 }
2799            }
2800            // Not equatable: treat as not-equal. The type checker rejects this in
2801            // well-typed code; the defensive default keeps runtime sound.
2802            None => 0,
2803        }
2804    })
2805}
2806
2807/// Order two GC values through their descriptor's `compare` callback (ADR-045).
2808/// Returns `-1`, `0` or `1` — the caller turns that into the `<`/`<=`/`>`/`>=`
2809/// it wanted by comparing against zero.
2810///
2811/// This is the ordering counterpart of [`praxis_struct_eq`], and it exists for
2812/// the same reason: a `Text` is a pointer-and-length structure, so ordering one
2813/// by loading its first eight payload bytes would compare *addresses*.
2814///
2815/// Raises `FaultKind::TypeMismatch` and answers `0` when the two operands are
2816/// not the same runtime type, or when the type has no `compare`. The type
2817/// checker rejects both in well-typed code (`Y006`), so reaching either is a
2818/// compiler bug — reported as a fault rather than a callback dispatched on a
2819/// foreign layout.
2820///
2821/// The second guard is a weak backstop, and deliberately named as one: ADR-138
2822/// populated `compare` on every type a `Map` key can be, including tuples and
2823/// records, so a *miscompile* that lowered `(1, 2) < (1, 3)` to this wrapper
2824/// would be answered rather than faulted. `capability::supports_ord` refuses it
2825/// at `praxis check`, so no well-typed program reaches here either way.
2826///
2827/// # Safety
2828/// `ctx` must be live and wired; `a` and `b` must be valid `GcRef`s.
2829#[unsafe(no_mangle)]
2830pub unsafe extern "C" fn praxis_value_cmp(ctx: *mut RuntimeContext, a: GcRef, b: GcRef) -> i64 {
2831    abi_guard!("praxis_value_cmp", ctx, {
2832        // SAFETY: caller guarantees both are valid GcRefs; every object carries a
2833        // descriptor in its header.
2834        let desc = a.descriptor();
2835        if !std::ptr::eq(desc, b.descriptor()) {
2836            unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
2837            return 0;
2838        }
2839        let Some(compare) = desc.compare else {
2840            unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
2841            return 0;
2842        };
2843        // SAFETY: both values carry `desc` (checked above), so both payloads are
2844        // values of its type.
2845        let ordering = unsafe {
2846            compare(
2847                a.payload::<u8>() as *const u8,
2848                b.payload::<u8>() as *const u8,
2849            )
2850        };
2851        match ordering {
2852            std::cmp::Ordering::Less => -1,
2853            std::cmp::Ordering::Equal => 0,
2854            std::cmp::Ordering::Greater => 1,
2855        }
2856    })
2857}
2858
2859/// `x min= candidate` on a name or a record field (ADR-161): the one of the two
2860/// to keep, answered rather than computed.
2861///
2862/// # Why this is a wrapper and the collections' updates are not
2863///
2864/// A `Map`, a `Vec`, a `Deque` and a `Grid` each own the storage their update
2865/// writes to, so the wrapper does the whole read-compare-write and answers Unit.
2866/// A `var` and a record field are storage the *compiler* owns — a MIR slot and a
2867/// field index — so there is nothing for a runtime wrapper to write. What is
2868/// left is the part the runtime is needed for at all: the ordering, which is the
2869/// value's own (ADR-045). MIR takes it from here and does the store it was
2870/// already doing for `+=`.
2871///
2872/// A tie answers `current`, which is every other updating store's rule and is
2873/// what makes `x min= v` idempotent.
2874///
2875/// # Safety
2876/// `ctx` must be live and wired; `current` and `candidate` must be valid
2877/// `GcRef`s.
2878#[unsafe(no_mangle)]
2879pub unsafe extern "C" fn praxis_value_keep_min(
2880    ctx: *mut RuntimeContext,
2881    current: GcRef,
2882    candidate: GcRef,
2883) -> GcRef {
2884    abi_guard!("praxis_value_keep_min", ctx, {
2885        // SAFETY: both are valid `GcRef`s, so each payload matches the
2886        // descriptor in its own header.
2887        if unsafe { update_cmp(candidate, current) } == std::cmp::Ordering::Less {
2888            candidate
2889        } else {
2890            current
2891        }
2892    })
2893}
2894
2895/// `x max= candidate` — the dual of [`praxis_value_keep_min`], including its tie
2896/// rule.
2897///
2898/// # Safety
2899/// As [`praxis_value_keep_min`].
2900#[unsafe(no_mangle)]
2901pub unsafe extern "C" fn praxis_value_keep_max(
2902    ctx: *mut RuntimeContext,
2903    current: GcRef,
2904    candidate: GcRef,
2905) -> GcRef {
2906    abi_guard!("praxis_value_keep_max", ctx, {
2907        // SAFETY: as `praxis_value_keep_min`.
2908        if unsafe { update_cmp(candidate, current) } == std::cmp::Ordering::Greater {
2909            candidate
2910        } else {
2911            current
2912        }
2913    })
2914}
2915
2916/// Allocate a closure value (§4.10) with `fn_ptr` as its entry point and
2917/// `n_captures` environment slots initialized to Unit. Captures are filled via
2918/// [`praxis_closure_set_capture`] after allocation. Returns the closure `GcRef`.
2919///
2920/// # Safety
2921/// `ctx` must be live and wired; `fn_ptr` must be a valid JIT'd function pointer
2922/// whose calling convention matches `fn(ctx, params..., env...) -> i64`.
2923#[unsafe(no_mangle)]
2924pub unsafe extern "C" fn praxis_alloc_closure(
2925    ctx: *mut RuntimeContext,
2926    fn_ptr: *const u8,
2927    n_captures: i64,
2928) -> GcRef {
2929    abi_guard!("praxis_alloc_closure", ctx, {
2930        // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2931        // caller already guarantees.
2932        let unit = unsafe { unit_sentinel(ctx) };
2933        let env = vec![unit; n_captures as usize];
2934        // SAFETY: ClosurePayload is CLOSURE's payload type.
2935        unsafe {
2936            gc_alloc_owned(ctx, &crate::closures::CLOSURE, || {
2937                crate::closures::ClosurePayload { fn_ptr, env }
2938            })
2939        }
2940    })
2941}
2942
2943/// Set capture slot `idx` of `closure` to `value` (§4.10). Returns the
2944/// closure (mutated in place).
2945///
2946/// # Safety
2947/// `ctx` must be live; `closure` must be a valid closure `GcRef`; `idx` in bounds.
2948#[unsafe(no_mangle)]
2949pub unsafe extern "C" fn praxis_closure_set_capture(
2950    ctx: *mut RuntimeContext,
2951    closure: GcRef,
2952    idx: i64,
2953    value: GcRef,
2954) -> GcRef {
2955    abi_guard!("praxis_closure_set_capture", ctx, {
2956        let _ = ctx;
2957        // SAFETY: caller guarantees closure is a valid closure GcRef.
2958        let payload = closure.payload::<u8>() as *mut crate::closures::ClosurePayload;
2959        let cp = unsafe { &mut *payload };
2960        if let Some(slot) = cp.env.get_mut(idx as usize) {
2961            *slot = value;
2962        }
2963        closure
2964    })
2965}
2966
2967/// Read the function pointer out of a closure `GcRef` (§4.10). Used by the
2968/// indirect-call lowering to obtain the entry point before a native call.
2969///
2970/// `ctx` is accepted (and unused) for ABI uniformity with every other `praxis_*`
2971/// wrapper — generated code calls all wrappers as `fn(ctx, args...)`, so this
2972/// keeps the calling convention consistent. The returned `*const u8` is carried
2973/// as an `i64` (pointer-sized) back into the JIT'd code.
2974///
2975/// # Safety
2976/// `ctx` must be live; `closure` must be a valid closure `GcRef`.
2977#[unsafe(no_mangle)]
2978pub unsafe extern "C" fn praxis_closure_fn_ptr(
2979    ctx: *mut RuntimeContext,
2980    closure: GcRef,
2981) -> *const u8 {
2982    abi_guard!("praxis_closure_fn_ptr", ctx, {
2983        let _ = ctx;
2984        // SAFETY: caller guarantees closure is a valid closure GcRef.
2985        let payload = closure.payload::<u8>() as *const crate::closures::ClosurePayload;
2986        unsafe { (*payload).fn_ptr }
2987    })
2988}
2989
2990/// Read capture slot `idx` out of a closure `GcRef` (§4.10). Used by the
2991/// closure's synthetic function to load its captured values from the env.
2992///
2993/// # Safety
2994/// `ctx` must be live; `closure` must be a valid closure `GcRef`; `idx` in bounds.
2995#[unsafe(no_mangle)]
2996pub unsafe extern "C" fn praxis_closure_capture(
2997    ctx: *mut RuntimeContext,
2998    closure: GcRef,
2999    idx: i64,
3000) -> GcRef {
3001    abi_guard!("praxis_closure_capture", ctx, {
3002        // SAFETY: caller guarantees closure is a valid closure GcRef.
3003        let payload = closure.payload::<u8>() as *const crate::closures::ClosurePayload;
3004        let cp = unsafe { &*payload };
3005        cp.env
3006            .get(idx as usize)
3007            .copied()
3008            .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
3009    })
3010}
3011
3012/// Allocate a `VarCell` holding `value` (§4.10). The cell is the shared
3013/// mutable storage for a captured `var` binding: the binding site and every
3014/// closure that captures the `var` refer to the same cell. Returns the cell
3015/// `GcRef`.
3016///
3017/// # Safety
3018/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
3019#[unsafe(no_mangle)]
3020pub unsafe extern "C" fn praxis_alloc_var_cell(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
3021    abi_guard!("praxis_alloc_var_cell", ctx, {
3022        // SAFETY: VarCellPayload is VAR_CELL's payload type.
3023        unsafe {
3024            gc_alloc_owned(ctx, &crate::var_cell::VAR_CELL, || {
3025                crate::var_cell::VarCellPayload { value }
3026            })
3027        }
3028    })
3029}
3030
3031/// Read the current value out of a `VarCell` (§4.10). Used by `Path`
3032/// reads of a captured `var` (the local holds the cell; this derefs it).
3033///
3034/// # Safety
3035/// `ctx` must be live; `cell` must be a valid `VarCell` `GcRef`.
3036#[unsafe(no_mangle)]
3037pub unsafe extern "C" fn praxis_var_cell_get(ctx: *mut RuntimeContext, cell: GcRef) -> GcRef {
3038    abi_guard!("praxis_var_cell_get", ctx, {
3039        let _ = ctx;
3040        // SAFETY: caller guarantees cell is a valid VarCell GcRef.
3041        let payload = cell.payload::<u8>() as *const crate::var_cell::VarCellPayload;
3042        unsafe { (*payload).value }
3043    })
3044}
3045
3046/// Store `value` into a `VarCell` (§4.10). Used by `Assign` to a
3047/// captured `var`. Returns the cell (mutated in place).
3048///
3049/// # Safety
3050/// `ctx` must be live; `cell` must be a valid `VarCell` `GcRef`; `value` valid.
3051#[unsafe(no_mangle)]
3052pub unsafe extern "C" fn praxis_var_cell_set(
3053    ctx: *mut RuntimeContext,
3054    cell: GcRef,
3055    value: GcRef,
3056) -> GcRef {
3057    abi_guard!("praxis_var_cell_set", ctx, {
3058        let _ = ctx;
3059        // SAFETY: caller guarantees cell is a valid VarCell GcRef.
3060        let payload = cell.payload::<u8>() as *mut crate::var_cell::VarCellPayload;
3061        unsafe {
3062            (*payload).value = value;
3063        }
3064        cell
3065    })
3066}
3067
3068/// Append `value` to `vec` in place (§11.1). Returns the Unit sentinel — the
3069/// receiver is mutated directly, so the caller's `GcRef` remains valid (the
3070/// `VecPayload` object does not move; only its internal buffer may grow).
3071///
3072/// # Safety
3073/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; `value`
3074/// must be a valid `GcRef` whose type matches the vector's element descriptor.
3075#[unsafe(no_mangle)]
3076pub unsafe extern "C" fn praxis_vec_push(
3077    ctx: *mut RuntimeContext,
3078    vec: GcRef,
3079    value: GcRef,
3080) -> GcRef {
3081    abi_guard!("praxis_vec_push", ctx, {
3082        // `push` may grow the Vec's backing buffer, which allocates Rust heap memory
3083        // (not GC memory). A GC collection during this would be safe (the vec
3084        // object is rooted by the caller's spilled `vec` local), but we trigger it
3085        // *before* the mutation to keep the rooting story simple: `value` is passed
3086        // by value and is not yet in the vec, so it must survive across this
3087        // collection via the caller's shadow frame.
3088        unsafe { maybe_collect(ctx) };
3089        // SAFETY: caller guarantees `vec` is a valid Vec.
3090        let scope = unsafe { NativeScope::new(ctx) };
3091        let p = unsafe { vec_payload_mut(scope.root(vec)) };
3092        // A vector that was never told its element type adopts the first pushed
3093        // value's — the `forall T. () -> Vec[T]` builtin leaves `T` generalized
3094        // until first use, so construction genuinely has nothing to record. A
3095        // vector that *was* told rejects a mismatch instead of retagging itself:
3096        // retagging would turn an explicitly typed `Vec[Int]` into a `Vec[Float]`
3097        // on one bad push, and every later `equals`/`hash`/`format` would then
3098        // read the remaining `Int` payloads as `f64`.
3099        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3100            return unsafe { unit_sentinel(ctx) };
3101        }
3102        // Charge the spine when it grows (ADR-121; see
3103        // `Heap::charge_owned_growth`). Measured either side of the mutation
3104        // through the payload's own `owned_bytes`, so the growth policy stays
3105        // `RawVec`'s and the size formula stays the descriptor's.
3106        let before = p.owned_bytes();
3107        p.items.push(value);
3108        charge_growth(ctx, before, p.owned_bytes());
3109        unsafe { unit_sentinel(ctx) }
3110    })
3111}
3112
3113/// Reconcile a collection's element descriptor with a value about to be stored
3114/// in it: adopt the value's descriptor if the collection has none, accept if
3115/// they agree, and raise `TypeMismatch` if they do not.
3116///
3117/// Returns whether the store may proceed. Descriptors are `static`, so pointer
3118/// identity is the authoritative test (ADR-038).
3119///
3120/// # Safety
3121/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
3122unsafe fn adopt_or_reject(
3123    ctx: *mut RuntimeContext,
3124    element_descriptor: &mut *const TypeDescriptor,
3125    value: GcRef,
3126) -> bool {
3127    let pushed = value.descriptor();
3128    if element_descriptor.is_null() {
3129        *element_descriptor = pushed;
3130        return true;
3131    }
3132    if std::ptr::eq(*element_descriptor, pushed) {
3133        return true;
3134    }
3135    unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3136    false
3137}
3138
3139/// The number of elements in `vec`, as a boxed `Int` (§11.1).
3140///
3141/// # Safety
3142/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3143#[unsafe(no_mangle)]
3144pub unsafe extern "C" fn praxis_vec_len(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3145    abi_guard!("praxis_vec_len", ctx, {
3146        // SAFETY: caller guarantees `vec` is a valid Vec.
3147        let p = unsafe { vec_payload(vec) };
3148        let len = p.items.len() as i64;
3149        // len allocates the returned Int, but the input vec is still live via `vec`.
3150        unsafe { int_ref(ctx, len) }
3151    })
3152}
3153
3154/// The element at `index`, or an `IndexOutOfBounds` fault if out of range
3155/// (§9.2, §11.1). Returns the Unit sentinel on fault.
3156///
3157/// # Safety
3158/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; `index`
3159/// must be a valid `Int` `GcRef`.
3160#[unsafe(no_mangle)]
3161pub unsafe extern "C" fn praxis_vec_get(
3162    ctx: *mut RuntimeContext,
3163    vec: GcRef,
3164    index: GcRef,
3165) -> GcRef {
3166    abi_guard!("praxis_vec_get", ctx, {
3167        // SAFETY: caller guarantees `vec` is a valid Vec.
3168        let p = unsafe { vec_payload(vec) };
3169        // SAFETY: caller guarantees `index` is a valid Int.
3170        let idx = unsafe { int_payload(index) };
3171        // SAFETY: `abi_guard!` established that `ctx` is live and wired.
3172        let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3173            return unsafe { unit_sentinel(ctx) };
3174        };
3175        // Return the element by value (a copy of the GcRef). No allocation, so no
3176        // collection is needed; the vec stays live via `vec`.
3177        p.items[idx]
3178    })
3179}
3180
3181/// Replace the element at `index`; faults `IndexOutOfBounds` if out of range
3182/// (§9.2, §11.1). Returns the Unit sentinel.
3183///
3184/// **Replaces, and never appends.** `v[v.len()] = x` is out of range rather than
3185/// a push, which is `praxis_vec_push`'s job: a store whose index decides between
3186/// the two operations makes an off-by-one grow the vector instead of reporting.
3187///
3188/// The element descriptor goes through the same [`adopt_or_reject`] every push
3189/// does, so a store into a vector that was never told its element type adopts
3190/// the first value's, and one into a `Vec[Int]` raises `TypeMismatch` rather
3191/// than retagging the collection — `push`'s rule, at the second door.
3192///
3193/// # Safety
3194/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; `index`
3195/// must be a valid `Int` `GcRef`; `value` must be a valid `GcRef`.
3196#[unsafe(no_mangle)]
3197pub unsafe extern "C" fn praxis_vec_set(
3198    ctx: *mut RuntimeContext,
3199    vec: GcRef,
3200    index: GcRef,
3201    value: GcRef,
3202) -> GcRef {
3203    abi_guard!("praxis_vec_set", ctx, {
3204        // SAFETY: caller guarantees `vec` is a valid Vec.
3205        let scope = unsafe { NativeScope::new(ctx) };
3206        let p = unsafe { vec_payload_mut(scope.root(vec)) };
3207        // SAFETY: caller guarantees `index` is a valid Int.
3208        let idx = unsafe { int_payload(index) };
3209        // SAFETY: `abi_guard!` established that `ctx` is live and wired.
3210        let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3211            return unsafe { unit_sentinel(ctx) };
3212        };
3213        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3214            return unsafe { unit_sentinel(ctx) };
3215        }
3216        // No allocation: the slot takes a `GcRef` the caller already holds.
3217        p.items[idx] = value;
3218        unsafe { unit_sentinel(ctx) }
3219    })
3220}
3221
3222/// `v[i] min= candidate` and `v[i] max= candidate` (ADR-161): keep whichever of
3223/// the two the operator asks for, at an index that must already exist.
3224///
3225/// # An indexed receiver has no absent entry, so it has no first value to accept
3226///
3227/// §6.2's "an absent entry accepts the first value" is the `Map` clause and it
3228/// has no analogue here: a `Vec` index is in range or it is out of range, and
3229/// out of range is the `IndexOutOfBounds` [`praxis_vec_set`] already raises. So
3230/// the row exists for the *comparison* rather than for the insert — `v[i] min= x`
3231/// is `v[i] = min(v[i], x)` with the place evaluated once and the ordering taken
3232/// from the element's own descriptor.
3233///
3234/// `keep` is the ordering the candidate must have against the incumbent for the
3235/// store to happen: `Less` is `min=` and `Greater` is `max=`. A tie keeps the
3236/// incumbent, as [`praxis_map_update_min`]'s does and for its reason.
3237///
3238/// # Safety
3239/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`, `index` a
3240/// valid `Int`, and `value` of the vector's element type.
3241unsafe fn vec_update(
3242    ctx: *mut RuntimeContext,
3243    vec: GcRef,
3244    index: GcRef,
3245    value: GcRef,
3246    keep: std::cmp::Ordering,
3247) -> GcRef {
3248    // SAFETY: the caller guarantees `vec` is a valid Vec.
3249    let scope = unsafe { NativeScope::new(ctx) };
3250    let p = unsafe { vec_payload_mut(scope.root(vec)) };
3251    // SAFETY: the caller guarantees `index` is a valid Int.
3252    let idx = unsafe { int_payload(index) };
3253    // SAFETY: `abi_guard!` established that `ctx` is live and wired.
3254    let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3255        return unsafe { unit_sentinel(ctx) };
3256    };
3257    // Before the comparison, exactly as the plain store does it: a candidate of
3258    // the wrong type is rejected whether or not it would have won.
3259    if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3260        return unsafe { unit_sentinel(ctx) };
3261    }
3262    // SAFETY: both are elements of this vector, so each payload matches the
3263    // descriptor in its own header.
3264    if unsafe { update_cmp(value, p.items[idx]) } == keep {
3265        p.items[idx] = value;
3266    }
3267    unsafe { unit_sentinel(ctx) }
3268}
3269
3270/// `v[i] min= candidate` — keep the smaller of the two. See [`vec_update`].
3271///
3272/// # Safety
3273/// As [`vec_update`].
3274#[unsafe(no_mangle)]
3275pub unsafe extern "C" fn praxis_vec_update_min(
3276    ctx: *mut RuntimeContext,
3277    vec: GcRef,
3278    index: GcRef,
3279    value: GcRef,
3280) -> GcRef {
3281    abi_guard!("praxis_vec_update_min", ctx, {
3282        unsafe { vec_update(ctx, vec, index, value, std::cmp::Ordering::Less) }
3283    })
3284}
3285
3286/// `v[i] max= candidate` — keep the larger of the two. See [`vec_update`].
3287///
3288/// # Safety
3289/// As [`vec_update`].
3290#[unsafe(no_mangle)]
3291pub unsafe extern "C" fn praxis_vec_update_max(
3292    ctx: *mut RuntimeContext,
3293    vec: GcRef,
3294    index: GcRef,
3295    value: GcRef,
3296) -> GcRef {
3297    abi_guard!("praxis_vec_update_max", ctx, {
3298        unsafe { vec_update(ctx, vec, index, value, std::cmp::Ordering::Greater) }
3299    })
3300}
3301
3302/// True iff `vec` has no elements, as a boxed `Bool` (§11.1).
3303///
3304/// # Safety
3305/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3306#[unsafe(no_mangle)]
3307pub unsafe extern "C" fn praxis_vec_is_empty(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3308    abi_guard!("praxis_vec_is_empty", ctx, {
3309        // SAFETY: caller guarantees `vec` is a valid Vec.
3310        let p = unsafe { vec_payload(vec) };
3311        let empty = p.items.is_empty();
3312        // SAFETY: ctx/heap valid; Bool immortal path.
3313        unsafe { bool_ref(ctx, empty) }
3314    })
3315}
3316
3317// --- the §6.3 barrier combinators ------------------------------------------
3318//
3319// A *barrier* is a pipeline stage that cannot be fused into the loop feeding it
3320// because it needs the whole sequence before it can answer its first element:
3321// `sorted` has to see the largest element before it knows the smallest is first.
3322// So each is a real runtime call over a materialized `Vec` rather than an
3323// intrinsic the MIR fuser expands, and the fuser's own recognizer already knows
3324// to end a chain at one and start a fresh chain from its result.
3325//
3326// All three rebuild through [`vec_of`] rather than mutating the receiver, which
3327// is the shape `praxis_set_items` and `praxis_counter_keys` already use.
3328// `v.sorted()` is an expression, not a statement: §6.3 lists it beside `map` and
3329// `filter`, and a caller that also holds `v` must still see `v`'s own order.
3330
3331/// `v.sorted()` — the elements of `vec` in ascending order, as a **new** `Vec`
3332/// (§6.3). The receiver is not touched.
3333///
3334/// Ordering goes through the element descriptor's `compare` callback — the same
3335/// callback [`praxis_value_cmp`] uses, and for the same reason: a `Text` is a
3336/// pointer-and-length structure, so ordering one by its first eight payload
3337/// bytes compares *addresses*. That sorts `Vec[Int]` correctly and `Vec[Text]`
3338/// into allocation order, which is the failure that looks like it works.
3339///
3340/// The sort is **stable**, so equal elements keep their input order and the
3341/// answer is a function of the input alone.
3342///
3343/// Raises `FaultKind::TypeMismatch` and answers Unit when the elements are not
3344/// all one type, or when that type has no `compare`. The catalog row's `Ord`
3345/// bound (ADR-093, `Bound::Kind`) rejects both at `praxis check`, so reaching
3346/// either is a compiler bug — reported as a fault rather than as a callback
3347/// dispatched on a foreign layout. As [`praxis_value_cmp`], the second guard
3348/// covers fewer types since ADR-138 populated `compare` on the composites.
3349///
3350/// It is the callback a `Set` and a `Map` order their keys through too
3351/// (ADR-138), which is what makes `out(s)` and `out(s.sorted())` print one
3352/// sequence rather than one numeric and one lexicographic.
3353///
3354/// # Safety
3355/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3356#[unsafe(no_mangle)]
3357pub unsafe extern "C" fn praxis_vec_sorted(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3358    abi_guard!("praxis_vec_sorted", ctx, {
3359        // SAFETY: caller guarantees `vec` is a valid Vec.
3360        let p = unsafe { vec_payload(vec) };
3361        let mut items: Vec<GcRef> = p.items.to_vec();
3362        // Nothing to order, and nothing to check: a zero- or one-element Vec has
3363        // no pair to compare, so an empty `Vec[fn(Int) -> Int]` sorts rather than
3364        // faulting on a `compare` it would never have called.
3365        if items.len() > 1 {
3366            // The elements' *own* descriptors decide, not the Vec's label: the
3367            // label may be null (the construction site knew no element type)
3368            // while every member is a perfectly good `Text`.
3369            let desc = items[0].descriptor();
3370            if !items.iter().all(|i| std::ptr::eq(i.descriptor(), desc)) {
3371                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3372                return unsafe { unit_sentinel(ctx) };
3373            }
3374            let Some(compare) = desc.compare else {
3375                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3376                return unsafe { unit_sentinel(ctx) };
3377            };
3378            items.sort_by(|a, b| {
3379                // SAFETY: every element carries `desc` (checked above), so both
3380                // payloads are values of its type; the non-moving GC keeps them
3381                // stable, and `sort_by` allocates nothing that could collect.
3382                unsafe {
3383                    compare(
3384                        a.payload::<u8>() as *const u8,
3385                        b.payload::<u8>() as *const u8,
3386                    )
3387                }
3388            });
3389        }
3390        unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
3391    })
3392}
3393
3394/// `v.sorted_by_key(f)` — the elements of `vec` ordered by the key `f` extracts,
3395/// as a **new** `Vec` (§6.3, ADR-127 decision 5). The receiver is not touched.
3396///
3397/// # Why this row exists, and why it is not `sorted_by`
3398///
3399/// ADR-045 decided that no composite is orderable, so the moment a pipeline's
3400/// item is a pair — which is the moment its source is a `Map` or a `Counter` —
3401/// `sorted` is unavailable and "the five most common values" has no spelling.
3402/// The closure extracts an orderable key from an item that is not.
3403///
3404/// Not a `(T, T) -> Bool` comparator: `min_by`/`max_by` already own the
3405/// less-than-predicate shape, and a comparator is O(n log n) calls back into
3406/// JIT'd code where a key extractor is n.
3407///
3408/// **Decorate–sort–undecorate.** Every key is extracted once, up front, and the
3409/// sort orders the (key, element) pairs — which is what makes it n calls. The
3410/// keys are held in a `Vec<GcRef>` the collector cannot see, so they are rooted
3411/// in a native scope: extraction allocates, and a collection triggered by the
3412/// *next* call would otherwise free the key the previous one produced.
3413///
3414/// Ordering goes through the same `compare` callback [`praxis_value_cmp`] uses,
3415/// so the `Ord` bound the catalog row puts on the *key* is the rule this
3416/// enforces. The sort is **stable**, so items with equal keys keep their input
3417/// order and the answer is a function of the input alone.
3418///
3419/// Raises `FaultKind::TypeMismatch` and answers Unit when the keys are not all
3420/// one type, or when that type has no ordering; a fault the closure itself
3421/// raised stops the sort and is left for the call site's own check.
3422///
3423/// # Safety
3424/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `key`
3425/// a valid closure `GcRef`.
3426#[unsafe(no_mangle)]
3427pub unsafe extern "C" fn praxis_vec_sorted_by_key(
3428    ctx: *mut RuntimeContext,
3429    vec: GcRef,
3430    key: GcRef,
3431) -> GcRef {
3432    abi_guard!("praxis_vec_sorted_by_key", ctx, {
3433        let scope = unsafe { NativeScope::new(ctx) };
3434        // The receiver is rooted explicitly: its items are read into a Rust
3435        // `Vec` and held across one closure call *per element*, which is a far
3436        // longer window than a single-allocation wrapper's.
3437        let _receiver = scope.root(vec);
3438        // SAFETY: caller guarantees `vec` is a valid Vec.
3439        let p = unsafe { vec_payload(vec) };
3440        let element_descriptor = p.element_descriptor;
3441        let items: Vec<GcRef> = p.items.to_vec();
3442        for item in &items {
3443            scope.root(*item);
3444        }
3445        // Decorate: one call per element, keys rooted as they arrive.
3446        let mut decorated: Vec<(GcRef, GcRef)> = Vec::with_capacity(items.len());
3447        for item in items {
3448            let Some(k) = (unsafe { call_unary_closure(ctx, key, item) }) else {
3449                // The closure faulted (or is not a closure, which the type
3450                // checker already refused). Its answer is the Unit sentinel, so
3451                // sorting on it would order garbage; stop and leave the fault
3452                // for the call site.
3453                return unsafe { unit_sentinel(ctx) };
3454            };
3455            decorated.push((scope.root(k).get(), item));
3456        }
3457        if decorated.len() > 1 {
3458            // The keys' *own* descriptors decide, as `praxis_vec_sorted`'s
3459            // elements' do: the source Vec's label says nothing about them.
3460            let desc = decorated[0].0.descriptor();
3461            if !decorated
3462                .iter()
3463                .all(|(k, _)| std::ptr::eq(k.descriptor(), desc))
3464            {
3465                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3466                return unsafe { unit_sentinel(ctx) };
3467            }
3468            let Some(compare) = desc.compare else {
3469                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3470                return unsafe { unit_sentinel(ctx) };
3471            };
3472            decorated.sort_by(|(a, _), (b, _)| {
3473                // SAFETY: every key carries `desc` (checked above), so both
3474                // payloads are values of its type; the non-moving GC keeps them
3475                // stable, and `sort_by` allocates nothing that could collect.
3476                unsafe {
3477                    compare(
3478                        a.payload::<u8>() as *const u8,
3479                        b.payload::<u8>() as *const u8,
3480                    )
3481                }
3482            });
3483        }
3484        // Undecorate.
3485        unsafe {
3486            vec_of(
3487                ctx,
3488                element_descriptor,
3489                decorated.into_iter().map(|(_, item)| item),
3490            )
3491        }
3492    })
3493}
3494
3495/// Call a `(T) -> U` Praxis closure with one argument, or `None` if it faulted —
3496/// or if it is not a closure at all.
3497///
3498/// The descriptor is checked rather than assumed: the type checker says the
3499/// operand is a function and the only runtime representation of one is a closure
3500/// object, but the alternative to a `TypeMismatch` fault is transmuting whatever
3501/// the payload's first word happens to be into a function pointer and jumping to
3502/// it.
3503///
3504/// # Safety
3505/// `ctx` must be live and wired; `closure` and `arg` must be valid `GcRef`s.
3506unsafe fn call_unary_closure(
3507    ctx: *mut RuntimeContext,
3508    closure: GcRef,
3509    arg: GcRef,
3510) -> Option<GcRef> {
3511    if !std::ptr::eq(closure.descriptor(), &crate::closures::CLOSURE) {
3512        unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3513        return None;
3514    }
3515    // SAFETY: the descriptor check proves the payload is a `ClosurePayload`, so
3516    // `fn_ptr` is the entry point the codegen wrote there.
3517    let fn_ptr = unsafe { (*closure.payload::<crate::closures::ClosurePayload>()).fn_ptr };
3518    // A closure's entry point is `fn(ctx, closure_self, params...) -> GcRef`
3519    // (§4.10, Approach B): the closure value itself is a hidden first argument,
3520    // and the prologue loads its captures from it.
3521    //
3522    // SAFETY: `fn_ptr` is a finalized JIT entry whose parameter count is the one
3523    // the type checker enforced for this operand; every value crossing is a
3524    // `GcRef`, which is the ABI's only value kind.
3525    let result = unsafe {
3526        let f: unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef) -> GcRef =
3527            std::mem::transmute(fn_ptr);
3528        f(ctx, closure, arg)
3529    };
3530    // The closure ran arbitrary Praxis code and may have faulted; its result on
3531    // that path is the Unit sentinel.
3532    if unsafe { praxis_check_fault(ctx) } != 0 {
3533        return None;
3534    }
3535    Some(result)
3536}
3537
3538/// `v.unique()` — the elements of `vec` with later duplicates dropped, as a
3539/// **new** `Vec`, in first-occurrence order (§6.3). The receiver is not touched.
3540///
3541/// First-occurrence order rather than sorted-and-deduped: `unique` is listed
3542/// separately from `sorted` in §6.3, so composing them has to be the user's
3543/// choice, and an order that depends on a hash map's iteration would make the
3544/// same program answer differently on two runs — and here the order is the
3545/// program's *answer*, not only its printing.
3546///
3547/// Sameness is [`DynamicKey`]'s — the descriptor's `hash` and `equals`
3548/// callbacks, which is what "the same value" means everywhere else in this
3549/// runtime (§5.5, §11.3). The catalog row's `HashStable` bound is what keeps a
3550/// mutable element out; a key that can change after it is stored cannot be found
3551/// again.
3552///
3553/// # Safety
3554/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3555#[unsafe(no_mangle)]
3556pub unsafe extern "C" fn praxis_vec_unique(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3557    abi_guard!("praxis_vec_unique", ctx, {
3558        // SAFETY: caller guarantees `vec` is a valid Vec.
3559        let p = unsafe { vec_payload(vec) };
3560        let mut seen: std::collections::HashSet<DynamicKey> = std::collections::HashSet::new();
3561        let mut kept: Vec<GcRef> = Vec::new();
3562        for item in &p.items {
3563            if seen.insert(DynamicKey::new(*item)) {
3564                kept.push(*item);
3565            }
3566        }
3567        unsafe { vec_of(ctx, p.element_descriptor, kept.into_iter()) }
3568    })
3569}
3570
3571/// `v.reversed()` — the elements of `vec` in the opposite order, as a **new**
3572/// `Vec` (ADR-145). The receiver is not touched.
3573///
3574/// A barrier for `praxis_vec_sorted`'s reason and not a fused stage: reversal
3575/// cannot answer its first element until it has seen the last one.
3576///
3577/// It reads **no descriptor callback** — not `compare`, not `equals`, not
3578/// `hash` — so unlike `sorted` and `unique` there is no element it can be handed
3579/// that it cannot reverse, and its catalog row carries no capability bound. That
3580/// is why the manifest row is `Allocates` and there is no `TypeMismatch` path
3581/// here to read.
3582///
3583/// The element label is copied through unchanged, the null a construction site
3584/// that knew no element type leaves included.
3585///
3586/// # Safety
3587/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3588#[unsafe(no_mangle)]
3589pub unsafe extern "C" fn praxis_vec_reversed(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3590    abi_guard!("praxis_vec_reversed", ctx, {
3591        // SAFETY: caller guarantees `vec` is a valid Vec.
3592        let p = unsafe { vec_payload(vec) };
3593        let items: Vec<GcRef> = p.items.iter().rev().copied().collect();
3594        unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
3595    })
3596}
3597
3598/// The group size `chunks(n)` and `windows(n)` share, or `None` when `n` names no
3599/// group at all (ADR-149).
3600///
3601/// **The only thing either wrapper refuses.** A run of zero elements is not a
3602/// short run — chunking a non-empty sequence into them has no finite answer, and
3603/// sliding one along it has no useful one — and a negative run names nothing.
3604/// Every other `n` has an answer, including one larger than the receiver: a
3605/// `chunks` wider than the sequence is one short chunk, a `windows` wider than it
3606/// is no windows. So this returns an `Option` of a size rather than clamping to
3607/// one, and the two callers spell those two answers themselves.
3608///
3609/// There is no upper bound here and none is missing. Both results are *shorter*
3610/// than the receiver — one group per start position at most — so neither can
3611/// ask for an extent [`VecExtent`](crate::collections::VecExtent) would refuse,
3612/// which is the bound `praxis_vec_filled` needs and these do not.
3613fn group_size(n: i64) -> Option<usize> {
3614    if n <= 0 {
3615        return None;
3616    }
3617    usize::try_from(n).ok()
3618}
3619
3620/// The `Vec[Vec[T]]` both groupings answer, built from the half-open source
3621/// ranges `groups` names (ADR-149).
3622///
3623/// **The outer label is `collections::VEC` at every length, and it is *passed*
3624/// rather than inferred** (ADR-149 decision 1). Which label belongs there is not
3625/// this wrapper's choice — `outer.push(inner)` builds a `Vec[Vec[T]]` today and
3626/// `adopt_or_reject` labels it `VEC`, so anything else would disagree with
3627/// `push`. What is chosen here is only that it is written down: letting
3628/// [`vec_of`] infer it from the first group would answer `VEC` for
3629/// `[1].chunks(2)` and *null* for `[].chunks(2)` — one type with two labels, and
3630/// the null is the one `vec_format` renders as `[]`.
3631///
3632/// That is [`praxis_grid_positions`]'s argument, not a new one: it passes
3633/// `&tuples::TUPLE` for the same reason, and `Grid(0, 0, 1).positions()` is the
3634/// same empty case. Naming the label is what a wrapper does whenever its result's
3635/// element kind is not its receiver's.
3636///
3637/// The inner labels *are* the receiver's own, passed through unchanged the way
3638/// `praxis_vec_reversed` passes its one through, null included.
3639///
3640/// # Safety
3641/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; every
3642/// range `groups` yields must lie within its length.
3643unsafe fn vec_of_groups(
3644    ctx: *mut RuntimeContext,
3645    vec: GcRef,
3646    groups: impl Iterator<Item = (usize, usize)>,
3647) -> GcRef {
3648    // SAFETY: caller guarantees `vec` is a valid Vec.
3649    let element_descriptor = unsafe { vec_payload(vec) }.element_descriptor;
3650    let outer = unsafe { praxis_vec_new(ctx, &crate::collections::VEC as *const _) };
3651    let scope = unsafe { NativeScope::new(ctx) };
3652    let op = unsafe { vec_payload_mut(scope.root(outer)) };
3653    for (start, end) in groups {
3654        // The elements are read out of the receiver, which the caller's shadow
3655        // frame roots across this call, so the untraced `Vec<GcRef>` below holds
3656        // nothing a collection inside `vec_of` could reclaim — the receiver
3657        // holds every one of them too. That is `praxis_vec_unique`'s argument at
3658        // a second site, and it is why the *groups* are what need rooting and
3659        // the items are not: an inner `Vec` is reachable from nothing until it
3660        // is pushed, which is why it is pushed before the next one is built.
3661        //
3662        // SAFETY: caller guarantees `vec` is a valid Vec and that `start..end`
3663        // lies within its length.
3664        let items: Vec<GcRef> = unsafe { vec_payload(vec) }.items[start..end].to_vec();
3665        let inner = unsafe { vec_of(ctx, element_descriptor, items.into_iter()) };
3666        op.items.push(inner);
3667    }
3668    outer
3669}
3670
3671/// `seq.chunks(n)` — these elements in consecutive non-overlapping runs of `n`,
3672/// the last short if the length does not divide (ADR-149). The receiver is not
3673/// touched.
3674///
3675/// `[1, 2, 3, 4, 5].chunks(2)` is `[[1, 2], [3, 4], [5]]`. An empty receiver
3676/// answers `[]` at any size, and an `n` at or above the length answers one chunk
3677/// holding everything.
3678///
3679/// Raises `FaultKind::InvalidSize` and answers Unit when `n` is not positive —
3680/// [`group_size`] has the reason, and it is the wrapper's whole faulting
3681/// surface. It reads **no descriptor callback**, so unlike `sorted` there is no
3682/// element it can be handed that it cannot group.
3683///
3684/// # Safety
3685/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `n` a
3686/// valid `Int` `GcRef`.
3687#[unsafe(no_mangle)]
3688pub unsafe extern "C" fn praxis_vec_chunks(
3689    ctx: *mut RuntimeContext,
3690    vec: GcRef,
3691    n: GcRef,
3692) -> GcRef {
3693    abi_guard!("praxis_vec_chunks", ctx, {
3694        // SAFETY: caller guarantees `n` is a valid Int.
3695        let Some(size) = group_size(unsafe { int_payload(n) }) else {
3696            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
3697            return unsafe { unit_sentinel(ctx) };
3698        };
3699        // SAFETY: caller guarantees `vec` is a valid Vec.
3700        let len = unsafe { vec_payload(vec) }.items.len();
3701        // Every `size`th position starts a chunk; the last one stops at the end
3702        // rather than past it, which is the short tail.
3703        let groups = (0..len)
3704            .step_by(size)
3705            .map(move |s| (s, (s + size).min(len)));
3706        unsafe { vec_of_groups(ctx, vec, groups) }
3707    })
3708}
3709
3710/// `seq.windows(n)` — every consecutive run of exactly `n`, each starting one
3711/// element after the last (ADR-149). The receiver is not touched.
3712///
3713/// `[1, 2, 3, 4].windows(2)` is `[[1, 2], [2, 3], [3, 4]]`. Elements are shared,
3714/// not copied: the `2` in the first window and the `2` in the second are one
3715/// object, which is the language's reference semantics rather than a rule of
3716/// this wrapper.
3717///
3718/// **A window that does not fit is dropped rather than shortened**, which is the
3719/// one place this and [`praxis_vec_chunks`] answer differently: `[1, 2].windows(5)`
3720/// is `[]`, because a run of five is a run of five. It is not the fault below
3721/// arriving late — "which runs of five are there" has an answer for a sequence
3722/// of two, and that answer is none.
3723///
3724/// Raises `FaultKind::InvalidSize` and answers Unit when `n` is not positive,
3725/// for [`praxis_vec_chunks`]'s reason.
3726///
3727/// # Safety
3728/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `n` a
3729/// valid `Int` `GcRef`.
3730#[unsafe(no_mangle)]
3731pub unsafe extern "C" fn praxis_vec_windows(
3732    ctx: *mut RuntimeContext,
3733    vec: GcRef,
3734    n: GcRef,
3735) -> GcRef {
3736    abi_guard!("praxis_vec_windows", ctx, {
3737        // SAFETY: caller guarantees `n` is a valid Int.
3738        let Some(size) = group_size(unsafe { int_payload(n) }) else {
3739            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
3740            return unsafe { unit_sentinel(ctx) };
3741        };
3742        // SAFETY: caller guarantees `vec` is a valid Vec.
3743        let len = unsafe { vec_payload(vec) }.items.len();
3744        // Written as a subtraction guarded by its own comparison rather than a
3745        // `saturating_sub`: `len - size` saturating to zero would answer *one*
3746        // window for a receiver too short to hold any, and the empty answer is
3747        // the whole point of the branch.
3748        let starts = if size <= len { len - size + 1 } else { 0 };
3749        let groups = (0..starts).map(move |s| (s, s + size));
3750        unsafe { vec_of_groups(ctx, vec, groups) }
3751    })
3752}
3753
3754/// `seq.join(sep)` — these `Text` elements concatenated with `sep` between them
3755/// (ADR-144). An empty sequence answers `""`; a one-element sequence answers
3756/// that element's characters and no separator.
3757///
3758/// Raises `FaultKind::TypeMismatch` and answers Unit when an element is not a
3759/// `Text`. The catalog row bounds the item to `Text`, so reaching that is a
3760/// compiler bug — reported the way `praxis_vec_sorted` reports its own, rather
3761/// than reading a foreign payload as a pointer-and-length pair.
3762///
3763/// This does **not** render: a `Vec[Int]` is refused at `praxis check` rather
3764/// than stringified here, which is what keeps `join` from being a back door
3765/// around ADR-143's decision about which types have a `to_text`.
3766///
3767/// # Safety
3768/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `sep` a
3769/// valid `Text` `GcRef`.
3770#[unsafe(no_mangle)]
3771pub unsafe extern "C" fn praxis_vec_join(
3772    ctx: *mut RuntimeContext,
3773    vec: GcRef,
3774    sep: GcRef,
3775) -> GcRef {
3776    abi_guard!("praxis_vec_join", ctx, {
3777        // SAFETY: caller guarantees `vec` is a valid Vec and `sep` a valid Text.
3778        let p = unsafe { vec_payload(vec) };
3779        if !p
3780            .items
3781            .iter()
3782            .all(|item| std::ptr::eq(item.descriptor(), &crate::text::TEXT))
3783        {
3784            unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3785            return unsafe { unit_sentinel(ctx) };
3786        }
3787        let separator = unsafe { text_str(sep) };
3788        let mut joined = String::new();
3789        for (i, item) in p.items.iter().enumerate() {
3790            if i > 0 {
3791                joined.push_str(separator);
3792            }
3793            // SAFETY: the loop above proved every element's descriptor is TEXT.
3794            joined.push_str(unsafe { text_str(*item) });
3795        }
3796        // SAFETY: `joined` is valid UTF-8; ctx/heap valid.
3797        unsafe { text_ref(ctx, joined) }
3798    })
3799}
3800
3801/// `chars.to_text()` — these `Char`s as one `Text`, with nothing between them
3802/// (ADR-144). The inverse of walking a `Text`, and what renders a `Grid` row
3803/// back as the line it was read from.
3804///
3805/// Each code point is read through [`read_scalar`] with the `Char` handle, never
3806/// a bare payload read: the payload is **four** bytes and an `i64` read would
3807/// take eight of them. A foreign element is `TypeMismatch` and the Unit
3808/// sentinel, for [`praxis_vec_join`]'s reason.
3809///
3810/// # Safety
3811/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3812#[unsafe(no_mangle)]
3813pub unsafe extern "C" fn praxis_vec_to_text(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3814    abi_guard!("praxis_vec_to_text", ctx, {
3815        // SAFETY: caller guarantees `vec` is a valid Vec.
3816        let p = unsafe { vec_payload(vec) };
3817        let mut rendered = String::new();
3818        for item in &p.items {
3819            // SAFETY: `read_scalar` proves the descriptor is `CHAR` before
3820            // reading its four bytes, and answers `None` otherwise.
3821            let Some(code) = (unsafe { read_scalar(*item, scalars::CHAR_PAYLOAD) }) else {
3822                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3823                return unsafe { unit_sentinel(ctx) };
3824            };
3825            // The descriptor's own writer, so a line rebuilt from a `Grid` row
3826            // holds the characters `out` would have written one at a time.
3827            scalars::write_char(&mut rendered, code);
3828        }
3829        // SAFETY: `rendered` is valid UTF-8; ctx/heap valid.
3830        unsafe { text_ref(ctx, rendered) }
3831    })
3832}
3833
3834/// `v.frequencies()` — a `Counter[T]` holding how many times each element of
3835/// `vec` occurs (§6.3, §6.2).
3836///
3837/// The first combinator whose result is a **keyed** collection, which is why the
3838/// catalog row carries a `HashStable` bound of its own:
3839/// `require_collection_invariants` is applied to a method's *receiver*, and the
3840/// receiver here is an ordinary `Vec` that may legitimately hold anything. It is
3841/// the result that has a key rule.
3842///
3843/// The counter's key descriptor is the source `Vec`'s element label, which may
3844/// be null when the construction site knew no element type — the same null
3845/// [`praxis_counter_new`] already accepts and means "not told yet".
3846///
3847/// # Safety
3848/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3849#[unsafe(no_mangle)]
3850pub unsafe extern "C" fn praxis_vec_frequencies(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3851    abi_guard!("praxis_vec_frequencies", ctx, {
3852        let scope = unsafe { NativeScope::new(ctx) };
3853        // The receiver is rooted **explicitly**, unlike the one-allocation
3854        // wrappers around it. The keys below are `GcRef`s into this `Vec`'s
3855        // items and they are held across one allocation *per distinct element*,
3856        // which is a much longer window than `praxis_set_items`' single
3857        // `vec_of`; relying on the caller's shadow frame alone for that long is
3858        // an assumption worth not making.
3859        let _receiver = scope.root(vec);
3860        // SAFETY: caller guarantees `vec` is a valid Vec.
3861        let p = unsafe { vec_payload(vec) };
3862        // Count first, with no allocation at all, so the tally cannot be
3863        // disturbed by a collection mid-loop. The tally is a `Vec` with a side
3864        // index rather than a bare map so the counts come out in
3865        // first-occurrence order, which makes the *allocation* order a function
3866        // of the input; the `Counter` itself is unordered either way.
3867        let mut counts: Vec<(DynamicKey, i64)> = Vec::new();
3868        let mut index: std::collections::HashMap<DynamicKey, usize> =
3869            std::collections::HashMap::new();
3870        for item in &p.items {
3871            let key = DynamicKey::new(*item);
3872            match index.get(&key) {
3873                Some(at) => counts[*at].1 += 1,
3874                None => {
3875                    index.insert(key, counts.len());
3876                    counts.push((key, 1));
3877                }
3878            }
3879        }
3880        let counter = unsafe { praxis_counter_new(ctx, p.element_descriptor) };
3881        let rooted = scope.root(counter);
3882        for (key, count) in counts {
3883            // Allocate first, then take the payload borrow — the boxed `Int`
3884            // allocation can collect, and the counter has to be reachable
3885            // through the native root store rather than through a `&mut` this
3886            // frame is holding across it.
3887            let boxed = unsafe { int_ref(ctx, count) };
3888            unsafe { counter_payload_mut(rooted) }
3889                .entries
3890                .insert(key, boxed);
3891        }
3892        counter
3893    })
3894}
3895
3896// ---------------------------------------------------------------------------
3897// Deque[T] methods (§6.1). Mirrors the Vec surface but adds the
3898// front/back distinction: `push_front`/`push_back`/`pop_front`/`pop_back`.
3899// `pop_*` fault on an empty deque (§9.1 `EmptyCollection`).
3900// ---------------------------------------------------------------------------
3901
3902use crate::collections::DequePayload;
3903
3904/// Read the `DequePayload` out of a `GcRef` as a shared ref, asserting Deque.
3905///
3906/// # Safety
3907/// `r` must be a valid `Deque` `GcRef`.
3908unsafe fn deque_payload(r: GcRef) -> &'static DequePayload {
3909    // SAFETY: caller guarantees `r` is a Deque; see `payload_ref`.
3910    unsafe { payload_ref::<DequePayload>(r) }
3911}
3912
3913/// Read the `DequePayload` out of a `GcRef` as a mutable ref, asserting Deque.
3914///
3915/// # Safety
3916/// `r` must be a valid `Deque` `GcRef`, rooted for `'s`.
3917unsafe fn deque_payload_mut<'s>(r: Rooted<'s>) -> &'s mut DequePayload {
3918    // SAFETY: caller guarantees `r` is a Deque; see `payload_mut`.
3919    unsafe { payload_mut::<DequePayload>(r) }
3920}
3921
3922/// Allocate a new empty `Deque[T]` with the given element descriptor (§11.2).
3923/// A null descriptor stays null — "not told yet" — exactly as `praxis_vec_new`.
3924///
3925/// # Safety
3926/// `ctx` must be live and wired. `element_descriptor` must be a valid pointer to
3927/// a `'static TypeDescriptor` (or null).
3928#[unsafe(no_mangle)]
3929pub unsafe extern "C" fn praxis_deque_new(
3930    ctx: *mut RuntimeContext,
3931    element_descriptor: *const TypeDescriptor,
3932) -> GcRef {
3933    abi_guard!("praxis_deque_new", ctx, {
3934        // SAFETY: DequePayload is DEQUE's payload type.
3935        unsafe {
3936            gc_alloc_owned(ctx, &crate::collections::DEQUE, || DequePayload {
3937                element_descriptor,
3938                items: std::collections::VecDeque::new(),
3939            })
3940        }
3941    })
3942}
3943
3944/// Prepend `value` to the front of `deque`; returns Unit (§6.1).
3945///
3946/// # Safety
3947/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
3948/// `value` must be a valid `GcRef`.
3949#[unsafe(no_mangle)]
3950pub unsafe extern "C" fn praxis_deque_push_front(
3951    ctx: *mut RuntimeContext,
3952    deque: GcRef,
3953    value: GcRef,
3954) -> GcRef {
3955    abi_guard!("praxis_deque_push_front", ctx, {
3956        unsafe { maybe_collect(ctx) };
3957        let scope = unsafe { NativeScope::new(ctx) };
3958        let p = unsafe { deque_payload_mut(scope.root(deque)) };
3959        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3960            return unsafe { unit_sentinel(ctx) };
3961        }
3962        let before = p.owned_bytes();
3963        p.items.push_front(value);
3964        charge_growth(ctx, before, p.owned_bytes());
3965        unsafe { unit_sentinel(ctx) }
3966    })
3967}
3968
3969/// Append `value` to the back of `deque`; returns Unit (§6.1).
3970///
3971/// # Safety
3972/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
3973/// `value` must be a valid `GcRef`.
3974#[unsafe(no_mangle)]
3975pub unsafe extern "C" fn praxis_deque_push_back(
3976    ctx: *mut RuntimeContext,
3977    deque: GcRef,
3978    value: GcRef,
3979) -> GcRef {
3980    abi_guard!("praxis_deque_push_back", ctx, {
3981        unsafe { maybe_collect(ctx) };
3982        let scope = unsafe { NativeScope::new(ctx) };
3983        let p = unsafe { deque_payload_mut(scope.root(deque)) };
3984        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3985            return unsafe { unit_sentinel(ctx) };
3986        }
3987        let before = p.owned_bytes();
3988        p.items.push_back(value);
3989        charge_growth(ctx, before, p.owned_bytes());
3990        unsafe { unit_sentinel(ctx) }
3991    })
3992}
3993
3994/// Remove and return the front element; faults `EmptyCollection` if empty.
3995///
3996/// # Safety
3997/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
3998#[unsafe(no_mangle)]
3999pub unsafe extern "C" fn praxis_deque_pop_front(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
4000    abi_guard!("praxis_deque_pop_front", ctx, {
4001        // No allocation in the common case, but `pop_front` on a VecDeque does not
4002        // allocate Rust heap, so no collection is needed; `deque` stays live.
4003        let scope = unsafe { NativeScope::new(ctx) };
4004        let p = unsafe { deque_payload_mut(scope.root(deque)) };
4005        match p.items.pop_front() {
4006            Some(v) => v,
4007            None => {
4008                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4009                unsafe { unit_sentinel(ctx) }
4010            }
4011        }
4012    })
4013}
4014
4015/// Remove and return the back element; faults `EmptyCollection` if empty.
4016///
4017/// # Safety
4018/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
4019#[unsafe(no_mangle)]
4020pub unsafe extern "C" fn praxis_deque_pop_back(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
4021    abi_guard!("praxis_deque_pop_back", ctx, {
4022        let scope = unsafe { NativeScope::new(ctx) };
4023        let p = unsafe { deque_payload_mut(scope.root(deque)) };
4024        match p.items.pop_back() {
4025            Some(v) => v,
4026            None => {
4027                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4028                unsafe { unit_sentinel(ctx) }
4029            }
4030        }
4031    })
4032}
4033
4034/// The number of elements in `deque`, as a boxed `Int` (§6.1).
4035///
4036/// # Safety
4037/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
4038#[unsafe(no_mangle)]
4039pub unsafe extern "C" fn praxis_deque_len(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
4040    abi_guard!("praxis_deque_len", ctx, {
4041        let p = unsafe { deque_payload(deque) };
4042        let len = p.items.len() as i64;
4043        unsafe { int_ref(ctx, len) }
4044    })
4045}
4046
4047/// The element at `index` (0-based from the front); faults `IndexOutOfBounds`.
4048///
4049/// # Safety
4050/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
4051/// `index` must be a valid `Int` `GcRef`.
4052#[unsafe(no_mangle)]
4053pub unsafe extern "C" fn praxis_deque_get(
4054    ctx: *mut RuntimeContext,
4055    deque: GcRef,
4056    index: GcRef,
4057) -> GcRef {
4058    abi_guard!("praxis_deque_get", ctx, {
4059        let p = unsafe { deque_payload(deque) };
4060        let idx = unsafe { int_payload(index) };
4061        let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
4062            return unsafe { unit_sentinel(ctx) };
4063        };
4064        p.items[idx]
4065    })
4066}
4067
4068/// Replace the element at `index` (0-based from the front); faults
4069/// `IndexOutOfBounds` if out of range. Returns the Unit sentinel.
4070///
4071/// A replacement and never an insertion, and the element descriptor is
4072/// reconciled the same way, for [`praxis_vec_set`]'s reasons.
4073///
4074/// # Safety
4075/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
4076/// `index` must be a valid `Int` `GcRef`; `value` must be a valid `GcRef`.
4077#[unsafe(no_mangle)]
4078pub unsafe extern "C" fn praxis_deque_set(
4079    ctx: *mut RuntimeContext,
4080    deque: GcRef,
4081    index: GcRef,
4082    value: GcRef,
4083) -> GcRef {
4084    abi_guard!("praxis_deque_set", ctx, {
4085        let scope = unsafe { NativeScope::new(ctx) };
4086        let p = unsafe { deque_payload_mut(scope.root(deque)) };
4087        let idx = unsafe { int_payload(index) };
4088        let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
4089            return unsafe { unit_sentinel(ctx) };
4090        };
4091        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
4092            return unsafe { unit_sentinel(ctx) };
4093        }
4094        p.items[idx] = value;
4095        unsafe { unit_sentinel(ctx) }
4096    })
4097}
4098
4099/// `d[i] min= candidate` and `d[i] max= candidate` — [`vec_update`] over a
4100/// `Deque`, indexing 0-based from the front as [`praxis_deque_set`] does.
4101///
4102/// # Safety
4103/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`,
4104/// `index` a valid `Int`, and `value` of the deque's element type.
4105unsafe fn deque_update(
4106    ctx: *mut RuntimeContext,
4107    deque: GcRef,
4108    index: GcRef,
4109    value: GcRef,
4110    keep: std::cmp::Ordering,
4111) -> GcRef {
4112    // SAFETY: as `praxis_deque_set`, whose body this is with a comparison in
4113    // front of the store.
4114    let scope = unsafe { NativeScope::new(ctx) };
4115    let p = unsafe { deque_payload_mut(scope.root(deque)) };
4116    let idx = unsafe { int_payload(index) };
4117    let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
4118        return unsafe { unit_sentinel(ctx) };
4119    };
4120    if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
4121        return unsafe { unit_sentinel(ctx) };
4122    }
4123    if unsafe { update_cmp(value, p.items[idx]) } == keep {
4124        p.items[idx] = value;
4125    }
4126    unsafe { unit_sentinel(ctx) }
4127}
4128
4129/// `d[i] min= candidate` — keep the smaller of the two. See [`deque_update`].
4130///
4131/// # Safety
4132/// As [`deque_update`].
4133#[unsafe(no_mangle)]
4134pub unsafe extern "C" fn praxis_deque_update_min(
4135    ctx: *mut RuntimeContext,
4136    deque: GcRef,
4137    index: GcRef,
4138    value: GcRef,
4139) -> GcRef {
4140    abi_guard!("praxis_deque_update_min", ctx, {
4141        unsafe { deque_update(ctx, deque, index, value, std::cmp::Ordering::Less) }
4142    })
4143}
4144
4145/// `d[i] max= candidate` — keep the larger of the two. See [`deque_update`].
4146///
4147/// # Safety
4148/// As [`deque_update`].
4149#[unsafe(no_mangle)]
4150pub unsafe extern "C" fn praxis_deque_update_max(
4151    ctx: *mut RuntimeContext,
4152    deque: GcRef,
4153    index: GcRef,
4154    value: GcRef,
4155) -> GcRef {
4156    abi_guard!("praxis_deque_update_max", ctx, {
4157        unsafe { deque_update(ctx, deque, index, value, std::cmp::Ordering::Greater) }
4158    })
4159}
4160
4161/// True iff `deque` has no elements, as a boxed `Bool` (§6.1).
4162///
4163/// # Safety
4164/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
4165#[unsafe(no_mangle)]
4166pub unsafe extern "C" fn praxis_deque_is_empty(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
4167    abi_guard!("praxis_deque_is_empty", ctx, {
4168        let p = unsafe { deque_payload(deque) };
4169        let empty = p.items.is_empty();
4170        unsafe { bool_ref(ctx, empty) }
4171    })
4172}
4173
4174// ---------------------------------------------------------------------------
4175// Map[K, V] / Set[T] / Counter[T] (§6.1, §11.3).
4176//
4177// All three reuse Rust hash collections behind opaque GC objects. Keys are
4178// wrapped in `DynamicKey`, which delegates Rust `Hash`/`Eq` to the descriptor's
4179// structural callbacks — this is what makes tuples/records/enums/nested
4180// collections work as keys (§19.7 criterion). Counter's absent keys read as
4181// zero (§6.2); `min=`/`max=` update a map entry in place (§6.2).
4182// ---------------------------------------------------------------------------
4183
4184use crate::maps::{CounterPayload, MapPayload, SetPayload};
4185
4186/// Read a `MapPayload` as a shared ref. See `payload_ref` for the safety model.
4187unsafe fn map_payload(r: GcRef) -> &'static MapPayload {
4188    unsafe { payload_ref::<MapPayload>(r) }
4189}
4190
4191unsafe fn map_payload_mut<'s>(r: Rooted<'s>) -> &'s mut MapPayload {
4192    unsafe { payload_mut::<MapPayload>(r) }
4193}
4194
4195unsafe fn set_payload(r: GcRef) -> &'static SetPayload {
4196    unsafe { payload_ref::<SetPayload>(r) }
4197}
4198
4199unsafe fn set_payload_mut<'s>(r: Rooted<'s>) -> &'s mut SetPayload {
4200    unsafe { payload_mut::<SetPayload>(r) }
4201}
4202
4203unsafe fn counter_payload(r: GcRef) -> &'static CounterPayload {
4204    unsafe { payload_ref::<CounterPayload>(r) }
4205}
4206
4207unsafe fn counter_payload_mut<'s>(r: Rooted<'s>) -> &'s mut CounterPayload {
4208    unsafe { payload_mut::<CounterPayload>(r) }
4209}
4210
4211/// Allocate an empty `Map[K, V]`. `key_descriptor` is the key type the
4212/// construction site knew, or **null** when it knew none — which is kept null,
4213/// the way `praxis_vec_new` keeps it. Spelling an unknown type `INT` is a claim,
4214/// and every reader that believed it would read the wrong type.
4215///
4216/// # Safety
4217/// `ctx` must be live and wired. `key_descriptor` must be a valid pointer to a
4218/// `'static TypeDescriptor` (or null).
4219#[unsafe(no_mangle)]
4220pub unsafe extern "C" fn praxis_map_new(
4221    ctx: *mut RuntimeContext,
4222    key_descriptor: *const TypeDescriptor,
4223) -> GcRef {
4224    abi_guard!("praxis_map_new", ctx, {
4225        // The `Map` row carries one type argument, so the value type never reaches
4226        // this wrapper at all — it is unknown here by construction, and says so.
4227        // `praxis_map_insert` adopts the first inserted value's own descriptor,
4228        // which is how a `Vec` learns its element type.
4229        let value_descriptor: *const TypeDescriptor = std::ptr::null();
4230        // SAFETY: MapPayload is MAP's payload type.
4231        unsafe {
4232            gc_alloc_owned(ctx, &crate::maps::MAP, || MapPayload {
4233                key_descriptor,
4234                value_descriptor,
4235                entries: std::collections::HashMap::new(),
4236            })
4237        }
4238    })
4239}
4240
4241/// Insert `(key, value)` into `map`, replacing any prior value; returns Unit.
4242///
4243/// # Safety
4244/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`; `key` and
4245/// `value` must be valid `GcRef`s.
4246#[unsafe(no_mangle)]
4247pub unsafe extern "C" fn praxis_map_insert(
4248    ctx: *mut RuntimeContext,
4249    map: GcRef,
4250    key: GcRef,
4251    value: GcRef,
4252) -> GcRef {
4253    abi_guard!("praxis_map_insert", ctx, {
4254        unsafe { maybe_collect(ctx) };
4255        let scope = unsafe { NativeScope::new(ctx) };
4256        let p = unsafe { map_payload_mut(scope.root(map)) };
4257        // Learn the value type from the first value inserted, the way a `Vec`
4258        // learns its element type from the first `push`. Null is the encoding of
4259        // "never been told", so it is distinguishable from a `Map` that really
4260        // holds `Int`s.
4261        //
4262        // A later value of a different type un-learns it rather than faulting: the
4263        // type checker makes a `Map` homogeneous, so this is unreachable for a
4264        // well-typed program, and `praxis_map_insert` is a non-faulting row (its
4265        // caller emits no fault check). Null is now representable and means "the
4266        // value's own descriptor answers", so forgetting is the safe direction.
4267        let val_desc = value.descriptor();
4268        match p.value() {
4269            None => p.value_descriptor = val_desc,
4270            Some(known) if !std::ptr::eq(known, val_desc) => {
4271                p.value_descriptor = std::ptr::null();
4272            }
4273            Some(_) => {}
4274        }
4275        let before = p.owned_bytes();
4276        p.entries.insert(DynamicKey::new(key), value);
4277        charge_growth(ctx, before, p.owned_bytes());
4278        unsafe { unit_sentinel(ctx) }
4279    })
4280}
4281
4282/// `Some(value)` for `key`, or `None` if absent (§4.7, §5.7).
4283///
4284/// §5.7 writes the signature `Map[K,V].get(K) -> Option[V]` and §4.7 opens
4285/// "Option[T] represents normal domain-level absence. It is not an error
4286/// channel." Answering the Unit sentinel under a `V` static type instead would
4287/// hand the program a value it could not distinguish from a real one without
4288/// `contains`, while the type system insisted it was a `V`.
4289///
4290/// The `Option` is built through the runtime's own `option_schema`, whose
4291/// `Some` slot is unknown — `V` is learned from the value found, never from a
4292/// static type — and which `EnumSchema::same_type` therefore recognizes as the
4293/// same type as the codegen's `Option[Int]`. That is what lets the result match
4294/// against arms the program wrote.
4295///
4296/// # Safety
4297/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`; `key`
4298/// must be a valid `GcRef`.
4299#[unsafe(no_mangle)]
4300pub unsafe extern "C" fn praxis_map_get(ctx: *mut RuntimeContext, map: GcRef, key: GcRef) -> GcRef {
4301    abi_guard!("praxis_map_get", ctx, {
4302        let found = {
4303            let p = unsafe { map_payload(map) };
4304            p.entries.get(&DynamicKey::new(key)).copied()
4305        };
4306        match found {
4307            Some(v) => unsafe { option_some(ctx, v) },
4308            None => unsafe { option_none(ctx) },
4309        }
4310    })
4311}
4312
4313/// `map[key]` (§4.7): the value for `key`, **faulting** if it is absent.
4314///
4315/// A different wrapper from [`praxis_map_get`] because the two answers are the
4316/// language's own choice, not an implementation detail: §4.7 says "indexing a
4317/// missing map key faults instead of returning an option… the user chooses
4318/// between explicit absence with `.get` and assertion-like access with
4319/// indexing". Sharing one wrapper would take that choice away from the user.
4320///
4321/// The fault is [`FaultKind::IndexOutOfBounds`](crate::FaultKind::IndexOutOfBounds)
4322/// — an index the collection does not hold, which is what its doc already
4323/// describes. A dedicated `MissingKey` kind would read better, and adding one is
4324/// a `#[repr(C)]` change that costs an ABI bump (ADR-075).
4325///
4326/// # Safety
4327/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`; `key`
4328/// must be a valid `GcRef`.
4329#[unsafe(no_mangle)]
4330pub unsafe extern "C" fn praxis_map_index(
4331    ctx: *mut RuntimeContext,
4332    map: GcRef,
4333    key: GcRef,
4334) -> GcRef {
4335    abi_guard!("praxis_map_index", ctx, {
4336        let p = unsafe { map_payload(map) };
4337        match p.entries.get(&DynamicKey::new(key)) {
4338            Some(v) => *v,
4339            None => {
4340                unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
4341                unsafe { unit_sentinel(ctx) }
4342            }
4343        }
4344    })
4345}
4346
4347/// True iff `key` is present, as a boxed Bool.
4348///
4349/// # Safety
4350/// `ctx` must be live and wired; `map` and `key` must be valid `GcRef`s.
4351#[unsafe(no_mangle)]
4352pub unsafe extern "C" fn praxis_map_contains(
4353    ctx: *mut RuntimeContext,
4354    map: GcRef,
4355    key: GcRef,
4356) -> GcRef {
4357    abi_guard!("praxis_map_contains", ctx, {
4358        let p = unsafe { map_payload(map) };
4359        let present = p.entries.contains_key(&DynamicKey::new(key));
4360        unsafe { bool_ref(ctx, present) }
4361    })
4362}
4363
4364/// Remove `key`; returns Unit (the removed value, if any, is dropped).
4365///
4366/// # Safety
4367/// `ctx` must be live and wired; `map` and `key` must be valid `GcRef`s.
4368#[unsafe(no_mangle)]
4369pub unsafe extern "C" fn praxis_map_remove(
4370    ctx: *mut RuntimeContext,
4371    map: GcRef,
4372    key: GcRef,
4373) -> GcRef {
4374    abi_guard!("praxis_map_remove", ctx, {
4375        let scope = unsafe { NativeScope::new(ctx) };
4376        let p = unsafe { map_payload_mut(scope.root(map)) };
4377        p.entries.remove(&DynamicKey::new(key));
4378        unsafe { unit_sentinel(ctx) }
4379    })
4380}
4381
4382/// The number of entries, as a boxed Int.
4383///
4384/// # Safety
4385/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4386#[unsafe(no_mangle)]
4387pub unsafe extern "C" fn praxis_map_len(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4388    abi_guard!("praxis_map_len", ctx, {
4389        let p = unsafe { map_payload(map) };
4390        unsafe { int_ref(ctx, p.entries.len() as i64) }
4391    })
4392}
4393
4394/// `m.keys()` — every key, as a `Vec[K]`. Ordered like
4395/// [`praxis_counter_keys`], and index-aligned with [`praxis_map_values`].
4396///
4397/// This and `values()` are the only way to enumerate a `Map`: `for kv in m` has
4398/// no lowering.
4399///
4400/// # Safety
4401/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4402#[unsafe(no_mangle)]
4403pub unsafe extern "C" fn praxis_map_keys(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4404    abi_guard!("praxis_map_keys", ctx, {
4405        let key_desc = unsafe { map_payload(map) }.key_descriptor;
4406        let rows = unsafe { crate::maps::ordered_entries(&map_payload(map).entries) };
4407        unsafe { vec_of(ctx, key_desc, rows.into_iter().map(|(k, _)| k)) }
4408    })
4409}
4410
4411/// `m.values()` — every value, as a `Vec[V]`. See [`praxis_map_keys`].
4412///
4413/// # Safety
4414/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4415#[unsafe(no_mangle)]
4416pub unsafe extern "C" fn praxis_map_values(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4417    abi_guard!("praxis_map_values", ctx, {
4418        let val_desc = unsafe { map_payload(map) }.value_descriptor;
4419        let rows = unsafe { crate::maps::ordered_entries(&map_payload(map).entries) };
4420        unsafe { vec_of(ctx, val_desc, rows.into_iter().map(|(_, v)| v)) }
4421    })
4422}
4423
4424/// True iff the map is empty, as a boxed Bool.
4425///
4426/// # Safety
4427/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4428#[unsafe(no_mangle)]
4429pub unsafe extern "C" fn praxis_map_is_empty(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4430    abi_guard!("praxis_map_is_empty", ctx, {
4431        let p = unsafe { map_payload(map) };
4432        unsafe { bool_ref(ctx, p.entries.is_empty()) }
4433    })
4434}
4435
4436/// Order a candidate against the value already stored, for `min=` and `max=`.
4437///
4438/// Through the descriptor each value carries (ADR-045) — the same `compare`
4439/// callback `sorted`, a heap and `<` order through, which is what makes
4440/// `d[k] min= v` keep the value `[cur, v].sorted()` puts first. Reading the two
4441/// payloads as `i64`s instead is what this replaced, and it is wrong for three
4442/// of the four orderable types a program can write: it compares a `Text` by its
4443/// *address*, puts `-2.0` after `-1.0`, and reads four bytes past a `Char`.
4444///
4445/// **`Equal` is the answer to both degenerate cases**, and for
4446/// [`HeapEntry::cmp`](crate::heaps::HeapEntry)'s reason. Descriptors that differ
4447/// cannot happen — a `Map[K, V]`'s values are all one type — and a `V` with no
4448/// order at all is refused at `praxis check` by the row's `Ord` bound, so each
4449/// is a miscompile rather than a program. An updating store that compares
4450/// `Equal` keeps the entry it has, which is a consistent semantics one step from
4451/// the bug; dispatching a callback on a foreign payload is not.
4452///
4453/// # Safety
4454/// Both must be valid `GcRef`s, so each payload matches the descriptor in its
4455/// own header.
4456unsafe fn update_cmp(candidate: GcRef, current: GcRef) -> std::cmp::Ordering {
4457    let desc = candidate.descriptor();
4458    if !std::ptr::eq(desc, current.descriptor()) {
4459        return std::cmp::Ordering::Equal;
4460    }
4461    let Some(compare) = desc.compare else {
4462        return std::cmp::Ordering::Equal;
4463    };
4464    // SAFETY: both values carry `desc` (checked above), so both payloads are
4465    // values of its type.
4466    unsafe {
4467        compare(
4468            candidate.payload::<u8>() as *const u8,
4469            current.payload::<u8>() as *const u8,
4470        )
4471    }
4472}
4473
4474/// `distance[key] min= candidate` (§6.2): keep the smaller value, or insert if
4475/// absent (an absent entry accepts the first value). The value is of any
4476/// orderable type (§5.4 `SupportsOrd`), ordered by [`update_cmp`]; returns Unit.
4477///
4478/// **A tie keeps the entry that is there**, which is what the `<` this replaced
4479/// did and is now observable rather than academic: `+0.0` and `-0.0` compare
4480/// equal and print differently, so a tie that replaced would let `d[k] min= v`
4481/// answer by how many times it had been written.
4482///
4483/// # Safety
4484/// `ctx` must be live and wired; `map`, `key`, `value` must be valid `GcRef`s,
4485/// and `value` must be of the map's value type.
4486#[unsafe(no_mangle)]
4487pub unsafe extern "C" fn praxis_map_update_min(
4488    ctx: *mut RuntimeContext,
4489    map: GcRef,
4490    key: GcRef,
4491    value: GcRef,
4492) -> GcRef {
4493    abi_guard!("praxis_map_update_min", ctx, {
4494        unsafe { maybe_collect(ctx) };
4495        let scope = unsafe { NativeScope::new(ctx) };
4496        let p = unsafe { map_payload_mut(scope.root(map)) };
4497        match p.entries.get_mut(&DynamicKey::new(key)) {
4498            Some(existing) => {
4499                // SAFETY: both are values this map holds, so each payload
4500                // matches the descriptor in its own header.
4501                if unsafe { update_cmp(value, *existing) } == std::cmp::Ordering::Less {
4502                    *existing = value;
4503                }
4504            }
4505            None => {
4506                p.entries.insert(DynamicKey::new(key), value);
4507            }
4508        }
4509        unsafe { unit_sentinel(ctx) }
4510    })
4511}
4512
4513/// `best[key] max= score` (§6.2): keep the larger value, or insert if absent.
4514/// The dual of [`praxis_map_update_min`], including its tie rule.
4515///
4516/// # Safety
4517/// `ctx` must be live and wired; `map`, `key`, `value` must be valid `GcRef`s,
4518/// and `value` must be of the map's value type.
4519#[unsafe(no_mangle)]
4520pub unsafe extern "C" fn praxis_map_update_max(
4521    ctx: *mut RuntimeContext,
4522    map: GcRef,
4523    key: GcRef,
4524    value: GcRef,
4525) -> GcRef {
4526    abi_guard!("praxis_map_update_max", ctx, {
4527        unsafe { maybe_collect(ctx) };
4528        let scope = unsafe { NativeScope::new(ctx) };
4529        let p = unsafe { map_payload_mut(scope.root(map)) };
4530        match p.entries.get_mut(&DynamicKey::new(key)) {
4531            Some(existing) => {
4532                // SAFETY: as `praxis_map_update_min`.
4533                if unsafe { update_cmp(value, *existing) } == std::cmp::Ordering::Greater {
4534                    *existing = value;
4535                }
4536            }
4537            None => {
4538                p.entries.insert(DynamicKey::new(key), value);
4539            }
4540        }
4541        unsafe { unit_sentinel(ctx) }
4542    })
4543}
4544
4545// --- Set[T] -----------------------------------------------------------------
4546
4547/// Allocate an empty `Set[T]`. `element_descriptor` is the element type the
4548/// construction site knew, or **null** when it knew none — kept null.
4549///
4550/// # Safety
4551/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
4552/// a `'static TypeDescriptor` (or null).
4553#[unsafe(no_mangle)]
4554pub unsafe extern "C" fn praxis_set_new(
4555    ctx: *mut RuntimeContext,
4556    element_descriptor: *const TypeDescriptor,
4557) -> GcRef {
4558    abi_guard!("praxis_set_new", ctx, {
4559        // SAFETY: SetPayload is SET's payload type.
4560        unsafe {
4561            gc_alloc_owned(ctx, &crate::maps::SET, || SetPayload {
4562                element_descriptor,
4563                entries: std::collections::HashSet::new(),
4564            })
4565        }
4566    })
4567}
4568
4569/// Insert `value` into `set`; returns Unit.
4570///
4571/// # Safety
4572/// `ctx` must be live and wired; `set` and `value` must be valid `GcRef`s.
4573#[unsafe(no_mangle)]
4574pub unsafe extern "C" fn praxis_set_insert(
4575    ctx: *mut RuntimeContext,
4576    set: GcRef,
4577    value: GcRef,
4578) -> GcRef {
4579    abi_guard!("praxis_set_insert", ctx, {
4580        unsafe { maybe_collect(ctx) };
4581        let scope = unsafe { NativeScope::new(ctx) };
4582        let p = unsafe { set_payload_mut(scope.root(set)) };
4583        let before = p.owned_bytes();
4584        p.entries.insert(DynamicKey::new(value));
4585        charge_growth(ctx, before, p.owned_bytes());
4586        unsafe { unit_sentinel(ctx) }
4587    })
4588}
4589
4590/// Remove `value` from `set`; returns Unit.
4591///
4592/// # Safety
4593/// `ctx` must be live and wired; `set` and `value` must be valid `GcRef`s.
4594#[unsafe(no_mangle)]
4595pub unsafe extern "C" fn praxis_set_remove(
4596    ctx: *mut RuntimeContext,
4597    set: GcRef,
4598    value: GcRef,
4599) -> GcRef {
4600    abi_guard!("praxis_set_remove", ctx, {
4601        let scope = unsafe { NativeScope::new(ctx) };
4602        let p = unsafe { set_payload_mut(scope.root(set)) };
4603        p.entries.remove(&DynamicKey::new(value));
4604        unsafe { unit_sentinel(ctx) }
4605    })
4606}
4607
4608/// True iff `value` is in the set, as a boxed Bool.
4609///
4610/// # Safety
4611/// `ctx` must be live and wired; `set` and `value` must be valid `GcRef`s.
4612#[unsafe(no_mangle)]
4613pub unsafe extern "C" fn praxis_set_contains(
4614    ctx: *mut RuntimeContext,
4615    set: GcRef,
4616    value: GcRef,
4617) -> GcRef {
4618    abi_guard!("praxis_set_contains", ctx, {
4619        let p = unsafe { set_payload(set) };
4620        let present = p.entries.contains(&DynamicKey::new(value));
4621        unsafe { bool_ref(ctx, present) }
4622    })
4623}
4624
4625/// The number of elements, as a boxed Int.
4626///
4627/// # Safety
4628/// `ctx` must be live and wired; `set` must be a valid `Set` `GcRef`.
4629#[unsafe(no_mangle)]
4630pub unsafe extern "C" fn praxis_set_len(ctx: *mut RuntimeContext, set: GcRef) -> GcRef {
4631    abi_guard!("praxis_set_len", ctx, {
4632        let p = unsafe { set_payload(set) };
4633        unsafe { int_ref(ctx, p.entries.len() as i64) }
4634    })
4635}
4636
4637/// True iff the set is empty, as a boxed Bool.
4638///
4639/// # Safety
4640/// `ctx` must be live and wired; `set` must be a valid `Set` `GcRef`.
4641#[unsafe(no_mangle)]
4642pub unsafe extern "C" fn praxis_set_is_empty(ctx: *mut RuntimeContext, set: GcRef) -> GcRef {
4643    abi_guard!("praxis_set_is_empty", ctx, {
4644        let p = unsafe { set_payload(set) };
4645        unsafe { bool_ref(ctx, p.entries.is_empty()) }
4646    })
4647}
4648
4649/// Every member, as a `Vec[T]` in [`crate::maps::ordered_members`] order — the
4650/// snapshot `for x in s` iterates (ADR-066).
4651///
4652/// There is no `praxis_set_get`, and this is why: a `HashSet` has no nth member,
4653/// so an indexed accessor would be a linear scan per step and the loop would be
4654/// quadratic. The snapshot is one pass, and it is what makes the order
4655/// deterministic — which for `for` is the program's *answer* and not only its
4656/// printing.
4657///
4658/// # Safety
4659/// `ctx` must be live and wired; `set` must be a valid `Set` `GcRef`.
4660#[unsafe(no_mangle)]
4661pub unsafe extern "C" fn praxis_set_items(ctx: *mut RuntimeContext, set: GcRef) -> GcRef {
4662    abi_guard!("praxis_set_items", ctx, {
4663        let elem_desc = unsafe { set_payload(set) }.element_descriptor;
4664        let members = unsafe { crate::maps::ordered_members(&set_payload(set).entries) };
4665        unsafe { vec_of(ctx, elem_desc, members.into_iter()) }
4666    })
4667}
4668
4669// --- Counter[T] -------------------------------------------------------------
4670
4671/// Allocate an empty `Counter[T]`. `key_descriptor` is the key type the
4672/// construction site knew, or **null** when it knew none — kept null.
4673///
4674/// # Safety
4675/// `ctx` must be live and wired; `key_descriptor` must be a valid pointer to a
4676/// `'static TypeDescriptor` (or null).
4677#[unsafe(no_mangle)]
4678pub unsafe extern "C" fn praxis_counter_new(
4679    ctx: *mut RuntimeContext,
4680    key_descriptor: *const TypeDescriptor,
4681) -> GcRef {
4682    abi_guard!("praxis_counter_new", ctx, {
4683        // SAFETY: CounterPayload is COUNTER's payload type.
4684        unsafe {
4685            gc_alloc_owned(ctx, &crate::maps::COUNTER, || CounterPayload {
4686                key_descriptor,
4687                entries: std::collections::HashMap::new(),
4688            })
4689        }
4690    })
4691}
4692
4693/// The count for `key`, or zero if absent (§6.2: "absent values read as zero").
4694/// Never faults. Returns a boxed Int.
4695///
4696/// # Safety
4697/// `ctx` must be live and wired; `counter` and `key` must be valid `GcRef`s.
4698#[unsafe(no_mangle)]
4699pub unsafe extern "C" fn praxis_counter_get(
4700    ctx: *mut RuntimeContext,
4701    counter: GcRef,
4702    key: GcRef,
4703) -> GcRef {
4704    abi_guard!("praxis_counter_get", ctx, {
4705        let p = unsafe { counter_payload(counter) };
4706        let count = match p.entries.get(&DynamicKey::new(key)) {
4707            Some(v) => unsafe { int_payload(*v) },
4708            None => 0, // §6.2: absent reads as zero.
4709        };
4710        unsafe { int_ref(ctx, count) }
4711    })
4712}
4713
4714/// Increment the count for `key` by one (inserting 1 if absent); returns Unit.
4715///
4716/// # Safety
4717/// `ctx` must be live and wired; `counter` and `key` must be valid `GcRef`s.
4718#[unsafe(no_mangle)]
4719pub unsafe extern "C" fn praxis_counter_inc(
4720    ctx: *mut RuntimeContext,
4721    counter: GcRef,
4722    key: GcRef,
4723) -> GcRef {
4724    abi_guard!("praxis_counter_inc", ctx, {
4725        let scope = unsafe { NativeScope::new(ctx) };
4726        let p = unsafe { counter_payload_mut(scope.root(counter)) };
4727        let dk = DynamicKey::new(key);
4728        match p.entries.get_mut(&dk) {
4729            Some(v) => {
4730                let cur = unsafe { int_payload(*v) };
4731                // Checked, like every other integer computation in this file
4732                // (§4.12): a raw `cur + 1` panics across `extern "C"` in debug — the
4733                // non-unwinding panic §10.4 forbids — and wraps to `i64::MIN` in
4734                // release, which is a silently wrong count. A `Counter`'s values are
4735                // set to arbitrary `Int`s by `c[k] = n`, so this is reachable from
4736                // source and not only from `i64::MAX` increments.
4737                let Some(next) = cur.checked_add(1) else {
4738                    unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
4739                    return unsafe { unit_sentinel(ctx) };
4740                };
4741                // SAFETY: ctx is wired; alloc a fresh Int for the incremented value.
4742                *v = unsafe { int_ref(ctx, next) };
4743            }
4744            None => {
4745                let one = unsafe { int_ref(ctx, 1_i64) };
4746                p.entries.insert(dk, one);
4747            }
4748        }
4749        unsafe { unit_sentinel(ctx) }
4750    })
4751}
4752
4753/// `counts[key] = value` (§6.2): set the count for `key`, replacing any prior
4754/// one; returns Unit.
4755///
4756/// [`praxis_counter_inc`] adds exactly one, so it cannot express
4757/// `counts[key] += n` or `counts[key] = n`. A subscript assignment is a
4758/// read-modify-write over the pair (`praxis_counter_get`, this), which is what
4759/// makes every assignment operator work on a `Counter` rather than only `+= 1`.
4760///
4761/// # Safety
4762/// `ctx` must be live and wired; `counter` and `key` must be valid `GcRef`s and
4763/// `value` must be an `Int`.
4764#[unsafe(no_mangle)]
4765pub unsafe extern "C" fn praxis_counter_set(
4766    ctx: *mut RuntimeContext,
4767    counter: GcRef,
4768    key: GcRef,
4769    value: GcRef,
4770) -> GcRef {
4771    abi_guard!("praxis_counter_set", ctx, {
4772        unsafe { maybe_collect(ctx) };
4773        let scope = unsafe { NativeScope::new(ctx) };
4774        let p = unsafe { counter_payload_mut(scope.root(counter)) };
4775        let before = p.owned_bytes();
4776        p.entries.insert(DynamicKey::new(key), value);
4777        charge_growth(ctx, before, p.owned_bytes());
4778        unsafe { unit_sentinel(ctx) }
4779    })
4780}
4781
4782/// `c.keys()` — every key, as a `Vec[T]`.
4783///
4784/// Ordered by the key's own `compare` (ADR-138), so it is the *same* order
4785/// [`praxis_counter_values`] uses and the two are index-aligned. A `HashMap`'s
4786/// own order is randomized per process, so returning it would make the same
4787/// program answer differently on two runs — and here the order is the program's
4788/// *answer*, not only its printing.
4789///
4790/// # Safety
4791/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4792#[unsafe(no_mangle)]
4793pub unsafe extern "C" fn praxis_counter_keys(ctx: *mut RuntimeContext, counter: GcRef) -> GcRef {
4794    abi_guard!("praxis_counter_keys", ctx, {
4795        let key_desc = unsafe { counter_payload(counter) }.key_descriptor;
4796        let rows = unsafe { crate::maps::ordered_entries(&counter_payload(counter).entries) };
4797        unsafe { vec_of(ctx, key_desc, rows.into_iter().map(|(k, _)| k)) }
4798    })
4799}
4800
4801/// `c.values()` — every count, as a `Vec[Int]`.
4802///
4803/// §3.3's representative program is `counts.values().count(|n| n >= 2)`. Ordered
4804/// like [`praxis_counter_keys`]; see it for why the order is fixed.
4805///
4806/// # Safety
4807/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4808#[unsafe(no_mangle)]
4809pub unsafe extern "C" fn praxis_counter_values(ctx: *mut RuntimeContext, counter: GcRef) -> GcRef {
4810    abi_guard!("praxis_counter_values", ctx, {
4811        let rows = unsafe { crate::maps::ordered_entries(&counter_payload(counter).entries) };
4812        unsafe { vec_of(ctx, &scalars::INT, rows.into_iter().map(|(_, v)| v)) }
4813    })
4814}
4815
4816/// The number of distinct keys, as a boxed Int.
4817///
4818/// # Safety
4819/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4820#[unsafe(no_mangle)]
4821pub unsafe extern "C" fn praxis_counter_len(ctx: *mut RuntimeContext, counter: GcRef) -> GcRef {
4822    abi_guard!("praxis_counter_len", ctx, {
4823        let p = unsafe { counter_payload(counter) };
4824        unsafe { int_ref(ctx, p.entries.len() as i64) }
4825    })
4826}
4827
4828/// True iff the counter has no keys, as a boxed Bool.
4829///
4830/// # Safety
4831/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4832#[unsafe(no_mangle)]
4833pub unsafe extern "C" fn praxis_counter_is_empty(
4834    ctx: *mut RuntimeContext,
4835    counter: GcRef,
4836) -> GcRef {
4837    abi_guard!("praxis_counter_is_empty", ctx, {
4838        let p = unsafe { counter_payload(counter) };
4839        unsafe { bool_ref(ctx, p.entries.is_empty()) }
4840    })
4841}
4842
4843// ---------------------------------------------------------------------------
4844// MinHeap[T] / MaxHeap[T] (§6.1, §11.2).
4845//
4846// `MaxHeap` maps directly to Rust's max-`BinaryHeap`; `MinHeap` wraps entries in
4847// `Reverse` so the smallest surfaces first. `pop`/`peek` fault `EmptyCollection`
4848// on an empty heap.
4849// ---------------------------------------------------------------------------
4850
4851use crate::heaps::{HeapEntry, MaxHeapPayload, MinHeapPayload};
4852use std::collections::BinaryHeap;
4853
4854unsafe fn max_heap_payload_mut<'s>(r: Rooted<'s>) -> &'s mut MaxHeapPayload {
4855    unsafe { payload_mut::<MaxHeapPayload>(r) }
4856}
4857
4858unsafe fn max_heap_payload(r: GcRef) -> &'static MaxHeapPayload {
4859    unsafe { payload_ref::<MaxHeapPayload>(r) }
4860}
4861
4862unsafe fn min_heap_payload_mut<'s>(r: Rooted<'s>) -> &'s mut MinHeapPayload {
4863    unsafe { payload_mut::<MinHeapPayload>(r) }
4864}
4865
4866unsafe fn min_heap_payload(r: GcRef) -> &'static MinHeapPayload {
4867    unsafe { payload_ref::<MinHeapPayload>(r) }
4868}
4869
4870/// Allocate an empty `MaxHeap[T]`. A null `element_descriptor` — the codegen's
4871/// "no static element type" — is kept null.
4872///
4873/// # Safety
4874/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
4875/// a `'static TypeDescriptor` (or null).
4876#[unsafe(no_mangle)]
4877pub unsafe extern "C" fn praxis_max_heap_new(
4878    ctx: *mut RuntimeContext,
4879    element_descriptor: *const TypeDescriptor,
4880) -> GcRef {
4881    abi_guard!("praxis_max_heap_new", ctx, {
4882        // SAFETY: MaxHeapPayload is MAX_HEAP's payload type.
4883        unsafe {
4884            gc_alloc_owned(ctx, &crate::heaps::MAX_HEAP, || MaxHeapPayload {
4885                element_descriptor,
4886                items: BinaryHeap::new(),
4887            })
4888        }
4889    })
4890}
4891
4892/// Push `value` onto the max-heap; returns Unit.
4893///
4894/// # Safety
4895/// `ctx` must be live and wired; `heap` and `value` must be valid `GcRef`s.
4896#[unsafe(no_mangle)]
4897pub unsafe extern "C" fn praxis_max_heap_push(
4898    ctx: *mut RuntimeContext,
4899    heap_ref: GcRef,
4900    value: GcRef,
4901) -> GcRef {
4902    abi_guard!("praxis_max_heap_push", ctx, {
4903        unsafe { maybe_collect(ctx) };
4904        let scope = unsafe { NativeScope::new(ctx) };
4905        let p = unsafe { max_heap_payload_mut(scope.root(heap_ref)) };
4906        let before = p.owned_bytes();
4907        p.items.push(HeapEntry {
4908            value,
4909            descriptor: value.descriptor(),
4910        });
4911        charge_growth(ctx, before, p.owned_bytes());
4912        unsafe { unit_sentinel(ctx) }
4913    })
4914}
4915
4916/// Remove and return the largest element; faults `EmptyCollection` if empty.
4917///
4918/// # Safety
4919/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4920#[unsafe(no_mangle)]
4921pub unsafe extern "C" fn praxis_max_heap_pop(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4922    abi_guard!("praxis_max_heap_pop", ctx, {
4923        let scope = unsafe { NativeScope::new(ctx) };
4924        let p = unsafe { max_heap_payload_mut(scope.root(heap_ref)) };
4925        match p.items.pop() {
4926            Some(e) => e.value,
4927            None => {
4928                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4929                unsafe { unit_sentinel(ctx) }
4930            }
4931        }
4932    })
4933}
4934
4935/// The largest element without removing it; faults `EmptyCollection` if empty.
4936///
4937/// # Safety
4938/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4939#[unsafe(no_mangle)]
4940pub unsafe extern "C" fn praxis_max_heap_peek(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4941    abi_guard!("praxis_max_heap_peek", ctx, {
4942        let p = unsafe { max_heap_payload(heap_ref) };
4943        match p.items.peek() {
4944            Some(e) => e.value,
4945            None => {
4946                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4947                unsafe { unit_sentinel(ctx) }
4948            }
4949        }
4950    })
4951}
4952
4953/// The number of elements, as a boxed Int.
4954///
4955/// # Safety
4956/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4957#[unsafe(no_mangle)]
4958pub unsafe extern "C" fn praxis_max_heap_len(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4959    abi_guard!("praxis_max_heap_len", ctx, {
4960        let p = unsafe { max_heap_payload(heap_ref) };
4961        unsafe { int_ref(ctx, p.items.len() as i64) }
4962    })
4963}
4964
4965/// Every element, as a `Vec[T]` in [`crate::heaps::in_pop_order`] — the snapshot
4966/// `for x in h` iterates (ADR-066). The heap is **not** drained.
4967///
4968/// A heap's backing array is heap-ordered only at its root, so an indexed
4969/// accessor over it would answer in insertion-history order — reading the array
4970/// as if it were a `Vec`'s.
4971///
4972/// # Safety
4973/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4974#[unsafe(no_mangle)]
4975pub unsafe extern "C" fn praxis_max_heap_items(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4976    abi_guard!("praxis_max_heap_items", ctx, {
4977        let p = unsafe { max_heap_payload(heap_ref) };
4978        let items = crate::heaps::in_pop_order(&p.items, |e| e.value);
4979        unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
4980    })
4981}
4982
4983/// True iff the heap is empty, as a boxed Bool.
4984///
4985/// # Safety
4986/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4987#[unsafe(no_mangle)]
4988pub unsafe extern "C" fn praxis_max_heap_is_empty(
4989    ctx: *mut RuntimeContext,
4990    heap_ref: GcRef,
4991) -> GcRef {
4992    abi_guard!("praxis_max_heap_is_empty", ctx, {
4993        let p = unsafe { max_heap_payload(heap_ref) };
4994        unsafe { bool_ref(ctx, p.items.is_empty()) }
4995    })
4996}
4997
4998// --- MinHeap (mirrors MaxHeap with Reverse wrapping) -----------------------
4999
5000/// Allocate an empty `MinHeap[T]`. A null `element_descriptor` — the codegen's
5001/// "no static element type" — is kept null.
5002///
5003/// # Safety
5004/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
5005/// a `'static TypeDescriptor` (or null).
5006#[unsafe(no_mangle)]
5007pub unsafe extern "C" fn praxis_min_heap_new(
5008    ctx: *mut RuntimeContext,
5009    element_descriptor: *const TypeDescriptor,
5010) -> GcRef {
5011    abi_guard!("praxis_min_heap_new", ctx, {
5012        // SAFETY: MinHeapPayload is MIN_HEAP's payload type.
5013        unsafe {
5014            gc_alloc_owned(ctx, &crate::heaps::MIN_HEAP, || MinHeapPayload {
5015                element_descriptor,
5016                items: BinaryHeap::new(),
5017            })
5018        }
5019    })
5020}
5021
5022/// Push `value` onto the min-heap; returns Unit.
5023///
5024/// # Safety
5025/// `ctx` must be live and wired; `heap` and `value` must be valid `GcRef`s.
5026#[unsafe(no_mangle)]
5027pub unsafe extern "C" fn praxis_min_heap_push(
5028    ctx: *mut RuntimeContext,
5029    heap_ref: GcRef,
5030    value: GcRef,
5031) -> GcRef {
5032    abi_guard!("praxis_min_heap_push", ctx, {
5033        unsafe { maybe_collect(ctx) };
5034        let scope = unsafe { NativeScope::new(ctx) };
5035        let p = unsafe { min_heap_payload_mut(scope.root(heap_ref)) };
5036        let before = p.owned_bytes();
5037        p.items.push(std::cmp::Reverse(HeapEntry {
5038            value,
5039            descriptor: value.descriptor(),
5040        }));
5041        charge_growth(ctx, before, p.owned_bytes());
5042        unsafe { unit_sentinel(ctx) }
5043    })
5044}
5045
5046/// Remove and return the smallest element; faults `EmptyCollection` if empty.
5047///
5048/// # Safety
5049/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5050#[unsafe(no_mangle)]
5051pub unsafe extern "C" fn praxis_min_heap_pop(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
5052    abi_guard!("praxis_min_heap_pop", ctx, {
5053        let scope = unsafe { NativeScope::new(ctx) };
5054        let p = unsafe { min_heap_payload_mut(scope.root(heap_ref)) };
5055        match p.items.pop() {
5056            Some(e) => e.0.value,
5057            None => {
5058                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
5059                unsafe { unit_sentinel(ctx) }
5060            }
5061        }
5062    })
5063}
5064
5065/// The smallest element without removing it; faults `EmptyCollection` if empty.
5066///
5067/// # Safety
5068/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5069#[unsafe(no_mangle)]
5070pub unsafe extern "C" fn praxis_min_heap_peek(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
5071    abi_guard!("praxis_min_heap_peek", ctx, {
5072        let p = unsafe { min_heap_payload(heap_ref) };
5073        match p.items.peek() {
5074            Some(e) => e.0.value,
5075            None => {
5076                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
5077                unsafe { unit_sentinel(ctx) }
5078            }
5079        }
5080    })
5081}
5082
5083/// The number of elements, as a boxed Int.
5084///
5085/// # Safety
5086/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5087#[unsafe(no_mangle)]
5088pub unsafe extern "C" fn praxis_min_heap_len(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
5089    abi_guard!("praxis_min_heap_len", ctx, {
5090        let p = unsafe { min_heap_payload(heap_ref) };
5091        unsafe { int_ref(ctx, p.items.len() as i64) }
5092    })
5093}
5094
5095/// Every element, as a `Vec[T]` in [`crate::heaps::in_pop_order`] — ascending,
5096/// because the stored entry is a `Reverse<HeapEntry>`. See
5097/// [`praxis_max_heap_items`].
5098///
5099/// # Safety
5100/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5101#[unsafe(no_mangle)]
5102pub unsafe extern "C" fn praxis_min_heap_items(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
5103    abi_guard!("praxis_min_heap_items", ctx, {
5104        let p = unsafe { min_heap_payload(heap_ref) };
5105        let items = crate::heaps::in_pop_order(&p.items, |e| e.0.value);
5106        unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
5107    })
5108}
5109
5110/// True iff the heap is empty, as a boxed Bool.
5111///
5112/// # Safety
5113/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5114#[unsafe(no_mangle)]
5115pub unsafe extern "C" fn praxis_min_heap_is_empty(
5116    ctx: *mut RuntimeContext,
5117    heap_ref: GcRef,
5118) -> GcRef {
5119    abi_guard!("praxis_min_heap_is_empty", ctx, {
5120        let p = unsafe { min_heap_payload(heap_ref) };
5121        unsafe { bool_ref(ctx, p.items.is_empty()) }
5122    })
5123}
5124
5125// ---------------------------------------------------------------------------
5126// BitSet (§6.1). A compact set of non-negative integers.
5127// ---------------------------------------------------------------------------
5128
5129use crate::bitset::{BitIndex, BitSetPayload};
5130
5131unsafe fn bitset_payload(r: GcRef) -> &'static BitSetPayload {
5132    unsafe { payload_ref::<BitSetPayload>(r) }
5133}
5134
5135unsafe fn bitset_payload_mut<'s>(r: Rooted<'s>) -> &'s mut BitSetPayload {
5136    unsafe { payload_mut::<BitSetPayload>(r) }
5137}
5138
5139/// Allocate an empty `BitSet` (§6.1). Nullary — no element descriptor.
5140///
5141/// # Safety
5142/// `ctx` must be live and wired.
5143#[unsafe(no_mangle)]
5144pub unsafe extern "C" fn praxis_bitset_new(ctx: *mut RuntimeContext) -> GcRef {
5145    abi_guard!("praxis_bitset_new", ctx, {
5146        // SAFETY: BitSetPayload is BITSET's payload type.
5147        unsafe {
5148            gc_alloc_owned(ctx, &crate::bitset::BITSET, || BitSetPayload {
5149                words: ReprCVec::new(),
5150            })
5151        }
5152    })
5153}
5154
5155/// Set bit `value`; returns Unit. Faults `InvalidSize` if `value` is negative
5156/// or above [`BitIndex::MAX`] — a member this set cannot hold.
5157///
5158/// # Safety
5159/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`; `value`
5160/// must be a valid `Int` `GcRef`.
5161#[unsafe(no_mangle)]
5162pub unsafe extern "C" fn praxis_bitset_insert(
5163    ctx: *mut RuntimeContext,
5164    bs: GcRef,
5165    value: GcRef,
5166) -> GcRef {
5167    abi_guard!("praxis_bitset_insert", ctx, {
5168        unsafe { maybe_collect(ctx) };
5169        let scope = unsafe { NativeScope::new(ctx) };
5170        let p = unsafe { bitset_payload_mut(scope.root(bs)) };
5171        let i = unsafe { int_payload(value) };
5172        // An insert that cannot be honoured is a fault, not a silent no-op: the
5173        // caller asked the set to contain something, and it will not.
5174        let Some(index) = BitIndex::new(i) else {
5175            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
5176            return unsafe { unit_sentinel(ctx) };
5177        };
5178        // A `BitSet` grows its word vector to reach the index, so an insert far
5179        // past the current high-water is a large uncharged allocation — the
5180        // shape `bfs` has, one visited-set per search.
5181        let before = p.owned_bytes();
5182        p.insert(index);
5183        charge_growth(ctx, before, p.owned_bytes());
5184        unsafe { unit_sentinel(ctx) }
5185    })
5186}
5187
5188/// Clear bit `value`; returns Unit. A value the set cannot hold is a value it
5189/// does not contain, so removing one is a no-op rather than a fault.
5190///
5191/// # Safety
5192/// `ctx` must be live and wired; `bs` and `value` must be valid `GcRef`s.
5193#[unsafe(no_mangle)]
5194pub unsafe extern "C" fn praxis_bitset_remove(
5195    ctx: *mut RuntimeContext,
5196    bs: GcRef,
5197    value: GcRef,
5198) -> GcRef {
5199    abi_guard!("praxis_bitset_remove", ctx, {
5200        let scope = unsafe { NativeScope::new(ctx) };
5201        let p = unsafe { bitset_payload_mut(scope.root(bs)) };
5202        let i = unsafe { int_payload(value) };
5203        if let Some(index) = BitIndex::new(i) {
5204            p.remove(index);
5205        }
5206        unsafe { unit_sentinel(ctx) }
5207    })
5208}
5209
5210/// True iff bit `value` is set, as a raw `0`/`1` in the scalar channel. A value
5211/// the set cannot hold is simply absent — the query is total.
5212///
5213/// **It answers an `i64` and not a boxed `Bool` (ADR-118 decision 6.)** A boxed
5214/// answer would be unboxed again on the next instruction — `if bs.contains(x)`
5215/// as a `Materialize{Bool}`, an `ExtractScalar{Bool}` and then the branch that
5216/// wanted the predicate. `praxis_struct_eq` and `praxis_value_cmp` answer the
5217/// scalar channel for the same reason, and MIR carries this one as
5218/// [`Inst::BitsetContains`](praxis_mir::Inst::BitsetContains) — a
5219/// `Scalar(Bool)` result, and, because it neither allocates nor faults, not a
5220/// GC safepoint.
5221///
5222/// `0` and `1` and nothing else, which is what the `Bool` payload byte holds
5223/// and what `emit_inline_bool` re-boxes with a `!= 0` test.
5224///
5225/// # Safety
5226/// `ctx` must be live and wired; `bs` and `value` must be valid `GcRef`s.
5227#[unsafe(no_mangle)]
5228pub unsafe extern "C" fn praxis_bitset_contains(
5229    ctx: *mut RuntimeContext,
5230    bs: GcRef,
5231    value: GcRef,
5232) -> i64 {
5233    abi_guard!("praxis_bitset_contains", ctx, {
5234        let p = unsafe { bitset_payload(bs) };
5235        let i = unsafe { int_payload(value) };
5236        let present = BitIndex::new(i).is_some_and(|index| p.contains(index));
5237        i64::from(present)
5238    })
5239}
5240
5241/// The number of set bits, as a boxed Int.
5242///
5243/// # Safety
5244/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`.
5245#[unsafe(no_mangle)]
5246pub unsafe extern "C" fn praxis_bitset_len(ctx: *mut RuntimeContext, bs: GcRef) -> GcRef {
5247    abi_guard!("praxis_bitset_len", ctx, {
5248        let p = unsafe { bitset_payload(bs) };
5249        unsafe { int_ref(ctx, p.count() as i64) }
5250    })
5251}
5252
5253/// Every member, as a `Vec[Int]` **ascending** — the snapshot `for i in b`
5254/// iterates (ADR-066).
5255///
5256/// This is the one iterable whose members are not objects: they are bit
5257/// positions, so each one is boxed here rather than copied from the payload.
5258///
5259/// # Safety
5260/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`.
5261#[unsafe(no_mangle)]
5262pub unsafe extern "C" fn praxis_bitset_items(ctx: *mut RuntimeContext, bs: GcRef) -> GcRef {
5263    abi_guard!("praxis_bitset_items", ctx, {
5264        // The members are read out before the first allocation: `vec_of` allocates
5265        // per element, and a collection during the walk would move nothing here
5266        // (the bits are not objects) but would leave the borrow of the payload
5267        // spanning a safepoint, which is not allowed.
5268        let members: Vec<i64> = unsafe { bitset_payload(bs) }.members().collect();
5269        let result = unsafe { praxis_vec_new(ctx, &scalars::INT as *const _) };
5270        let scope = unsafe { NativeScope::new(ctx) };
5271        let rooted = scope.root(result);
5272        for value in members {
5273            let boxed = unsafe { int_ref(ctx, value) };
5274            unsafe { vec_payload_mut(rooted) }.items.push(boxed);
5275        }
5276        result
5277    })
5278}
5279
5280/// True iff the bitset is empty, as a boxed Bool.
5281///
5282/// # Safety
5283/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`.
5284#[unsafe(no_mangle)]
5285pub unsafe extern "C" fn praxis_bitset_is_empty(ctx: *mut RuntimeContext, bs: GcRef) -> GcRef {
5286    abi_guard!("praxis_bitset_is_empty", ctx, {
5287        let p = unsafe { bitset_payload(bs) };
5288        unsafe { bool_ref(ctx, p.count() == 0) }
5289    })
5290}
5291
5292// ---------------------------------------------------------------------------
5293// Grid[T] methods (§6.4). `GridPayload` is a row-major `Vec<GcRef>` plus a
5294// width. Coordinates are (x, y) with x rightward, y downward (§6.4). Indexing
5295// stays behind runtime wrappers (§11.5 realloc safety).
5296// ---------------------------------------------------------------------------
5297
5298use crate::collections::{GridExtent, GridPayload};
5299
5300unsafe fn grid_payload(r: GcRef) -> &'static GridPayload {
5301    unsafe { payload_ref::<GridPayload>(r) }
5302}
5303
5304unsafe fn grid_payload_mut<'s>(r: Rooted<'s>) -> &'s mut GridPayload {
5305    unsafe { payload_mut::<GridPayload>(r) }
5306}
5307
5308/// Allocate a `(x, y)` point tuple from two `i64` coordinates. The schema is
5309/// the cached `(Int, Int)` point schema; elements are filled via
5310/// `praxis_tuple_set`. Returns the point `GcRef`.
5311///
5312/// Three allocations, and each one may collect: the tuple must survive the two
5313/// coordinate allocations, and the x coordinate must survive the y's. Nothing
5314/// generated is on the stack here — the caller is a runtime helper — so the
5315/// only thing that can root them is a native scope.
5316unsafe fn alloc_point(ctx: *mut RuntimeContext, x: i64, y: i64) -> GcRef {
5317    let scope = unsafe { NativeScope::new(ctx) };
5318    let schema = crate::tuples::point_schema();
5319    let schema_ptr = schema as *const crate::tuples::TupleSchema;
5320    let tup = scope.root(unsafe { praxis_alloc_tuple(ctx, schema_ptr) });
5321    let x_ref = scope.root(unsafe { int_ref(ctx, x) });
5322    unsafe { praxis_tuple_set(ctx, tup.get(), 0, x_ref.get()) };
5323    let y_ref = unsafe { int_ref(ctx, y) };
5324    unsafe { praxis_tuple_set(ctx, tup.get(), 1, y_ref) };
5325    tup.get()
5326}
5327
5328/// The (x, y) coordinates of a flat `idx` in a grid of `width`.
5329fn grid_xy(idx: usize, width: usize) -> (i64, i64) {
5330    ((idx % width) as i64, (idx / width) as i64)
5331}
5332
5333/// Read the two coordinates out of a `(Int, Int)` point tuple.
5334///
5335/// The inverse of [`alloc_point`], and the one place the grid wrappers unpack a
5336/// point: every `Grid` method taking a position takes it as this tuple, so
5337/// there is one shape to read and no reason for two readings of it.
5338///
5339/// # Safety
5340/// `point` must be a valid `(Int, Int)` tuple `GcRef` — which the type checker
5341/// guarantees for every catalog row whose parameter is `Tuple[Int, Int]`.
5342unsafe fn point_xy(point: GcRef) -> (i64, i64) {
5343    let tp = point.payload::<crate::tuples::TuplePayload>() as *const crate::tuples::TuplePayload;
5344    // SAFETY: caller guarantees `point` is a tuple, so its payload is a
5345    // `TuplePayload`, and a `(Int, Int)` shape has both slots filled with `Int`s.
5346    let pt = unsafe { &*tp };
5347    unsafe { (int_payload(pt.items[0]), int_payload(pt.items[1])) }
5348}
5349
5350/// The height (row count) of a grid: `items.len() / width`, or 0 if width is 0
5351/// (avoids division by zero on a degenerate empty grid).
5352fn grid_height(items_len: usize, width: usize) -> usize {
5353    items_len.checked_div(width).unwrap_or(0)
5354}
5355
5356/// The in-bounds neighbour at `(px + dx, py + dy)`, or `None` if it falls
5357/// outside a `width × height` grid.
5358///
5359/// The offsets are `checked_add` because `px`/`py` come out of a user-supplied
5360/// point tuple, so `(i64::MAX, 0).neighbors4()` would otherwise overflow the
5361/// addition and panic *inside* `extern "C"`. A coordinate that overflows is
5362/// outside every grid — `GridExtent` bounds the extents far below `i64::MAX` —
5363/// so "outside" is the whole answer, not a special case.
5364fn grid_neighbor(
5365    px: i64,
5366    py: i64,
5367    dx: i64,
5368    dy: i64,
5369    width: usize,
5370    height: usize,
5371) -> Option<(i64, i64)> {
5372    let nx = px.checked_add(dx)?;
5373    let ny = py.checked_add(dy)?;
5374    // Both non-negative below, so the casts are exact.
5375    (nx >= 0 && ny >= 0 && (nx as usize) < width && (ny as usize) < height).then_some((nx, ny))
5376}
5377
5378/// The zero value of the type `descriptor` names, or `None` if that type has no
5379/// natural default.
5380///
5381/// Only the scalars and `Unit` have one. A `Grid[Vec[Int]](3, 3)` would need
5382/// nine distinct empty vectors and, worse, no way to know their element type —
5383/// so it is refused rather than filled with something of the wrong type. A null
5384/// descriptor means the caller never said what the cells are, which is likewise
5385/// nothing this can invent.
5386///
5387/// # Safety
5388/// `ctx` must be live and wired.
5389unsafe fn default_cell(
5390    ctx: *mut RuntimeContext,
5391    descriptor: *const TypeDescriptor,
5392) -> Option<GcRef> {
5393    use crate::descriptor::BuiltinTypeId as B;
5394    // SAFETY: a non-null descriptor is a valid `&'static`.
5395    let builtin = unsafe { descriptor.as_ref() }?.as_builtin()?;
5396    unsafe {
5397        match builtin {
5398            B::Unit => Some(unit_sentinel(ctx)),
5399            B::Bool => Some(bool_ref(ctx, false)),
5400            B::Int => Some(int_ref(ctx, 0_i64)),
5401            B::Byte => Some(gc_alloc(ctx, scalars::BYTE_PAYLOAD, 0_u8)),
5402            // `0_u32`, not `'\0'`: a `Char`'s payload is the scalar *value*,
5403            // and a Rust `char` only fits because it shares `u32`'s layout. NUL
5404            // is inside the interned range, so this is the immortal, like the
5405            // `Int` arm above.
5406            B::Char => Some(char_ref(ctx, 0_u32)),
5407            B::Float => Some(gc_alloc(ctx, scalars::FLOAT_PAYLOAD, 0.0_f64)),
5408            // `(null, 0)` meets `praxis_alloc_text`'s UTF-8 precondition
5409            // trivially: the wrapper's own `bytes.is_null() || len == 0` branch
5410            // turns it into the empty slice, and the empty slice is UTF-8.
5411            // The precondition is load-bearing (ADR-111): a violation here
5412            // would abort, not fault. `alloc_text_empty_string_round_trips`
5413            // pins the branch this depends on.
5414            B::Text => Some(praxis_alloc_text(ctx, std::ptr::null(), 0)),
5415            // A composite has no zero value the runtime can invent: a
5416            // `Grid[Vec[Int]]` must be filled by the program that knows what its
5417            // cells are.
5418            B::Vec
5419            | B::Deque
5420            | B::Grid
5421            | B::Map
5422            | B::Set
5423            | B::Counter
5424            | B::MinHeap
5425            | B::MaxHeap
5426            | B::BitSet
5427            // A `Range`'s zero value would be a pair of bounds nobody chose;
5428            // `0..0` is *a* range but it is not "the empty one" in any sense a
5429            // `Grid[Range]` cell wants.
5430            | B::Range
5431            | B::Tuple
5432            | B::Record
5433            | B::Enum
5434            | B::Closure
5435            | B::VarCell => None,
5436        }
5437    }
5438}
5439
5440/// Allocate an empty `Grid[T]` with the given element descriptor, width, and
5441/// height, all cells initialized to the cell type's zero value. (The input parser also constructs
5442/// grids directly; this wrapper is for source `Grid[T]()` + a follow-up fill.)
5443///
5444/// Faults `InvalidSize` if either extent is negative or the grid would exceed
5445/// [`GridExtent::MAX_CELLS`] — the sizes arrive from source, where a negative
5446/// value would otherwise land near `usize::MAX` on the cast.
5447///
5448/// # Safety
5449/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
5450/// a `'static TypeDescriptor` (or null).
5451#[unsafe(no_mangle)]
5452pub unsafe extern "C" fn praxis_grid_new(
5453    ctx: *mut RuntimeContext,
5454    element_descriptor: *const TypeDescriptor,
5455    width: i64,
5456    height: i64,
5457) -> GcRef {
5458    abi_guard!("praxis_grid_new", ctx, {
5459        let Some(extent) = GridExtent::new(width, height) else {
5460            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
5461            return unsafe { unit_sentinel(ctx) };
5462        };
5463        // Every cell of a `Grid[T]` must *be* a `T`. Filling with the Unit sentinel
5464        // under a `T` element descriptor is the same lie as a mislabelled element
5465        // descriptor, one level down: `get`, `format`, `equals` and `hash` all
5466        // dispatch `T`'s callbacks against a zero-sized Unit payload.
5467        let cells = if extent.cells() == 0 {
5468            Vec::new()
5469        } else {
5470            let Some(fill) = (unsafe { default_cell(ctx, element_descriptor) }) else {
5471                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
5472                return unsafe { unit_sentinel(ctx) };
5473            };
5474            vec![fill; extent.cells()]
5475        };
5476        // SAFETY: GridPayload is GRID's payload type.
5477        unsafe {
5478            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
5479                element_descriptor,
5480                items: cells,
5481                width: extent.width(),
5482            })
5483        }
5484    })
5485}
5486
5487/// Allocate a `Grid[T]` of `width` × `height` cells, every one holding `fill`
5488/// (ADR-146's `Grid(w, h, fill)`) — the working grid an algorithm allocates for
5489/// itself: an occupancy board, a visited mask, a distance table.
5490///
5491/// Faults `InvalidSize` on the extents [`praxis_grid_new`] refuses, through the
5492/// same [`GridExtent::new`], since this is the very allocation ADR-041 was
5493/// written about and a fill changes nothing about the arithmetic.
5494///
5495/// **It does not call [`default_cell`], and that is the whole difference.**
5496/// `praxis_grid_new` has to invent a zero value for the cell type and has none
5497/// for a composite, so it raises `TypeMismatch` for a `Grid[Vec[Int]]` rather
5498/// than filling it with Unit sentinels under a `Vec` descriptor. An explicit
5499/// fill removes the question — the caller supplied a value of the cell type —
5500/// so a grid of collections is constructible here and not there. The descriptor
5501/// is still reconciled through [`adopt_or_reject`], so a *declared* cell type
5502/// the fill does not match is `TypeMismatch` rather than a silent retag.
5503///
5504/// Every cell is the same `GcRef`, exactly as [`praxis_vec_filled`]'s are; see
5505/// its comment for why that is the language's existing rule rather than a new
5506/// one. The extents arrive boxed for the reason stated there too.
5507///
5508/// # Safety
5509/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
5510/// a `'static TypeDescriptor` (or null); `width` and `height` must be valid
5511/// `Int` `GcRef`s; `fill` must be a valid `GcRef`.
5512#[unsafe(no_mangle)]
5513pub unsafe extern "C" fn praxis_grid_filled(
5514    ctx: *mut RuntimeContext,
5515    element_descriptor: *const TypeDescriptor,
5516    width: GcRef,
5517    height: GcRef,
5518    fill: GcRef,
5519) -> GcRef {
5520    abi_guard!("praxis_grid_filled", ctx, {
5521        // SAFETY: caller guarantees `width` and `height` are valid Ints.
5522        let (w, h) = unsafe { (int_payload(width), int_payload(height)) };
5523        let Some(extent) = GridExtent::new(w, h) else {
5524            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
5525            return unsafe { unit_sentinel(ctx) };
5526        };
5527        let mut descriptor = element_descriptor;
5528        if !unsafe { adopt_or_reject(ctx, &mut descriptor, fill) } {
5529            return unsafe { unit_sentinel(ctx) };
5530        }
5531        let scope = unsafe { NativeScope::new(ctx) };
5532        let fill = scope.root(fill).get();
5533        // The cells are built inside the initializer, which `gc_alloc_owned` runs
5534        // *after* the safepoint — `praxis_vec_filled`'s rule, for the same
5535        // untraced `Vec<GcRef>`.
5536        // SAFETY: GridPayload is GRID's payload type.
5537        unsafe {
5538            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
5539                element_descriptor: descriptor,
5540                items: vec![fill; extent.cells()],
5541                width: extent.width(),
5542            })
5543        }
5544    })
5545}
5546
5547/// The grid width (number of columns), as a boxed Int.
5548///
5549/// # Safety
5550/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5551#[unsafe(no_mangle)]
5552pub unsafe extern "C" fn praxis_grid_width(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5553    abi_guard!("praxis_grid_width", ctx, {
5554        let p = unsafe { grid_payload(grid) };
5555        unsafe { int_ref(ctx, p.width as i64) }
5556    })
5557}
5558
5559/// The grid height (number of rows), as a boxed Int.
5560///
5561/// # Safety
5562/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5563#[unsafe(no_mangle)]
5564pub unsafe extern "C" fn praxis_grid_height(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5565    abi_guard!("praxis_grid_height", ctx, {
5566        let p = unsafe { grid_payload(grid) };
5567        // height = items.len() / width.
5568        let height = grid_height(p.items.len(), p.width);
5569        unsafe { int_ref(ctx, height as i64) }
5570    })
5571}
5572
5573/// The cell at `(x, y)`; faults `IndexOutOfBounds` if out of range.
5574///
5575/// # Safety
5576/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`/`y`
5577/// must be valid `Int` `GcRef`s.
5578#[unsafe(no_mangle)]
5579pub unsafe extern "C" fn praxis_grid_get(
5580    ctx: *mut RuntimeContext,
5581    grid: GcRef,
5582    x: GcRef,
5583    y: GcRef,
5584) -> GcRef {
5585    abi_guard!("praxis_grid_get", ctx, {
5586        let p = unsafe { grid_payload(grid) };
5587        let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5588        let height = grid_height(p.items.len(), p.width);
5589        let Some(idx) = (unsafe { checked_cell(ctx, xi, yi, p.width, height) }) else {
5590            return unsafe { unit_sentinel(ctx) };
5591        };
5592        p.items[idx]
5593    })
5594}
5595
5596/// Set the cell at `(x, y)`; faults `IndexOutOfBounds` if out of range.
5597///
5598/// # Safety
5599/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`/`y`
5600/// must be valid `Int` `GcRef`s; `value` must be a valid `GcRef`.
5601#[unsafe(no_mangle)]
5602pub unsafe extern "C" fn praxis_grid_set(
5603    ctx: *mut RuntimeContext,
5604    grid: GcRef,
5605    x: GcRef,
5606    y: GcRef,
5607    value: GcRef,
5608) -> GcRef {
5609    abi_guard!("praxis_grid_set", ctx, {
5610        let scope = unsafe { NativeScope::new(ctx) };
5611        let p = unsafe { grid_payload_mut(scope.root(grid)) };
5612        let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5613        let height = grid_height(p.items.len(), p.width);
5614        let Some(idx) = (unsafe { checked_cell(ctx, xi, yi, p.width, height) }) else {
5615            return unsafe { unit_sentinel(ctx) };
5616        };
5617        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
5618            return unsafe { unit_sentinel(ctx) };
5619        }
5620        p.items[idx] = value;
5621        unsafe { unit_sentinel(ctx) }
5622    })
5623}
5624
5625/// `g[x, y] min= candidate` and `g[x, y] max= candidate` — [`vec_update`] over a
5626/// `Grid`, bounds-checked at the cell as [`praxis_grid_set`] is.
5627///
5628/// This is the shape the operator was wanted for most: a `Grid[Int]` of best
5629/// costs relaxed cell by cell, where the grid is already sized and every cell
5630/// already holds its `fill`.
5631///
5632/// # Safety
5633/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`, `x` and
5634/// `y` valid `Int`s, and `value` of the grid's cell type.
5635unsafe fn grid_update(
5636    ctx: *mut RuntimeContext,
5637    grid: GcRef,
5638    x: GcRef,
5639    y: GcRef,
5640    value: GcRef,
5641    keep: std::cmp::Ordering,
5642) -> GcRef {
5643    // SAFETY: as `praxis_grid_set`, whose body this is with a comparison in
5644    // front of the store.
5645    let scope = unsafe { NativeScope::new(ctx) };
5646    let p = unsafe { grid_payload_mut(scope.root(grid)) };
5647    let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5648    let height = grid_height(p.items.len(), p.width);
5649    let Some(idx) = (unsafe { checked_cell(ctx, xi, yi, p.width, height) }) else {
5650        return unsafe { unit_sentinel(ctx) };
5651    };
5652    if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
5653        return unsafe { unit_sentinel(ctx) };
5654    }
5655    if unsafe { update_cmp(value, p.items[idx]) } == keep {
5656        p.items[idx] = value;
5657    }
5658    unsafe { unit_sentinel(ctx) }
5659}
5660
5661/// `g[x, y] min= candidate` — keep the smaller of the two. See [`grid_update`].
5662///
5663/// # Safety
5664/// As [`grid_update`].
5665#[unsafe(no_mangle)]
5666pub unsafe extern "C" fn praxis_grid_update_min(
5667    ctx: *mut RuntimeContext,
5668    grid: GcRef,
5669    x: GcRef,
5670    y: GcRef,
5671    value: GcRef,
5672) -> GcRef {
5673    abi_guard!("praxis_grid_update_min", ctx, {
5674        unsafe { grid_update(ctx, grid, x, y, value, std::cmp::Ordering::Less) }
5675    })
5676}
5677
5678/// `g[x, y] max= candidate` — keep the larger of the two. See [`grid_update`].
5679///
5680/// # Safety
5681/// As [`grid_update`].
5682#[unsafe(no_mangle)]
5683pub unsafe extern "C" fn praxis_grid_update_max(
5684    ctx: *mut RuntimeContext,
5685    grid: GcRef,
5686    x: GcRef,
5687    y: GcRef,
5688    value: GcRef,
5689) -> GcRef {
5690    abi_guard!("praxis_grid_update_max", ctx, {
5691        unsafe { grid_update(ctx, grid, x, y, value, std::cmp::Ordering::Greater) }
5692    })
5693}
5694
5695/// True iff `(x, y)` is within the grid, as a boxed Bool.
5696///
5697/// # Safety
5698/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`/`y`
5699/// must be valid `Int` `GcRef`s.
5700#[unsafe(no_mangle)]
5701pub unsafe extern "C" fn praxis_grid_contains(
5702    ctx: *mut RuntimeContext,
5703    grid: GcRef,
5704    x: GcRef,
5705    y: GcRef,
5706) -> GcRef {
5707    abi_guard!("praxis_grid_contains", ctx, {
5708        let p = unsafe { grid_payload(grid) };
5709        let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5710        let height = grid_height(p.items.len(), p.width);
5711        // The **pure** [`cell_index`], never `checked_cell`: this wrapper's
5712        // manifest row is `Pure`, so generated code emits no `CheckFault` after
5713        // it and a fault raised on every legitimate `false` would sit pending
5714        // until an unrelated check picked it up.
5715        let inside = cell_index(xi, yi, p.width, height).is_some();
5716        unsafe { bool_ref(ctx, inside) }
5717    })
5718}
5719
5720/// The 4 orthogonal neighbors of `point` that lie inside the grid, as a `Vec`.
5721///
5722/// # Safety
5723/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5724#[unsafe(no_mangle)]
5725pub unsafe extern "C" fn praxis_grid_neighbors4(
5726    ctx: *mut RuntimeContext,
5727    grid: GcRef,
5728    point: GcRef,
5729) -> GcRef {
5730    abi_guard!("praxis_grid_neighbors4", ctx, {
5731        let p = unsafe { grid_payload(grid) };
5732        let (px, py) = unsafe { point_xy(point) };
5733        let height = grid_height(p.items.len(), p.width);
5734        let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
5735        let scope = unsafe { NativeScope::new(ctx) };
5736        let rp = unsafe { vec_payload_mut(scope.root(result)) };
5737        for (dx, dy) in [(0i64, -1), (0, 1), (-1, 0), (1, 0)] {
5738            if let Some((nx, ny)) = grid_neighbor(px, py, dx, dy, p.width, height) {
5739                let pt_ref = unsafe { alloc_point(ctx, nx, ny) };
5740                rp.items.push(pt_ref);
5741            }
5742        }
5743        result
5744    })
5745}
5746
5747/// The 8 neighbors of `point` that lie inside the grid, as a `Vec`.
5748///
5749/// # Safety
5750/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5751#[unsafe(no_mangle)]
5752pub unsafe extern "C" fn praxis_grid_neighbors8(
5753    ctx: *mut RuntimeContext,
5754    grid: GcRef,
5755    point: GcRef,
5756) -> GcRef {
5757    abi_guard!("praxis_grid_neighbors8", ctx, {
5758        let p = unsafe { grid_payload(grid) };
5759        let (px, py) = unsafe { point_xy(point) };
5760        let height = grid_height(p.items.len(), p.width);
5761        let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
5762        let scope = unsafe { NativeScope::new(ctx) };
5763        let rp = unsafe { vec_payload_mut(scope.root(result)) };
5764        for dy in -1i64..=1 {
5765            for dx in -1i64..=1 {
5766                if dx == 0 && dy == 0 {
5767                    continue;
5768                }
5769                if let Some((nx, ny)) = grid_neighbor(px, py, dx, dy, p.width, height) {
5770                    let pt_ref = unsafe { alloc_point(ctx, nx, ny) };
5771                    rp.items.push(pt_ref);
5772                }
5773            }
5774        }
5775        result
5776    })
5777}
5778
5779/// Build one neighbourhood record: a field per direction, in `directions`
5780/// order, each `Some((x, y))` or `None`.
5781///
5782/// # What the field order means, and what it costs to get wrong
5783///
5784/// Slot *i* of the record is `directions[i]`, and the reader's slot index comes
5785/// from the *static* type — the catalog's `Around4`/`Around8` row. The two
5786/// orders are required to agree and nothing derives one from the other, so
5787/// `around_schemas_match_the_catalog` asserts it: a mismatch is a field read
5788/// that quietly answers the wrong direction.
5789///
5790/// # Rooting
5791///
5792/// Every field costs up to four allocations (two `Int`s, a point tuple, a
5793/// `Some`), each a safepoint, so the record is rooted for the whole loop. Each
5794/// field's own value is stored the moment it exists — `praxis_record_set_field`
5795/// allocates nothing — so no second reference is ever live across a safepoint.
5796///
5797/// # Safety
5798/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef` and
5799/// `point` a valid `(Int, Int)` tuple.
5800unsafe fn grid_around(
5801    ctx: *mut RuntimeContext,
5802    grid: GcRef,
5803    point: GcRef,
5804    schema: &'static crate::records::RecordSchema,
5805    directions: &'static [crate::records::Direction],
5806) -> GcRef {
5807    // SAFETY: the caller upholds every argument's validity.
5808    let (width, height) = unsafe {
5809        let p = grid_payload(grid);
5810        (p.width, grid_height(p.items.len(), p.width))
5811    };
5812    let (px, py) = unsafe { point_xy(point) };
5813    let scope = unsafe { NativeScope::new(ctx) };
5814    let record = scope.root(unsafe { praxis_alloc_record(ctx, schema) });
5815    for (i, d) in directions.iter().enumerate() {
5816        let field = match grid_neighbor(px, py, d.dx, d.dy, width, height) {
5817            // `option_some` roots the point across the enum allocation.
5818            Some((nx, ny)) => unsafe { option_some(ctx, alloc_point(ctx, nx, ny)) },
5819            None => unsafe { option_none(ctx) },
5820        };
5821        unsafe { praxis_record_set_field(ctx, record.get(), i as u32, field) };
5822    }
5823    record.get()
5824}
5825
5826/// The four orthogonal neighbours of `point` as an `Around4` record — `up`,
5827/// `left`, `right`, `down`, each `Some((x, y))` or `None` (§6.4).
5828///
5829/// **Not a shorter `neighbors4`.** That wrapper answers a `Vec` clipped to what
5830/// is in bounds, which is what a graph walk wants and what
5831/// `bfs(start, |p| g.neighbors4(p))` passes; it throws away *which* direction
5832/// each neighbour was, and off the edge of the grid it throws away that there
5833/// was a direction at all. This answers exactly that, and the `None` is the
5834/// half a `Vec` cannot carry.
5835///
5836/// # Safety
5837/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5838#[unsafe(no_mangle)]
5839pub unsafe extern "C" fn praxis_grid_around4(
5840    ctx: *mut RuntimeContext,
5841    grid: GcRef,
5842    point: GcRef,
5843) -> GcRef {
5844    abi_guard!("praxis_grid_around4", ctx, {
5845        unsafe {
5846            grid_around(
5847                ctx,
5848                grid,
5849                point,
5850                crate::records::around4_schema(),
5851                crate::records::AROUND4_DIRECTIONS,
5852            )
5853        }
5854    })
5855}
5856
5857/// All eight neighbours of `point` as an `Around8` record, in reading order —
5858/// `up_left`, `up`, `up_right`, `left`, `right`, `down_left`, `down`,
5859/// `down_right` (§6.4). See [`praxis_grid_around4`].
5860///
5861/// # Safety
5862/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5863#[unsafe(no_mangle)]
5864pub unsafe extern "C" fn praxis_grid_around8(
5865    ctx: *mut RuntimeContext,
5866    grid: GcRef,
5867    point: GcRef,
5868) -> GcRef {
5869    abi_guard!("praxis_grid_around8", ctx, {
5870        unsafe {
5871            grid_around(
5872                ctx,
5873                grid,
5874                point,
5875                crate::records::around8_schema(),
5876                crate::records::AROUND8_DIRECTIONS,
5877            )
5878        }
5879    })
5880}
5881
5882/// How many of `point`'s in-bounds neighbours in `directions` hold a cell equal
5883/// to `value`.
5884///
5885/// Equality is the value's own descriptor callback — the same path
5886/// `praxis_grid_find` and `praxis_grid_find_all` take, so "equals" means one
5887/// thing across every `Grid` row (§5.5).
5888///
5889/// A direction that leaves the grid has no cell, so it is not counted. Nothing
5890/// allocates until the answer is boxed, so `grid` needs no root.
5891///
5892/// # Safety
5893/// `ctx` must be live and wired; `grid`, `point` and `value` must be valid
5894/// `GcRef`s.
5895unsafe fn grid_count_equal(
5896    ctx: *mut RuntimeContext,
5897    grid: GcRef,
5898    point: GcRef,
5899    value: GcRef,
5900    directions: &'static [crate::records::Direction],
5901) -> GcRef {
5902    // SAFETY: the caller upholds every argument's validity.
5903    let p = unsafe { grid_payload(grid) };
5904    let height = grid_height(p.items.len(), p.width);
5905    let (px, py) = unsafe { point_xy(point) };
5906    let eq = value.descriptor().equals;
5907    let mut n = 0_i64;
5908    for d in directions {
5909        let Some((nx, ny)) = grid_neighbor(px, py, d.dx, d.dy, p.width, height) else {
5910            continue;
5911        };
5912        let cell = p.items[ny as usize * p.width + nx as usize];
5913        let matches = match eq {
5914            // SAFETY: `equals` came off `value`'s descriptor, and the grid's
5915            // cells are values of the element type the catalog row unified
5916            // `value` with.
5917            Some(equals) => unsafe {
5918                equals(
5919                    cell.payload::<u8>() as *const u8,
5920                    value.payload::<u8>() as *const u8,
5921                )
5922            },
5923            None => cell == value,
5924        };
5925        n += i64::from(matches);
5926    }
5927    unsafe { int_ref(ctx, n) }
5928}
5929
5930/// `g.count4(p, v)` — how many of the four orthogonal in-bounds neighbours hold
5931/// `v` (§6.4).
5932///
5933/// # Safety
5934/// `ctx` must be live and wired; `grid`, `point` and `value` must be valid
5935/// `GcRef`s.
5936#[unsafe(no_mangle)]
5937pub unsafe extern "C" fn praxis_grid_count4(
5938    ctx: *mut RuntimeContext,
5939    grid: GcRef,
5940    point: GcRef,
5941    value: GcRef,
5942) -> GcRef {
5943    abi_guard!("praxis_grid_count4", ctx, {
5944        unsafe { grid_count_equal(ctx, grid, point, value, crate::records::AROUND4_DIRECTIONS) }
5945    })
5946}
5947
5948/// `g.count8(p, v)` — how many of the eight in-bounds neighbours hold `v`
5949/// (§6.4).
5950///
5951/// # Safety
5952/// `ctx` must be live and wired; `grid`, `point` and `value` must be valid
5953/// `GcRef`s.
5954#[unsafe(no_mangle)]
5955pub unsafe extern "C" fn praxis_grid_count8(
5956    ctx: *mut RuntimeContext,
5957    grid: GcRef,
5958    point: GcRef,
5959    value: GcRef,
5960) -> GcRef {
5961    abi_guard!("praxis_grid_count8", ctx, {
5962        unsafe { grid_count_equal(ctx, grid, point, value, crate::records::AROUND8_DIRECTIONS) }
5963    })
5964}
5965
5966/// How many of `point`'s in-bounds neighbours in `directions` hold a cell the
5967/// closure accepts.
5968///
5969/// A direction that leaves the grid has no cell, so the closure is not called
5970/// for it — the predicate never sees a position that is not on the grid.
5971///
5972/// # Rooting, and what a faulting closure does
5973///
5974/// The closure runs arbitrary Praxis code between iterations, so it allocates
5975/// and it collects. Every cell the loop will hand it is read out of the grid
5976/// and rooted **before the first call**: `grid_payload` hands back a borrow of
5977/// a heap object, and a collection triggered by call *i* would otherwise be
5978/// free to reclaim the cell call *i + 1* is about to receive.
5979///
5980/// A fault stops the count and answers the Unit sentinel, exactly as
5981/// `praxis_vec_sorted_by_key` does: the call site's own fault check is what
5982/// reports, and a half-finished count is not an answer.
5983///
5984/// # Safety
5985/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s and
5986/// `pred` a valid closure `GcRef`.
5987unsafe fn grid_count_where(
5988    ctx: *mut RuntimeContext,
5989    grid: GcRef,
5990    point: GcRef,
5991    pred: GcRef,
5992    directions: &'static [crate::records::Direction],
5993) -> GcRef {
5994    let scope = unsafe { NativeScope::new(ctx) };
5995    // SAFETY: the caller upholds every argument's validity.
5996    let cells: Vec<GcRef> = unsafe {
5997        let p = grid_payload(grid);
5998        let height = grid_height(p.items.len(), p.width);
5999        let (px, py) = point_xy(point);
6000        directions
6001            .iter()
6002            .filter_map(|d| grid_neighbor(px, py, d.dx, d.dy, p.width, height))
6003            .map(|(nx, ny)| {
6004                scope
6005                    .root(p.items[ny as usize * p.width + nx as usize])
6006                    .get()
6007            })
6008            .collect()
6009    };
6010    let mut n = 0_i64;
6011    for cell in cells {
6012        let Some(answer) = (unsafe { call_unary_closure(ctx, pred, cell) }) else {
6013            // The closure faulted (or is not a closure, which the type checker
6014            // already refused). Leave the fault for the call site's check.
6015            return unsafe { unit_sentinel(ctx) };
6016        };
6017        // A `Bool`'s payload is **one byte**, and `read_scalar` takes the width
6018        // from `BOOL_PAYLOAD`'s own type after checking the descriptor — so a
6019        // closure that answered something else is a `TypeMismatch` rather than
6020        // seven bytes of uninitialized alignment padding.
6021        // SAFETY: `answer` is the `GcRef` the call just produced.
6022        let Some(byte) = (unsafe { read_scalar(answer, scalars::BOOL_PAYLOAD) }) else {
6023            unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
6024            return unsafe { unit_sentinel(ctx) };
6025        };
6026        n += i64::from(byte != 0);
6027    }
6028    unsafe { int_ref(ctx, n) }
6029}
6030
6031/// `g.count4_where(p, f)` — how many of the four orthogonal in-bounds
6032/// neighbours hold a cell `f` accepts (§6.4).
6033///
6034/// # Safety
6035/// `ctx` must be live and wired; `grid`, `point` and `pred` must be valid
6036/// `GcRef`s.
6037#[unsafe(no_mangle)]
6038pub unsafe extern "C" fn praxis_grid_count4_where(
6039    ctx: *mut RuntimeContext,
6040    grid: GcRef,
6041    point: GcRef,
6042    pred: GcRef,
6043) -> GcRef {
6044    abi_guard!("praxis_grid_count4_where", ctx, {
6045        unsafe { grid_count_where(ctx, grid, point, pred, crate::records::AROUND4_DIRECTIONS) }
6046    })
6047}
6048
6049/// `g.count8_where(p, f)` — how many of the eight in-bounds neighbours hold a
6050/// cell `f` accepts (§6.4).
6051///
6052/// # Safety
6053/// `ctx` must be live and wired; `grid`, `point` and `pred` must be valid
6054/// `GcRef`s.
6055#[unsafe(no_mangle)]
6056pub unsafe extern "C" fn praxis_grid_count8_where(
6057    ctx: *mut RuntimeContext,
6058    grid: GcRef,
6059    point: GcRef,
6060    pred: GcRef,
6061) -> GcRef {
6062    abi_guard!("praxis_grid_count8_where", ctx, {
6063        unsafe { grid_count_where(ctx, grid, point, pred, crate::records::AROUND8_DIRECTIONS) }
6064    })
6065}
6066
6067/// All `(x, y)` positions in row-major order, as a `Vec`.
6068///
6069/// # Safety
6070/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6071#[unsafe(no_mangle)]
6072pub unsafe extern "C" fn praxis_grid_positions(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6073    abi_guard!("praxis_grid_positions", ctx, {
6074        unsafe { maybe_collect(ctx) };
6075        let p = unsafe { grid_payload(grid) };
6076        let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
6077        let scope = unsafe { NativeScope::new(ctx) };
6078        let rp = unsafe { vec_payload_mut(scope.root(result)) };
6079        for i in 0..p.items.len() {
6080            let (x, y) = grid_xy(i, p.width);
6081            rp.items.push(unsafe { alloc_point(ctx, x, y) });
6082        }
6083        result
6084    })
6085}
6086
6087/// All cells in row-major order, as a `Vec`.
6088///
6089/// # Safety
6090/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6091#[unsafe(no_mangle)]
6092pub unsafe extern "C" fn praxis_grid_cells(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6093    abi_guard!("praxis_grid_cells", ctx, {
6094        let p = unsafe { grid_payload(grid) };
6095        let result = unsafe { praxis_vec_new(ctx, p.element_descriptor) };
6096        let scope = unsafe { NativeScope::new(ctx) };
6097        let rp = unsafe { vec_payload_mut(scope.root(result)) };
6098        for cell in p.items.iter() {
6099            rp.items.push(*cell);
6100        }
6101        result
6102    })
6103}
6104
6105/// Row `y` as a `Vec`; faults `IndexOutOfBounds` if out of range.
6106///
6107/// # Safety
6108/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `y`
6109/// must be a valid `Int` `GcRef`.
6110#[unsafe(no_mangle)]
6111pub unsafe extern "C" fn praxis_grid_row(ctx: *mut RuntimeContext, grid: GcRef, y: GcRef) -> GcRef {
6112    abi_guard!("praxis_grid_row", ctx, {
6113        let p = unsafe { grid_payload(grid) };
6114        let yi = unsafe { int_payload(y) };
6115        let height = grid_height(p.items.len(), p.width);
6116        // One axis of [`cell_index`]'s rule: a row is bounded by the height
6117        // alone, and every `x` in it is in range by construction.
6118        let Some(row) = (unsafe { checked_index(ctx, yi, height) }) else {
6119            return unsafe { unit_sentinel(ctx) };
6120        };
6121        let start = row * p.width;
6122        let result = unsafe { praxis_vec_new(ctx, p.element_descriptor) };
6123        let scope = unsafe { NativeScope::new(ctx) };
6124        let rp = unsafe { vec_payload_mut(scope.root(result)) };
6125        for x in 0..p.width {
6126            rp.items.push(p.items[start + x]);
6127        }
6128        result
6129    })
6130}
6131
6132/// Column `x` as a `Vec`; faults `IndexOutOfBounds` if out of range.
6133///
6134/// # Safety
6135/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`
6136/// must be a valid `Int` `GcRef`.
6137#[unsafe(no_mangle)]
6138pub unsafe extern "C" fn praxis_grid_column(
6139    ctx: *mut RuntimeContext,
6140    grid: GcRef,
6141    x: GcRef,
6142) -> GcRef {
6143    abi_guard!("praxis_grid_column", ctx, {
6144        let p = unsafe { grid_payload(grid) };
6145        let xi = unsafe { int_payload(x) };
6146        // The other axis: a column is bounded by the width alone, and the
6147        // stride below walks only the rows that exist.
6148        let Some(col) = (unsafe { checked_index(ctx, xi, p.width) }) else {
6149            return unsafe { unit_sentinel(ctx) };
6150        };
6151        let result = unsafe { praxis_vec_new(ctx, p.element_descriptor) };
6152        let scope = unsafe { NativeScope::new(ctx) };
6153        let rp = unsafe { vec_payload_mut(scope.root(result)) };
6154        let mut idx = col;
6155        while idx < p.items.len() {
6156            rp.items.push(p.items[idx]);
6157            idx += p.width;
6158        }
6159        result
6160    })
6161}
6162
6163/// `Some((x, y))` for the first position whose cell equals `value`, or `None`
6164/// (§4.7).
6165///
6166/// An `Option` rather than a sentinel: the Unit sentinel under a `(Int, Int)`
6167/// static type is indistinguishable from a real answer. `find_all` needs no
6168/// equivalent — a `Vec` already encodes "nothing matched" as emptiness.
6169///
6170/// # Safety
6171/// `ctx` must be live and wired; `grid` and `value` must be valid `GcRef`s.
6172#[unsafe(no_mangle)]
6173pub unsafe extern "C" fn praxis_grid_find(
6174    ctx: *mut RuntimeContext,
6175    grid: GcRef,
6176    value: GcRef,
6177) -> GcRef {
6178    abi_guard!("praxis_grid_find", ctx, {
6179        let p = unsafe { grid_payload(grid) };
6180        let val_desc = value.descriptor();
6181        let eq = val_desc.equals;
6182        for (i, cell) in p.items.iter().enumerate() {
6183            let matches = match eq {
6184                Some(equals) => {
6185                    let a = cell.payload::<u8>() as *const u8;
6186                    let b = value.payload::<u8>() as *const u8;
6187                    unsafe { equals(a, b) }
6188                }
6189                None => *cell == value,
6190            };
6191            if matches {
6192                let (x, y) = grid_xy(i, p.width);
6193                // `option_some` roots the point across the enum allocation.
6194                return unsafe { option_some(ctx, alloc_point(ctx, x, y)) };
6195            }
6196        }
6197        unsafe { option_none(ctx) }
6198    })
6199}
6200
6201/// All `(x, y)` positions whose cell equals `value`, as a `Vec`.
6202///
6203/// # Safety
6204/// `ctx` must be live and wired; `grid` and `value` must be valid `GcRef`s.
6205#[unsafe(no_mangle)]
6206pub unsafe extern "C" fn praxis_grid_find_all(
6207    ctx: *mut RuntimeContext,
6208    grid: GcRef,
6209    value: GcRef,
6210) -> GcRef {
6211    abi_guard!("praxis_grid_find_all", ctx, {
6212        unsafe { maybe_collect(ctx) };
6213        let p = unsafe { grid_payload(grid) };
6214        let val_desc = value.descriptor();
6215        let eq = val_desc.equals;
6216        let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
6217        let scope = unsafe { NativeScope::new(ctx) };
6218        let rp = unsafe { vec_payload_mut(scope.root(result)) };
6219        for (i, cell) in p.items.iter().enumerate() {
6220            let matches = match eq {
6221                Some(equals) => {
6222                    let a = cell.payload::<u8>() as *const u8;
6223                    let b = value.payload::<u8>() as *const u8;
6224                    unsafe { equals(a, b) }
6225                }
6226                None => *cell == value,
6227            };
6228            if matches {
6229                let (x, y) = grid_xy(i, p.width);
6230                rp.items.push(unsafe { alloc_point(ctx, x, y) });
6231            }
6232        }
6233        result
6234    })
6235}
6236
6237/// A transposed copy of the grid (rows ↔ columns), as a new `Grid`.
6238///
6239/// # Safety
6240/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6241#[unsafe(no_mangle)]
6242pub unsafe extern "C" fn praxis_grid_transpose(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6243    abi_guard!("praxis_grid_transpose", ctx, {
6244        let p = unsafe { grid_payload(grid) };
6245        let height = grid_height(p.items.len(), p.width);
6246        let new_width = height;
6247        let new_height = p.width;
6248        let mut cells = Vec::with_capacity(p.items.len());
6249        for y in 0..new_height {
6250            for x in 0..new_width {
6251                // new[x,y] = old[y,x]
6252                cells.push(p.items[x * p.width + y]);
6253            }
6254        }
6255        let _ = ctx;
6256        // SAFETY: GridPayload is GRID's payload type.
6257        unsafe {
6258            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
6259                element_descriptor: p.element_descriptor,
6260                items: cells,
6261                width: new_width,
6262            })
6263        }
6264    })
6265}
6266
6267/// A copy of the grid rotated 90° left (counter-clockwise), as a new `Grid`.
6268///
6269/// # Safety
6270/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6271#[unsafe(no_mangle)]
6272pub unsafe extern "C" fn praxis_grid_rotate_left(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6273    abi_guard!("praxis_grid_rotate_left", ctx, {
6274        let p = unsafe { grid_payload(grid) };
6275        let height = grid_height(p.items.len(), p.width);
6276        // Rotate left (90° CCW): result is H×W (width=height, height=width).
6277        // With x rightward and y downward, turning counter-clockwise carries the
6278        // *rightmost* column to the top row, top-to-bottom:
6279        // result[x, y] = original[width-1-y, x], for x in 0..height, y in 0..width.
6280        let new_width = height;
6281        let new_height = p.width;
6282        let mut cells = Vec::with_capacity(p.items.len());
6283        for y in 0..new_height {
6284            for x in 0..new_width {
6285                let ox = p.width - 1 - y;
6286                let oy = x;
6287                cells.push(p.items[oy * p.width + ox]);
6288            }
6289        }
6290        // SAFETY: GridPayload is GRID's payload type.
6291        unsafe {
6292            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
6293                element_descriptor: p.element_descriptor,
6294                items: cells,
6295                width: new_width,
6296            })
6297        }
6298    })
6299}
6300
6301/// A copy of the grid rotated 90° right (clockwise), as a new `Grid`.
6302///
6303/// # Safety
6304/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6305#[unsafe(no_mangle)]
6306pub unsafe extern "C" fn praxis_grid_rotate_right(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6307    abi_guard!("praxis_grid_rotate_right", ctx, {
6308        let p = unsafe { grid_payload(grid) };
6309        let height = grid_height(p.items.len(), p.width);
6310        // Rotate right (90° CW): result is H×W (width=height, height=width).
6311        // With x rightward and y downward, turning clockwise carries the *leftmost*
6312        // column to the top row, bottom-to-top:
6313        // result[x, y] = original[y, height-1-x], for x in 0..height, y in 0..width.
6314        let new_width = height;
6315        let new_height = p.width;
6316        let mut cells = Vec::with_capacity(p.items.len());
6317        for y in 0..new_height {
6318            for x in 0..new_width {
6319                let ox = y;
6320                let oy = height - 1 - x;
6321                cells.push(p.items[oy * p.width + ox]);
6322            }
6323        }
6324        // SAFETY: GridPayload is GRID's payload type.
6325        unsafe {
6326            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
6327                element_descriptor: p.element_descriptor,
6328                items: cells,
6329                width: new_width,
6330            })
6331        }
6332    })
6333}
6334
6335// ---------------------------------------------------------------------------
6336// Text methods (§4.3).
6337//
6338// `Text` is an immutable UTF-8 payload (`Box<str>`). The methods are pure
6339// (no allocation beyond the result object) and never fault.
6340// ---------------------------------------------------------------------------
6341
6342/// Read the `Text` payload of a `GcRef` as a `&str`, following slice owners.
6343///
6344/// # Safety
6345/// `r` must be a valid `Text` `GcRef`. Non-moving GC keeps it stable.
6346unsafe fn text_str(r: GcRef) -> &'static str {
6347    // SAFETY: caller guarantees `r` is Text; payload is a TextPayload.
6348    let payload = r.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload;
6349    unsafe { crate::text::text_str(payload) }
6350}
6351
6352/// The `Text` payload behind a `GcRef`.
6353///
6354/// # Safety
6355/// `r` must be a valid `Text` `GcRef`. Non-moving GC keeps it stable.
6356#[inline]
6357unsafe fn text_payload(r: GcRef) -> *const crate::text::TextPayload {
6358    r.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload
6359}
6360
6361/// The number of Unicode scalar values (chars) in `text`, as a boxed `Int`.
6362///
6363/// **O(1) after the text or its owner has been counted once** (ADR-115). The
6364/// count is cached rather than recomputed as `text_str(text).chars().count()`,
6365/// which is two passes over every byte — `text_str` re-validates the UTF-8 the
6366/// payload is already known to hold, and `chars().count()` then decodes it —
6367/// and this is called *once per iteration* of `for c in t`, because `lower_for`
6368/// puts the plan's `len` call in the loop **header**
6369/// (`praxis-mir/src/build.rs`, `lower_for`).
6370///
6371/// # Safety
6372/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6373#[unsafe(no_mangle)]
6374pub unsafe extern "C" fn praxis_text_len(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6375    abi_guard!("praxis_text_len", ctx, {
6376        // SAFETY: caller guarantees `text` is Text.
6377        let len = unsafe { crate::text::text_char_count(text_payload(text)) } as i64;
6378        unsafe { int_ref(ctx, len) }
6379    })
6380}
6381
6382/// The whole of `text`, trimmed, if `run` accepts all of it — the shared half of
6383/// [`praxis_text_int`] and [`praxis_text_float`] (ADR-136).
6384///
6385/// **`run` is the input parser's own scanner** (`parser::take_int_run`,
6386/// `parser::take_float_run`), and that is the point rather than a convenience.
6387/// `parse(t, int)` and `t.int()` are two spellings of "read a number out of
6388/// text", and a program that gets different answers from them has found a defect
6389/// in one of them. Sharing the scanner makes the disagreement unrepresentable.
6390///
6391/// The difference between the method and the atomic is *how much* must match,
6392/// not what: an atomic stops where its run stops and hands the rest of the line
6393/// to the template, and a method has no rest to hand anywhere — so a run that
6394/// covers less than the whole trimmed text is `None`. That is what makes
6395/// `"1 2"`, `"12abc"` and `"1."` rejections rather than partial answers.
6396///
6397/// Trimming is the one liberty taken, and it is what makes a line read off input
6398/// usable without a second call.
6399fn whole_trimmed(s: &str, run: fn(&[u8]) -> (&str, usize)) -> Option<&str> {
6400    let trimmed = s.trim();
6401    let (text, len) = run(trimmed.as_bytes());
6402    (!text.is_empty() && len == trimmed.len()).then_some(trimmed)
6403}
6404
6405/// The `Int` `text` spells, as `Some(n)`, or `None` when it spells no `Int`
6406/// (ADR-136).
6407///
6408/// `Y001`'s help on `var count: Int = raw` names `.int()`, so this is the method
6409/// that help sends the reader to.
6410///
6411/// `Option[Int]` and not `Int`, for §4.7's reason: a text that is not a number
6412/// is *absence*, not a fault. Input arrives as text and is routinely not what
6413/// the program hoped, so a panicking conversion would make `"abc".int()` a crash
6414/// the program has no way to prevent — where `read lines(int)`, the other half
6415/// of that help, reports at the parser and never produces the value at all.
6416///
6417/// The accepted spelling is **§7.4's `int` atomic** over the whole trimmed text:
6418/// an optional `-` and then digits (see [`whole_trimmed`]). `"1 2"`, `"0x10"`,
6419/// `"1_000"`, `"+5"` and `""` are all `None`, and so is a value outside `Int`'s
6420/// range — for the reason `Y013` exists: a saturated answer is a number nobody
6421/// wrote.
6422///
6423/// # Safety
6424/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6425#[unsafe(no_mangle)]
6426pub unsafe extern "C" fn praxis_text_int(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6427    abi_guard!("praxis_text_int", ctx, {
6428        // SAFETY: caller guarantees `text` is Text.
6429        let s = unsafe { text_str(text) };
6430        match whole_trimmed(s, crate::parser::take_int_run).and_then(|t| t.parse::<i64>().ok()) {
6431            // SAFETY: `ctx` is live and wired; `int_ref` allocates the payload
6432            // and `option_some` roots it across the enum allocation.
6433            Some(n) => unsafe {
6434                let boxed = int_ref(ctx, n);
6435                option_some(ctx, boxed)
6436            },
6437            // SAFETY: `ctx` is live and wired.
6438            None => unsafe { option_none(ctx) },
6439        }
6440    })
6441}
6442
6443/// The `Float` `text` spells, as `Some(x)`, or `None` when it spells no `Float`
6444/// (ADR-136).
6445///
6446/// [`praxis_text_int`]'s twin, over §7.4's `float` atomic: an optional sign,
6447/// digits, an optional `.` **with** a fraction, and an optional complete
6448/// exponent. `"1.5"`, `"-2"`, `"+5.0"` and `"1e10"` are values; `"1."`, `"1e"`,
6449/// `"inf"`, `"nan"` and `""` are `None`, because none of them is a token the
6450/// input parser reads either.
6451///
6452/// `inf` and `nan` are the answer worth stating: Rust's `f64::from_str` accepts
6453/// both, §7.4's `float` accepts neither, and a method that took them would be a
6454/// second opinion about what a number is. `Float` still *has* those values —
6455/// `1.0 / 0.0` is one — and `Float.to_text()` prints them; what has no spelling
6456/// is reading one back out of arbitrary text.
6457///
6458/// The leading `+` this accepts and [`praxis_text_int`] does not is §7.4's own
6459/// asymmetry, carried over rather than papered over: changing an atomic's
6460/// accepted set is a change to the input language.
6461///
6462/// # Safety
6463/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6464#[unsafe(no_mangle)]
6465pub unsafe extern "C" fn praxis_text_float(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6466    abi_guard!("praxis_text_float", ctx, {
6467        // SAFETY: caller guarantees `text` is Text.
6468        let s = unsafe { text_str(text) };
6469        match whole_trimmed(s, crate::parser::take_float_run).and_then(|t| t.parse::<f64>().ok()) {
6470            // SAFETY: `ctx` is live and wired. `praxis_alloc_float` takes the
6471            // bit pattern the uniform scalar ABI carries (§4.3), and
6472            // `option_some` roots the box across the enum allocation.
6473            Some(x) => unsafe {
6474                let boxed = praxis_alloc_float(ctx, x.to_bits() as i64);
6475                option_some(ctx, boxed)
6476            },
6477            // SAFETY: `ctx` is live and wired.
6478            None => unsafe { option_none(ctx) },
6479        }
6480    })
6481}
6482
6483/// True iff `text` has no chars, as a boxed `Bool`.
6484///
6485/// Asks the bytes rather than a `&str`: `text_str` validates the whole payload
6486/// to hand back a `&str`, which would make an O(1) question O(n) (ADR-115). A
6487/// text is empty iff it has no bytes — no scalar encodes to zero of them.
6488///
6489/// # Safety
6490/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6491#[unsafe(no_mangle)]
6492pub unsafe extern "C" fn praxis_text_is_empty(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6493    abi_guard!("praxis_text_is_empty", ctx, {
6494        // SAFETY: caller guarantees `text` is Text.
6495        let empty = unsafe { crate::text::text_bytes(text_payload(text)) }.is_empty();
6496        // SAFETY: ctx/heap valid; Bool immortal path.
6497        unsafe { bool_ref(ctx, empty) }
6498    })
6499}
6500
6501/// `a + b` on two `Text`s — a new owned `Text` holding their concatenation
6502/// (ADR-085).
6503///
6504/// Declared `Allocates` rather than `AllocatesAndFaults`, which is
6505/// `praxis_float_to_text`'s row and for the same reason: both payloads are
6506/// UTF-8 by construction, so their concatenation is too, and there is nothing
6507/// for the `InvalidText` fault to check. Since ADR-111 `praxis_alloc_text` is
6508/// `Allocates` on the same footing — every wrapper here trusts its caller about
6509/// encoding, and the one place that cannot (`praxis_get_input`, which holds the
6510/// host's raw bytes) validates and faults there.
6511///
6512/// The result is `Owned` and never a `Slice`: a concatenation has no single
6513/// owner to point into, and a slice of one would be a lie about its extent.
6514///
6515/// # Safety
6516/// `ctx` must be live and wired; `a` and `b` must be valid `Text` `GcRef`s.
6517#[unsafe(no_mangle)]
6518pub unsafe extern "C" fn praxis_text_concat(ctx: *mut RuntimeContext, a: GcRef, b: GcRef) -> GcRef {
6519    abi_guard!("praxis_text_concat", ctx, {
6520        // SAFETY: caller guarantees both are Text.
6521        let left = unsafe { text_str(a) };
6522        let right = unsafe { text_str(b) };
6523        let mut joined = String::with_capacity(left.len() + right.len());
6524        joined.push_str(left);
6525        joined.push_str(right);
6526        // SAFETY: TextPayload matches TEXT's size/align and is fully initialized.
6527        unsafe { text_ref(ctx, joined) }
6528    })
6529}
6530
6531/// Render `value` into a fresh `Text`, **exactly as `out` renders it** (§8.1,
6532/// ADR-147).
6533///
6534/// This is the whole of an interpolation hole. `"{v}"` on a `Vec[Int]` is
6535/// `[1, 2, 3]` because this function and [`praxis_write_stdout`] are the same
6536/// two lines with a different destination: both call [`GcRef::format`], which
6537/// dispatches through the value's type descriptor. There is no second renderer
6538/// here and there must never be one — writing a `write!` inline instead of
6539/// calling `format` is the mistake this wrapper exists to make unnecessary, and
6540/// it is the mistake ADR-143 decision 2 records for the three scalar rows.
6541///
6542/// That is also why a hole may hold **any** type (ADR-147 decision 2). Every
6543/// `GcRef` has a descriptor and every descriptor has a `format` callback, so
6544/// there is no value this can be handed that it cannot render — which is what
6545/// lets inference impose no requirement on a hole at all.
6546///
6547/// Declared `Allocates`, never `AllocatesAndFaults`: nothing above can fail, and
6548/// a `String` built by `format` is valid UTF-8 by construction, so there is
6549/// nothing for an `InvalidText` fault to check. That is `praxis_text_concat`'s
6550/// row exactly.
6551///
6552/// # Safety
6553/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6554#[unsafe(no_mangle)]
6555pub unsafe extern "C" fn praxis_value_to_text(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6556    abi_guard!("praxis_value_to_text", ctx, {
6557        let mut s = String::new();
6558        value.format(&mut s);
6559        // SAFETY: `s` is valid UTF-8; ctx/heap valid.
6560        unsafe { text_ref(ctx, s) }
6561    })
6562}
6563
6564/// The `Char` at `index`, or an `IndexOutOfBounds` fault if out of range
6565/// (ADR-086). `index` counts Unicode scalar values, not bytes.
6566///
6567/// # Safety
6568/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`; `index`
6569/// must be a valid `Int` `GcRef`.
6570#[unsafe(no_mangle)]
6571pub unsafe extern "C" fn praxis_text_get(
6572    ctx: *mut RuntimeContext,
6573    text: GcRef,
6574    index: GcRef,
6575) -> GcRef {
6576    abi_guard!("praxis_text_get", ctx, {
6577        // SAFETY: caller guarantees `text` is Text.
6578        let payload = unsafe { text_payload(text) };
6579        // SAFETY: caller guarantees `index` is a valid Int.
6580        let idx = unsafe { int_payload(index) };
6581        if idx < 0 {
6582            unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6583            return unsafe { unit_sentinel(ctx) };
6584        }
6585        // **The byte index is the character index exactly when every scalar is
6586        // one byte, and `text_ascii_bytes` answers that in O(1)** (ADR-115).
6587        // The fallback is `chars().nth(i)`, which is O(i): a multi-byte text
6588        // has no random access without either a wider representation or a
6589        // cursor, and ADR-115 declines the cursor with its arithmetic. `idx` is
6590        // non-negative above, so the `as usize` cannot wrap.
6591        // SAFETY: caller guarantees `text` is Text.
6592        if let Some(bytes) = unsafe { crate::text::text_ascii_bytes(payload) } {
6593            return match bytes.get(idx as usize) {
6594                // One-byte scalars are exactly the ASCII range, so the byte
6595                // *is* the code point (§4.3, ADR-086).
6596                Some(&b) => unsafe { char_ref(ctx, u32::from(b)) },
6597                None => {
6598                    unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6599                    unsafe { unit_sentinel(ctx) }
6600                }
6601            };
6602        }
6603        // SAFETY: caller guarantees `text` is Text.
6604        let s = unsafe { text_str(text) };
6605        match s.chars().nth(idx as usize) {
6606            Some(ch) => {
6607                // No validity check, and none belongs here: `ch` is a Rust `char`,
6608                // so `ch as u32` is a valid Unicode scalar by construction. The
6609                // check `praxis_int_to_char` needs is for the values that did not
6610                // come from one — which is why this goes to `char_ref` directly
6611                // rather than through `checked_alloc_char`.
6612                //
6613                // This is the interning's largest site (ADR-107): the same call
6614                // is `t[i]` and every step of `for c in t` (the `iter_plan`
6615                // lowering), so a program that walks a line of ASCII text would
6616                // otherwise box one object per character.
6617                unsafe { char_ref(ctx, ch as u32) }
6618            }
6619            None => {
6620                unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6621                unsafe { unit_sentinel(ctx) }
6622            }
6623        }
6624    })
6625}
6626
6627// ---------------------------------------------------------------------------
6628// `out(...)` — write a value to stdout followed by a newline (§16.1).
6629// ---------------------------------------------------------------------------
6630
6631/// Format `value` through its descriptor and write it to stdout followed by a
6632/// newline. Returns the Unit sentinel (§4.3), matching `out`'s `(T) -> Unit`
6633/// type. Never faults.
6634///
6635/// # Safety
6636/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6637#[unsafe(no_mangle)]
6638pub unsafe extern "C" fn praxis_write_stdout(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6639    abi_guard!("praxis_write_stdout", ctx, {
6640        use std::io::Write;
6641        let mut out = String::new();
6642        value.format(&mut out);
6643        let _ = std::io::stdout().write_all(out.as_bytes());
6644        let _ = std::io::stdout().write_all(b"\n");
6645        // `out` is `(T) -> Unit`: return the Unit sentinel so a Unit-typed value
6646        // flows out, not the printed argument (which would otherwise leak as the
6647        // function's result and be printed a second time by the host).
6648        unsafe { unit_sentinel(ctx) }
6649    })
6650}
6651
6652// ---------------------------------------------------------------------------
6653// `dbg(...)`, `panic(...)`, `assert(...)` — the rest of §16.1's control names.
6654// ---------------------------------------------------------------------------
6655
6656/// Format `value` through its descriptor, write it to stderr followed by a
6657/// newline, and hand **the same reference back** (§8.1). `dbg` is `forall T.
6658/// (T) -> T`, so it can be wrapped around any subexpression without changing
6659/// what the program computes. Never faults, never allocates.
6660///
6661/// # Safety
6662/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6663#[unsafe(no_mangle)]
6664pub unsafe extern "C" fn praxis_dbg(_ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6665    abi_guard!("praxis_dbg", _ctx, {
6666        use std::io::Write;
6667        let mut rendered = String::new();
6668        value.format(&mut rendered);
6669        let _ = std::io::stderr().write_all(rendered.as_bytes());
6670        let _ = std::io::stderr().write_all(b"\n");
6671        value
6672    })
6673}
6674
6675/// Record `value` as the fault message and raise [`FaultKind::Panic`] (§9.1).
6676///
6677/// The message is rendered **here**, through the value's descriptor, exactly as
6678/// `out` renders its argument. It has to be: the host reads the message after
6679/// the heap the `GcRef` points into has been torn down, so a stored reference
6680/// would outlive what it names.
6681///
6682/// Returns the Unit sentinel. `panic` is `forall T. (T) -> Never`, so no caller
6683/// can use the result — but the ABI returns a `GcRef` on every path, and a
6684/// fault epilogue needs a defined value to carry out (§10.4).
6685///
6686/// # Safety
6687/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6688#[unsafe(no_mangle)]
6689pub unsafe extern "C" fn praxis_panic(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6690    abi_guard!("praxis_panic", ctx, {
6691        let mut message = String::new();
6692        value.format(&mut message);
6693        unsafe { set_fault_message(ctx, message) };
6694        unsafe { set_fault(ctx, RaisedFault::PANIC) };
6695        unsafe { unit_sentinel(ctx) }
6696    })
6697}
6698
6699/// Raise [`FaultKind::AssertFailed`] when `condition` is false (§9.1), and do
6700/// nothing at all when it is true.
6701///
6702/// `assert` is `(Bool) -> Unit`, so the argument is one of the two `Bool`
6703/// immortals and reading its payload needs no descriptor check.
6704///
6705/// It sets **no** message: `assert` takes a condition and nothing else, so the
6706/// only text available would restate the fault kind. `panic` is the name that
6707/// carries words.
6708///
6709/// # Safety
6710/// `ctx` must be live and wired; `condition` must be a valid `Bool` `GcRef`.
6711#[unsafe(no_mangle)]
6712pub unsafe extern "C" fn praxis_assert(ctx: *mut RuntimeContext, condition: GcRef) -> GcRef {
6713    abi_guard!("praxis_assert", ctx, {
6714        // SAFETY: `assert`'s scheme is `(Bool) -> Unit`, so the argument is a Bool.
6715        if !unsafe { crate::immortal::read_bool(condition) } {
6716            unsafe { set_fault(ctx, RaisedFault::ASSERT_FAILED) };
6717        }
6718        unsafe { unit_sentinel(ctx) }
6719    })
6720}
6721
6722// ---------------------------------------------------------------------------
6723// `Range` (§4.11, ADR-059).
6724//
6725// `a..b` and `a..=b` are two symbols rather than one symbol with a flag: the
6726// choice is already a syntactic fact the MIR builder holds, and a boolean
6727// smuggled through an `i64` parameter would have 2^64 spellings for two states.
6728// Both bounds arrive as `Int` `GcRef`s, because a bound is an arbitrary
6729// expression and every other wrapper takes its operands boxed.
6730// ---------------------------------------------------------------------------
6731
6732/// Build the half-open range `start..end` (§4.11). A descending range is
6733/// **empty** — [`RangeVal::new`](crate::range::RangeVal::new) normalizes it, so
6734/// no range with a negative length exists.
6735///
6736/// # Safety
6737/// `ctx` must be live and wired; both bounds must be valid `Int` `GcRef`s.
6738#[unsafe(no_mangle)]
6739pub unsafe extern "C" fn praxis_range_new(
6740    ctx: *mut RuntimeContext,
6741    start: GcRef,
6742    end: GcRef,
6743) -> GcRef {
6744    abi_guard!("praxis_range_new", ctx, {
6745        let a = unsafe { int_payload(start) };
6746        let b = unsafe { int_payload(end) };
6747        unsafe {
6748            gc_alloc(
6749                ctx,
6750                crate::range::RANGE_PAYLOAD,
6751                crate::range::RangeVal::new(a, b),
6752            )
6753        }
6754    })
6755}
6756
6757/// Build the inclusive range `start..=end` (§4.11).
6758///
6759/// # Safety
6760/// `ctx` must be live and wired; both bounds must be valid `Int` `GcRef`s.
6761#[unsafe(no_mangle)]
6762pub unsafe extern "C" fn praxis_range_new_inclusive(
6763    ctx: *mut RuntimeContext,
6764    start: GcRef,
6765    end: GcRef,
6766) -> GcRef {
6767    abi_guard!("praxis_range_new_inclusive", ctx, {
6768        let a = unsafe { int_payload(start) };
6769        let b = unsafe { int_payload(end) };
6770        unsafe {
6771            gc_alloc(
6772                ctx,
6773                crate::range::RANGE_PAYLOAD,
6774                crate::range::RangeVal::new_inclusive(a, b),
6775            )
6776        }
6777    })
6778}
6779
6780/// The number of integers in a range (§4.11) — what a `for` loop reads to
6781/// bound itself.
6782///
6783/// **Faults when the count does not fit an `Int`.** Only the very widest ranges
6784/// reach it (`Int::MIN..Int::MAX` holds `2^64 - 1` integers), and reporting a
6785/// wrapped negative length instead would be a `for` loop that ran zero times
6786/// over every integer there is.
6787///
6788/// The kind is `IntOverflow`, which is what `gcd`, `lcm` and A\*'s path cost
6789/// already answer for a result with no `Int`. It is deliberately not
6790/// `EmptyRange`: the range this fires on is the *fullest* one there is, so that
6791/// message would lie about it (ADR-059, ADR-075).
6792///
6793/// # Safety
6794/// `ctx` must be live and wired; `r` must be a valid `Range` `GcRef`.
6795#[unsafe(no_mangle)]
6796pub unsafe extern "C" fn praxis_range_len(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
6797    abi_guard!("praxis_range_len", ctx, {
6798        // SAFETY: the compiler only emits this with a Range-typed operand.
6799        let range = unsafe { &*r.payload::<crate::range::RangeVal>() };
6800        match i64::try_from(range.len()) {
6801            Ok(len) => unsafe { int_ref(ctx, len) },
6802            Err(_) => {
6803                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
6804                unsafe { unit_sentinel(ctx) }
6805            }
6806        }
6807    })
6808}
6809
6810/// The `index`-th integer of a range (§4.11). Faults when `index` is outside
6811/// it, exactly as `Vec.get` does.
6812///
6813/// # Safety
6814/// `ctx` must be live and wired; `r` must be a valid `Range` `GcRef` and
6815/// `index` a valid `Int` one.
6816#[unsafe(no_mangle)]
6817pub unsafe extern "C" fn praxis_range_get(
6818    ctx: *mut RuntimeContext,
6819    r: GcRef,
6820    index: GcRef,
6821) -> GcRef {
6822    abi_guard!("praxis_range_get", ctx, {
6823        // SAFETY: the compiler only emits this with a Range-typed receiver.
6824        let range = unsafe { &*r.payload::<crate::range::RangeVal>() };
6825        let i = unsafe { int_payload(index) };
6826        match range.get(i) {
6827            Some(value) => unsafe { int_ref(ctx, value) },
6828            None => {
6829                unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6830                unsafe { unit_sentinel(ctx) }
6831            }
6832        }
6833    })
6834}
6835
6836// ---------------------------------------------------------------------------
6837// Input parser (§7).
6838//
6839// `read` / `parse` lower to runtime calls that fetch the input buffer and run
6840// a compiled parser plan against it. The plan is compiled at HIR time and
6841// registered in a global slab; its index is passed as a boxed Int.
6842// ---------------------------------------------------------------------------
6843
6844/// Return the process-input source buffer (§7.10), reading it the **first**
6845/// time a program asks.
6846///
6847/// A `read` lowers to this call and then to `praxis_run_parser`, so this is
6848/// where §7.10's "the first `read` lazily reads standard input once" happens.
6849/// The host installs a [`crate::input::InputReader`] rather than a buffer; it
6850/// is called at most once — [`crate::input::take_input_reader`] removes it, so
6851/// "once" is structural rather than a flag — and the result is installed as
6852/// `input_source`, which every later `read` reuses.
6853///
6854/// Nothing before a program's first `read` touches the host's input. Reading it
6855/// up front would make a program with no `read` in it still consume standard
6856/// input, so `praxis run` against an open pipe would block forever.
6857///
6858/// A host that installs no reader — every JIT test, and the crash debugger's
6859/// re-run path, which installs the buffer directly to keep re-runs identical
6860/// (§9.7) — reaches the plain `input_source` read below.
6861///
6862/// **A reader that answers zero bytes has given empty input, not no input.** Its
6863/// answer is installed as `input_source` whatever its length, so `read` runs
6864/// against a zero-length buffer and the parser constructors answer from their own
6865/// rules — `lines(int)` over it is `[]` by `split_lines`'s rule, and one that
6866/// requires content faults at `0..0` naming what it expected. That is what §7.11
6867/// asks a mismatch to carry, and a fault raised before any buffer existed can
6868/// carry none of it: it has no input span to name. A zero-byte `--input` file is
6869/// the same decision, made at `praxis-cli/src/run.rs` (ADR-087).
6870///
6871/// The one remaining Unit-source state belongs to a host that installs **neither**
6872/// a buffer nor a reader — every JIT test, every embedder. `praxis_run_parser`'s
6873/// descriptor guard (§6.3) is what keeps that state survivable; no `praxis run`
6874/// reaches it.
6875///
6876/// **This wrapper owns the UTF-8 judgement, and it is the only producer of
6877/// [`FaultKind::InvalidText`](crate::FaultKind::InvalidText)** (ADR-111). A
6878/// reader's bytes are the host's, not the compiler's, so they are checked here
6879/// and `INVALID_TEXT` is raised here — where `lower_read`'s `CheckFault` makes
6880/// it divert at the `read`. Raising it inside `praxis_alloc_text` instead would
6881/// cost a check after every text *literal* for a fault a literal cannot
6882/// produce; that wrapper trusts its caller, and this is the caller that has to
6883/// earn the trust.
6884///
6885/// `praxis run` cannot reach the fault: `lazy_stdin::read` goes through
6886/// `std::io::read_to_string` and exits 2 on non-UTF-8 stdin before the runtime
6887/// sees a byte. An embedder installing its own reader can.
6888///
6889/// # Safety
6890/// `ctx` must be live and wired.
6891#[unsafe(no_mangle)]
6892pub unsafe extern "C" fn praxis_get_input(ctx: *mut RuntimeContext) -> GcRef {
6893    abi_guard!("praxis_get_input", ctx, {
6894        if let Some(read) = crate::input::take_input_reader() {
6895            let bytes = read();
6896            // **This is the one place in the runtime that holds raw host bytes,
6897            // so it is the one place the UTF-8 judgement §4.3 assigns belongs**
6898            // (ADR-111). Here the fault is real: a host's `InputReader` is
6899            // infallible about I/O by design (`crate::input`) and says nothing
6900            // about encoding, so these bytes are exactly as trustworthy as the
6901            // host. `GetInput`'s row is `AllocatesAndFaults` and `lower_read`
6902            // emits the check, so `InvalidText` diverts *at the `read`*.
6903            //
6904            // The Unit sentinel is the defined dummy (§10.4); `input_source`
6905            // holds it until a buffer is installed, so answering it below is
6906            // the same value by a shorter route.
6907            let Ok(text) = std::str::from_utf8(&bytes) else {
6908                unsafe { set_fault(ctx, RaisedFault::INVALID_TEXT) };
6909                return unsafe { (*ctx).input_source };
6910            };
6911            // **The validation is strictly before the allocation, and must
6912            // stay there.** SAFETY: `text` borrows a live, initialized buffer
6913            // for this call, and `ctx` is the caller's live context. The result
6914            // is stored into `input_source` — a root (`RuntimeRoots`) — with no
6915            // allocation in between, so the collection this allocation paces
6916            // cannot reclaim it. `praxis_alloc_text` takes `&[]` for
6917            // `len == 0`, so the empty answer needs no special case here and
6918            // must not get one.
6919            let text = unsafe { praxis_alloc_text(ctx, text.as_ptr(), text.len()) };
6920            unsafe { (*ctx).input_source = text };
6921        }
6922        unsafe { (*ctx).input_source }
6923    })
6924}
6925
6926/// Run a compiled parser plan against `input`, returning the parsed result as a
6927/// `GcRef` (§7.1). `plan_index_gc` is a boxed `Int` whose payload is the
6928/// plan's index in the HIR's global slab.
6929///
6930/// On a parse mismatch (or a non-Text `input`), sets `FaultKind::ParseFailed`
6931/// and returns the Unit sentinel (§7.11). No Rust panic crosses the ABI.
6932///
6933/// The non-Text guard is load-bearing (§6.3 host-safety gap): the parser
6934/// interpreter reinterprets `input`'s payload as a `TextPayload`, so a non-Text
6935/// `input` (e.g. the default Unit singleton when no input buffer was installed)
6936/// would be dereferenced as a Text buffer and segfault. Both `read` (whose
6937/// `input` comes from `praxis_get_input`) and `parse(text, expr)` (whose `input`
6938/// is an arbitrary expression) funnel through here, so guarding at this ABI
6939/// boundary closes the gap regardless of how the input was produced.
6940///
6941/// The guard **clears** the parse detail and records none of its own. It runs no
6942/// parse, so it has nothing to report — and fabricating a [`ParseFail`] there
6943/// would be worse than silence: with no buffer there is no input span, and an
6944/// invented `expected` would make an embedder's host bug read as a parse failure
6945/// at an offset that does not exist. Clearing is also what stops it reporting a
6946/// *previous* parse's offset: this is the one entry into the parser that does
6947/// not go through `run_plan`'s own clear.
6948///
6949/// # Safety
6950/// `ctx` must be live and wired; `plan_index_gc` must be a valid `Int`; `input`
6951/// must be a valid `GcRef` (any descriptor — a non-Text descriptor faults cleanly
6952/// rather than dereferencing garbage).
6953#[unsafe(no_mangle)]
6954pub unsafe extern "C" fn praxis_run_parser(
6955    ctx: *mut RuntimeContext,
6956    plan_index_gc: GcRef,
6957    input: GcRef,
6958) -> GcRef {
6959    abi_guard!("praxis_run_parser", ctx, {
6960        // Guard the parser interpreter against a non-Text input (§6.3). Reaching
6961        // `run_plan` with a non-Text payload would reinterpret foreign bytes as a
6962        // TextPayload and segfault; fault cleanly instead.
6963        if input.descriptor().id() != crate::text::TEXT.id() {
6964            unsafe { crate::parser::clear_parse_detail(ctx) };
6965            unsafe { set_fault(ctx, RaisedFault::PARSE_FAILED) };
6966            return unsafe { unit_sentinel(ctx) };
6967        }
6968        let idx = unsafe { int_payload(plan_index_gc) };
6969        // Delegate to the parser interpreter. It validates the id, reads the
6970        // plan from the process-wide arena, runs it against the input bytes, and
6971        // allocates the result.
6972        // SAFETY: `ctx` is the wrapper's own context argument, and `input` was
6973        // checked to carry a Text payload above.
6974        match unsafe { crate::parser::run_plan_by_id(ctx, idx, input) } {
6975            Some(result) => result,
6976            None => {
6977                // A `None` return means the value named no registered plan (out of
6978                // range, negative, or zero) or the interpreter was not linked.
6979                // Treat as a parse fault.
6980                unsafe { set_fault(ctx, RaisedFault::PARSE_FAILED) };
6981                unsafe { unit_sentinel(ctx) }
6982            }
6983        }
6984    })
6985}
6986
6987// ---------------------------------------------------------------------------
6988// Graph helpers (§6.5, ADR-060).
6989//
6990// Twelve prelude names whose graph is a closure: the caller passes a start
6991// state and a function from a state to its neighbours, and the wrapper walks
6992// whatever that function describes. `crate::graph` owns the walks and never
6993// touches a closure; `ClosureOracle` below is the one thing that does.
6994//
6995// The bare name is the whole walk, `_distance` is the number of the cheapest
6996// route to a goal, and `_path` is the route. The two goal-directed forms of a
6997// family share one walk and project the `cost` or the `states` out of the one
6998// `graph::Route` it answers, so they can never disagree about which route was
6999// found.
7000// ---------------------------------------------------------------------------
7001
7002/// A [`GraphOracle`](crate::graph::GraphOracle) backed by the closures a
7003/// program passed, with every state it is handed rooted in a native frame.
7004///
7005/// The scope is what makes the walks safe: a state lives in a Rust visited set
7006/// or queue, which the collector cannot see, and every closure call may
7007/// allocate. `retain` roots each state the moment the walk decides to remember
7008/// it, so a collection triggered inside the *next* call finds it.
7009struct ClosureOracle<'s, 'c> {
7010    ctx: *mut RuntimeContext,
7011    scope: &'s NativeScope<'c>,
7012    /// `(T) -> Vec[T]`.
7013    neighbours: GcRef,
7014    /// `(T, T) -> Int`, or the Unit sentinel for a helper that has no weights.
7015    weight: GcRef,
7016    /// `(T) -> Int`, or the Unit sentinel for a helper with no heuristic.
7017    heuristic: GcRef,
7018    /// `(T) -> Bool`, or the Unit sentinel for a helper with no goal test.
7019    goal: GcRef,
7020}
7021
7022impl ClosureOracle<'_, '_> {
7023    /// Call `closure` with `args`, or `Err` if it faulted — or if it is not a
7024    /// closure at all.
7025    ///
7026    /// The type checker says every one of these operands is a function, and the
7027    /// only runtime representation of a function value is a closure object. The
7028    /// descriptor is checked anyway: the alternative to a `TypeMismatch` fault
7029    /// is transmuting whatever the payload's first word happens to be into a
7030    /// function pointer and jumping to it.
7031    unsafe fn call(
7032        &mut self,
7033        closure: GcRef,
7034        args: &[GcRef],
7035    ) -> Result<GcRef, crate::graph::Aborted> {
7036        if !std::ptr::eq(closure.descriptor(), &crate::closures::CLOSURE) {
7037            return Err(self.abort(crate::context::FaultKind::TypeMismatch));
7038        }
7039        // SAFETY: the descriptor check above proves the payload is a
7040        // `ClosurePayload`, so `fn_ptr` is the entry point the codegen wrote
7041        // there (`praxis_alloc_closure`).
7042        let fn_ptr = unsafe { (*closure.payload::<crate::closures::ClosurePayload>()).fn_ptr };
7043        // A closure's entry point is `fn(ctx, closure_self, params...) -> GcRef`
7044        // (§4.10, Approach B): the closure value itself is a hidden first
7045        // explicit argument, and the prologue loads its captures from it. The
7046        // arity is fixed by the helper's signature, which inference has already
7047        // checked, so only the shapes the six helpers use exist here.
7048        let result = match args {
7049            // SAFETY: `fn_ptr` is a finalized JIT entry whose parameter count is
7050            // the one the type checker enforced for this operand; every value
7051            // crossing is a `GcRef`, which is the ABI's only value kind.
7052            [a] => unsafe {
7053                let f: unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef) -> GcRef =
7054                    std::mem::transmute(fn_ptr);
7055                f(self.ctx, closure, *a)
7056            },
7057            // SAFETY: as above, at the two-parameter shape.
7058            [a, b] => unsafe {
7059                let f: unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef, GcRef) -> GcRef =
7060                    std::mem::transmute(fn_ptr);
7061                f(self.ctx, closure, *a, *b)
7062            },
7063            // Unreachable: `GraphParam` has no shape with another arity, and the
7064            // match on it in `seed_builtin_schemes` is exhaustive. Faulting is
7065            // still the only safe answer, because the alternative is calling
7066            // with the wrong number of arguments.
7067            _ => return Err(self.abort(crate::context::FaultKind::TypeMismatch)),
7068        };
7069        // The closure ran arbitrary Praxis code and may have faulted. Its result
7070        // on that path is the Unit sentinel, so continuing would walk a graph of
7071        // Units; stop instead, leaving the fault for the call site's own check.
7072        if unsafe { praxis_check_fault(self.ctx) } != 0 {
7073            return Err(crate::graph::Aborted);
7074        }
7075        Ok(self.scope.root(result).get())
7076    }
7077
7078    /// The `i64` inside a boxed `Int` a closure returned, or a fault if it is
7079    /// not one.
7080    unsafe fn int_result(&mut self, value: GcRef) -> Result<i64, crate::graph::Aborted> {
7081        if !std::ptr::eq(value.descriptor(), &scalars::INT) {
7082            return Err(self.abort(crate::context::FaultKind::TypeMismatch));
7083        }
7084        Ok(unsafe { int_payload(value) })
7085    }
7086}
7087
7088impl crate::graph::GraphOracle for ClosureOracle<'_, '_> {
7089    fn neighbours(&mut self, state: GcRef) -> Result<Vec<GcRef>, crate::graph::Aborted> {
7090        // SAFETY: `ctx` is live for the wrapper's duration and `neighbours` is
7091        // the operand the type checker typed `(T) -> Vec[T]`.
7092        let result = unsafe { self.call(self.neighbours, &[state])? };
7093        if !std::ptr::eq(result.descriptor(), &crate::collections::VEC) {
7094            return Err(self.abort(crate::context::FaultKind::TypeMismatch));
7095        }
7096        // SAFETY: the descriptor check proves the payload is a `VecPayload`, and
7097        // the result is rooted by `call`, so reading its items cannot race a
7098        // collection — nothing allocates between here and the copy.
7099        let items = unsafe { (*result.payload::<VecPayload>()).items.to_vec() };
7100        for item in &items {
7101            self.scope.root(*item);
7102        }
7103        Ok(items)
7104    }
7105
7106    fn weight(&mut self, from: GcRef, to: GcRef) -> Result<i64, crate::graph::Aborted> {
7107        // SAFETY: as above, at the `(T, T) -> Int` operand.
7108        let result = unsafe { self.call(self.weight, &[from, to])? };
7109        // SAFETY: `result` is a live, rooted `GcRef`.
7110        unsafe { self.int_result(result) }
7111    }
7112
7113    fn heuristic(&mut self, state: GcRef) -> Result<i64, crate::graph::Aborted> {
7114        // SAFETY: as above, at the `(T) -> Int` operand.
7115        let result = unsafe { self.call(self.heuristic, &[state])? };
7116        // SAFETY: `result` is a live, rooted `GcRef`.
7117        unsafe { self.int_result(result) }
7118    }
7119
7120    fn is_goal(&mut self, state: GcRef) -> Result<bool, crate::graph::Aborted> {
7121        // SAFETY: as above, at the `(T) -> Bool` operand.
7122        let result = unsafe { self.call(self.goal, &[state])? };
7123        // A `Bool`'s payload is **one byte**. Reading it as an `i64` would take
7124        // seven further bytes of the block's alignment padding, which the bump
7125        // allocator never initialized. `read_scalar` checks the descriptor and
7126        // takes the width from `BOOL_PAYLOAD`'s own type, so neither half is
7127        // written here.
7128        //
7129        // SAFETY: `result` is a `GcRef` the oracle's own call just produced.
7130        match unsafe { read_scalar(result, scalars::BOOL_PAYLOAD) } {
7131            Some(b) => Ok(b != 0),
7132            None => Err(self.abort(crate::context::FaultKind::TypeMismatch)),
7133        }
7134    }
7135
7136    fn retain(&mut self, state: GcRef) {
7137        self.scope.root(state);
7138    }
7139
7140    fn abort(&mut self, kind: crate::context::FaultKind) -> crate::graph::Aborted {
7141        if let Some(fault) = RaisedFault::new(kind) {
7142            // SAFETY: `ctx` is live and wired for the wrapper's duration.
7143            unsafe { set_fault(self.ctx, fault) };
7144        }
7145        crate::graph::Aborted
7146    }
7147}
7148
7149/// The descriptor every state in this walk shares: the start state's own.
7150///
7151/// The type checker guarantees one state type per call, and a `GcRef` carries
7152/// its descriptor in its header — so the start state is the authority on what
7153/// the result collection holds, and no separate type argument has to cross the
7154/// ABI.
7155#[inline]
7156fn state_descriptor(start: GcRef) -> *const TypeDescriptor {
7157    start.descriptor() as *const TypeDescriptor
7158}
7159
7160/// Build a `Vec[T]` holding `states`, in order.
7161///
7162/// # Safety
7163/// `ctx` must be live and wired; every state must be a valid, rooted `GcRef`.
7164unsafe fn states_as_vec(
7165    ctx: *mut RuntimeContext,
7166    element: *const TypeDescriptor,
7167    states: &[GcRef],
7168) -> GcRef {
7169    let result = unsafe { praxis_vec_new(ctx, element) };
7170    let scope = unsafe { NativeScope::new(ctx) };
7171    let rooted = scope.root(result);
7172    // SAFETY: `result` is the `Vec` just allocated, and `rooted` proves it is in
7173    // the collector's root set for the borrow.
7174    let payload = unsafe { vec_payload_mut(rooted) };
7175    payload.items.extend_from_slice(states);
7176    result
7177}
7178
7179/// `bfs(start, neighbours)` — every reachable state, in breadth-first order
7180/// (§6.5).
7181///
7182/// # Safety
7183/// `ctx` must be live and wired; `start` must be a valid `GcRef` and
7184/// `neighbours` a closure value of type `(T) -> Vec[T]`.
7185#[unsafe(no_mangle)]
7186pub unsafe extern "C" fn praxis_bfs(
7187    ctx: *mut RuntimeContext,
7188    start: GcRef,
7189    neighbours: GcRef,
7190) -> GcRef {
7191    abi_guard!("praxis_bfs", ctx, {
7192        // SAFETY: the caller upholds ctx/operand validity.
7193        unsafe {
7194            let scope = NativeScope::new(ctx);
7195            let mut oracle = ClosureOracle {
7196                ctx,
7197                scope: &scope,
7198                neighbours,
7199                weight: unit_sentinel(ctx),
7200                heuristic: unit_sentinel(ctx),
7201                goal: unit_sentinel(ctx),
7202            };
7203            match crate::graph::bfs_order(&mut oracle, start) {
7204                Ok(states) => states_as_vec(ctx, state_descriptor(start), &states),
7205                Err(_) => unit_sentinel(ctx),
7206            }
7207        }
7208    })
7209}
7210
7211/// `dfs(start, neighbours)` — every reachable state, in depth-first pre-order
7212/// (§6.5).
7213///
7214/// # Safety
7215/// As [`praxis_bfs`].
7216#[unsafe(no_mangle)]
7217pub unsafe extern "C" fn praxis_dfs(
7218    ctx: *mut RuntimeContext,
7219    start: GcRef,
7220    neighbours: GcRef,
7221) -> GcRef {
7222    abi_guard!("praxis_dfs", ctx, {
7223        // SAFETY: the caller upholds ctx/operand validity.
7224        unsafe {
7225            let scope = NativeScope::new(ctx);
7226            let mut oracle = ClosureOracle {
7227                ctx,
7228                scope: &scope,
7229                neighbours,
7230                weight: unit_sentinel(ctx),
7231                heuristic: unit_sentinel(ctx),
7232                goal: unit_sentinel(ctx),
7233            };
7234            match crate::graph::dfs_order(&mut oracle, start) {
7235                Ok(states) => states_as_vec(ctx, state_descriptor(start), &states),
7236                Err(_) => unit_sentinel(ctx),
7237            }
7238        }
7239    })
7240}
7241
7242/// `flood_fill(start, neighbours)` — every reachable state, as a `Set` (§6.5).
7243///
7244/// # Safety
7245/// As [`praxis_bfs`].
7246#[unsafe(no_mangle)]
7247pub unsafe extern "C" fn praxis_flood_fill(
7248    ctx: *mut RuntimeContext,
7249    start: GcRef,
7250    neighbours: GcRef,
7251) -> GcRef {
7252    abi_guard!("praxis_flood_fill", ctx, {
7253        // SAFETY: the caller upholds ctx/operand validity.
7254        unsafe {
7255            let scope = NativeScope::new(ctx);
7256            let mut oracle = ClosureOracle {
7257                ctx,
7258                scope: &scope,
7259                neighbours,
7260                weight: unit_sentinel(ctx),
7261                heuristic: unit_sentinel(ctx),
7262                goal: unit_sentinel(ctx),
7263            };
7264            let states = match crate::graph::reachable(&mut oracle, start) {
7265                Ok(states) => states,
7266                Err(_) => return unit_sentinel(ctx),
7267            };
7268            let result = praxis_set_new(ctx, state_descriptor(start));
7269            let rooted = scope.root(result);
7270            let payload = set_payload_mut(rooted);
7271            for state in states {
7272                payload.entries.insert(DynamicKey::new(state));
7273            }
7274            result
7275        }
7276    })
7277}
7278
7279/// `bfs_distance(start, neighbours, is_goal)` — the fewest steps to a goal, or
7280/// `None` (§6.5).
7281///
7282/// # Safety
7283/// `ctx` must be live and wired; `start` must be a valid `GcRef`, `neighbours` a
7284/// `(T) -> Vec[T]` closure and `goal` a `(T) -> Bool` closure.
7285#[unsafe(no_mangle)]
7286pub unsafe extern "C" fn praxis_bfs_distance(
7287    ctx: *mut RuntimeContext,
7288    start: GcRef,
7289    neighbours: GcRef,
7290    goal: GcRef,
7291) -> GcRef {
7292    abi_guard!("praxis_bfs_distance", ctx, {
7293        // SAFETY: the caller upholds ctx/operand validity.
7294        unsafe {
7295            let scope = NativeScope::new(ctx);
7296            let mut oracle = ClosureOracle {
7297                ctx,
7298                scope: &scope,
7299                neighbours,
7300                weight: unit_sentinel(ctx),
7301                heuristic: unit_sentinel(ctx),
7302                goal,
7303            };
7304            match crate::graph::bfs_route(&mut oracle, start) {
7305                Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7306                Err(_) => unit_sentinel(ctx),
7307            }
7308        }
7309    })
7310}
7311
7312/// `bfs_path(start, neighbours, is_goal)` — a shortest route to a goal, from
7313/// the start to the goal inclusive, or `None` (§6.5).
7314///
7315/// The same walk [`praxis_bfs_distance`] runs; this projects the route out of
7316/// its answer where that one projects the number.
7317///
7318/// # Safety
7319/// As [`praxis_bfs_distance`].
7320#[unsafe(no_mangle)]
7321pub unsafe extern "C" fn praxis_bfs_path(
7322    ctx: *mut RuntimeContext,
7323    start: GcRef,
7324    neighbours: GcRef,
7325    goal: GcRef,
7326) -> GcRef {
7327    abi_guard!("praxis_bfs_path", ctx, {
7328        // SAFETY: the caller upholds ctx/operand validity.
7329        unsafe {
7330            let scope = NativeScope::new(ctx);
7331            let mut oracle = ClosureOracle {
7332                ctx,
7333                scope: &scope,
7334                neighbours,
7335                weight: unit_sentinel(ctx),
7336                heuristic: unit_sentinel(ctx),
7337                goal,
7338            };
7339            match crate::graph::bfs_route(&mut oracle, start) {
7340                Ok(route) => states_as_optional_vec(
7341                    ctx,
7342                    state_descriptor(start),
7343                    route.as_ref().map(|r| r.states.as_slice()),
7344                ),
7345                Err(_) => unit_sentinel(ctx),
7346            }
7347        }
7348    })
7349}
7350
7351/// `dfs_distance(start, neighbours, is_goal)` — the number of edges on the
7352/// route depth-first search found to a goal, or `None` (§6.5).
7353///
7354/// Depth-first arrives by the route it descended into first, which need not be
7355/// a short one, so this and [`praxis_bfs_distance`] answer different numbers on
7356/// the same graph.
7357///
7358/// # Safety
7359/// As [`praxis_bfs_distance`].
7360#[unsafe(no_mangle)]
7361pub unsafe extern "C" fn praxis_dfs_distance(
7362    ctx: *mut RuntimeContext,
7363    start: GcRef,
7364    neighbours: GcRef,
7365    goal: GcRef,
7366) -> GcRef {
7367    abi_guard!("praxis_dfs_distance", ctx, {
7368        // SAFETY: the caller upholds ctx/operand validity.
7369        unsafe {
7370            let scope = NativeScope::new(ctx);
7371            let mut oracle = ClosureOracle {
7372                ctx,
7373                scope: &scope,
7374                neighbours,
7375                weight: unit_sentinel(ctx),
7376                heuristic: unit_sentinel(ctx),
7377                goal,
7378            };
7379            match crate::graph::dfs_route(&mut oracle, start) {
7380                Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7381                Err(_) => unit_sentinel(ctx),
7382            }
7383        }
7384    })
7385}
7386
7387/// `dfs_path(start, neighbours, is_goal)` — the route depth-first search found
7388/// to a goal, from the start to the goal inclusive, or `None` (§6.5).
7389///
7390/// # Safety
7391/// As [`praxis_bfs_distance`].
7392#[unsafe(no_mangle)]
7393pub unsafe extern "C" fn praxis_dfs_path(
7394    ctx: *mut RuntimeContext,
7395    start: GcRef,
7396    neighbours: GcRef,
7397    goal: GcRef,
7398) -> GcRef {
7399    abi_guard!("praxis_dfs_path", ctx, {
7400        // SAFETY: the caller upholds ctx/operand validity.
7401        unsafe {
7402            let scope = NativeScope::new(ctx);
7403            let mut oracle = ClosureOracle {
7404                ctx,
7405                scope: &scope,
7406                neighbours,
7407                weight: unit_sentinel(ctx),
7408                heuristic: unit_sentinel(ctx),
7409                goal,
7410            };
7411            match crate::graph::dfs_route(&mut oracle, start) {
7412                Ok(route) => states_as_optional_vec(
7413                    ctx,
7414                    state_descriptor(start),
7415                    route.as_ref().map(|r| r.states.as_slice()),
7416                ),
7417                Err(_) => unit_sentinel(ctx),
7418            }
7419        }
7420    })
7421}
7422
7423/// `dijkstra(start, neighbours, weight)` — the least cost to every reachable
7424/// state, as a `Map[T, Int]` (§6.5).
7425///
7426/// # Safety
7427/// `ctx` must be live and wired; `start` must be a valid `GcRef`, `neighbours` a
7428/// `(T) -> Vec[T]` closure and `weight` a `(T, T) -> Int` closure.
7429#[unsafe(no_mangle)]
7430pub unsafe extern "C" fn praxis_dijkstra(
7431    ctx: *mut RuntimeContext,
7432    start: GcRef,
7433    neighbours: GcRef,
7434    weight: GcRef,
7435) -> GcRef {
7436    abi_guard!("praxis_dijkstra", ctx, {
7437        // SAFETY: the caller upholds ctx/operand validity.
7438        unsafe {
7439            let scope = NativeScope::new(ctx);
7440            let mut oracle = ClosureOracle {
7441                ctx,
7442                scope: &scope,
7443                neighbours,
7444                weight,
7445                heuristic: unit_sentinel(ctx),
7446                goal: unit_sentinel(ctx),
7447            };
7448            let costs = match crate::graph::dijkstra_costs(&mut oracle, start) {
7449                Ok(costs) => costs,
7450                Err(_) => return unit_sentinel(ctx),
7451            };
7452            let result = scope.root(praxis_map_new(ctx, state_descriptor(start)));
7453            for (state, cost) in costs {
7454                // Each boxed cost is allocated *before* the payload borrow: an
7455                // allocation while a `&mut MapPayload` is live is what `Rooted`
7456                // exists to make impossible, and taking the borrow per entry is
7457                // what keeps that true.
7458                let boxed = scope.root(int_ref(ctx, cost));
7459                map_payload_mut(result)
7460                    .entries
7461                    .insert(DynamicKey::new(state), boxed.get());
7462            }
7463            result.get()
7464        }
7465    })
7466}
7467
7468/// `dijkstra_distance(start, neighbours, weight, is_goal)` — the cost of the
7469/// cheapest route to a goal, or `None` (§6.5).
7470///
7471/// The search [`praxis_dijkstra`] runs, stopped at the first goal it settles,
7472/// so it makes the same two refusals: a negative edge weight and a cost with no
7473/// `Int`.
7474///
7475/// # Safety
7476/// `ctx` must be live and wired; `start` must be a valid `GcRef`, `neighbours` a
7477/// `(T) -> Vec[T]` closure, `weight` a `(T, T) -> Int` closure and `goal` a
7478/// `(T) -> Bool` closure.
7479#[unsafe(no_mangle)]
7480pub unsafe extern "C" fn praxis_dijkstra_distance(
7481    ctx: *mut RuntimeContext,
7482    start: GcRef,
7483    neighbours: GcRef,
7484    weight: GcRef,
7485    goal: GcRef,
7486) -> GcRef {
7487    abi_guard!("praxis_dijkstra_distance", ctx, {
7488        // SAFETY: the caller upholds ctx/operand validity.
7489        unsafe {
7490            let scope = NativeScope::new(ctx);
7491            let mut oracle = ClosureOracle {
7492                ctx,
7493                scope: &scope,
7494                neighbours,
7495                weight,
7496                heuristic: unit_sentinel(ctx),
7497                goal,
7498            };
7499            match crate::graph::dijkstra_route(&mut oracle, start) {
7500                Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7501                Err(_) => unit_sentinel(ctx),
7502            }
7503        }
7504    })
7505}
7506
7507/// `dijkstra_path(start, neighbours, weight, is_goal)` — the cheapest route to
7508/// a goal, from the start to the goal inclusive, or `None` (§6.5).
7509///
7510/// # Safety
7511/// As [`praxis_dijkstra_distance`].
7512#[unsafe(no_mangle)]
7513pub unsafe extern "C" fn praxis_dijkstra_path(
7514    ctx: *mut RuntimeContext,
7515    start: GcRef,
7516    neighbours: GcRef,
7517    weight: GcRef,
7518    goal: GcRef,
7519) -> GcRef {
7520    abi_guard!("praxis_dijkstra_path", ctx, {
7521        // SAFETY: the caller upholds ctx/operand validity.
7522        unsafe {
7523            let scope = NativeScope::new(ctx);
7524            let mut oracle = ClosureOracle {
7525                ctx,
7526                scope: &scope,
7527                neighbours,
7528                weight,
7529                heuristic: unit_sentinel(ctx),
7530                goal,
7531            };
7532            match crate::graph::dijkstra_route(&mut oracle, start) {
7533                Ok(route) => states_as_optional_vec(
7534                    ctx,
7535                    state_descriptor(start),
7536                    route.as_ref().map(|r| r.states.as_slice()),
7537                ),
7538                Err(_) => unit_sentinel(ctx),
7539            }
7540        }
7541    })
7542}
7543
7544/// `a_star_distance(start, neighbours, weight, heuristic, is_goal)` — the cost
7545/// of the cheapest route to a goal, or `None` (§6.5).
7546///
7547/// # Safety
7548/// `ctx` must be live and wired; `start` must be a valid `GcRef` and each of
7549/// `neighbours`, `weight`, `heuristic` and `goal` a closure value of the type
7550/// the helper's signature declares.
7551#[unsafe(no_mangle)]
7552pub unsafe extern "C" fn praxis_a_star_distance(
7553    ctx: *mut RuntimeContext,
7554    start: GcRef,
7555    neighbours: GcRef,
7556    weight: GcRef,
7557    heuristic: GcRef,
7558    goal: GcRef,
7559) -> GcRef {
7560    abi_guard!("praxis_a_star_distance", ctx, {
7561        // SAFETY: the caller upholds ctx/operand validity.
7562        unsafe {
7563            let scope = NativeScope::new(ctx);
7564            let mut oracle = ClosureOracle {
7565                ctx,
7566                scope: &scope,
7567                neighbours,
7568                weight,
7569                heuristic,
7570                goal,
7571            };
7572            match crate::graph::a_star_route(&mut oracle, start) {
7573                Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7574                Err(_) => unit_sentinel(ctx),
7575            }
7576        }
7577    })
7578}
7579
7580/// `a_star_path(start, neighbours, weight, heuristic, is_goal)` — the cheapest
7581/// route to a goal, from the start to the goal inclusive, or `None` (§6.5).
7582///
7583/// # Safety
7584/// As [`praxis_a_star_distance`].
7585#[unsafe(no_mangle)]
7586pub unsafe extern "C" fn praxis_a_star_path(
7587    ctx: *mut RuntimeContext,
7588    start: GcRef,
7589    neighbours: GcRef,
7590    weight: GcRef,
7591    heuristic: GcRef,
7592    goal: GcRef,
7593) -> GcRef {
7594    abi_guard!("praxis_a_star_path", ctx, {
7595        // SAFETY: the caller upholds ctx/operand validity.
7596        unsafe {
7597            let scope = NativeScope::new(ctx);
7598            let mut oracle = ClosureOracle {
7599                ctx,
7600                scope: &scope,
7601                neighbours,
7602                weight,
7603                heuristic,
7604                goal,
7605            };
7606            match crate::graph::a_star_route(&mut oracle, start) {
7607                Ok(route) => states_as_optional_vec(
7608                    ctx,
7609                    state_descriptor(start),
7610                    route.as_ref().map(|r| r.states.as_slice()),
7611                ),
7612                Err(_) => unit_sentinel(ctx),
7613            }
7614        }
7615    })
7616}
7617
7618/// Allocate `Some(n)` or `None` for an `Option[Int]` result.
7619///
7620/// The tags are `Option`'s own declaration order — `Some` first, `None` second
7621/// (`TypeDb::new`) — which is the same order the codegen uses for a `Some(x)`
7622/// the program writes, so a runtime-built `Option` matches against the same
7623/// arms.
7624///
7625/// # Safety
7626/// `ctx` must be live and wired.
7627unsafe fn alloc_optional_int(ctx: *mut RuntimeContext, value: Option<i64>) -> GcRef {
7628    // SAFETY: the caller upholds ctx validity.
7629    unsafe {
7630        match value {
7631            Some(n) => {
7632                let boxed = int_ref(ctx, n);
7633                option_some(ctx, boxed)
7634            }
7635            None => option_none(ctx),
7636        }
7637    }
7638}
7639
7640/// Allocate `Some(states)` as an `Option[Vec[T]]`, or `None` when the search
7641/// found no route.
7642///
7643/// The `Vec` is rooted before the `Some` is built: `option_some` allocates an
7644/// enum, an allocation is a safepoint, and a bare `GcRef` in a local is in
7645/// nobody's root set. That ordering is the whole of what this adds to
7646/// [`states_as_vec`] and [`option_some`].
7647///
7648/// # Safety
7649/// `ctx` must be live and wired; every state must be a valid, rooted `GcRef`.
7650unsafe fn states_as_optional_vec(
7651    ctx: *mut RuntimeContext,
7652    element: *const TypeDescriptor,
7653    states: Option<&[GcRef]>,
7654) -> GcRef {
7655    // SAFETY: the caller upholds ctx/state validity.
7656    unsafe {
7657        match states {
7658            Some(states) => {
7659                let scope = NativeScope::new(ctx);
7660                let vec = scope.root(states_as_vec(ctx, element, states));
7661                option_some(ctx, vec.get())
7662            }
7663            None => option_none(ctx),
7664        }
7665    }
7666}
7667
7668#[cfg(test)]
7669mod tests {
7670    use super::*;
7671    use crate::context::{Fault, FaultKind, Runtime};
7672    use crate::parse_detail::ParseFail;
7673    use crate::shadow_stack::{SlotCount, push_frame};
7674
7675    /// A wired context backed by a real runtime.
7676    pub(super) fn wired_ctx(rt: &mut Runtime) -> *mut RuntimeContext {
7677        let ctx = Box::leak(Box::new(rt.context()));
7678        ctx as *mut RuntimeContext
7679    }
7680
7681    pub(super) unsafe fn drop_ctx(ctx: *mut RuntimeContext) {
7682        // Reclaim the leaked Box. The runtime outlives this call in tests.
7683        let _ = unsafe { Box::from_raw(ctx) };
7684    }
7685
7686    /// The first `Int` value the runtime does **not** intern.
7687    ///
7688    /// Every test below that detects a collection by watching the live registry
7689    /// *shrink* must allocate above this. An interned `Int` never enters the
7690    /// registry, so `praxis_alloc_int(ctx, 5)` in such a loop makes
7691    /// `after < before + 1` true on the first iteration and the test reports
7692    /// success without a collection ever having run — a false pass, which is
7693    /// strictly worse than the failure it replaces.
7694    const UNINTERNED: i64 = crate::small_int::SMALL_INT_MAX + 1;
7695
7696    /// Allocate through a safepointed ABI wrapper until its pre-allocation
7697    /// collection causes the live registry to shrink. Returns the live count
7698    /// immediately after that wrapper allocates its result.
7699    unsafe fn allocate_until_automatic_collection(rt: &Runtime, ctx: *mut RuntimeContext) -> usize {
7700        let mut before = rt.heap().stats().live_count;
7701        for i in 0..10_000_i64 {
7702            // Above the interned range: see `UNINTERNED`.
7703            let _ = unsafe { praxis_alloc_int(ctx, UNINTERNED + i) };
7704            let after = rt.heap().stats().live_count;
7705            if after < before.saturating_add(1) {
7706                return after;
7707            }
7708            before = after;
7709        }
7710        panic!("automatic collection did not run after 10,000 allocations");
7711    }
7712
7713    /// The version number this build declares.
7714    ///
7715    /// Named for the version rather than for any one change, because a version
7716    /// is a statement about a build and several changes share one bump. This
7717    /// pins the numeral so a build cannot ship a layout change without moving
7718    /// it.
7719    ///
7720    /// `gc::tests::the_folded_payload_offset_moved_at_v19_and_is_pinned_here`
7721    /// asserts the other direction, pinning the payload offset *to* a version
7722    /// number, so a layout change and the version that declares it cannot drift
7723    /// apart.
7724    #[test]
7725    fn version_is_twenty_for_the_batch_this_build_ships() {
7726        assert_eq!(RUNTIME_ABI_VERSION, 20);
7727    }
7728
7729    #[test]
7730    fn assert_passes_within_a_single_build() {
7731        assert_abi_version();
7732    }
7733
7734    /// [`int_payload`]'s width check must be a real branch, not a
7735    /// `debug_assert` — the read has to be bounded in the profile users
7736    /// actually run.
7737    ///
7738    /// `debug_assert_eq!` is compiled out of a release build, leaving
7739    /// `unsafe { *r.payload::<i64>() }` against a descriptor that may be zero
7740    /// bytes wide: an 8-byte out-of-bounds heap read, reachable from a program
7741    /// that passes `praxis check`.
7742    ///
7743    /// **This is a source gate on purpose, and it is the only kind that works
7744    /// here.** The defect is a difference *between profiles*, and `cargo test`
7745    /// builds exactly one of them — a behavioural test is green under
7746    /// `debug_assertions` whether the check is conditional or not. The companion
7747    /// below asserts the branch actually refuses; this asserts it is still
7748    /// *there* at `-O`.
7749    ///
7750    /// It reads the file rather than the function because there is nothing in a
7751    /// compiled artifact to ask. `every_no_mangle_wrapper_is_behind_the_panic_guard`
7752    /// is the same technique for the same reason.
7753    #[test]
7754    fn every_scalar_payload_read_goes_through_the_bounded_reader() {
7755        let source = include_str!("abi.rs");
7756
7757        // 1. The reader itself checks before it reads, and the check is an
7758        //    ordinary branch — not a `debug_assert`, which compiles out of a
7759        //    release build. That distinction is the point: with the check
7760        //    compiled out, a `praxis check`-clean program does an out-of-bounds
7761        //    read where a debug build aborts cleanly.
7762        const SIGNATURE: &str = "unsafe fn read_scalar<T: Copy>(r: GcRef, handle: crate::descriptor::Payload<T>) -> Option<T> {";
7763        let at = source
7764            .find(SIGNATURE)
7765            .expect("`read_scalar`'s definition moved; this gate names it by signature");
7766        let body_start = at + SIGNATURE.len();
7767        let body_len = source[body_start..]
7768            .find("\n}")
7769            .expect("`read_scalar` has no closing brace in the first column");
7770        let body = &source[body_start..body_start + body_len];
7771
7772        assert!(
7773            !body.contains("debug_assert"),
7774            "`read_scalar`'s type check is a `debug_assert`, which is compiled out of a \
7775             release build — and what is left is an unchecked read off a payload that may \
7776             be narrower (REP-56). Make it an ordinary branch.\nbody was:{body}"
7777        );
7778        assert!(
7779            body.contains("std::ptr::eq(r.descriptor(), handle.descriptor())"),
7780            "`read_scalar` no longer proves the value is the handle's type before reading \
7781             it (REP-37, REP-56).\nbody was:{body}"
7782        );
7783
7784        // 2. Nothing else in this file reads a scalar payload directly. This
7785        //    is the half that matters: a gate that names one function can only
7786        //    ever gate that function, and every scalar reader needs bounding.
7787        //
7788        //    Scanned over the crate's own code only: `include_str!` hands us this
7789        //    test too, whose list below would otherwise match itself, and
7790        //    comments naming the pattern are describing it rather than doing it.
7791        let code: String = source[..source
7792            .find("#[cfg(test)]")
7793            .expect("abi.rs has no test module marker")]
7794            .lines()
7795            .filter(|l| !l.trim_start().starts_with("//"))
7796            .collect::<Vec<_>>()
7797            .join("\n");
7798        //
7799        //    The patterns are the *bare* calls, not the dereferenced ones:
7800        //    binding `r.payload::<f64>()` to a local and dereferencing it on the
7801        //    next line breaks the spelling `*r.payload::<f64>()` without
7802        //    breaking the defect. Forbidding the call means no phrasing of it
7803        //    passes. `payload::<u8>()` stays legal: it is how
7804        //    `read_scalar` itself reaches the bytes, and how every *compound*
7805        //    payload (record, tuple, closure) is reached — those are cast to a
7806        //    struct the descriptor already vouched for, not read at a width.
7807        for forbidden in [
7808            "r.payload::<i64>()",
7809            "r.payload::<f64>()",
7810            "r.payload::<u32>()",
7811            "r.payload::<bool>()",
7812        ] {
7813            assert!(
7814                !code.contains(forbidden),
7815                "a scalar payload is read directly as `{forbidden}` instead of through \
7816                 `read_scalar`, so its type is unchecked in release (REP-56). Route it \
7817                 through `read_scalar(r, scalars::…_PAYLOAD)` instead."
7818            );
7819        }
7820
7821        // 3. And no Rust `bool` is ever materialized from a payload byte: a
7822        //    `bool` whose byte is not 0 or 1 is an *invalid value*, which is
7823        //    undefined behaviour independently of whether the read was in
7824        //    bounds. `BoolPayload` is a `u8` precisely so it never has to be.
7825        assert!(
7826            !code.contains("Payload<bool>") && !code.contains("read_scalar::<bool>"),
7827            "a `bool` is read straight out of a payload; read `scalars::BOOL_PAYLOAD` \
7828             (a `u8`) and compare it instead (REP-56)."
7829        );
7830    }
7831
7832    /// **ADR-111.** `praxis_alloc_text`'s UTF-8 backstop is unconditional in
7833    /// every profile, and it never becomes an unchecked read.
7834    ///
7835    /// The same source-gate technique as
7836    /// [`every_scalar_payload_read_goes_through_the_bounded_reader`], for the
7837    /// same reason and against a sharper temptation. Making the row `Allocates`
7838    /// says the caller promises UTF-8; the next tidy-up reads that as licence to
7839    /// delete the check — either into a `debug_assert` (which compiles out of a
7840    /// release build, so debug aborts and release builds a `Box<str>` of
7841    /// non-UTF-8 bytes that `text_str` later hands out as a `&str`) or into
7842    /// `from_utf8_unchecked` (the same hole, with the check deleted rather than
7843    /// compiled out). Both give two profiles two answers, and `just ci` never
7844    /// builds the one users get.
7845    ///
7846    /// A behavioural test cannot see this: under `cfg(debug_assertions)` a
7847    /// `debug_assert` version passes every test the branch version passes.
7848    #[test]
7849    fn the_text_precondition_backstop_is_unconditional_in_every_profile() {
7850        let source = include_str!("abi.rs");
7851        const SIGNATURE: &str = "pub unsafe extern \"C\" fn praxis_alloc_text(";
7852        let at = source
7853            .find(SIGNATURE)
7854            .expect("`praxis_alloc_text`'s definition moved; this gate names it by signature");
7855        let body_len = source[at..]
7856            .find("\n}")
7857            .expect("`praxis_alloc_text` has no closing brace in the first column");
7858        let body = &source[at..at + body_len];
7859
7860        assert!(
7861            body.contains("std::str::from_utf8(slice)"),
7862            "`praxis_alloc_text` no longer validates its buffer. The check is the \
7863             backstop on a raw read, not an optimization the `Allocates` row traded \
7864             away (ADR-111).\nbody was:{body}"
7865        );
7866        assert!(
7867            !body.contains("debug_assert"),
7868            "`praxis_alloc_text`'s UTF-8 check is a `debug_assert`, which is compiled \
7869             out of a release build — leaving a `Box<str>` built from bytes that are \
7870             not UTF-8 (REP-56's shape). Make it an ordinary branch.\nbody was:{body}"
7871        );
7872        assert!(
7873            !body.contains("from_utf8_unchecked"),
7874            "`praxis_alloc_text` skips the check outright. A precondition is not a \
7875             licence to read unvalidated bytes as a `str` — the refusal is \
7876             `text_bytes_are_not_utf8`, which costs a never-taken branch \
7877             (ADR-111).\nbody was:{body}"
7878        );
7879        // And the refusal is not a fault. If it were, the fault sweep would
7880        // classify the wrapper as faulting and correctly refuse the
7881        // `Allocates` row — this says so at the site rather than leaving the
7882        // failure to be diagnosed three tests away.
7883        assert!(
7884            !body.contains("set_fault"),
7885            "`praxis_alloc_text` sets a fault. Its row is `Effect::Allocates`, so no \
7886             `CheckFault` follows the call and nothing would ever observe it \
7887             (ADR-088, ADR-111).\nbody was:{body}"
7888        );
7889    }
7890
7891    /// The companion to the source gate above: the branch it insists on is real,
7892    /// and it refuses rather than reading.
7893    ///
7894    /// A `Unit` is zero bytes wide, which is the shape that must be refused.
7895    /// The refusal is a panic, which is ADR-080's defined path — inside a
7896    /// wrapper `abi_guard!` turns it into a `Panic` fault (or a message and an
7897    /// abort where the manifest makes that fault unobservable). What must not
7898    /// happen, in any profile, is the read.
7899    #[test]
7900    fn a_scalar_read_refuses_a_value_that_is_not_its_type() {
7901        let mut rt = Runtime::new();
7902        let ctx = wired_ctx(&mut rt);
7903        // SAFETY: ctx is wired to rt; the Unit immortal is a valid GcRef.
7904        let unit = unsafe { praxis_alloc_unit(ctx) };
7905        assert_eq!(unit.descriptor().size(), 0, "Unit is a zero-width payload");
7906
7907        // The panic is the refusal. `catch_unwind` here is the test standing in
7908        // for `abi_guard!`, which is what catches it in a real wrapper.
7909        let previous = std::panic::take_hook();
7910        std::panic::set_hook(Box::new(|_| {}));
7911        // `AssertUnwindSafe` for the same reason `abi_guard!` uses it: the
7912        // capture is a `Copy` C type and nothing observes a half-finished read.
7913        // SAFETY: `unit` is a valid GcRef into rt's live heap.
7914        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
7915            int_payload(unit)
7916        }));
7917        std::panic::set_hook(previous);
7918        unsafe { drop_ctx(ctx) };
7919
7920        let payload = outcome.expect_err("a zero-width payload must not be read as eight bytes");
7921        let message = payload
7922            .downcast_ref::<String>()
7923            .map(String::as_str)
7924            .or_else(|| payload.downcast_ref::<&str>().copied())
7925            .unwrap_or("");
7926        assert!(
7927            message.contains("int_payload wants a `Int` payload")
7928                && message.contains("this value is a `Unit`"),
7929            "unexpected panic message: {message:?}"
7930        );
7931    }
7932
7933    #[test]
7934    fn alloc_int_and_load_round_trip() {
7935        let mut rt = Runtime::new();
7936        let ctx = wired_ctx(&mut rt);
7937        // SAFETY: ctx is wired to rt.
7938        let r = unsafe { praxis_alloc_int(ctx, 9001) };
7939        // SAFETY: r is a valid Int allocated above.
7940        assert_eq!(unsafe { praxis_int_load(ctx, r) }, 9001);
7941        unsafe { drop_ctx(ctx) };
7942    }
7943
7944    /// The `Int` counterpart of
7945    /// [`bool_and_unit_abi_allocations_reuse_runtime_singletons`]: a small `Int`
7946    /// is one object per value, and an out-of-range one is still a fresh box.
7947    ///
7948    /// Both halves matter. The first is the optimization; the second is the
7949    /// branch a regression would silently delete, leaving every large `Int` in
7950    /// the language reading slot `value - SMALL_INT_MIN` of a table that ends
7951    /// long before it.
7952    #[test]
7953    fn small_ints_are_one_object_per_value_and_large_ones_are_not() {
7954        let mut rt = Runtime::new();
7955        let ctx = wired_ctx(&mut rt);
7956        // SAFETY: ctx is wired to rt throughout.
7957        unsafe {
7958            // In range: two calls, one object — and it is the runtime's own
7959            // table entry, not some other cache.
7960            let a = praxis_alloc_int(ctx, 7);
7961            let b = praxis_alloc_int(ctx, 7);
7962            assert_eq!(a.as_ptr(), b.as_ptr());
7963            assert_eq!(a.as_ptr(), rt.immortals().small_int(7).unwrap().as_ptr());
7964            assert_eq!(praxis_int_load(ctx, a), 7);
7965
7966            // The four boundary cases, through the ABI: the exact endpoints are
7967            // interned and one step outside either is not.
7968            for v in [
7969                crate::small_int::SMALL_INT_MIN,
7970                crate::small_int::SMALL_INT_MAX,
7971            ] {
7972                assert_eq!(
7973                    praxis_alloc_int(ctx, v).as_ptr(),
7974                    praxis_alloc_int(ctx, v).as_ptr(),
7975                    "{v} is the edge of the range and must be interned"
7976                );
7977            }
7978            for v in [
7979                crate::small_int::SMALL_INT_MIN - 1,
7980                crate::small_int::SMALL_INT_MAX + 1,
7981            ] {
7982                let x = praxis_alloc_int(ctx, v);
7983                let y = praxis_alloc_int(ctx, v);
7984                assert_ne!(
7985                    x.as_ptr(),
7986                    y.as_ptr(),
7987                    "{v} is outside the range and must still allocate"
7988                );
7989                assert_eq!(praxis_int_load(ctx, x), v, "and still hold its value");
7990                assert_eq!(praxis_int_load(ctx, y), v);
7991            }
7992
7993            // Distinct in-range values are distinct objects: interning shares
7994            // an object across *calls*, never across values.
7995            assert_ne!(
7996                praxis_alloc_int(ctx, 7).as_ptr(),
7997                praxis_alloc_int(ctx, 8).as_ptr()
7998            );
7999
8000            // The host helper and the ABI wrapper answer the same object, as
8001            // `Runtime::alloc_bool` and `praxis_alloc_bool` already do.
8002            assert_eq!(rt.alloc_int(7).as_ptr(), a.as_ptr());
8003        }
8004        unsafe { drop_ctx(ctx) };
8005    }
8006
8007    /// The `Char` counterpart of
8008    /// [`small_ints_are_one_object_per_value_and_large_ones_are_not`] (ADR-107).
8009    ///
8010    /// Both halves matter. The first is the optimization; the second is the
8011    /// branch a regression would silently delete, leaving every non-ASCII `Char`
8012    /// in the language reading slot `code` of a table that ends at 128.
8013    #[test]
8014    fn alloc_char_answers_one_object_per_ascii_code_point_and_a_large_one_still_allocates() {
8015        let mut rt = Runtime::new();
8016        let ctx = wired_ctx(&mut rt);
8017        // SAFETY: ctx is wired to rt throughout.
8018        unsafe {
8019            // In range: two calls, one object — and it is the runtime's own
8020            // table entry, not some other cache.
8021            let a = praxis_alloc_char(ctx, i64::from('a' as u32));
8022            let b = praxis_alloc_char(ctx, i64::from('a' as u32));
8023            assert_eq!(a.as_ptr(), b.as_ptr());
8024            assert_eq!(
8025                a.as_ptr(),
8026                rt.immortals().small_char('a' as u32).unwrap().as_ptr()
8027            );
8028            assert_eq!(a.as_char(), 'a');
8029
8030            // The ceiling is interned; one above it is not. There is no floor
8031            // case — the payload is unsigned and NUL is interned.
8032            let max = i64::from(crate::small_char::SMALL_CHAR_MAX);
8033            assert_eq!(
8034                praxis_alloc_char(ctx, max).as_ptr(),
8035                praxis_alloc_char(ctx, max).as_ptr(),
8036                "the last ASCII scalar is the edge of the range and must be interned"
8037            );
8038            assert_eq!(
8039                praxis_alloc_char(ctx, 0).as_ptr(),
8040                praxis_alloc_char(ctx, 0).as_ptr(),
8041                "NUL is the floor and must be interned"
8042            );
8043            for code in [max + 1, i64::from('é' as u32), 0x10_FFFF] {
8044                let x = praxis_alloc_char(ctx, code);
8045                let y = praxis_alloc_char(ctx, code);
8046                assert_ne!(
8047                    x.as_ptr(),
8048                    y.as_ptr(),
8049                    "{code:#x} is outside the range and must still allocate"
8050                );
8051                assert_eq!(
8052                    u32::from(x.as_char()),
8053                    code as u32,
8054                    "and still hold its code point"
8055                );
8056            }
8057
8058            // Distinct in-range code points are distinct objects: interning
8059            // shares an object across *calls*, never across values.
8060            assert_ne!(
8061                praxis_alloc_char(ctx, i64::from('a' as u32)).as_ptr(),
8062                praxis_alloc_char(ctx, i64::from('b' as u32)).as_ptr()
8063            );
8064
8065            // The validity rule is untouched by the table: an interned slot is
8066            // reached only after `checked_alloc_char` has approved the value, so
8067            // a code point that is not a scalar still faults.
8068            for bad in [-1_i64, 0xD800, 0x11_0000, 0x1_0000_0041] {
8069                let _ = praxis_alloc_char(ctx, bad);
8070                assert_eq!(
8071                    rt.take_fault(),
8072                    Some(FaultKind::InvalidChar),
8073                    "{bad:#x} is not a scalar value"
8074                );
8075            }
8076
8077            // The host helper and the ABI wrapper answer the same object, as
8078            // `Runtime::alloc_int`/`praxis_alloc_int` already do. This is what
8079            // makes "a `Char` is interned" one fact rather than two.
8080            assert_eq!(rt.alloc_char('a' as u32).as_ptr(), a.as_ptr());
8081            // …and the out-of-range halves still disagree as objects, which is
8082            // the same statement from the other side.
8083            assert_ne!(
8084                rt.alloc_char('é' as u32).as_ptr(),
8085                rt.alloc_char('é' as u32).as_ptr()
8086            );
8087        }
8088        unsafe { drop_ctx(ctx) };
8089    }
8090
8091    /// `praxis_text_get` is the interning's largest site: it is `t[i]` *and*
8092    /// every step of `for c in t`. It reaches [`char_ref`] directly rather than
8093    /// through [`checked_alloc_char`] — a Rust `char` needs no validity check —
8094    /// so it is its own door and must be pinned as one.
8095    ///
8096    /// `text_get_answers_a_char_object` covers the uninterned half (`é`) and the
8097    /// descriptor; this covers the identity.
8098    #[test]
8099    fn text_get_answers_the_interned_char() {
8100        let mut rt = Runtime::new();
8101        let ctx = wired_ctx(&mut rt);
8102        // SAFETY: ctx wired.
8103        unsafe {
8104            let s = "abca";
8105            let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
8106            let zero = praxis_alloc_int(ctx, 0);
8107            let three = praxis_alloc_int(ctx, 3);
8108            let first = praxis_text_get(ctx, text, zero);
8109            let last = praxis_text_get(ctx, text, three);
8110            assert!(!rt.has_pending_fault());
8111
8112            // Two reads of the same character are one object, and it is the same
8113            // object every other door answers.
8114            assert_eq!(first.as_ptr(), last.as_ptr());
8115            assert_eq!(
8116                first.as_ptr(),
8117                praxis_alloc_char(ctx, i64::from('a' as u32)).as_ptr()
8118            );
8119            assert_eq!(
8120                first.as_ptr(),
8121                rt.immortals().small_char('a' as u32).unwrap().as_ptr()
8122            );
8123            assert_eq!(first.as_char(), 'a');
8124
8125            // The non-ASCII half still allocates, and still answers the right
8126            // scalar — the branch a regression would delete.
8127            let u = "éé";
8128            let utext = praxis_alloc_text(ctx, u.as_ptr(), u.len());
8129            let one = praxis_alloc_int(ctx, 1);
8130            let x = praxis_text_get(ctx, utext, zero);
8131            let y = praxis_text_get(ctx, utext, one);
8132            assert_ne!(x.as_ptr(), y.as_ptr(), "`é` is outside the interned range");
8133            assert_eq!(x.as_char(), 'é');
8134            assert_eq!(y.as_char(), 'é');
8135        }
8136        unsafe { drop_ctx(ctx) };
8137    }
8138
8139    /// The two doors into [`checked_alloc_char`] answer the same object, which
8140    /// is that helper's whole reason for existing — a rule stated at both goes
8141    /// stale at one, and now the rule includes which object.
8142    #[test]
8143    fn int_to_char_answers_the_same_object_as_alloc_char() {
8144        let mut rt = Runtime::new();
8145        let ctx = wired_ctx(&mut rt);
8146        // SAFETY: ctx wired.
8147        unsafe {
8148            let code = praxis_alloc_int(ctx, i64::from('Z' as u32));
8149            let via_to_char = praxis_int_to_char(ctx, code);
8150            let via_alloc = praxis_alloc_char(ctx, i64::from('Z' as u32));
8151            assert!(!rt.has_pending_fault());
8152            assert_eq!(via_to_char.as_ptr(), via_alloc.as_ptr());
8153            assert_eq!(via_to_char.as_char(), 'Z');
8154
8155            // Outside the range both still allocate, and still not each other.
8156            let big = praxis_alloc_int(ctx, i64::from('é' as u32));
8157            assert_ne!(
8158                praxis_int_to_char(ctx, big).as_ptr(),
8159                praxis_alloc_char(ctx, i64::from('é' as u32)).as_ptr()
8160            );
8161        }
8162        unsafe { drop_ctx(ctx) };
8163    }
8164
8165    /// **ADR-143.** `Int.to_text()` answers exactly what `out` writes, and the
8166    /// assertion is against `out`'s own path rather than a literal.
8167    ///
8168    /// Comparing to `"1660"` would pass while the two renderers disagreed about
8169    /// everything else; comparing to `GcRef::format`'s output cannot, because
8170    /// that is the function `praxis_write_stdout` calls. `i64::MIN` is in the
8171    /// list because it is the one value whose negation does not fit, and
8172    /// therefore the first thing a hand-rolled renderer gets wrong.
8173    #[test]
8174    fn int_to_text_renders_exactly_what_out_renders() {
8175        let mut rt = Runtime::new();
8176        let ctx = wired_ctx(&mut rt);
8177        // SAFETY: ctx wired; every receiver is an Int.
8178        unsafe {
8179            for v in [0_i64, 1, -1, 1660, i64::MAX, i64::MIN, UNINTERNED] {
8180                let receiver = praxis_alloc_int(ctx, v);
8181                let answer = praxis_int_to_text(ctx, receiver);
8182                assert!(!rt.has_pending_fault(), "{v} faulted");
8183                let mut printed = String::new();
8184                receiver.format(&mut printed);
8185                assert_eq!(answer.as_text(), printed, "to_text and out disagree on {v}");
8186            }
8187        }
8188        unsafe { drop_ctx(ctx) };
8189    }
8190
8191    /// **ADR-143.** The same claim for `Char.to_text()`, at an interned ASCII
8192    /// character and an uninterned multi-byte one.
8193    ///
8194    /// The multi-byte case is the one that would catch reading the four-byte
8195    /// payload as an `i64`: `'é'` is `0xE9`, and eight bytes from a four-byte
8196    /// payload picks up whatever follows it.
8197    #[test]
8198    fn char_to_text_renders_exactly_what_out_renders() {
8199        let mut rt = Runtime::new();
8200        let ctx = wired_ctx(&mut rt);
8201        // SAFETY: ctx wired; every receiver is a Char.
8202        unsafe {
8203            for c in ['#', 'a', 'é', '☃', '\u{10FFFF}'] {
8204                let receiver = praxis_alloc_char(ctx, i64::from(u32::from(c)));
8205                let answer = praxis_char_to_text(ctx, receiver);
8206                assert!(!rt.has_pending_fault(), "{c} faulted");
8207                let mut printed = String::new();
8208                receiver.format(&mut printed);
8209                assert_eq!(answer.as_text(), printed, "to_text and out disagree on {c}");
8210                assert_eq!(answer.as_text(), c.to_string());
8211            }
8212        }
8213        unsafe { drop_ctx(ctx) };
8214    }
8215
8216    /// **ADR-147.** An interpolation hole renders exactly what `out` writes, for
8217    /// **every** type — including the ones with no `to_text()` row.
8218    ///
8219    /// This is the wrapper-level half of ADR-147 decision 2, and it is asserted
8220    /// against `GcRef::format` — `praxis_write_stdout`'s own call — rather than
8221    /// against a literal, for `int_to_text_renders_exactly_what_out_renders`'s
8222    /// reason: a literal comparison passes while the two agree by coincidence.
8223    ///
8224    /// The receivers deliberately span a scalar, a `Text` (whose rendering is
8225    /// its own characters and not a quoted form), a collection and a tuple, so a
8226    /// wrapper that reached for a scalar payload instead of the descriptor fails
8227    /// on the last two rather than on none of them.
8228    #[test]
8229    fn value_to_text_renders_exactly_what_out_renders() {
8230        let mut rt = Runtime::new();
8231        let ctx = wired_ctx(&mut rt);
8232        // SAFETY: ctx wired; every receiver below is freshly allocated here.
8233        unsafe {
8234            let empty = praxis_alloc_text(ctx, std::ptr::null(), 0);
8235            let hello = "hello";
8236            let text = praxis_alloc_text(ctx, hello.as_ptr(), hello.len());
8237            let vec = praxis_vec_new(ctx, &scalars::INT as *const _);
8238            for n in [1_i64, 2, 3] {
8239                let _ = praxis_vec_push(ctx, vec, praxis_alloc_int(ctx, n));
8240            }
8241            let receivers = [
8242                praxis_alloc_int(ctx, UNINTERNED),
8243                praxis_alloc_int(ctx, 0),
8244                praxis_alloc_bool(ctx, 1),
8245                praxis_alloc_char(ctx, i64::from(u32::from('☃'))),
8246                empty,
8247                text,
8248                vec,
8249            ];
8250            for receiver in receivers {
8251                let answer = praxis_value_to_text(ctx, receiver);
8252                assert!(!rt.has_pending_fault(), "value_to_text faulted");
8253                let mut printed = String::new();
8254                receiver.format(&mut printed);
8255                assert_eq!(
8256                    answer.as_text(),
8257                    printed,
8258                    "a hole and `out` must write the same characters"
8259                );
8260            }
8261            // The `Text` rows pin the shape a caller is most likely to assume
8262            // wrong: `out("hello")` writes `hello`, not `"hello"`, so `"{s}"`
8263            // must not add quotes either.
8264            assert_eq!(praxis_value_to_text(ctx, text).as_text(), "hello");
8265            assert_eq!(praxis_value_to_text(ctx, empty).as_text(), "");
8266        }
8267        unsafe { drop_ctx(ctx) };
8268    }
8269
8270    /// **ADR-144.** `join` puts the separator *between* elements and nowhere
8271    /// else, which is the whole of the specification and the whole of what an
8272    /// off-by-one gets wrong.
8273    #[test]
8274    fn vec_join_puts_the_separator_between_and_nowhere_else() {
8275        let mut rt = Runtime::new();
8276        let ctx = wired_ctx(&mut rt);
8277        // SAFETY: ctx wired; every element and separator is a Text.
8278        unsafe {
8279            let cases: [(&[&str], &str, &str); 5] = [
8280                (&[], ", ", ""),
8281                (&["only"], ", ", "only"),
8282                (&["a", "b", "c"], ", ", "a, b, c"),
8283                (&["a", "b", "c"], "", "abc"),
8284                (&["é", "☃"], " — ", "é — ☃"),
8285            ];
8286            for (items, sep, want) in cases {
8287                let members: Vec<GcRef> = items.iter().map(|s| rt.alloc_text(s)).collect();
8288                let vec = rt.alloc_vec(&crate::text::TEXT, members);
8289                let separator = rt.alloc_text(sep);
8290                let answer = praxis_vec_join(ctx, vec, separator);
8291                assert!(!rt.has_pending_fault(), "{items:?} faulted");
8292                assert_eq!(answer.as_text(), want);
8293            }
8294        }
8295        unsafe { drop_ctx(ctx) };
8296    }
8297
8298    /// **ADR-144.** A non-`Text` element is `TypeMismatch` and the Unit
8299    /// sentinel, not a `Text` payload read out of an `Int`.
8300    ///
8301    /// The catalog row's `Text` bound means only a compiler bug gets here, and
8302    /// this is what that bug looks like when it does: a fault the program can
8303    /// see, rather than a pointer-and-length pair read from eight bytes of
8304    /// integer.
8305    #[test]
8306    fn vec_join_refuses_a_non_text_element() {
8307        let mut rt = Runtime::new();
8308        let ctx = wired_ctx(&mut rt);
8309        // SAFETY: ctx wired.
8310        unsafe {
8311            let mixed = rt.alloc_vec(&scalars::INT, vec![rt.alloc_text("a"), rt.alloc_int(1)]);
8312            let sep = rt.alloc_text(",");
8313            let answer = praxis_vec_join(ctx, mixed, sep);
8314            assert!(rt.has_pending_fault());
8315            assert!(std::ptr::eq(answer.descriptor(), &scalars::UNIT));
8316        }
8317        unsafe { drop_ctx(ctx) };
8318    }
8319
8320    /// **ADR-144.** `Vec[Char].to_text()` is the characters with nothing between
8321    /// them, and it agrees with `out` on each of them for ADR-143's reason: it
8322    /// goes through `scalars::write_char` too.
8323    #[test]
8324    fn vec_to_text_renders_every_char() {
8325        let mut rt = Runtime::new();
8326        let ctx = wired_ctx(&mut rt);
8327        // SAFETY: ctx wired; every element is a Char.
8328        unsafe {
8329            for want in ["", ".", "..|", "héllo", "☃☃"] {
8330                let members: Vec<GcRef> = want
8331                    .chars()
8332                    .map(|c| praxis_alloc_char(ctx, i64::from(u32::from(c))))
8333                    .collect();
8334                let vec = rt.alloc_vec(&scalars::CHAR, members);
8335                let answer = praxis_vec_to_text(ctx, vec);
8336                assert!(!rt.has_pending_fault(), "{want:?} faulted");
8337                assert_eq!(answer.as_text(), want);
8338            }
8339        }
8340        unsafe { drop_ctx(ctx) };
8341    }
8342
8343    /// **ADR-144.** A non-`Char` element faults rather than being read as four
8344    /// bytes of something else.
8345    #[test]
8346    fn vec_to_text_refuses_a_non_char_element() {
8347        let mut rt = Runtime::new();
8348        let ctx = wired_ctx(&mut rt);
8349        // SAFETY: ctx wired.
8350        unsafe {
8351            let mixed = rt.alloc_vec(&scalars::CHAR, vec![rt.alloc_int(65)]);
8352            let answer = praxis_vec_to_text(ctx, mixed);
8353            assert!(rt.has_pending_fault());
8354            assert!(std::ptr::eq(answer.descriptor(), &scalars::UNIT));
8355        }
8356        unsafe { drop_ctx(ctx) };
8357    }
8358
8359    /// **ADR-145.** `reversed` answers a **new** `Vec` and leaves the receiver
8360    /// alone — the rule every barrier in this block states, and the one a
8361    /// wrapper that reversed in place would break invisibly for a caller still
8362    /// holding `v`.
8363    ///
8364    /// The empty case is here because `praxis_vec_sorted` needs a `len() > 1`
8365    /// guard for the analogous one and this needs none: there is no callback to
8366    /// avoid calling.
8367    #[test]
8368    fn vec_reversed_answers_a_new_vec_and_leaves_the_receiver_alone() {
8369        let mut rt = Runtime::new();
8370        let ctx = wired_ctx(&mut rt);
8371        // SAFETY: ctx wired.
8372        unsafe {
8373            let source = rt.alloc_vec(
8374                &scalars::INT,
8375                vec![rt.alloc_int(3), rt.alloc_int(1), rt.alloc_int(2)],
8376            );
8377            let answer = praxis_vec_reversed(ctx, source);
8378            assert!(!rt.has_pending_fault());
8379            let got: Vec<i64> = answer.as_vec().iter().map(|r| r.as_int()).collect();
8380            assert_eq!(got, vec![2, 1, 3]);
8381            let still: Vec<i64> = source.as_vec().iter().map(|r| r.as_int()).collect();
8382            assert_eq!(still, vec![3, 1, 2], "the receiver is not touched");
8383            assert_ne!(answer.as_ptr(), source.as_ptr());
8384
8385            let empty = rt.alloc_vec(&scalars::INT, vec![]);
8386            assert!(praxis_vec_reversed(ctx, empty).as_vec().is_empty());
8387            assert!(!rt.has_pending_fault());
8388        }
8389        unsafe { drop_ctx(ctx) };
8390    }
8391
8392    /// **ADR-145.** Reversal reads no descriptor callback, so a `Vec` of a type
8393    /// with no `compare` reverses where `sorted` faults.
8394    ///
8395    /// This is the runtime half of the catalog row carrying no capability bound.
8396    /// A `Unit` has no `compare` — `praxis_vec_sorted` raises `TypeMismatch` on
8397    /// one — and it reverses without a word.
8398    #[test]
8399    fn vec_reversed_needs_no_callback_where_sorted_needs_compare() {
8400        let mut rt = Runtime::new();
8401        let ctx = wired_ctx(&mut rt);
8402        // SAFETY: ctx wired.
8403        unsafe {
8404            let closures = rt.alloc_vec(
8405                &crate::closures::CLOSURE,
8406                vec![
8407                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8408                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8409                ],
8410            );
8411            assert_eq!(praxis_vec_reversed(ctx, closures).as_vec().len(), 2);
8412            assert!(!rt.has_pending_fault(), "reversal asks for no callback");
8413
8414            praxis_vec_sorted(ctx, closures);
8415            assert!(rt.has_pending_fault(), "ordering still asks for `compare`");
8416        }
8417        unsafe { drop_ctx(ctx) };
8418    }
8419
8420    /// The shape of both groupings, read back as nested `Int`s.
8421    ///
8422    /// # Safety
8423    /// `answer` must be a valid `Vec[Vec[Int]]` `GcRef`.
8424    unsafe fn groups_of_int(answer: GcRef) -> Vec<Vec<i64>> {
8425        answer
8426            .as_vec()
8427            .iter()
8428            .map(|inner| inner.as_vec().iter().map(|r| r.as_int()).collect())
8429            .collect()
8430    }
8431
8432    /// **ADR-149.** `chunks` partitions: every element appears once, in order,
8433    /// and a length the size does not divide leaves a *short last chunk* rather
8434    /// than dropping the tail or padding it.
8435    #[test]
8436    fn vec_chunks_partitions_and_keeps_a_short_tail() {
8437        let mut rt = Runtime::new();
8438        let ctx = wired_ctx(&mut rt);
8439        // SAFETY: ctx wired.
8440        unsafe {
8441            let ints: Vec<GcRef> = (1..=5).map(|n| rt.alloc_int(n)).collect();
8442            let source = rt.alloc_vec(&scalars::INT, ints);
8443
8444            let two = rt.alloc_int(2);
8445            let answer = praxis_vec_chunks(ctx, source, two);
8446            assert!(!rt.has_pending_fault());
8447            assert_eq!(groups_of_int(answer), vec![vec![1, 2], vec![3, 4], vec![5]]);
8448
8449            // A size that divides leaves no short chunk, which is the same rule
8450            // and is worth pinning beside the one that does.
8451            let five = rt.alloc_int(5);
8452            assert_eq!(
8453                groups_of_int(praxis_vec_chunks(ctx, source, five)),
8454                vec![vec![1, 2, 3, 4, 5]],
8455            );
8456
8457            // Wider than the receiver is not a fault: it is one short chunk.
8458            let nine = rt.alloc_int(9);
8459            assert_eq!(
8460                groups_of_int(praxis_vec_chunks(ctx, source, nine)),
8461                vec![vec![1, 2, 3, 4, 5]],
8462            );
8463
8464            let still: Vec<i64> = source.as_vec().iter().map(|r| r.as_int()).collect();
8465            assert_eq!(still, vec![1, 2, 3, 4, 5], "the receiver is not touched");
8466        }
8467        unsafe { drop_ctx(ctx) };
8468    }
8469
8470    /// **ADR-149.** `windows` slides by one and keeps only the runs that fit, so
8471    /// a receiver shorter than the size answers `[]` rather than one short run —
8472    /// the one place the two groupings differ.
8473    #[test]
8474    fn vec_windows_slide_by_one_and_drop_a_run_that_does_not_fit() {
8475        let mut rt = Runtime::new();
8476        let ctx = wired_ctx(&mut rt);
8477        // SAFETY: ctx wired.
8478        unsafe {
8479            let ints: Vec<GcRef> = (1..=4).map(|n| rt.alloc_int(n)).collect();
8480            let source = rt.alloc_vec(&scalars::INT, ints);
8481
8482            let two = rt.alloc_int(2);
8483            assert_eq!(
8484                groups_of_int(praxis_vec_windows(ctx, source, two)),
8485                vec![vec![1, 2], vec![2, 3], vec![3, 4]],
8486            );
8487            assert!(!rt.has_pending_fault());
8488
8489            // Exactly the length is one window; one past it is none. Off by one
8490            // here is the whole difference between `[]` and a wrong answer.
8491            let four = rt.alloc_int(4);
8492            assert_eq!(
8493                groups_of_int(praxis_vec_windows(ctx, source, four)),
8494                vec![vec![1, 2, 3, 4]],
8495            );
8496            let five = rt.alloc_int(5);
8497            let none = praxis_vec_windows(ctx, source, five);
8498            assert!(
8499                none.as_vec().is_empty(),
8500                "a run of five does not fit in four"
8501            );
8502            assert!(
8503                !rt.has_pending_fault(),
8504                "not fitting is an answer, not a fault"
8505            );
8506
8507            // Windows share their elements rather than copying them, which is
8508            // the language's reference semantics and not a rule of this wrapper.
8509            let answer = praxis_vec_windows(ctx, source, two);
8510            let first = answer.as_vec()[0].as_vec()[1].as_ptr();
8511            let second = answer.as_vec()[1].as_vec()[0].as_ptr();
8512            assert_eq!(first, second, "the overlapping element is one object");
8513        }
8514        unsafe { drop_ctx(ctx) };
8515    }
8516
8517    /// **ADR-149.** The only thing either grouping refuses: a run of `n <= 0` is
8518    /// not a short run, it is not a run, so there is no sequence of them to
8519    /// answer with.
8520    ///
8521    /// The empty receiver is here beside it because it is the case that looks
8522    /// like a fault and is not — `[].chunks(2)` is `[]`, the same way `[]`
8523    /// reverses to `[]`.
8524    #[test]
8525    fn a_group_size_of_zero_or_less_is_an_invalid_size_fault() {
8526        for size in [0i64, -1, i64::MIN] {
8527            for (name, wrapper) in [
8528                (
8529                    "chunks",
8530                    praxis_vec_chunks as unsafe extern "C" fn(_, _, _) -> _,
8531                ),
8532                ("windows", praxis_vec_windows),
8533            ] {
8534                let mut rt = Runtime::new();
8535                let ctx = wired_ctx(&mut rt);
8536                // SAFETY: ctx wired.
8537                unsafe {
8538                    let source = rt.alloc_vec(&scalars::INT, vec![rt.alloc_int(1)]);
8539                    let n = rt.alloc_int(size);
8540                    let answer = wrapper(ctx, source, n);
8541                    assert!(rt.has_pending_fault(), "{name}({size}) must fault");
8542                    assert_eq!(rt.fault(), crate::FaultKind::InvalidSize, "{name}({size})");
8543                    assert!(
8544                        std::ptr::eq(answer.descriptor(), &scalars::UNIT),
8545                        "{name}({size}) answers the Unit sentinel"
8546                    );
8547                }
8548                unsafe { drop_ctx(ctx) };
8549            }
8550        }
8551
8552        let mut rt = Runtime::new();
8553        let ctx = wired_ctx(&mut rt);
8554        // SAFETY: ctx wired.
8555        unsafe {
8556            let empty = rt.alloc_vec(&scalars::INT, vec![]);
8557            let two = rt.alloc_int(2);
8558            assert!(praxis_vec_chunks(ctx, empty, two).as_vec().is_empty());
8559            assert!(praxis_vec_windows(ctx, empty, two).as_vec().is_empty());
8560            assert!(
8561                !rt.has_pending_fault(),
8562                "an empty receiver is an empty answer"
8563            );
8564        }
8565        unsafe { drop_ctx(ctx) };
8566    }
8567
8568    /// **ADR-149 decision 1.** The outer `Vec` is labelled `VEC` at every length
8569    /// and the inner ones carry the receiver's element descriptor.
8570    ///
8571    /// The **empty** answer is the whole reason this test exists, and it is the
8572    /// only part that is a choice: `VEC` is what `outer.push(inner)` already
8573    /// produces, so a non-empty grouping could hardly answer anything else and
8574    /// asserting it proves little. With the label inferred from the first group
8575    /// there would be none to read, and `[1, 2].windows(5)` would carry a null
8576    /// where `[1, 2].windows(2)` carries `VEC` — one type with two labels, and
8577    /// the null is the one `vec_format` renders as `[]` and `push` treats as
8578    /// "adopt whatever arrives".
8579    #[test]
8580    fn a_grouping_labels_the_outer_vec_even_when_it_is_empty() {
8581        let mut rt = Runtime::new();
8582        let ctx = wired_ctx(&mut rt);
8583        // SAFETY: ctx wired.
8584        unsafe {
8585            let source = rt.alloc_vec(&scalars::INT, vec![rt.alloc_int(1), rt.alloc_int(2)]);
8586            let two = rt.alloc_int(2);
8587            let five = rt.alloc_int(5);
8588
8589            for answer in [
8590                praxis_vec_chunks(ctx, source, two),
8591                praxis_vec_windows(ctx, source, two),
8592                // The two that come out empty, and the reason this test exists.
8593                praxis_vec_windows(ctx, source, five),
8594                praxis_vec_chunks(ctx, rt.alloc_vec(&scalars::INT, vec![]), two),
8595            ] {
8596                let p = vec_payload(answer);
8597                assert!(
8598                    std::ptr::eq(p.element_descriptor, &crate::collections::VEC),
8599                    "the outer Vec holds Vecs whether or not it holds any"
8600                );
8601                for inner in p.items.iter() {
8602                    assert!(std::ptr::eq(
8603                        vec_payload(*inner).element_descriptor,
8604                        &scalars::INT
8605                    ));
8606                }
8607            }
8608        }
8609        unsafe { drop_ctx(ctx) };
8610    }
8611
8612    /// **ADR-149.** A grouping reads no descriptor callback, so a `Vec` of a
8613    /// type with no `compare` groups where `sorted` faults — `reversed`'s claim,
8614    /// and the runtime half of these two rows carrying no capability bound.
8615    #[test]
8616    fn a_grouping_needs_no_callback_where_sorted_needs_compare() {
8617        let mut rt = Runtime::new();
8618        let ctx = wired_ctx(&mut rt);
8619        // SAFETY: ctx wired.
8620        unsafe {
8621            let closures = rt.alloc_vec(
8622                &crate::closures::CLOSURE,
8623                vec![
8624                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8625                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8626                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8627                ],
8628            );
8629            let two = rt.alloc_int(2);
8630            assert_eq!(praxis_vec_chunks(ctx, closures, two).as_vec().len(), 2);
8631            assert_eq!(praxis_vec_windows(ctx, closures, two).as_vec().len(), 2);
8632            assert!(!rt.has_pending_fault(), "grouping asks for no callback");
8633
8634            praxis_vec_sorted(ctx, closures);
8635            assert!(rt.has_pending_fault(), "ordering still asks for `compare`");
8636        }
8637        unsafe { drop_ctx(ctx) };
8638    }
8639
8640    /// **ADR-107's pacing half, and ADR-100 §3's analogue.** [`char_ref`] must
8641    /// give the collector its turn on the path where it allocates *nothing*.
8642    ///
8643    /// `TextGet` is `AllocatesAndFaults` in the manifest, which is generated
8644    /// code's contract that the call site is a GC safepoint. A `for c in line`
8645    /// loop over ASCII touches nothing else that bumps the pacing counter, and
8646    /// the counter is the collector's only trigger — so an early return here
8647    /// would make such a loop run arbitrarily long with no collection at all.
8648    ///
8649    /// **The interleaved allocation is load-bearing and must stay unpaced.** The
8650    /// observable is the live registry *shrinking*, and an interned `Char` never
8651    /// enters it, so a loop of nothing but `praxis_text_get` could not shrink
8652    /// anything however well it paced — a guaranteed false pass (see
8653    /// `UNINTERNED`). `Runtime::alloc_int` is the one helper that grows the heap
8654    /// **without** pacing, so it supplies the pressure and the population while
8655    /// leaving `praxis_text_get` as the only safepoint in the loop. Swapping it
8656    /// for `praxis_alloc_int` would make the test pass with this function's
8657    /// safepoint deleted.
8658    #[test]
8659    fn char_ref_paces_the_collector_even_when_it_answers_from_the_table() {
8660        let mut rt = Runtime::new();
8661        let ctx = wired_ctx(&mut rt);
8662        // SAFETY: ctx wired throughout; `text` and `index` are rooted below.
8663        unsafe {
8664            let s = "abcdefgh";
8665            let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
8666            let index = praxis_alloc_int(ctx, 3);
8667            let mut frame = push_frame(ctx, SlotCount::new(2).unwrap());
8668            frame.set(0, text);
8669            frame.set(1, index);
8670
8671            let mut before = rt.heap().stats().live_count;
8672            let mut paced = false;
8673            for i in 0..100_000_i64 {
8674                // Registered, unrooted, and *unpaced*: pressure the collector can
8675                // see and reclaim, contributed by something that never offers a
8676                // turn itself.
8677                let _ = rt.alloc_int(UNINTERNED + i);
8678                // The wrapper under test. Every character of `s` is ASCII, so
8679                // this answers from the table and allocates nothing.
8680                let c = praxis_text_get(ctx, text, index);
8681                assert_eq!(c.as_char(), 'd');
8682                let after = rt.heap().stats().live_count;
8683                if after < before.saturating_add(1) {
8684                    paced = true;
8685                    break;
8686                }
8687                before = after;
8688            }
8689            drop(frame);
8690            assert!(
8691                paced,
8692                "praxis_text_get never gave the collector a turn on the interned path"
8693            );
8694        }
8695        unsafe { drop_ctx(ctx) };
8696    }
8697
8698    /// `default_cell`'s `Char` arm, the fourth boxing site. A `Grid[Char]`'s
8699    /// fill is NUL, which is inside the range.
8700    #[test]
8701    fn a_grid_of_char_fills_with_the_interned_nul() {
8702        let mut rt = Runtime::new();
8703        let ctx = wired_ctx(&mut rt);
8704        // SAFETY: ctx wired.
8705        unsafe {
8706            let grid = praxis_grid_new(ctx, &crate::scalars::CHAR, 3, 2);
8707            assert!(!rt.has_pending_fault());
8708            let nul = rt.immortals().small_char(0).expect("NUL is interned");
8709            for y in 0..2 {
8710                for x in 0..3 {
8711                    let xi = praxis_alloc_int(ctx, x);
8712                    let yi = praxis_alloc_int(ctx, y);
8713                    let cell = praxis_grid_get(ctx, grid, xi, yi);
8714                    assert_eq!(
8715                        cell.as_ptr(),
8716                        nul.as_ptr(),
8717                        "every cell of a fresh Grid[Char] is the one interned NUL"
8718                    );
8719                }
8720            }
8721        }
8722        unsafe { drop_ctx(ctx) };
8723    }
8724
8725    /// The executable form of "nothing in the language can observe `Char`
8726    /// identity", and the [`crate::dynamic_key::DynamicKey`] leg of ADR-107's
8727    /// argument.
8728    ///
8729    /// `DynamicKey::eq` opens with a pointer comparison, and that is the line
8730    /// interning could in principle have moved — but it is a fast path *for*
8731    /// structural equality and `char_equals` is a reflexive `u32 ==`, so sharing
8732    /// can only make it fire more often. This asserts the consequence rather than
8733    /// the argument: the same shape is run twice, once with interned keys and
8734    /// once with keys the runtime does not intern, and the two must agree.
8735    #[test]
8736    fn interning_a_char_does_not_change_keyed_collection_behaviour() {
8737        // (key, a different key, label): once inside the ASCII range and once
8738        // outside it. `é` and `ü` are two scalars the table does not hold.
8739        for (a_ch, b_ch, label) in [('a', 'b', "interned"), ('é', 'ü', "allocated")] {
8740            let mut rt = Runtime::new();
8741            let ctx = wired_ctx(&mut rt);
8742            // SAFETY: ctx wired; every ref below comes from the ABI.
8743            unsafe {
8744                let a = praxis_alloc_char(ctx, i64::from(a_ch as u32));
8745                // A *second* reference to the same value, built separately.
8746                // Interned it is `a`; uninterned it is a different object with
8747                // the same payload. Both must key the same slot.
8748                let a_again = praxis_alloc_char(ctx, i64::from(a_ch as u32));
8749                let b = praxis_alloc_char(ctx, i64::from(b_ch as u32));
8750                assert_eq!(
8751                    std::ptr::eq(a.as_ptr(), a_again.as_ptr()),
8752                    label == "interned",
8753                    "the fixture must actually be {label}"
8754                );
8755
8756                let set = praxis_set_new(ctx, &crate::scalars::CHAR);
8757                let _ = praxis_set_insert(ctx, set, a);
8758                assert_eq!(
8759                    praxis_bool_load(ctx, praxis_set_contains(ctx, set, a_again)),
8760                    1,
8761                    "{label}: an equal Char is the same set member"
8762                );
8763                assert_eq!(
8764                    praxis_bool_load(ctx, praxis_set_contains(ctx, set, b)),
8765                    0,
8766                    "{label}: a different Char is not"
8767                );
8768                // Inserting the equal-but-possibly-distinct object must not add
8769                // a second member — the property that would break if the pointer
8770                // fast path and `char_equals` ever disagreed.
8771                let _ = praxis_set_insert(ctx, set, a_again);
8772                assert_eq!(praxis_int_load(ctx, praxis_set_len(ctx, set)), 1, "{label}");
8773
8774                let counter = praxis_counter_new(ctx, &crate::scalars::CHAR);
8775                let _ = praxis_counter_inc(ctx, counter, a);
8776                let _ = praxis_counter_inc(ctx, counter, a_again);
8777                assert_eq!(
8778                    praxis_int_load(ctx, praxis_counter_get(ctx, counter, a)),
8779                    2,
8780                    "{label}: two bumps of an equal key are one key"
8781                );
8782                assert_eq!(
8783                    praxis_int_load(ctx, praxis_counter_len(ctx, counter)),
8784                    1,
8785                    "{label}"
8786                );
8787            }
8788            unsafe { drop_ctx(ctx) };
8789        }
8790    }
8791
8792    /// An interned `Char` is never registered, so a collection cannot reclaim it
8793    /// however unrooted it is — the `Char` half of
8794    /// [`an_interned_int_survives_collection_unrooted`].
8795    #[test]
8796    fn an_interned_char_survives_collection_unrooted() {
8797        let mut rt = Runtime::new();
8798        let ctx = wired_ctx(&mut rt);
8799        // SAFETY: ctx is wired to rt throughout.
8800        unsafe {
8801            let _ = praxis_alloc_char(ctx, i64::from('q' as u32));
8802        }
8803        assert_eq!(
8804            rt.heap().stats().live_count,
8805            0,
8806            "an interned Char must not enter the live registry"
8807        );
8808        // Nothing roots `'q'`: no shadow frame, no native scope, no Rust local
8809        // the collector can see. A registered object here would be swept.
8810        rt.collect_now();
8811        // SAFETY: ctx is still wired; the reference must still be readable.
8812        unsafe {
8813            let q = praxis_alloc_char(ctx, i64::from('q' as u32));
8814            assert!(!q.header().is_poisoned(), "an immortal is never swept");
8815            assert_eq!(q.as_char(), 'q');
8816        }
8817        unsafe { drop_ctx(ctx) };
8818    }
8819
8820    /// An interned `Int` is never registered, so a collection cannot reclaim it
8821    /// however unrooted it is. The `Int` analogue of
8822    /// `runtime_collect_keeps_immortals_alive_unrooted`.
8823    #[test]
8824    fn an_interned_int_survives_collection_unrooted() {
8825        let mut rt = Runtime::new();
8826        let ctx = wired_ctx(&mut rt);
8827        // SAFETY: ctx is wired to rt throughout.
8828        unsafe {
8829            let _ = praxis_alloc_int(ctx, 5);
8830        }
8831        assert_eq!(
8832            rt.heap().stats().live_count,
8833            0,
8834            "an interned Int must not enter the live registry"
8835        );
8836        // Nothing roots `5`: no shadow frame, no native scope, no Rust local the
8837        // collector can see. A registered object here would be swept.
8838        rt.collect_now();
8839        // SAFETY: ctx is still wired; the reference must still be readable.
8840        unsafe {
8841            let five = praxis_alloc_int(ctx, 5);
8842            assert!(!five.header().is_poisoned(), "an immortal is never swept");
8843            assert_eq!(praxis_int_load(ctx, five), 5);
8844        }
8845        unsafe { drop_ctx(ctx) };
8846    }
8847
8848    /// The executable form of "nothing in the language can observe `Int`
8849    /// identity": the three keyed collections must behave identically whether
8850    /// their keys are shared objects or distinct ones.
8851    ///
8852    /// [`crate::dynamic_key::DynamicKey`]'s `eq` opens with a pointer
8853    /// comparison, and that is the line interning could in principle have moved
8854    /// — but it is a fast path *for* structural equality and `int_equals` is
8855    /// reflexive, so sharing can only make it fire more often. This asserts the
8856    /// consequence rather than the argument: every operation below is run twice
8857    /// at the same shape, once with interned keys and once with uninterned ones,
8858    /// and the two must agree.
8859    #[test]
8860    fn interning_does_not_change_keyed_collection_behaviour() {
8861        // (key_a, key_b) pairs: two distinct keys, once inside the interned
8862        // range and once outside it.
8863        for (a_val, b_val, label) in [
8864            (5_i64, 6_i64, "interned"),
8865            (UNINTERNED, UNINTERNED + 1, "allocated"),
8866        ] {
8867            let mut rt = Runtime::new();
8868            let ctx = wired_ctx(&mut rt);
8869            // SAFETY: ctx wired; every ref below comes from the ABI.
8870            unsafe {
8871                let a = praxis_alloc_int(ctx, a_val);
8872                // A *second* reference to the same value, allocated separately.
8873                // Interned it is `a`; uninterned it is a different object with
8874                // the same payload. Both must key the same slot.
8875                let a_again = praxis_alloc_int(ctx, a_val);
8876                let b = praxis_alloc_int(ctx, b_val);
8877
8878                let map = praxis_map_new(ctx, &scalars::INT);
8879                let one = praxis_alloc_int(ctx, 1);
8880                let _ = praxis_map_insert(ctx, map, a, one);
8881                // `map_index` rather than `map_get`: the subscript answers the
8882                // value where `.get` answers an `Option` (ADR-076), and the
8883                // value is what has to match.
8884                assert_eq!(
8885                    praxis_int_load(ctx, praxis_map_index(ctx, map, a_again)),
8886                    1,
8887                    "{label}: an equal key must find the entry"
8888                );
8889                assert_eq!(
8890                    rt.fault(),
8891                    FaultKind::None,
8892                    "{label}: an equal key is a present key"
8893                );
8894                assert_eq!(
8895                    praxis_bool_load(ctx, praxis_map_contains(ctx, map, b)),
8896                    0,
8897                    "{label}: a different key must not"
8898                );
8899                assert_eq!(praxis_int_load(ctx, praxis_map_len(ctx, map)), 1);
8900
8901                let set = praxis_set_new(ctx, &scalars::INT);
8902                let _ = praxis_set_insert(ctx, set, a);
8903                let _ = praxis_set_insert(ctx, set, a_again);
8904                assert_eq!(
8905                    praxis_int_load(ctx, praxis_set_len(ctx, set)),
8906                    1,
8907                    "{label}: re-inserting an equal value must not grow the set"
8908                );
8909                assert_eq!(praxis_bool_load(ctx, praxis_set_contains(ctx, set, b)), 0);
8910
8911                let counter = praxis_counter_new(ctx, &scalars::INT);
8912                let _ = praxis_counter_inc(ctx, counter, a);
8913                let _ = praxis_counter_inc(ctx, counter, a_again);
8914                assert_eq!(
8915                    praxis_int_load(ctx, praxis_counter_get(ctx, counter, a)),
8916                    2,
8917                    "{label}: two bumps of an equal key are two bumps of one key"
8918                );
8919                assert_eq!(praxis_int_load(ctx, praxis_counter_len(ctx, counter)), 1);
8920            }
8921            unsafe { drop_ctx(ctx) };
8922        }
8923    }
8924
8925    #[test]
8926    fn bool_and_unit_abi_allocations_reuse_runtime_singletons() {
8927        let mut rt = Runtime::new();
8928        let ctx = wired_ctx(&mut rt);
8929        let (true_ref, false_ref, unit_ref) = unsafe {
8930            (
8931                praxis_alloc_bool(ctx, 1),
8932                praxis_alloc_bool(ctx, 0),
8933                praxis_alloc_unit(ctx),
8934            )
8935        };
8936        let expected = (
8937            rt.immortals().true_(),
8938            rt.immortals().false_(),
8939            rt.immortals().unit(),
8940        );
8941        unsafe { drop_ctx(ctx) };
8942
8943        assert_eq!(true_ref.as_ptr(), expected.0.as_ptr());
8944        assert_eq!(false_ref.as_ptr(), expected.1.as_ptr());
8945        assert_eq!(unit_ref.as_ptr(), expected.2.as_ptr());
8946    }
8947
8948    #[test]
8949    fn repeated_bool_allocation_mints_no_new_objects() {
8950        // A fresh *immortal* per call would be unregistered storage no
8951        // collection can reclaim, leaking one Bool per loop iteration. There
8952        // are two Bools; a hundred calls must name two objects.
8953        let mut rt = Runtime::new();
8954        let ctx = wired_ctx(&mut rt);
8955        let mut seen = std::collections::HashSet::new();
8956        // SAFETY: ctx wired.
8957        unsafe {
8958            for i in 0..100_i64 {
8959                seen.insert(praxis_alloc_bool(ctx, i % 2).as_ptr());
8960                seen.insert(praxis_alloc_unit(ctx).as_ptr());
8961            }
8962        }
8963        unsafe { drop_ctx(ctx) };
8964        assert_eq!(seen.len(), 3, "true, false and unit — and nothing else");
8965    }
8966
8967    /// Every wrapper that answers a *predicate* hands back one of the two `Bool`
8968    /// singletons — the comparisons and the `is_empty`/`contains` family, which
8969    /// are what a real program calls in a loop. It is also what makes their
8970    /// `Effect::Pure` rows honest: nothing here can collect, so the call site is
8971    /// not a safepoint.
8972    #[test]
8973    fn predicate_wrappers_return_bool_singletons_and_allocate_nothing() {
8974        let mut rt = Runtime::new();
8975        let ctx = wired_ctx(&mut rt);
8976        let (immortal_true, immortal_false) = (rt.immortals().true_(), rt.immortals().false_());
8977        // SAFETY: ctx wired; every argument below is allocated through the ABI.
8978        unsafe {
8979            let one = praxis_alloc_int(ctx, 1);
8980            let two = praxis_alloc_int(ctx, 2);
8981            let empty_vec = praxis_vec_new(ctx, &scalars::INT);
8982            let empty_text = praxis_alloc_text(ctx, std::ptr::null(), 0);
8983            let live_before = rt.heap().stats().live_count;
8984
8985            let answers = [
8986                (praxis_int_eq(ctx, one, two), false),
8987                (praxis_int_ne(ctx, one, two), true),
8988                (praxis_int_lt(ctx, one, two), true),
8989                (praxis_int_gt(ctx, one, two), false),
8990                (praxis_int_le(ctx, one, one), true),
8991                (praxis_int_ge(ctx, one, two), false),
8992                (praxis_vec_is_empty(ctx, empty_vec), true),
8993                (praxis_text_is_empty(ctx, empty_text), true),
8994            ];
8995
8996            assert_eq!(
8997                rt.heap().stats().live_count,
8998                live_before,
8999                "a predicate wrapper must not allocate"
9000            );
9001            for (answer, expected) in answers {
9002                let want = if expected {
9003                    immortal_true
9004                } else {
9005                    immortal_false
9006                };
9007                assert_eq!(
9008                    answer.as_ptr(),
9009                    want.as_ptr(),
9010                    "predicate answered with a fresh Bool instead of the singleton"
9011                );
9012            }
9013        }
9014        unsafe { drop_ctx(ctx) };
9015    }
9016
9017    /// Every wrapper that boxes a *derived* scalar — `Text` construction, the
9018    /// `.len()` family, `Grid` extents, checked arithmetic — paces the
9019    /// collector. Without that, a program whose pressure comes from those (a
9020    /// text-processing loop, say) could run arbitrarily long with the collector
9021    /// never offered a turn. Each is driven here until its own pacing collects.
9022    ///
9023    /// **Every receiver is sized past the interned range on purpose.** The first
9024    /// four wrappers answer a *length*, and a length inside
9025    /// [`crate::small_int`]'s range is an immortal that never enters the live
9026    /// registry — so with a five-byte `Text` or a 2×2 `Grid` the shrink test
9027    /// below is true on the first iteration and the test passes without a
9028    /// collection ever running (see `UNINTERNED`). Interning removes the
9029    /// allocation, not the pacing, and it is the pacing this test is about; the
9030    /// oversized receivers are what keep the observable in place.
9031    #[test]
9032    fn every_scalar_boxing_wrapper_paces_the_collector() {
9033        // (name, a closure that performs one allocating call)
9034        type Call = unsafe extern "C" fn(*mut RuntimeContext, GcRef) -> GcRef;
9035        let cases: [(&str, Call); 7] = [
9036            ("praxis_text_len", praxis_text_len),
9037            ("praxis_vec_len", praxis_vec_len),
9038            ("praxis_grid_width", praxis_grid_width),
9039            ("praxis_grid_height", praxis_grid_height),
9040            ("praxis_float_to_text", praxis_float_to_text),
9041            ("praxis_int_to_text", praxis_int_to_text),
9042            ("praxis_char_to_text", praxis_char_to_text),
9043        ];
9044        // One past the interned range, in whatever unit the receiver measures.
9045        let big = UNINTERNED as usize;
9046        for (name, call) in cases {
9047            let mut rt = Runtime::new();
9048            let ctx = wired_ctx(&mut rt);
9049            // SAFETY: ctx wired; each receiver matches its wrapper.
9050            unsafe {
9051                let text = "x".repeat(big);
9052                let receiver = match name {
9053                    "praxis_text_len" => praxis_alloc_text(ctx, text.as_ptr(), big),
9054                    "praxis_vec_len" => {
9055                        // The elements are all the same interned `0`, so the Vec
9056                        // costs one allocation regardless of its length — only
9057                        // its `len()` matters here.
9058                        rt.alloc_vec(&scalars::INT, vec![rt.alloc_int(0); big])
9059                    }
9060                    "praxis_float_to_text" => praxis_alloc_float(ctx, 1.5_f64.to_bits() as i64),
9061                    // The two `to_text` rows answer a fresh owned `Text` every
9062                    // call whatever the receiver is, so an interned receiver is
9063                    // the honest case: the allocation is the *answer*, not the
9064                    // argument.
9065                    "praxis_int_to_text" => praxis_alloc_int(ctx, big as i64),
9066                    "praxis_char_to_text" => praxis_alloc_char(ctx, i64::from(u32::from('e'))),
9067                    // `width` reads the first dimension and `height` the second,
9068                    // so each case makes *its own* answer uninterned and leaves
9069                    // the other dimension at one cell.
9070                    "praxis_grid_width" => praxis_grid_new(ctx, &scalars::INT, big as i64, 1),
9071                    _ => praxis_grid_new(ctx, &scalars::INT, 1, big as i64),
9072                };
9073                let mut frame = push_frame(ctx, SlotCount::new(1).unwrap());
9074                frame.set(0, receiver);
9075
9076                let mut before = rt.heap().stats().live_count;
9077                let mut paced = false;
9078                for _ in 0..10_000 {
9079                    let _ = call(ctx, receiver);
9080                    let after = rt.heap().stats().live_count;
9081                    if after < before.saturating_add(1) {
9082                        paced = true;
9083                        break;
9084                    }
9085                    before = after;
9086                }
9087                drop(frame);
9088                assert!(paced, "{name} never gave the collector a turn");
9089            }
9090            unsafe { drop_ctx(ctx) };
9091        }
9092    }
9093
9094    #[test]
9095    fn checked_add_returns_sum() {
9096        let mut rt = Runtime::new();
9097        let ctx = wired_ctx(&mut rt);
9098        // SAFETY: ctx wired; operands allocated as Ints.
9099        unsafe {
9100            let a = praxis_alloc_int(ctx, 40);
9101            let b = praxis_alloc_int(ctx, 2);
9102            let s = praxis_int_add(ctx, a, b);
9103            assert_eq!(praxis_int_load(ctx, s), 42);
9104            assert!(!rt.has_pending_fault());
9105        }
9106        unsafe { drop_ctx(ctx) };
9107    }
9108
9109    #[test]
9110    fn float_sign_of_zero_is_zero() {
9111        let mut rt = Runtime::new();
9112        let ctx = wired_ctx(&mut rt);
9113        let signed = unsafe {
9114            let zero = praxis_alloc_float(ctx, 0.0_f64.to_bits() as i64);
9115            let result = praxis_float_sign(ctx, zero);
9116            f64::from_bits(praxis_float_load(ctx, result) as u64)
9117        };
9118        unsafe { drop_ctx(ctx) };
9119
9120        assert_eq!(signed, 0.0);
9121    }
9122
9123    /// `-0.0` is still zero: `signum` reports the sign *bit* and would answer
9124    /// `-1.0` here.
9125    #[test]
9126    fn float_sign_of_negative_zero_is_zero() {
9127        let mut rt = Runtime::new();
9128        let ctx = wired_ctx(&mut rt);
9129        let signed = unsafe {
9130            let zero = praxis_alloc_float(ctx, (-0.0_f64).to_bits() as i64);
9131            let result = praxis_float_sign(ctx, zero);
9132            f64::from_bits(praxis_float_load(ctx, result) as u64)
9133        };
9134        unsafe { drop_ctx(ctx) };
9135
9136        assert_eq!(signed, 0.0);
9137    }
9138
9139    #[test]
9140    fn float_sign_of_nan_is_nan() {
9141        let mut rt = Runtime::new();
9142        let ctx = wired_ctx(&mut rt);
9143        let signed = unsafe {
9144            let nan = praxis_alloc_float(ctx, f64::NAN.to_bits() as i64);
9145            let result = praxis_float_sign(ctx, nan);
9146            f64::from_bits(praxis_float_load(ctx, result) as u64)
9147        };
9148        unsafe { drop_ctx(ctx) };
9149
9150        assert!(signed.is_nan());
9151    }
9152
9153    /// `min`/`max`/`clamp` hand back **the reference they were given**, not an
9154    /// equal copy (ADR-058). That is what makes them `Effect::Pure` — no
9155    /// allocation, so their call site is not a safepoint — and a version that
9156    /// allocated would pass every value test while quietly making three of the
9157    /// seven helpers collect.
9158    #[test]
9159    fn the_selecting_helpers_return_an_operand_and_allocate_nothing() {
9160        let mut rt = Runtime::new();
9161        let ctx = wired_ctx(&mut rt);
9162        // SAFETY: ctx wired; every operand is a valid Int.
9163        unsafe {
9164            let lo = praxis_alloc_int(ctx, 3);
9165            let hi = praxis_alloc_int(ctx, 7);
9166            assert_eq!(praxis_int_min(ctx, lo, hi).as_ptr(), lo.as_ptr());
9167            assert_eq!(praxis_int_min(ctx, hi, lo).as_ptr(), lo.as_ptr());
9168            assert_eq!(praxis_int_max(ctx, lo, hi).as_ptr(), hi.as_ptr());
9169            assert_eq!(praxis_int_max(ctx, hi, lo).as_ptr(), hi.as_ptr());
9170            // Equal operands pick the left one — arbitrary but fixed, so the
9171            // choice is a decision and not a coin flip.
9172            let three = praxis_alloc_int(ctx, 3);
9173            assert_eq!(praxis_int_min(ctx, lo, three).as_ptr(), lo.as_ptr());
9174            assert_eq!(praxis_int_max(ctx, lo, three).as_ptr(), lo.as_ptr());
9175            // `clamp` returns whichever of its three operands is the answer.
9176            let v = praxis_alloc_int(ctx, 5);
9177            assert_eq!(praxis_int_clamp(ctx, v, lo, hi).as_ptr(), v.as_ptr());
9178            let below = praxis_alloc_int(ctx, 1);
9179            assert_eq!(praxis_int_clamp(ctx, below, lo, hi).as_ptr(), lo.as_ptr());
9180            let above = praxis_alloc_int(ctx, 9);
9181            assert_eq!(praxis_int_clamp(ctx, above, lo, hi).as_ptr(), hi.as_ptr());
9182            assert!(!rt.has_pending_fault());
9183        }
9184        unsafe { drop_ctx(ctx) };
9185    }
9186
9187    /// An inverted `clamp` range is empty, so there is no operand to return and
9188    /// no answer that is not invented. It faults (ADR-058) and returns the Unit
9189    /// sentinel, like every other faulting wrapper.
9190    #[test]
9191    fn an_inverted_clamp_range_faults_rather_than_guessing() {
9192        let mut rt = Runtime::new();
9193        let ctx = wired_ctx(&mut rt);
9194        // SAFETY: ctx wired; every operand is a valid Int.
9195        unsafe {
9196            let v = praxis_alloc_int(ctx, 5);
9197            let lo = praxis_alloc_int(ctx, 10);
9198            let hi = praxis_alloc_int(ctx, 0);
9199            let r = praxis_int_clamp(ctx, v, lo, hi);
9200            assert!(rt.has_pending_fault());
9201            assert_eq!(rt.fault(), FaultKind::EmptyRange);
9202            assert_eq!(r.as_ptr(), rt.immortals().unit().as_ptr());
9203        }
9204        let _ = rt.take_fault();
9205        // A degenerate but *legal* range — one value wide — is not inverted.
9206        // SAFETY: ctx wired; every operand is a valid Int.
9207        unsafe {
9208            let v = praxis_alloc_int(ctx, 5);
9209            let same = praxis_alloc_int(ctx, 4);
9210            let r = praxis_int_clamp(ctx, v, same, same);
9211            assert!(!rt.has_pending_fault());
9212            assert_eq!(r.as_ptr(), same.as_ptr());
9213        }
9214        unsafe { drop_ctx(ctx) };
9215    }
9216
9217    /// A range whose member count has no `Int` faults with `IntOverflow`,
9218    /// answering the Unit sentinel like every other faulting wrapper.
9219    ///
9220    /// The kind is `IntOverflow` and not `EmptyRange` (ADR-059, ADR-075):
9221    /// `Int::MIN..Int::MAX` is the *widest* range expressible, so "empty range"
9222    /// would be a fault message that contradicts the input. `gcd`, `lcm` and
9223    /// A\*'s path cost answer `IntOverflow` for a result with no `Int` too.
9224    #[test]
9225    fn a_range_whose_count_has_no_int_faults_rather_than_wrapping_negative() {
9226        let mut rt = Runtime::new();
9227        let ctx = wired_ctx(&mut rt);
9228        // SAFETY: ctx wired; both bounds are valid Ints.
9229        unsafe {
9230            let lo = praxis_alloc_int(ctx, i64::MIN);
9231            let hi = praxis_alloc_int(ctx, i64::MAX);
9232            let r = praxis_range_new(ctx, lo, hi);
9233            let len = praxis_range_len(ctx, r);
9234            assert!(rt.has_pending_fault());
9235            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9236            assert_eq!(len.as_ptr(), rt.immortals().unit().as_ptr());
9237        }
9238        let _ = rt.take_fault();
9239        // A range one narrower is countable, so the refusal is the edge and not
9240        // the rule.
9241        // SAFETY: ctx wired; both bounds are valid Ints.
9242        unsafe {
9243            let lo = praxis_alloc_int(ctx, 0);
9244            let hi = praxis_alloc_int(ctx, i64::MAX);
9245            let r = praxis_range_new(ctx, lo, hi);
9246            let len = praxis_range_len(ctx, r);
9247            assert!(!rt.has_pending_fault());
9248            assert_eq!(praxis_int_load(ctx, len), i64::MAX);
9249        }
9250        unsafe { drop_ctx(ctx) };
9251    }
9252
9253    /// `gcd` and `lcm` at the edges of what an `Int` can hold. Both are computed
9254    /// in `i128` and range-checked on the way out, so the only refusal is a
9255    /// result that genuinely has no `Int` — and `gcd`'s is exactly one input
9256    /// pair, which a naive `i64` implementation would have wrapped instead.
9257    #[test]
9258    fn gcd_and_lcm_are_non_negative_and_refuse_only_what_has_no_int() {
9259        let mut rt = Runtime::new();
9260        let ctx = wired_ctx(&mut rt);
9261        // SAFETY: ctx wired; every operand is a valid Int.
9262        unsafe {
9263            let load = |r: GcRef| praxis_int_load(ctx, r);
9264            // `Int::MIN`'s divisors: |Int::MIN| is out of range, but every gcd
9265            // *with* it that is not itself is in range.
9266            let min = praxis_alloc_int(ctx, i64::MIN);
9267            let two = praxis_alloc_int(ctx, 2);
9268            assert_eq!(load(praxis_int_gcd(ctx, min, two)), 2);
9269            assert!(!rt.has_pending_fault());
9270            // …and the one pair whose answer is 2^63 faults.
9271            let min2 = praxis_alloc_int(ctx, i64::MIN);
9272            let _ = praxis_int_gcd(ctx, min, min2);
9273            assert!(rt.has_pending_fault());
9274            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9275        }
9276        let _ = rt.take_fault();
9277        // SAFETY: ctx wired; every operand is a valid Int.
9278        unsafe {
9279            let load = |r: GcRef| praxis_int_load(ctx, r);
9280            // Both signs, one answer: the lcm is non-negative.
9281            let neg = praxis_alloc_int(ctx, -4);
9282            let six = praxis_alloc_int(ctx, 6);
9283            assert_eq!(load(praxis_int_lcm(ctx, neg, six)), 12);
9284            let neg6 = praxis_alloc_int(ctx, -6);
9285            assert_eq!(load(praxis_int_lcm(ctx, neg, neg6)), 12);
9286            // `lcm(n, 0)` is 0, and the pair `(0, 0)` does not divide by zero.
9287            let zero = praxis_alloc_int(ctx, 0);
9288            assert_eq!(load(praxis_int_lcm(ctx, six, zero)), 0);
9289            assert_eq!(load(praxis_int_lcm(ctx, zero, zero)), 0);
9290            assert_eq!(load(praxis_int_gcd(ctx, zero, zero)), 0);
9291            assert!(!rt.has_pending_fault());
9292            // An lcm that does not fit: two coprime halves of the range.
9293            let big = praxis_alloc_int(ctx, i64::MAX);
9294            let three = praxis_alloc_int(ctx, 3);
9295            let _ = praxis_int_lcm(ctx, big, three);
9296            assert!(rt.has_pending_fault());
9297            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9298        }
9299        let _ = rt.take_fault();
9300        unsafe { drop_ctx(ctx) };
9301    }
9302
9303    /// `abs` faults on the one input with no positive counterpart, and `sign` is
9304    /// total on the same input — the distinction the manifest records as
9305    /// `AllocatesAndFaults` versus `Allocates`.
9306    #[test]
9307    fn abs_faults_on_the_value_with_no_positive_and_sign_does_not() {
9308        let mut rt = Runtime::new();
9309        let ctx = wired_ctx(&mut rt);
9310        // SAFETY: ctx wired; every operand is a valid Int.
9311        unsafe {
9312            let min = praxis_alloc_int(ctx, i64::MIN);
9313            let r = praxis_int_abs(ctx, min);
9314            assert!(rt.has_pending_fault());
9315            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9316            assert_eq!(r.as_ptr(), rt.immortals().unit().as_ptr());
9317        }
9318        let _ = rt.take_fault();
9319        // SAFETY: ctx wired; every operand is a valid Int.
9320        unsafe {
9321            let min = praxis_alloc_int(ctx, i64::MIN);
9322            assert_eq!(praxis_int_load(ctx, praxis_int_sign(ctx, min)), -1);
9323            let max = praxis_alloc_int(ctx, i64::MAX);
9324            assert_eq!(praxis_int_load(ctx, praxis_int_abs(ctx, max)), i64::MAX);
9325            assert_eq!(praxis_int_load(ctx, praxis_int_sign(ctx, max)), 1);
9326            assert!(!rt.has_pending_fault());
9327        }
9328        unsafe { drop_ctx(ctx) };
9329    }
9330
9331    #[test]
9332    fn overflow_sets_fault_and_returns_sentinel() {
9333        let mut rt = Runtime::new();
9334        let ctx = wired_ctx(&mut rt);
9335        // SAFETY: ctx wired; operands are valid Ints.
9336        unsafe {
9337            let a = praxis_alloc_int(ctx, i64::MAX);
9338            let b = praxis_alloc_int(ctx, 1);
9339            let s = praxis_int_add(ctx, a, b);
9340            // The fault is set; the return is the Unit sentinel.
9341            assert!(rt.has_pending_fault());
9342            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9343            assert_eq!(s.as_ptr(), rt.immortals().unit().as_ptr());
9344        }
9345        let _ = rt.take_fault();
9346        unsafe { drop_ctx(ctx) };
9347    }
9348
9349    #[test]
9350    fn division_by_zero_sets_fault() {
9351        let mut rt = Runtime::new();
9352        let ctx = wired_ctx(&mut rt);
9353        // SAFETY: ctx wired.
9354        unsafe {
9355            let a = praxis_alloc_int(ctx, 10);
9356            let b = praxis_alloc_int(ctx, 0);
9357            let _ = praxis_int_div(ctx, a, b);
9358            assert!(rt.has_pending_fault());
9359            assert_eq!(rt.fault(), FaultKind::DivByZero);
9360        }
9361        let _ = rt.take_fault();
9362        unsafe { drop_ctx(ctx) };
9363    }
9364
9365    #[test]
9366    fn remainder_by_zero_sets_fault() {
9367        let mut rt = Runtime::new();
9368        let ctx = wired_ctx(&mut rt);
9369        // SAFETY: ctx wired.
9370        unsafe {
9371            let a = praxis_alloc_int(ctx, 10);
9372            let b = praxis_alloc_int(ctx, 0);
9373            let _ = praxis_int_rem(ctx, a, b);
9374            assert!(rt.has_pending_fault());
9375            assert_eq!(rt.fault(), FaultKind::DivByZero);
9376        }
9377        let _ = rt.take_fault();
9378        unsafe { drop_ctx(ctx) };
9379    }
9380
9381    #[test]
9382    fn subtraction_overflow_sets_fault() {
9383        // The add/sub/mul overflow paths are symmetric. Sub: `Int::MIN - 1`
9384        // overflows.
9385        let mut rt = Runtime::new();
9386        let ctx = wired_ctx(&mut rt);
9387        // SAFETY: ctx wired; operands are valid Ints.
9388        unsafe {
9389            let a = praxis_alloc_int(ctx, i64::MIN);
9390            let b = praxis_alloc_int(ctx, 1);
9391            let _ = praxis_int_sub(ctx, a, b);
9392            assert!(rt.has_pending_fault());
9393            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9394        }
9395        let _ = rt.take_fault();
9396        unsafe { drop_ctx(ctx) };
9397    }
9398
9399    #[test]
9400    fn multiplication_overflow_sets_fault() {
9401        // `Int::MIN * -1` is the canonical mul overflow (same magnitude as
9402        // `Int::MAX + 1`).
9403        let mut rt = Runtime::new();
9404        let ctx = wired_ctx(&mut rt);
9405        // SAFETY: ctx wired; operands are valid Ints.
9406        unsafe {
9407            let a = praxis_alloc_int(ctx, i64::MIN);
9408            let b = praxis_alloc_int(ctx, -1);
9409            let _ = praxis_int_mul(ctx, a, b);
9410            assert!(rt.has_pending_fault());
9411            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9412        }
9413        let _ = rt.take_fault();
9414        unsafe { drop_ctx(ctx) };
9415    }
9416
9417    #[test]
9418    fn division_truncates_toward_zero() {
9419        // §4.12 / abi.rs comment: division truncates toward zero, so -7 / 2 == -3
9420        // (not -4 as floor division would give). Remainder takes the sign of the
9421        // dividend.
9422        let mut rt = Runtime::new();
9423        let ctx = wired_ctx(&mut rt);
9424        // SAFETY: ctx wired.
9425        unsafe {
9426            let a = praxis_alloc_int(ctx, -7);
9427            let b = praxis_alloc_int(ctx, 2);
9428            let q = praxis_int_div(ctx, a, b);
9429            assert!(!rt.has_pending_fault());
9430            assert_eq!(praxis_int_load(ctx, q), -3);
9431        }
9432        unsafe { drop_ctx(ctx) };
9433    }
9434
9435    #[test]
9436    fn remainder_truncates_toward_zero() {
9437        // -7 % 2 == -1 (remainder takes the dividend's sign under truncation).
9438        let mut rt = Runtime::new();
9439        let ctx = wired_ctx(&mut rt);
9440        // SAFETY: ctx wired.
9441        unsafe {
9442            let a = praxis_alloc_int(ctx, -7);
9443            let b = praxis_alloc_int(ctx, 2);
9444            let r = praxis_int_rem(ctx, a, b);
9445            assert!(!rt.has_pending_fault());
9446            assert_eq!(praxis_int_load(ctx, r), -1);
9447        }
9448        unsafe { drop_ctx(ctx) };
9449    }
9450
9451    #[test]
9452    fn division_min_div_minus_one_overflows() {
9453        // Regression for the §10.4 no-panic-across-ABI contract: `Int::MIN / -1`
9454        // is the sole signed-division case that overflows. The raw `/` panics in
9455        // debug builds; the wrapper must instead fault `IntOverflow`.
9456        let mut rt = Runtime::new();
9457        let ctx = wired_ctx(&mut rt);
9458        // SAFETY: ctx wired; operands are valid Ints.
9459        unsafe {
9460            let a = praxis_alloc_int(ctx, i64::MIN);
9461            let b = praxis_alloc_int(ctx, -1);
9462            let _ = praxis_int_div(ctx, a, b);
9463            assert!(rt.has_pending_fault());
9464            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9465        }
9466        let _ = rt.take_fault();
9467        unsafe { drop_ctx(ctx) };
9468    }
9469
9470    #[test]
9471    fn remainder_min_div_minus_one_overflows() {
9472        // Companion to the division regression: `Int::MIN % -1` traps in debug
9473        // builds even though the mathematical remainder is 0, because the
9474        // corresponding quotient overflows. The wrapper must fault instead.
9475        let mut rt = Runtime::new();
9476        let ctx = wired_ctx(&mut rt);
9477        // SAFETY: ctx wired; operands are valid Ints.
9478        unsafe {
9479            let a = praxis_alloc_int(ctx, i64::MIN);
9480            let b = praxis_alloc_int(ctx, -1);
9481            let _ = praxis_int_rem(ctx, a, b);
9482            assert!(rt.has_pending_fault());
9483            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9484        }
9485        let _ = rt.take_fault();
9486        unsafe { drop_ctx(ctx) };
9487    }
9488
9489    #[test]
9490    fn comparisons_yield_bools() {
9491        let mut rt = Runtime::new();
9492        let ctx = wired_ctx(&mut rt);
9493        // SAFETY: ctx wired.
9494        unsafe {
9495            let one = praxis_alloc_int(ctx, 1);
9496            let two = praxis_alloc_int(ctx, 2);
9497            assert_eq!(praxis_bool_load(ctx, praxis_int_lt(ctx, one, two)), 1);
9498            assert_eq!(praxis_bool_load(ctx, praxis_int_gt(ctx, one, two)), 0);
9499            assert_eq!(praxis_bool_load(ctx, praxis_int_eq(ctx, one, one)), 1);
9500        }
9501        unsafe { drop_ctx(ctx) };
9502    }
9503
9504    #[test]
9505    fn neg_of_min_overflows() {
9506        let mut rt = Runtime::new();
9507        let ctx = wired_ctx(&mut rt);
9508        // SAFETY: ctx wired.
9509        unsafe {
9510            let min = praxis_alloc_int(ctx, i64::MIN);
9511            let _ = praxis_int_neg(ctx, min);
9512            assert!(rt.has_pending_fault());
9513            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9514        }
9515        let _ = rt.take_fault();
9516        unsafe { drop_ctx(ctx) };
9517    }
9518
9519    #[test]
9520    fn check_fault_reports_pending() {
9521        let mut rt = Runtime::new();
9522        let ctx = wired_ctx(&mut rt);
9523        // SAFETY: ctx wired.
9524        unsafe {
9525            assert_eq!(praxis_check_fault(ctx), 0);
9526            let a = praxis_alloc_int(ctx, 1);
9527            let b = praxis_alloc_int(ctx, 0);
9528            let _ = praxis_int_div(ctx, a, b);
9529            assert_eq!(praxis_check_fault(ctx), 1);
9530        }
9531        let _ = rt.take_fault();
9532        unsafe { drop_ctx(ctx) };
9533    }
9534
9535    #[test]
9536    fn alloc_text_round_trips() {
9537        let mut rt = Runtime::new();
9538        let ctx = wired_ctx(&mut rt);
9539        let s = "héllo";
9540        // SAFETY: ctx wired; `bytes` is a valid UTF-8 buffer for the call.
9541        unsafe {
9542            let r = praxis_alloc_text(ctx, s.as_ptr(), s.len());
9543            assert_eq!(r.as_text(), "héllo");
9544        }
9545        unsafe { drop_ctx(ctx) };
9546    }
9547
9548    #[test]
9549    fn alloc_bool_round_trips_value() {
9550        // This pins the *value*; `bool_and_unit_abi_allocations_reuse_runtime_singletons`
9551        // pins the identity. Bool equality is structural (§5.5).
9552        let mut rt = Runtime::new();
9553        let ctx = wired_ctx(&mut rt);
9554        // SAFETY: ctx wired.
9555        unsafe {
9556            let t = praxis_alloc_bool(ctx, 1);
9557            let f = praxis_alloc_bool(ctx, 0);
9558            assert_eq!(praxis_bool_load(ctx, t), 1);
9559            assert_eq!(praxis_bool_load(ctx, f), 0);
9560        }
9561        unsafe { drop_ctx(ctx) };
9562    }
9563
9564    #[test]
9565    fn fault_clear_default_is_none() {
9566        let f = Fault::clear();
9567        assert!(!f.is_pending());
9568        assert_eq!(f.kind(), FaultKind::None);
9569    }
9570
9571    // --- Vec[T] collection wrappers ----------------------------------------
9572
9573    #[test]
9574    fn vec_new_is_empty() {
9575        let mut rt = Runtime::new();
9576        let ctx = wired_ctx(&mut rt);
9577        // SAFETY: ctx wired; INT is a valid static descriptor.
9578        unsafe {
9579            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9580            assert_eq!(praxis_bool_load(ctx, praxis_vec_is_empty(ctx, v)), 1);
9581            assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 0);
9582        }
9583        unsafe { drop_ctx(ctx) };
9584    }
9585
9586    #[test]
9587    fn vec_push_grows_and_get_reads_back() {
9588        let mut rt = Runtime::new();
9589        let ctx = wired_ctx(&mut rt);
9590        // SAFETY: ctx wired; push mutates the vec in place (returns Unit), so we
9591        // keep using the same `v` GcRef throughout.
9592        unsafe {
9593            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9594            let a = praxis_alloc_int(ctx, 10);
9595            let b = praxis_alloc_int(ctx, 20);
9596            let c = praxis_alloc_int(ctx, 30);
9597            let _ = praxis_vec_push(ctx, v, a);
9598            let _ = praxis_vec_push(ctx, v, b);
9599            let _ = praxis_vec_push(ctx, v, c);
9600            assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 3);
9601            let i0 = praxis_alloc_int(ctx, 0);
9602            let i2 = praxis_alloc_int(ctx, 2);
9603            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, i0)), 10);
9604            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, i2)), 30);
9605        }
9606        unsafe { drop_ctx(ctx) };
9607    }
9608
9609    #[test]
9610    fn vec_get_out_of_bounds_faults() {
9611        let mut rt = Runtime::new();
9612        let ctx = wired_ctx(&mut rt);
9613        // SAFETY: ctx wired.
9614        unsafe {
9615            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9616            let one = praxis_alloc_int(ctx, 1);
9617            let _ = praxis_vec_get(ctx, v, one); // empty vec, index 0
9618            assert!(rt.has_pending_fault());
9619            assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9620        }
9621        let _ = rt.take_fault();
9622        unsafe { drop_ctx(ctx) };
9623    }
9624
9625    /// `praxis_vec_set` and `praxis_deque_set` **replace** — the property that
9626    /// separates them from the push beside them, and the one a store row pointed
9627    /// at the wrong wrapper would break silently.
9628    ///
9629    /// So each assertion is about what appending would get wrong: the length is
9630    /// unchanged, the neighbours are unchanged, an index one past the end faults
9631    /// instead of growing the collection, and a value of the wrong type is
9632    /// refused rather than retagging an explicitly typed collection.
9633    #[test]
9634    fn a_sequence_store_replaces_and_never_appends() {
9635        let mut rt = Runtime::new();
9636        let ctx = wired_ctx(&mut rt);
9637        // SAFETY: ctx wired; the stores mutate in place, so the same `GcRef`s
9638        // stay valid throughout.
9639        unsafe {
9640            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9641            for n in [10, 20, 30] {
9642                let _ = praxis_vec_push(ctx, v, praxis_alloc_int(ctx, n));
9643            }
9644            let d = praxis_deque_new(ctx, &crate::scalars::INT as *const _);
9645            for n in [10, 20] {
9646                let _ = praxis_deque_push_back(ctx, d, praxis_alloc_int(ctx, n));
9647            }
9648
9649            let one = praxis_alloc_int(ctx, 1);
9650            let ninety_nine = praxis_alloc_int(ctx, 99);
9651            let _ = praxis_vec_set(ctx, v, one, ninety_nine);
9652            let _ = praxis_deque_set(ctx, d, one, ninety_nine);
9653            assert!(!rt.has_pending_fault());
9654
9655            assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 3);
9656            assert_eq!(praxis_int_load(ctx, praxis_deque_len(ctx, d)), 2);
9657            let zero = praxis_alloc_int(ctx, 0);
9658            let two = praxis_alloc_int(ctx, 2);
9659            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, zero)), 10);
9660            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, one)), 99);
9661            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, two)), 30);
9662            assert_eq!(praxis_int_load(ctx, praxis_deque_get(ctx, d, zero)), 10);
9663            assert_eq!(praxis_int_load(ctx, praxis_deque_get(ctx, d, one)), 99);
9664
9665            // One past the end, and a negative index, are both out of range —
9666            // and neither grows the collection, which is what a store that fell
9667            // through to the appending wrapper would do.
9668            let three = praxis_alloc_int(ctx, 3);
9669            let neg = praxis_alloc_int(ctx, -1);
9670            type Store = unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef, GcRef) -> GcRef;
9671            type Len = unsafe extern "C" fn(*mut RuntimeContext, GcRef) -> GcRef;
9672            for (recv, idx, store, len_of) in [
9673                (v, three, praxis_vec_set as Store, praxis_vec_len as Len),
9674                (v, neg, praxis_vec_set as Store, praxis_vec_len as Len),
9675                (d, two, praxis_deque_set as Store, praxis_deque_len as Len),
9676                (d, neg, praxis_deque_set as Store, praxis_deque_len as Len),
9677            ] {
9678                let before = praxis_int_load(ctx, len_of(ctx, recv));
9679                let _ = store(ctx, recv, idx, ninety_nine);
9680                assert!(rt.has_pending_fault(), "an out-of-range store must fault");
9681                assert_eq!(rt.take_fault(), Some(FaultKind::IndexOutOfBounds));
9682                assert_eq!(
9683                    praxis_int_load(ctx, len_of(ctx, recv)),
9684                    before,
9685                    "a faulting store must not have grown the collection"
9686                );
9687            }
9688
9689            // A `Vec[Int]` refuses a `Float` rather than retagging itself.
9690            let float = praxis_alloc_float(ctx, 1.5_f64.to_bits() as i64);
9691            let _ = praxis_vec_set(ctx, v, zero, float);
9692            assert_eq!(rt.take_fault(), Some(FaultKind::TypeMismatch));
9693            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, zero)), 10);
9694        }
9695        unsafe { drop_ctx(ctx) };
9696    }
9697
9698    #[test]
9699    fn vec_push_many_survive_collection() {
9700        // Stress: root the receiver/current element exactly as generated code
9701        // does, push enough elements to force multiple automatic collections,
9702        // and leave one unrooted allocation per iteration so collection is
9703        // observable as a live-registry shrink.
9704        //
9705        // Both the pushed element and the deliberately-unrooted allocation are
9706        // offset past the interned range: an interned `Int` is never registered,
9707        // so an in-range element would trip the shrink test on iteration zero
9708        // and neither the collection nor the rooting would be exercised (see
9709        // `UNINTERNED`). The offset is carried through the spot checks below so
9710        // the values read back are still the values pushed.
9711        let mut rt = Runtime::new();
9712        let ctx = wired_ctx(&mut rt);
9713        // SAFETY: ctx wired; push mutates in place so `v` stays valid throughout.
9714        unsafe {
9715            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9716            let mut frame = push_frame(ctx, SlotCount::new(2).unwrap());
9717            frame.set(0, v);
9718            let mut observed_reclamation = false;
9719            for i in 0..5000_i64 {
9720                let before_alloc = rt.heap().stats().live_count;
9721                let elem = praxis_alloc_int(ctx, UNINTERNED + i);
9722                if rt.heap().stats().live_count < before_alloc.saturating_add(1) {
9723                    observed_reclamation = true;
9724                }
9725                frame.set(1, elem);
9726                let before_push = rt.heap().stats().live_count;
9727                let _ = praxis_vec_push(ctx, v, elem);
9728                if rt.heap().stats().live_count < before_push {
9729                    observed_reclamation = true;
9730                }
9731                frame.clear(1);
9732                let _ = rt.alloc_int(-UNINTERNED - i - 1);
9733            }
9734            assert!(
9735                observed_reclamation,
9736                "the test must observe an automatic collection, not merely allocation pressure"
9737            );
9738            assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 5000);
9739            // Spot-check first/middle/last. The *indices* stay small (they are
9740            // interned, which is fine — nothing here watches them); the values
9741            // carry the offset the elements were pushed with.
9742            let zero = praxis_alloc_int(ctx, 0);
9743            assert_eq!(
9744                praxis_int_load(ctx, praxis_vec_get(ctx, v, zero)),
9745                UNINTERNED
9746            );
9747            let middle = praxis_alloc_int(ctx, 2500);
9748            assert_eq!(
9749                praxis_int_load(ctx, praxis_vec_get(ctx, v, middle)),
9750                UNINTERNED + 2500
9751            );
9752            let last = praxis_alloc_int(ctx, 4999);
9753            assert_eq!(
9754                praxis_int_load(ctx, praxis_vec_get(ctx, v, last)),
9755                UNINTERNED + 4999
9756            );
9757            drop(frame);
9758        }
9759        unsafe { drop_ctx(ctx) };
9760    }
9761
9762    #[test]
9763    fn vec_get_negative_index_faults() {
9764        // The `idx < 0` guard in `praxis_vec_get`: a negative index is out of
9765        // bounds, not a wrapped-around large one.
9766        let mut rt = Runtime::new();
9767        let ctx = wired_ctx(&mut rt);
9768        // SAFETY: ctx wired.
9769        unsafe {
9770            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9771            let a = praxis_alloc_int(ctx, 1);
9772            let _ = praxis_vec_push(ctx, v, a); // non-empty vec, so only the sign can fail
9773            let neg = praxis_alloc_int(ctx, -1);
9774            let _ = praxis_vec_get(ctx, v, neg);
9775            assert!(rt.has_pending_fault());
9776            assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9777        }
9778        let _ = rt.take_fault();
9779        unsafe { drop_ctx(ctx) };
9780    }
9781
9782    #[test]
9783    fn text_get_negative_index_faults() {
9784        // Companion to `vec_get_negative_index_faults`, for `praxis_text_get`'s
9785        // own `idx < 0` guard.
9786        let mut rt = Runtime::new();
9787        let ctx = wired_ctx(&mut rt);
9788        // SAFETY: ctx wired.
9789        unsafe {
9790            let s = "ab";
9791            let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
9792            let neg = praxis_alloc_int(ctx, -1);
9793            let _ = praxis_text_get(ctx, text, neg);
9794            assert!(rt.has_pending_fault());
9795            assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9796        }
9797        let _ = rt.take_fault();
9798        unsafe { drop_ctx(ctx) };
9799    }
9800
9801    /// **ADR-086, the runtime half.** `praxis_text_get` allocates a `Char`.
9802    ///
9803    /// The catalog's twin (`the_two_text_reads_answer_a_char`) is pure data and
9804    /// cannot see this; this is pure runtime and cannot see that. Both halves
9805    /// are needed, and they must hold together: with only one, a `Char`-typed
9806    /// value routes into `praxis_char_load`, whose `read_scalar` answers `None`
9807    /// against the `INT` descriptor and panics.
9808    #[test]
9809    fn text_get_answers_a_char_object() {
9810        let mut rt = Runtime::new();
9811        let ctx = wired_ctx(&mut rt);
9812        // SAFETY: ctx wired.
9813        unsafe {
9814            // `"sddddd"[4]` must be `'d'` and not `100`: an object carrying the
9815            // `INT` descriptor would have the right value and the wrong type.
9816            let s = "sddddd";
9817            let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
9818            let four = praxis_alloc_int(ctx, 4);
9819            let got = praxis_text_get(ctx, text, four);
9820            assert!(!rt.has_pending_fault());
9821            assert!(
9822                std::ptr::eq(got.descriptor(), &crate::scalars::CHAR),
9823                "ADR-086: the read answers a Char, not the char's scalar value"
9824            );
9825            assert_eq!(got.as_char(), 'd');
9826
9827            // Scalar-not-byte indexing, pinned at the runtime level too: `é` is
9828            // one scalar and two UTF-8 bytes, so a byte index would answer 0xC3.
9829            let u = "héllo";
9830            let utext = praxis_alloc_text(ctx, u.as_ptr(), u.len());
9831            let one = praxis_alloc_int(ctx, 1);
9832            let got = praxis_text_get(ctx, utext, one);
9833            assert!(!rt.has_pending_fault());
9834            assert_eq!(got.as_char(), 'é');
9835        }
9836        unsafe { drop_ctx(ctx) };
9837    }
9838
9839    /// **ADR-115 at the ABI, on the shape that decides it.** `t.len()` and
9840    /// `t[i]` are defined on scalars (§4.3, ADR-086); indexing bytes is an
9841    /// optimization licensed by the count, and the licence must be refused on
9842    /// every text where it would be wrong.
9843    ///
9844    /// The cases are chosen to break a byte-indexing implementation that only
9845    /// looked at the text's own leading byte or only at its first scalar: a
9846    /// multi-byte scalar at the start, in the middle, at the end, a four-byte
9847    /// one, and a slice whose own bytes are all one-byte but whose owner's are
9848    /// not.
9849    #[test]
9850    fn a_text_reads_by_scalar_wherever_the_multi_byte_scalar_sits() {
9851        let mut rt = Runtime::new();
9852        let ctx = wired_ctx(&mut rt);
9853        // SAFETY: ctx wired for every call in this block.
9854        unsafe {
9855            for src in [
9856                "",
9857                "abc",
9858                "\u{0}\u{7f}",
9859                "éabc",
9860                "abéc",
9861                "abcé",
9862                "a\u{1F600}b",
9863                "\u{20AC}\u{20AC}",
9864                "héllo wörld",
9865            ] {
9866                let text = praxis_alloc_text(ctx, src.as_ptr(), src.len());
9867                let expected: Vec<char> = src.chars().collect();
9868
9869                let len = praxis_text_len(ctx, text);
9870                assert!(!rt.has_pending_fault(), "{src:?}");
9871                assert_eq!(len.as_int(), expected.len() as i64, "{src:?}");
9872
9873                let empty = praxis_text_is_empty(ctx, text);
9874                assert_eq!(empty.as_bool(), expected.is_empty(), "{src:?}");
9875
9876                for (i, want) in expected.iter().enumerate() {
9877                    let idx = praxis_alloc_int(ctx, i as i64);
9878                    let got = praxis_text_get(ctx, text, idx);
9879                    assert!(!rt.has_pending_fault(), "{src:?}[{i}]");
9880                    assert!(
9881                        std::ptr::eq(got.descriptor(), &crate::scalars::CHAR),
9882                        "{src:?}[{i}] answers a Char (ADR-086)"
9883                    );
9884                    assert_eq!(got.as_char(), *want, "{src:?}[{i}]");
9885                }
9886
9887                // One past the end faults, whichever path answered above.
9888                let past = praxis_alloc_int(ctx, expected.len() as i64);
9889                let _ = praxis_text_get(ctx, text, past);
9890                assert!(rt.has_pending_fault(), "{src:?}[{}]", expected.len());
9891                assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9892                let _ = rt.take_fault();
9893            }
9894
9895            // A view whose own bytes are all one-byte, inside an owner whose
9896            // are not. The answers are the same as the owner's corresponding
9897            // scalars; the byte-index path is refused because the licence is
9898            // the owner's to give.
9899            let owner_src = "héllo wörld";
9900            let owner = praxis_alloc_text(ctx, owner_src.as_ptr(), owner_src.len());
9901            // "llo " — bytes [3, 7) of the owner, all below 0x80.
9902            let view = rt
9903                .alloc_text_slice(owner, 3, 4)
9904                .expect("[3, 7) is on scalar boundaries");
9905            let len = praxis_text_len(ctx, view);
9906            assert_eq!(len.as_int(), 4);
9907            for (i, want) in "llo ".chars().enumerate() {
9908                let idx = praxis_alloc_int(ctx, i as i64);
9909                let got = praxis_text_get(ctx, view, idx);
9910                assert!(!rt.has_pending_fault());
9911                assert_eq!(got.as_char(), want);
9912            }
9913        }
9914        unsafe { drop_ctx(ctx) };
9915    }
9916
9917    /// **ADR-086's narrowing half.** `Int.to_char()` reaches the same
9918    /// range check `praxis_alloc_char` does, because they share one helper.
9919    ///
9920    /// A wrapper that forwarded `value as u32` without the check would answer
9921    /// `'A'` for `0x1_0000_0041` instead of faulting, which is the case that
9922    /// proves this door reaches the shared guard rather than restating it.
9923    #[test]
9924    fn int_to_char_rejects_what_is_not_a_scalar_value() {
9925        let mut rt = Runtime::new();
9926        let ctx = wired_ctx(&mut rt);
9927        // SAFETY: ctx wired.
9928        unsafe {
9929            for bad in [-1_i64, 0xD800, 0x11_0000, 0x1_0000_0041] {
9930                let n = praxis_alloc_int(ctx, bad);
9931                let got = praxis_int_to_char(ctx, n);
9932                assert!(rt.has_pending_fault(), "{bad} must not answer a Char");
9933                assert_eq!(rt.fault(), FaultKind::InvalidChar, "{bad}");
9934                assert!(std::ptr::eq(got.descriptor(), &crate::scalars::UNIT));
9935                let _ = rt.take_fault();
9936            }
9937
9938            // …and the round trip holds for one that is.
9939            let n = praxis_alloc_int(ctx, 233);
9940            let got = praxis_int_to_char(ctx, n);
9941            assert!(!rt.has_pending_fault());
9942            assert_eq!(got.as_char(), 'é');
9943            let back = praxis_char_to_int(ctx, got);
9944            assert!(!rt.has_pending_fault());
9945            assert_eq!(back.as_int(), 233);
9946        }
9947        unsafe { drop_ctx(ctx) };
9948    }
9949
9950    #[test]
9951    fn alloc_text_empty_string_round_trips() {
9952        // The `len == 0` branch in `praxis_alloc_text` treats an empty buffer as
9953        // the empty slice. An empty Text must format as "".
9954        let mut rt = Runtime::new();
9955        let ctx = wired_ctx(&mut rt);
9956        // SAFETY: ctx wired; null pointer + zero length is the documented empty path.
9957        unsafe {
9958            let r = praxis_alloc_text(ctx, std::ptr::null(), 0);
9959            assert_eq!(r.as_text(), "");
9960        }
9961        unsafe { drop_ctx(ctx) };
9962    }
9963
9964    #[test]
9965    fn vec_new_with_null_descriptor_defaults_to_int() {
9966        // A null element descriptor is kept null — "the caller has no static
9967        // element type" — and the vec must still be usable.
9968        let mut rt = Runtime::new();
9969        let ctx = wired_ctx(&mut rt);
9970        // SAFETY: ctx wired; null descriptor is the handled default case.
9971        unsafe {
9972            let v = praxis_vec_new(ctx, std::ptr::null());
9973            assert_eq!(praxis_bool_load(ctx, praxis_vec_is_empty(ctx, v)), 1);
9974        }
9975        unsafe { drop_ctx(ctx) };
9976    }
9977
9978    #[test]
9979    fn vec_push_rejects_a_value_with_the_wrong_descriptor() {
9980        let mut rt = Runtime::new();
9981        let ctx = wired_ctx(&mut rt);
9982        let length_after;
9983        unsafe {
9984            let ints = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9985            let float = praxis_alloc_float(ctx, 1.5_f64.to_bits() as i64);
9986            let _ = praxis_vec_push(ctx, ints, float);
9987            length_after = ints.as_vec().len();
9988        }
9989        unsafe { drop_ctx(ctx) };
9990
9991        assert_eq!(
9992            length_after, 0,
9993            "an ABI type mismatch must not silently retag and mutate an explicitly typed Vec[Int]"
9994        );
9995    }
9996
9997    #[test]
9998    fn alloc_char_rejects_values_that_only_become_valid_after_truncation() {
9999        let mut rt = Runtime::new();
10000        let ctx = wired_ctx(&mut rt);
10001        let result = unsafe { praxis_alloc_char(ctx, 0x1_0000_0041) };
10002        let unit = rt.immortals().unit();
10003        unsafe { drop_ctx(ctx) };
10004
10005        assert_eq!(
10006            result.as_ptr(),
10007            unit.as_ptr(),
10008            "the ABI must range-check the i64 code point before converting it to u32"
10009        );
10010        // And the fault it raises must name itself: a `FaultKind::None` here
10011        // would have the host report "no fault" while generated code took its
10012        // fault path.
10013        assert_eq!(rt.fault(), FaultKind::InvalidChar);
10014        assert!(rt.has_pending_fault());
10015    }
10016
10017    /// A negative code point is out of range for the same reason a too-large
10018    /// one is, and `as u32` wraps it into the valid range just as silently.
10019    #[test]
10020    fn alloc_char_rejects_a_negative_code_point() {
10021        let mut rt = Runtime::new();
10022        let ctx = wired_ctx(&mut rt);
10023        let result = unsafe { praxis_alloc_char(ctx, -1) };
10024        let unit = rt.immortals().unit();
10025        unsafe { drop_ctx(ctx) };
10026
10027        assert_eq!(result.as_ptr(), unit.as_ptr());
10028        assert_eq!(rt.fault(), FaultKind::InvalidChar);
10029    }
10030
10031    /// **ADR-111.** Input that is not UTF-8 faults at the `read`, because that
10032    /// is where the bytes stop being the compiler's and start being the host's.
10033    ///
10034    /// Asserted through `praxis_get_input` and never by feeding
10035    /// `praxis_alloc_text` bad bytes directly: that is a violated precondition,
10036    /// so it panics through `abi_guard!`, and `praxis_alloc_text`'s
10037    /// `Allocates` row makes the resulting `Panic` fault unobservable — the
10038    /// process aborts instead of failing. The property is that a program
10039    /// reading non-UTF-8 input gets `InvalidText` at its `read`.
10040    ///
10041    /// `praxis run` cannot reach this: `lazy_stdin::read` goes through
10042    /// `std::io::read_to_string` and exits 2 on non-UTF-8 stdin before the
10043    /// runtime sees a byte (`praxis-cli/src/run.rs`). The reachable caller is an
10044    /// embedder that installs its own `InputReader`, which is exactly what this
10045    /// test is.
10046    #[test]
10047    fn input_that_is_not_utf8_faults_at_the_read() {
10048        fn not_utf8() -> Vec<u8> {
10049            vec![0xF0, 0x28, 0x8C, 0x28]
10050        }
10051        let mut rt = Runtime::new();
10052        let ctx = wired_ctx(&mut rt);
10053        crate::input::install_input_reader(not_utf8);
10054        let result = unsafe { praxis_get_input(ctx) };
10055        let unit = rt.immortals().unit();
10056        crate::input::clear_input_reader();
10057        unsafe { drop_ctx(ctx) };
10058
10059        assert_eq!(rt.fault(), FaultKind::InvalidText);
10060        assert!(rt.has_pending_fault());
10061        assert_eq!(
10062            result.as_ptr(),
10063            unit.as_ptr(),
10064            "the fault path answers §10.4's defined dummy, not a half-built Text"
10065        );
10066    }
10067
10068    /// The mutation companion, and it is required: a `praxis_get_input` that
10069    /// faulted on *every* input would pass the gate above.
10070    ///
10071    /// Multi-byte on purpose — a validation that accepted only ASCII would also
10072    /// pass a test written with `"hi"`.
10073    #[test]
10074    fn input_that_is_utf8_still_becomes_the_buffer() {
10075        fn multibyte() -> Vec<u8> {
10076            "héllo wörld".as_bytes().to_vec()
10077        }
10078        let mut rt = Runtime::new();
10079        let ctx = wired_ctx(&mut rt);
10080        crate::input::install_input_reader(multibyte);
10081        let result = unsafe { praxis_get_input(ctx) };
10082        let contents = result.as_text().to_string();
10083        let descriptor = result.descriptor().name;
10084        crate::input::clear_input_reader();
10085        unsafe { drop_ctx(ctx) };
10086
10087        assert!(!rt.has_pending_fault(), "fault: {:?}", rt.fault());
10088        assert_eq!(descriptor, "Text");
10089        assert_eq!(contents, "héllo wörld");
10090    }
10091
10092    #[test]
10093    fn grid_cell_vectors_preserve_the_grid_element_descriptor() {
10094        let mut rt = Runtime::new();
10095        let ctx = wired_ctx(&mut rt);
10096        let cell = rt.alloc_text("x");
10097        let grid = rt.alloc_grid(&crate::text::TEXT, vec![cell], 1);
10098        let descriptors;
10099        unsafe {
10100            let zero = praxis_alloc_int(ctx, 0);
10101            let cells = praxis_grid_cells(ctx, grid);
10102            let row = praxis_grid_row(ctx, grid, zero);
10103            let column = praxis_grid_column(ctx, grid, zero);
10104            descriptors = [
10105                (*vec_payload(cells).element_descriptor).id(),
10106                (*vec_payload(row).element_descriptor).id(),
10107                (*vec_payload(column).element_descriptor).id(),
10108            ];
10109        }
10110        unsafe { drop_ctx(ctx) };
10111
10112        assert!(
10113            descriptors.iter().all(|id| *id == crate::text::TEXT.id()),
10114            "cells(), row(), and column() must return Vec values tagged with the Grid cell type"
10115        );
10116    }
10117
10118    #[test]
10119    fn constructed_grid_cells_satisfy_the_declared_element_descriptor() {
10120        let mut rt = Runtime::new();
10121        let ctx = wired_ctx(&mut rt);
10122        let cell_descriptor;
10123        unsafe {
10124            let grid = praxis_grid_new(ctx, &crate::scalars::INT as *const _, 1, 1);
10125            cell_descriptor = grid_payload(grid).items[0].descriptor().id();
10126        }
10127        unsafe { drop_ctx(ctx) };
10128
10129        assert_eq!(
10130            cell_descriptor,
10131            crate::scalars::INT.id(),
10132            "a live Grid[Int] must never contain a Unit placeholder observable through get/format/hash"
10133        );
10134    }
10135
10136    #[test]
10137    fn grid_position_vectors_use_the_point_tuple_descriptor() {
10138        let mut rt = Runtime::new();
10139        let ctx = wired_ctx(&mut rt);
10140        let cell = rt.alloc_int(1);
10141        let grid = rt.alloc_grid(&crate::scalars::INT, vec![cell], 1);
10142        let descriptors;
10143        unsafe {
10144            let point = alloc_point(ctx, 0, 0);
10145            let positions = praxis_grid_positions(ctx, grid);
10146            let neighbors4 = praxis_grid_neighbors4(ctx, grid, point);
10147            let neighbors8 = praxis_grid_neighbors8(ctx, grid, point);
10148            let matches = praxis_grid_find_all(ctx, grid, cell);
10149            descriptors = [
10150                (*vec_payload(positions).element_descriptor).id(),
10151                (*vec_payload(neighbors4).element_descriptor).id(),
10152                (*vec_payload(neighbors8).element_descriptor).id(),
10153                (*vec_payload(matches).element_descriptor).id(),
10154            ];
10155        }
10156        unsafe { drop_ctx(ctx) };
10157
10158        assert!(
10159            descriptors
10160                .iter()
10161                .all(|id| *id == crate::tuples::TUPLE.id()),
10162            "position-producing Grid methods must return Vec[Tuple[Int, Int]] at runtime"
10163        );
10164    }
10165
10166    /// An extent must be validated before it becomes a `usize`. Unchecked,
10167    /// `vec![unit; (w as usize) * (h as usize)]` turns `-1` into `usize::MAX`,
10168    /// and the products either overflow (a capacity panic across `extern "C"`)
10169    /// or ask the host for terabytes (an OOM abort). The wrapper must answer
10170    /// with a fault, and the heap must be untouched — a partly-built grid is as
10171    /// bad as a crash.
10172    #[test]
10173    fn a_negative_or_absurd_grid_extent_faults_instead_of_allocating() {
10174        let absurd = GridExtent::MAX_CELLS as i64 + 1;
10175        for (width, height) in [
10176            (-1_i64, 4_i64),
10177            (4, -1),
10178            (-1, -1),
10179            (i64::MIN, 1),
10180            // Overflows the `usize` multiplication outright.
10181            (i64::MAX, 2),
10182            (1 << 40, 1 << 40),
10183            // Multiplies cleanly and is still an allocation no host can serve.
10184            (absurd, 1),
10185            (1, absurd),
10186        ] {
10187            let mut rt = Runtime::new();
10188            let ctx = wired_ctx(&mut rt);
10189            let live_before = rt.heap().stats().live_count;
10190            let result =
10191                unsafe { praxis_grid_new(ctx, &crate::scalars::INT as *const _, width, height) };
10192            let live_after = rt.heap().stats().live_count;
10193            let unit = rt.immortals().unit();
10194            unsafe { drop_ctx(ctx) };
10195
10196            assert_eq!(
10197                rt.fault(),
10198                FaultKind::InvalidSize,
10199                "Grid[Int]({width}, {height}) must fault"
10200            );
10201            assert_eq!(
10202                result.as_ptr(),
10203                unit.as_ptr(),
10204                "a faulted Grid[Int]({width}, {height}) returns the Unit sentinel"
10205            );
10206            assert_eq!(
10207                live_after, live_before,
10208                "a rejected Grid[Int]({width}, {height}) allocates nothing"
10209            );
10210        }
10211    }
10212
10213    /// The other side of the same gate: an extent the runtime *can* serve still
10214    /// builds the grid it asked for, including the degenerate zero cases.
10215    #[test]
10216    fn an_in_range_grid_extent_still_builds_its_cells() {
10217        let mut rt = Runtime::new();
10218        let ctx = wired_ctx(&mut rt);
10219        let shapes: Vec<(i64, i64, usize, usize)> = vec![(0, 0, 0, 0), (0, 5, 0, 0), (3, 2, 6, 3)];
10220        let mut observed = Vec::new();
10221        for (width, height, _, _) in &shapes {
10222            let grid =
10223                unsafe { praxis_grid_new(ctx, &crate::scalars::INT as *const _, *width, *height) };
10224            let p = unsafe { grid_payload(grid) };
10225            observed.push((p.items.len(), p.width));
10226        }
10227        unsafe { drop_ctx(ctx) };
10228
10229        assert_eq!(rt.fault(), FaultKind::None, "no in-range extent faults");
10230        for ((w, h, cells, width), (got_cells, got_width)) in shapes.iter().zip(observed) {
10231            assert_eq!(
10232                (got_cells, got_width),
10233                (*cells, *width),
10234                "Grid[Int]({w}, {h}) shape"
10235            );
10236        }
10237    }
10238
10239    /// **ADR-146.** `Vec(n, fill)` builds `n` slots, all of them the fill, and
10240    /// the empty case is a `Vec` and not a fault.
10241    #[test]
10242    fn vec_filled_builds_n_copies_of_one_value() {
10243        let mut rt = Runtime::new();
10244        let ctx = wired_ctx(&mut rt);
10245        let observed: Vec<(usize, bool)> = [0_i64, 1, 7]
10246            .into_iter()
10247            .map(|n| unsafe {
10248                let count = praxis_alloc_int(ctx, n);
10249                let fill = praxis_alloc_int(ctx, 42);
10250                let v = praxis_vec_filled(ctx, &crate::scalars::INT as *const _, count, fill);
10251                let p = vec_payload(v);
10252                // Every slot is the *same* reference, which is the aliasing
10253                // ADR-146 decision 4 states rather than n copies of a value.
10254                let all_same = p.items.iter().all(|item| item.as_ptr() == fill.as_ptr());
10255                (p.items.len(), all_same)
10256            })
10257            .collect();
10258        unsafe { drop_ctx(ctx) };
10259
10260        assert_eq!(rt.fault(), FaultKind::None, "no in-range count faults");
10261        assert_eq!(observed, vec![(0, true), (1, true), (7, true)]);
10262    }
10263
10264    /// The other half of ADR-041 decision 1, for the newtype it added: a count
10265    /// the runtime cannot serve is a fault and not an allocation, and the heap
10266    /// is untouched — a half-built `Vec` is as bad as a crash.
10267    #[test]
10268    fn vec_filled_refuses_a_negative_or_absurd_count() {
10269        let absurd = crate::collections::VecExtent::MAX_ITEMS as i64 + 1;
10270        for n in [-1_i64, i64::MIN, absurd, i64::MAX] {
10271            let mut rt = Runtime::new();
10272            let ctx = wired_ctx(&mut rt);
10273            let (result, live_before, live_after, unit) = unsafe {
10274                let count = praxis_alloc_int(ctx, n);
10275                let fill = praxis_alloc_int(ctx, 0);
10276                let before = rt.heap().stats().live_count;
10277                let r = praxis_vec_filled(ctx, &crate::scalars::INT as *const _, count, fill);
10278                (
10279                    r,
10280                    before,
10281                    rt.heap().stats().live_count,
10282                    rt.immortals().unit(),
10283                )
10284            };
10285            unsafe { drop_ctx(ctx) };
10286
10287            assert_eq!(rt.fault(), FaultKind::InvalidSize, "Vec({n}, 0) must fault");
10288            assert_eq!(
10289                result.as_ptr(),
10290                unit.as_ptr(),
10291                "a faulted Vec({n}, 0) returns the Unit sentinel"
10292            );
10293            assert_eq!(
10294                live_after, live_before,
10295                "a rejected Vec({n}, 0) allocates nothing"
10296            );
10297        }
10298    }
10299
10300    /// A declared element type the fill is not is a `TypeMismatch`, through the
10301    /// same `adopt_or_reject` a `push` goes through — not a silent retag of the
10302    /// collection to the fill's type, which is the mislabelling defect one
10303    /// level down.
10304    /// A *null* static descriptor adopts instead, which is what "the caller has
10305    /// no static element type" already means for `praxis_vec_new`.
10306    #[test]
10307    fn vec_filled_reconciles_its_element_descriptor() {
10308        let mut rejecting = Runtime::new();
10309        let ctx = wired_ctx(&mut rejecting);
10310        unsafe {
10311            let count = praxis_alloc_int(ctx, 3);
10312            let text = praxis_alloc_text(ctx, b"x".as_ptr(), 1);
10313            praxis_vec_filled(ctx, &crate::scalars::INT as *const _, count, text);
10314            drop_ctx(ctx);
10315        }
10316        assert_eq!(
10317            rejecting.fault(),
10318            FaultKind::TypeMismatch,
10319            "a `Vec[Int]` filled with a `Text` is a mislabelled element descriptor"
10320        );
10321
10322        let mut adopting = Runtime::new();
10323        let ctx = wired_ctx(&mut adopting);
10324        let adopted = unsafe {
10325            let count = praxis_alloc_int(ctx, 3);
10326            let text = praxis_alloc_text(ctx, b"x".as_ptr(), 1);
10327            let v = praxis_vec_filled(ctx, std::ptr::null(), count, text);
10328            let matches = std::ptr::eq(vec_payload(v).element_descriptor, text.descriptor());
10329            drop_ctx(ctx);
10330            matches
10331        };
10332        assert_eq!(adopting.fault(), FaultKind::None);
10333        assert!(
10334            adopted,
10335            "a null static descriptor adopts the fill's, as `praxis_vec_new` already does"
10336        );
10337    }
10338
10339    /// **ADR-146 decision 6.** `Grid(w, h, fill)` accepts a fill
10340    /// `praxis_grid_new` cannot invent: `default_cell` has no zero value for a
10341    /// composite and answers `TypeMismatch`, and the explicit fill is exactly
10342    /// what removes the question. The contrast is the assertion — both calls
10343    /// are in one test so a later change that reintroduced `default_cell` here
10344    /// fails rather than passes quietly.
10345    #[test]
10346    fn grid_filled_accepts_a_composite_fill_where_grid_new_cannot() {
10347        let mut inventing = Runtime::new();
10348        let ctx = wired_ctx(&mut inventing);
10349        unsafe {
10350            praxis_grid_new(ctx, &crate::collections::VEC as *const _, 2, 2);
10351            drop_ctx(ctx);
10352        }
10353        assert_eq!(
10354            inventing.fault(),
10355            FaultKind::TypeMismatch,
10356            "`praxis_grid_new` still has no zero value for a `Vec` cell"
10357        );
10358
10359        let mut supplied = Runtime::new();
10360        let ctx = wired_ctx(&mut supplied);
10361        let (cells, all_same) = unsafe {
10362            let inner = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
10363            let (w, h) = (praxis_alloc_int(ctx, 2), praxis_alloc_int(ctx, 2));
10364            let g = praxis_grid_filled(ctx, &crate::collections::VEC as *const _, w, h, inner);
10365            let p = grid_payload(g);
10366            let same = p.items.iter().all(|c| c.as_ptr() == inner.as_ptr());
10367            let len = p.items.len();
10368            drop_ctx(ctx);
10369            (len, same)
10370        };
10371        assert_eq!(supplied.fault(), FaultKind::None);
10372        assert_eq!(cells, 4, "an explicit fill builds all four cells");
10373        assert!(
10374            all_same,
10375            "the four cells are one `Vec`, not four (ADR-146 decision 4)"
10376        );
10377    }
10378
10379    /// `Grid(w, h, fill)` takes the extents `praxis_grid_new` refuses, through
10380    /// the same `GridExtent::new` — a fill changes nothing about the
10381    /// arithmetic.
10382    #[test]
10383    fn grid_filled_refuses_the_extents_grid_new_refuses() {
10384        let absurd = GridExtent::MAX_CELLS as i64 + 1;
10385        for (width, height) in [(-1_i64, 4_i64), (4, -1), (i64::MAX, 2), (absurd, 1)] {
10386            let mut rt = Runtime::new();
10387            let ctx = wired_ctx(&mut rt);
10388            let (result, live_before, live_after, unit) = unsafe {
10389                let (w, h) = (praxis_alloc_int(ctx, width), praxis_alloc_int(ctx, height));
10390                let fill = praxis_alloc_int(ctx, 0);
10391                let before = rt.heap().stats().live_count;
10392                let r = praxis_grid_filled(ctx, &crate::scalars::INT as *const _, w, h, fill);
10393                (
10394                    r,
10395                    before,
10396                    rt.heap().stats().live_count,
10397                    rt.immortals().unit(),
10398                )
10399            };
10400            unsafe { drop_ctx(ctx) };
10401
10402            assert_eq!(
10403                rt.fault(),
10404                FaultKind::InvalidSize,
10405                "Grid({width}, {height}, 0) must fault"
10406            );
10407            assert_eq!(
10408                result.as_ptr(),
10409                unit.as_ptr(),
10410                "a faulted Grid({width}, {height}, 0) returns the Unit sentinel"
10411            );
10412            assert_eq!(
10413                live_after, live_before,
10414                "a rejected Grid({width}, {height}, 0) allocates nothing"
10415            );
10416        }
10417    }
10418
10419    /// A member the set cannot hold is a fault, and a negative one does not
10420    /// vanish silently. Unchecked, `bs.insert(10^18)` would ask `Vec::resize`
10421    /// for 10^16 words — an OOM abort from inside `extern "C"`.
10422    #[test]
10423    fn a_bitset_member_outside_the_representable_range_faults() {
10424        for member in [-1_i64, i64::MIN, i64::MAX, BitIndex::MAX + 1] {
10425            let mut rt = Runtime::new();
10426            let ctx = wired_ctx(&mut rt);
10427            let words;
10428            unsafe {
10429                let bs = praxis_bitset_new(ctx);
10430                let value = praxis_alloc_int(ctx, member);
10431                let _ = praxis_bitset_insert(ctx, bs, value);
10432                words = bitset_payload(bs).words.len();
10433            }
10434            unsafe { drop_ctx(ctx) };
10435
10436            assert_eq!(
10437                rt.fault(),
10438                FaultKind::InvalidSize,
10439                "BitSet.insert({member}) must fault"
10440            );
10441            assert_eq!(words, 0, "BitSet.insert({member}) must allocate no words");
10442        }
10443    }
10444
10445    /// The words of a **live, heap-allocated** `BitSet`, reached the way
10446    /// generated code reaches them: through
10447    /// [`INLINE_BITSET_SITE`](crate::bitset::INLINE_BITSET_SITE), from the
10448    /// object base, with no knowledge of the payload beyond what the site
10449    /// carries (ADR-118 part 2).
10450    ///
10451    /// `a_backend_can_read_the_length_and_the_elements_out_of_a_live_payload`
10452    /// in `collections.rs` is this test for `Vec`; this is the second payload's
10453    /// copy of the same agreement between the emitted load and the layout.
10454    ///
10455    /// Compiled out under `std-vec-payload`: that arm has no site to name, so
10456    /// naming one fails the *build* rather than miscompiling a load.
10457    #[cfg(not(feature = "std-vec-payload"))]
10458    #[test]
10459    fn the_inline_bitset_site_addresses_a_live_bitsets_words() {
10460        use crate::bitset::INLINE_BITSET_SITE;
10461
10462        let mut rt = Runtime::new();
10463        let ctx = wired_ctx(&mut rt);
10464        let (words, len) = unsafe {
10465            let bs = praxis_bitset_new(ctx);
10466            assert!(
10467                std::ptr::eq(INLINE_BITSET_SITE.type_id().descriptor(), bs.descriptor()),
10468                "the site names the descriptor the inline proof compares against"
10469            );
10470            for member in [0_i64, 63, 64, 200] {
10471                let value = praxis_alloc_int(ctx, member);
10472                let _ = praxis_bitset_insert(ctx, bs, value);
10473            }
10474            let base = bs.as_ptr().cast::<u8>().cast_const();
10475            (
10476                base.add(INLINE_BITSET_SITE.elements_offset())
10477                    .cast::<*const u64>()
10478                    .read(),
10479                base.add(INLINE_BITSET_SITE.len_offset())
10480                    .cast::<usize>()
10481                    .read(),
10482            )
10483        };
10484
10485        assert_eq!(len, 4, "bit 200 lives in the fourth word");
10486        assert_eq!(
10487            INLINE_BITSET_SITE.element_shift(),
10488            3,
10489            "a word is eight bytes"
10490        );
10491        for member in [0_u64, 63, 64, 200] {
10492            // SAFETY: `member >> 6 < len`, and `words` is the live buffer the
10493            // site's displacement just answered.
10494            let w = unsafe { *words.add((member >> 6) as usize) };
10495            assert!(
10496                (w >> (member & 63)) & 1 == 1,
10497                "bit {member} read back through the site's displacements"
10498            );
10499        }
10500        unsafe { drop_ctx(ctx) };
10501    }
10502
10503    /// Queries stay total: a value the set cannot hold is a value it does not
10504    /// contain, and removing one is a no-op. Neither may fault, and neither may
10505    /// grow the word vector.
10506    #[test]
10507    fn bitset_queries_outside_the_range_are_absent_rather_than_faults() {
10508        let mut rt = Runtime::new();
10509        let ctx = wired_ctx(&mut rt);
10510        let (present, words) = unsafe {
10511            let bs = praxis_bitset_new(ctx);
10512            let huge = praxis_alloc_int(ctx, i64::MAX);
10513            let _ = praxis_bitset_remove(ctx, bs, huge);
10514            // The answer is the scalar channel's `0`/`1` (ADR-118 decision 6),
10515            // so there is no box to load it back out of.
10516            let answer = praxis_bitset_contains(ctx, bs, huge);
10517            (answer != 0, bitset_payload(bs).words.len())
10518        };
10519        unsafe { drop_ctx(ctx) };
10520
10521        assert!(!present, "an unrepresentable member is absent");
10522        assert_eq!(words, 0, "a query allocates no words");
10523        assert_eq!(rt.fault(), FaultKind::None, "a query does not fault");
10524    }
10525
10526    /// `(i64::MAX, i64::MAX).neighbors4()` must not overflow the offset addition
10527    /// and panic across `extern "C"`. Every such neighbour is outside every
10528    /// grid, so the answer is an empty Vec.
10529    #[test]
10530    fn neighbors_of_an_extreme_point_are_empty_rather_than_a_panic() {
10531        let mut rt = Runtime::new();
10532        let ctx = wired_ctx(&mut rt);
10533        let cell = rt.alloc_int(1);
10534        let grid = rt.alloc_grid(&crate::scalars::INT, vec![cell], 1);
10535        let counts = unsafe {
10536            let mut counts = Vec::new();
10537            for (x, y) in [
10538                (i64::MAX, i64::MAX),
10539                (i64::MIN, i64::MIN),
10540                (i64::MAX, 0),
10541                (0, i64::MIN),
10542            ] {
10543                let point = alloc_point(ctx, x, y);
10544                counts.push((
10545                    vec_payload(praxis_grid_neighbors4(ctx, grid, point))
10546                        .items
10547                        .len(),
10548                    vec_payload(praxis_grid_neighbors8(ctx, grid, point))
10549                        .items
10550                        .len(),
10551                ));
10552            }
10553            counts
10554        };
10555        unsafe { drop_ctx(ctx) };
10556
10557        assert!(
10558            counts.iter().all(|(n4, n8)| *n4 == 0 && *n8 == 0),
10559            "an out-of-range point has no in-grid neighbours: {counts:?}"
10560        );
10561        assert_eq!(rt.fault(), FaultKind::None);
10562    }
10563
10564    /// A 3×3 grid of `Int`s holding `1..=9` in reading order, so a neighbour's
10565    /// cell names its own position.
10566    fn nine_grid(rt: &mut Runtime) -> GcRef {
10567        let cells: Vec<GcRef> = (1..=9).map(|n| rt.alloc_int(n)).collect();
10568        rt.alloc_grid(&crate::scalars::INT, cells, 3)
10569    }
10570
10571    /// Every field of a neighbourhood record as `(name, Some((x, y)) | None)`,
10572    /// **in slot order** — which is what a field read indexes, so a test that
10573    /// reads through this is a test of the order and not only of the values.
10574    ///
10575    /// # Safety
10576    /// `record` must be an `Around4`/`Around8` `GcRef`.
10577    unsafe fn around_fields(record: GcRef) -> Vec<(&'static str, Option<(i64, i64)>)> {
10578        // SAFETY: the caller guarantees a record built under one of the two
10579        // neighbourhood schemas, so every field is an `Option[(Int, Int)]`.
10580        unsafe {
10581            let rp = &*(record.payload::<u8>() as *const crate::records::RecordPayload);
10582            let schema = &*rp.schema;
10583            schema
10584                .fields
10585                .iter()
10586                .zip(&rp.items)
10587                .map(|(field, value)| {
10588                    let ep = &*(value.payload::<u8>() as *const crate::enums::EnumPayload);
10589                    let point = (i64::from(ep.tag) == crate::enums::OPTION_SOME_TAG)
10590                        .then(|| point_xy(ep.items[0]));
10591                    (field.name, point)
10592                })
10593                .collect()
10594        }
10595    }
10596
10597    /// `around4` names all four directions, and a direction that leaves the
10598    /// grid is `None` rather than absent.
10599    ///
10600    /// That is the whole difference from `neighbors4`, which answers a clipped
10601    /// `Vec` in which the corner case and the interior case are two lists of
10602    /// different lengths with no way to tell which entry was which direction.
10603    #[test]
10604    fn around4_answers_every_direction_and_the_missing_ones_are_none() {
10605        let mut rt = Runtime::new();
10606        let ctx = wired_ctx(&mut rt);
10607        let grid = nine_grid(&mut rt);
10608        let (middle, corner, far_corner) = unsafe {
10609            let m = around_fields(praxis_grid_around4(ctx, grid, alloc_point(ctx, 1, 1)));
10610            let c = around_fields(praxis_grid_around4(ctx, grid, alloc_point(ctx, 0, 0)));
10611            let f = around_fields(praxis_grid_around4(ctx, grid, alloc_point(ctx, 2, 2)));
10612            (m, c, f)
10613        };
10614        unsafe { drop_ctx(ctx) };
10615
10616        assert_eq!(
10617            middle,
10618            vec![
10619                ("up", Some((1, 0))),
10620                ("left", Some((0, 1))),
10621                ("right", Some((2, 1))),
10622                ("down", Some((1, 2))),
10623            ],
10624            "the plus in reading order, centre skipped"
10625        );
10626        assert_eq!(
10627            corner,
10628            vec![
10629                ("up", None),
10630                ("left", None),
10631                ("right", Some((1, 0))),
10632                ("down", Some((0, 1))),
10633            ]
10634        );
10635        assert_eq!(
10636            far_corner,
10637            vec![
10638                ("up", Some((2, 1))),
10639                ("left", Some((1, 2))),
10640                ("right", None),
10641                ("down", None),
10642            ]
10643        );
10644        assert_eq!(rt.fault(), FaultKind::None);
10645    }
10646
10647    /// `around8`'s slots are the 3×3 block in reading order, centre skipped —
10648    /// which is what makes a printed `Around8` look like the block it
10649    /// describes, and what a diagonal read depends on.
10650    #[test]
10651    fn around8_is_a_3x3_block_in_reading_order() {
10652        let mut rt = Runtime::new();
10653        let ctx = wired_ctx(&mut rt);
10654        let grid = nine_grid(&mut rt);
10655        let (middle, edge) = unsafe {
10656            let m = around_fields(praxis_grid_around8(ctx, grid, alloc_point(ctx, 1, 1)));
10657            let e = around_fields(praxis_grid_around8(ctx, grid, alloc_point(ctx, 1, 0)));
10658            (m, e)
10659        };
10660        unsafe { drop_ctx(ctx) };
10661
10662        assert_eq!(
10663            middle,
10664            vec![
10665                ("up_left", Some((0, 0))),
10666                ("up", Some((1, 0))),
10667                ("up_right", Some((2, 0))),
10668                ("left", Some((0, 1))),
10669                ("right", Some((2, 1))),
10670                ("down_left", Some((0, 2))),
10671                ("down", Some((1, 2))),
10672                ("down_right", Some((2, 2))),
10673            ]
10674        );
10675        // The top edge: the whole first row of the block is off the grid, and
10676        // says so three times rather than shortening the answer.
10677        assert_eq!(
10678            edge,
10679            vec![
10680                ("up_left", None),
10681                ("up", None),
10682                ("up_right", None),
10683                ("left", Some((0, 0))),
10684                ("right", Some((2, 0))),
10685                ("down_left", Some((0, 1))),
10686                ("down", Some((1, 1))),
10687                ("down_right", Some((2, 1))),
10688            ]
10689        );
10690        assert_eq!(rt.fault(), FaultKind::None);
10691    }
10692
10693    /// `neighbors_of_an_extreme_point_are_empty_rather_than_a_panic`'s case for
10694    /// the record and the counts: `grid_neighbor`'s `checked_add` is what keeps
10695    /// `(i64::MAX, i64::MAX)` from overflowing inside `extern "C"`, and every
10696    /// wrapper that steps a point has to be behind it.
10697    #[test]
10698    fn a_neighbourhood_of_an_extreme_point_is_all_absent_rather_than_a_panic() {
10699        let mut rt = Runtime::new();
10700        let ctx = wired_ctx(&mut rt);
10701        let grid = nine_grid(&mut rt);
10702        let one = rt.alloc_int(1);
10703        let observed = unsafe {
10704            let mut observed = Vec::new();
10705            for (x, y) in [
10706                (i64::MAX, i64::MAX),
10707                (i64::MIN, i64::MIN),
10708                (i64::MAX, 0),
10709                (0, i64::MIN),
10710            ] {
10711                let point = alloc_point(ctx, x, y);
10712                let four = around_fields(praxis_grid_around4(ctx, grid, point));
10713                let eight = around_fields(praxis_grid_around8(ctx, grid, point));
10714                observed.push((
10715                    four.iter().filter(|(_, p)| p.is_some()).count(),
10716                    eight.len(),
10717                    eight.iter().filter(|(_, p)| p.is_some()).count(),
10718                    int_payload(praxis_grid_count4(ctx, grid, point, one)),
10719                    int_payload(praxis_grid_count8(ctx, grid, point, one)),
10720                ));
10721            }
10722            observed
10723        };
10724        unsafe { drop_ctx(ctx) };
10725
10726        for row in &observed {
10727            assert_eq!(
10728                *row,
10729                (0, 8, 0, 0, 0),
10730                "an out-of-range point has no in-grid neighbours, and its \
10731                 record still has all eight fields: {observed:?}"
10732            );
10733        }
10734        assert_eq!(rt.fault(), FaultKind::None);
10735    }
10736
10737    /// `count4`/`count8` count cells, so a direction with no cell is not one of
10738    /// them — a corner counts over three neighbours, not eight.
10739    ///
10740    /// Equality is the descriptor's, which is `praxis_grid_find`'s path: the
10741    /// grid holds `1..=9`, so each count is the size of a known subset.
10742    #[test]
10743    fn a_neighbourhood_count_counts_only_the_cells_that_are_there() {
10744        let mut rt = Runtime::new();
10745        let ctx = wired_ctx(&mut rt);
10746        let grid = nine_grid(&mut rt);
10747        let counts = unsafe {
10748            let centre = alloc_point(ctx, 1, 1);
10749            let corner = alloc_point(ctx, 0, 0);
10750            let two = praxis_alloc_int(ctx, 2);
10751            let five = praxis_alloc_int(ctx, 5);
10752            let nine = praxis_alloc_int(ctx, 9);
10753            [
10754                // `2` is the cell above the centre: one orthogonal hit.
10755                int_payload(praxis_grid_count4(ctx, grid, centre, two)),
10756                // `9` is diagonal from the centre, so only the eight sees it.
10757                int_payload(praxis_grid_count4(ctx, grid, centre, nine)),
10758                int_payload(praxis_grid_count8(ctx, grid, centre, nine)),
10759                // The centre is never its own neighbour.
10760                int_payload(praxis_grid_count8(ctx, grid, centre, five)),
10761                // A corner's neighbourhood is two cells of four, three of eight.
10762                int_payload(praxis_grid_count4(ctx, grid, corner, five)),
10763                int_payload(praxis_grid_count8(ctx, grid, corner, five)),
10764            ]
10765        };
10766        unsafe { drop_ctx(ctx) };
10767
10768        assert_eq!(counts, [1, 0, 1, 0, 0, 1]);
10769        assert_eq!(rt.fault(), FaultKind::None);
10770    }
10771
10772    /// `map[key]` faults on an absent key where `.get` answers, and
10773    /// `praxis_counter_set` replaces a count where `praxis_counter_inc` only
10774    /// adds one.
10775    ///
10776    /// Here as well as in the JIT tests because §4.7's choice is the *runtime's*
10777    /// to make: the two map wrappers differ in one line, and a compiler that
10778    /// pointed both rows at `praxis_map_get` would still pass every type test.
10779    #[test]
10780    fn a_map_index_faults_where_get_answers_and_a_counter_set_replaces() {
10781        let mut rt = Runtime::new();
10782        let ctx = wired_ctx(&mut rt);
10783        let (present, absent_get) = unsafe {
10784            let map = praxis_map_new(ctx, &crate::scalars::INT as *const _);
10785            let key = praxis_alloc_int(ctx, 1);
10786            let val = praxis_alloc_int(ctx, 42);
10787            praxis_map_insert(ctx, map, key, val);
10788            let present = int_payload(praxis_map_index(ctx, map, key));
10789            assert_eq!(rt.fault(), FaultKind::None, "a present key does not fault");
10790            // `.get` on an absent key is `None` and no fault…
10791            let other = praxis_alloc_int(ctx, 2);
10792            let absent_get = praxis_map_get(ctx, map, other);
10793            assert_eq!(rt.fault(), FaultKind::None, "`.get` does not fault");
10794            // …and the subscript on the same key faults.
10795            praxis_map_index(ctx, map, other);
10796            (present, absent_get)
10797        };
10798        assert_eq!(present, 42);
10799        assert_eq!(
10800            absent_get.descriptor().id(),
10801            crate::enums::ENUM.id(),
10802            "`.get` answers with absence, and absence is an `Option` value"
10803        );
10804        assert_eq!(
10805            rt.fault(),
10806            FaultKind::IndexOutOfBounds,
10807            "§4.7: indexing a missing key faults"
10808        );
10809        unsafe { drop_ctx(ctx) };
10810
10811        // The counter half: `set` replaces, where `inc` adds one.
10812        let mut rt = Runtime::new();
10813        let ctx = wired_ctx(&mut rt);
10814        let (after_inc, after_set, len) = unsafe {
10815            let c = praxis_counter_new(ctx, &crate::scalars::INT as *const _);
10816            let key = praxis_alloc_int(ctx, 7);
10817            praxis_counter_inc(ctx, c, key);
10818            let after_inc = int_payload(praxis_counter_get(ctx, c, key));
10819            let five = praxis_alloc_int(ctx, 5);
10820            praxis_counter_set(ctx, c, key, five);
10821            let after_set = int_payload(praxis_counter_get(ctx, c, key));
10822            let len = int_payload(praxis_counter_len(ctx, c));
10823            (after_inc, after_set, len)
10824        };
10825        unsafe { drop_ctx(ctx) };
10826        assert_eq!(after_inc, 1);
10827        assert_eq!(after_set, 5, "a set replaces rather than adds");
10828        assert_eq!(len, 1, "and does not add a second entry for the same key");
10829        assert_eq!(rt.fault(), FaultKind::None);
10830    }
10831
10832    /// `min=` and `max=` order by the value's **own type** (ADR-045), which is
10833    /// the half no type test can see.
10834    ///
10835    /// Every case here is one the `int_payload` comparison this replaced answers
10836    /// *differently*, so this is a regression test rather than a demonstration —
10837    /// three of the four orderable types a program can write come out wrong when
10838    /// both payloads are read as `i64`s:
10839    ///
10840    /// * A `Text` payload is a pointer, so an `i64` compare orders two strings
10841    ///   by the addresses the allocator happened to hand out.
10842    /// * A negative `Float`'s bits *ascend* as the number descends, so `-2.0`
10843    ///   reads as the larger value.
10844    /// * A `Char` payload is four bytes wide, so an `i64` read takes four bytes
10845    ///   that are not the character's.
10846    #[test]
10847    fn an_updating_store_orders_by_the_values_own_type() {
10848        let mut rt = Runtime::new();
10849        let ctx = wired_ctx(&mut rt);
10850        // SAFETY: `ctx` is live and wired, and every ref below is freshly
10851        // allocated through the wrappers.
10852        let (text, float, ch, first) = unsafe {
10853            let key = text_ref(ctx, "k");
10854
10855            // `Text`: lexicographic, whichever order the candidates arrive in.
10856            let texts = praxis_map_new(ctx, &crate::text::TEXT as *const _);
10857            praxis_map_update_min(ctx, texts, key, text_ref(ctx, "pear"));
10858            praxis_map_update_min(ctx, texts, key, text_ref(ctx, "apple"));
10859            praxis_map_update_min(ctx, texts, key, text_ref(ctx, "quince"));
10860            let text = praxis_map_index(ctx, texts, key).as_text().to_string();
10861
10862            // `Float`: numeric, and the pair is chosen on the negative side of
10863            // zero where the bit order runs backwards.
10864            let floats = praxis_map_new(ctx, &crate::scalars::FLOAT as *const _);
10865            let minus_one = praxis_alloc_float(ctx, (-1.0f64).to_bits() as i64);
10866            let minus_two = praxis_alloc_float(ctx, (-2.0f64).to_bits() as i64);
10867            praxis_map_update_min(ctx, floats, key, minus_one);
10868            praxis_map_update_min(ctx, floats, key, minus_two);
10869            let float = praxis_map_index(ctx, floats, key).as_float();
10870
10871            // `Char`: by scalar value, out of a payload narrower than a word.
10872            let chars = praxis_map_new(ctx, &crate::scalars::CHAR as *const _);
10873            praxis_map_update_max(ctx, chars, key, praxis_alloc_char(ctx, 'b' as i64));
10874            praxis_map_update_max(ctx, chars, key, praxis_alloc_char(ctx, 'z' as i64));
10875            let ch = praxis_map_index(ctx, chars, key).as_char();
10876
10877            // …and an absent entry still accepts the first value it is handed,
10878            // which is the rule no comparison is reached for.
10879            let fresh = praxis_map_new(ctx, &crate::text::TEXT as *const _);
10880            praxis_map_update_max(ctx, fresh, key, text_ref(ctx, "only"));
10881            let first = praxis_map_index(ctx, fresh, key).as_text().to_string();
10882
10883            (text, float, ch, first)
10884        };
10885        assert_eq!(rt.fault(), FaultKind::None);
10886        unsafe { drop_ctx(ctx) };
10887
10888        assert_eq!(
10889            text, "apple",
10890            "a Text orders lexicographically, not by address"
10891        );
10892        assert_eq!(
10893            float, -2.0,
10894            "-2.0 is the smaller number, and the larger of the two bit patterns"
10895        );
10896        assert_eq!(ch, 'z');
10897        assert_eq!(first, "only", "an absent entry accepts the first value");
10898    }
10899
10900    /// The indexed receivers' updating stores (ADR-161): they order by the
10901    /// element's own type as the `Map` one does, and they part company with it
10902    /// on the index — there is no absent entry to accept a first value, so an
10903    /// index that is not there is the plain store's `IndexOutOfBounds`.
10904    ///
10905    /// Three properties no type test can see, and one of them is what separates
10906    /// this row from the `Map` row: **nothing is inserted**. A `Vec` that grew
10907    /// by one here would be `push` wearing the operator's spelling.
10908    #[test]
10909    fn an_indexed_updating_store_replaces_in_range_and_faults_outside_it() {
10910        let mut rt = Runtime::new();
10911        let ctx = wired_ctx(&mut rt);
10912        // SAFETY: `ctx` is live and wired; every ref below is freshly allocated
10913        // through the wrappers.
10914        let (kept, tied, cell, front, len_after) = unsafe {
10915            // A `Vec[Text]`, so the ordering is the one an `i64` compare cannot
10916            // do, and the loser leaves the element alone.
10917            let v = praxis_vec_new(ctx, &crate::text::TEXT as *const _);
10918            praxis_vec_push(ctx, v, text_ref(ctx, "pear"));
10919            praxis_vec_push(ctx, v, text_ref(ctx, "fig"));
10920            let zero = praxis_alloc_int(ctx, 0);
10921            praxis_vec_update_min(ctx, v, zero, text_ref(ctx, "apple"));
10922            praxis_vec_update_min(ctx, v, zero, text_ref(ctx, "quince"));
10923            let kept = praxis_vec_get(ctx, v, zero).as_text().to_string();
10924
10925            // A tie keeps the incumbent, which is only visible through identity:
10926            // the two are equal `Text`s in different objects.
10927            let one = praxis_alloc_int(ctx, 1);
10928            let incumbent = praxis_vec_get(ctx, v, one);
10929            praxis_vec_update_min(ctx, v, one, text_ref(ctx, "fig"));
10930            let tied = praxis_vec_get(ctx, v, one) == incumbent;
10931
10932            // A `Grid[Int]`: the relaxation shape, bounds-checked at the cell.
10933            let g = praxis_grid_new(ctx, &crate::scalars::INT as *const _, 2, 2);
10934            praxis_grid_set(ctx, g, zero, zero, praxis_alloc_int(ctx, 99));
10935            praxis_grid_update_min(ctx, g, zero, zero, praxis_alloc_int(ctx, 7));
10936            praxis_grid_update_min(ctx, g, zero, zero, praxis_alloc_int(ctx, 40));
10937            let cell = praxis_grid_get(ctx, g, zero, zero).as_int();
10938
10939            // A `Deque`, indexed 0-based from the front as its plain store is.
10940            let d = praxis_deque_new(ctx, &crate::scalars::INT as *const _);
10941            praxis_deque_push_back(ctx, d, praxis_alloc_int(ctx, 3));
10942            praxis_deque_push_front(ctx, d, praxis_alloc_int(ctx, 8));
10943            praxis_deque_update_max(ctx, d, zero, praxis_alloc_int(ctx, 20));
10944            let front = praxis_deque_get(ctx, d, zero).as_int();
10945
10946            assert_eq!(rt.fault(), FaultKind::None, "nothing so far faults");
10947
10948            // …and an index the vector does not hold is the plain store's
10949            // refusal, not a `Map`-style insert.
10950            let past_end = praxis_alloc_int(ctx, 2);
10951            praxis_vec_update_min(ctx, v, past_end, text_ref(ctx, "aardvark"));
10952            let len_after = praxis_vec_len(ctx, v).as_int();
10953
10954            (kept, tied, cell, front, len_after)
10955        };
10956        assert_eq!(
10957            rt.fault(),
10958            FaultKind::IndexOutOfBounds,
10959            "an index that is not there faults; it does not grow the vector"
10960        );
10961        unsafe { drop_ctx(ctx) };
10962
10963        assert_eq!(kept, "apple", "a Text element orders lexicographically");
10964        assert!(tied, "a tie keeps the element that is there");
10965        assert_eq!(cell, 7, "the grid cell relaxed and then held");
10966        assert_eq!(front, 20);
10967        assert_eq!(
10968            len_after, 2,
10969            "`min=` never appends — `push` is that spelling"
10970        );
10971    }
10972
10973    /// `Map.get` is statically value-typed, so an absent key cannot answer the
10974    /// Unit sentinel: a value whose static type is `V` and whose runtime
10975    /// descriptor is `Unit` is a type confusion the program cannot detect.
10976    ///
10977    /// The assertion names the variant, not merely `!= UNIT`: the answer is the
10978    /// `None` of the runtime's own `option_schema`, which is what makes it match
10979    /// a program's `None` arm.
10980    #[test]
10981    fn absent_map_get_does_not_return_an_untyped_unit_sentinel() {
10982        let mut rt = Runtime::new();
10983        let ctx = wired_ctx(&mut rt);
10984        let (missing, found);
10985        unsafe {
10986            let map = praxis_map_new(ctx, &crate::scalars::INT as *const _);
10987            let key = praxis_alloc_int(ctx, 1);
10988            missing = praxis_map_get(ctx, map, key);
10989            let value = praxis_alloc_int(ctx, 42);
10990            praxis_map_insert(ctx, map, key, value);
10991            found = praxis_map_get(ctx, map, key);
10992        }
10993
10994        assert_ne!(
10995            missing.descriptor().id(),
10996            crate::scalars::UNIT.id(),
10997            "Map.get is statically value-typed; absence needs Option or a checked fault, not Unit"
10998        );
10999        assert_eq!(missing.descriptor().id(), crate::enums::ENUM.id());
11000        assert_eq!(
11001            enum_tag_of(missing),
11002            crate::enums::OPTION_NONE_TAG as u32,
11003            "absence is `None`"
11004        );
11005        assert_eq!(enum_tag_of(found), crate::enums::OPTION_SOME_TAG as u32);
11006        // …and the `Some` carries the value, rather than merely not being Unit.
11007        let payload = unsafe { praxis_enum_payload(ctx, found, 0) };
11008        assert_eq!(unsafe { praxis_int_load(ctx, payload) }, 42);
11009        unsafe { drop_ctx(ctx) };
11010    }
11011
11012    /// A goal predicate that is `false` at every state, as a closure entry
11013    /// point. Rust-side `extern "C"` stands in for a JIT'd closure body: the
11014    /// oracle only cares that the pointer has the closure calling convention.
11015    ///
11016    /// # Safety
11017    /// Called only through [`ClosureOracle::call`], which upholds the ABI.
11018    unsafe extern "C" fn always_false(
11019        ctx: *mut RuntimeContext,
11020        _closure: GcRef,
11021        _state: GcRef,
11022    ) -> GcRef {
11023        // SAFETY: the oracle passes its own wired ctx.
11024        unsafe { bool_ref(ctx, false) }
11025    }
11026
11027    /// A neighbour function with no neighbours: the walk visits the start and
11028    /// stops, so the only thing that can decide the answer is the goal test.
11029    ///
11030    /// # Safety
11031    /// As [`always_false`].
11032    unsafe extern "C" fn no_neighbours(
11033        ctx: *mut RuntimeContext,
11034        _closure: GcRef,
11035        _state: GcRef,
11036    ) -> GcRef {
11037        // SAFETY: the oracle passes its own wired ctx.
11038        unsafe { praxis_vec_new(ctx, &crate::scalars::INT as *const _) }
11039    }
11040
11041    /// A `Bool` object laid out exactly the way [`Heap::alloc_raw`] lays one
11042    /// out — a `GcHeader` followed by its payload at
11043    /// `GcHeader::payload_offset_for(1)` — but in memory *this module* owns,
11044    /// and with the seven bytes after the one-byte payload set to `0xFF`.
11045    ///
11046    /// The heap cannot be asked for this shape. Under the page allocator
11047    /// (ADR-103) a `Bool` lands on the ladder's bottom rung, so its block is
11048    /// rounded up to an 8-byte boundary and the seven bytes after the one-byte
11049    /// payload are slack no object ever writes — and a fresh page is `mmap`ped
11050    /// zero, so an eight-byte read of a heap `Bool` answers *correctly* by
11051    /// accident. Owning the storage turns the padding from an accident into a
11052    /// fixture, which is the only way the read can be measured rather than
11053    /// sampled.
11054    ///
11055    /// The header carries a **freshly minted** `HeapId`, so the collector's
11056    /// provenance check (`Heap::mark`) skips this object instead of colouring
11057    /// it: the oracle roots every closure result, and a root the heap did not
11058    /// allocate is not the heap's to touch.
11059    #[repr(C)]
11060    struct DirtyPaddedBool {
11061        header: crate::gc::GcHeader,
11062        /// Byte 0 is the `BoolPayload`; bytes 1..8 are the neighbours a
11063        /// wrong-width or wrong-offset read would consume.
11064        payload: [u8; 8],
11065    }
11066
11067    /// A `false` whose seven following bytes are `0xFF`, leaked once per thread
11068    /// so a closure entry point can answer it. See [`DirtyPaddedBool`].
11069    fn dirty_padded_false() -> GcRef {
11070        thread_local! {
11071            static CELL: std::cell::Cell<*mut crate::gc::GcHeader> =
11072                const { std::cell::Cell::new(std::ptr::null_mut()) };
11073        }
11074        CELL.with(|cell| {
11075            if cell.get().is_null() {
11076                let object = Box::leak(Box::new(DirtyPaddedBool {
11077                    header: crate::gc::GcHeader::new(
11078                        &scalars::BOOL,
11079                        crate::gc::GcHeader::payload_offset_for(scalars::BOOL.align()) as u16,
11080                        crate::gc::HeapId::mint(),
11081                    ),
11082                    payload: [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
11083                }));
11084                cell.set(&mut object.header as *mut crate::gc::GcHeader);
11085            }
11086            // SAFETY: the pointer heads a leaked, correctly-laid-out `Bool`
11087            // object that lives for the rest of the process.
11088            unsafe { GcRef::from_raw(cell.get()) }
11089        })
11090    }
11091
11092    /// As [`always_false`], but answering the [`dirty_padded_false`] fixture.
11093    ///
11094    /// # Safety
11095    /// As [`always_false`].
11096    unsafe extern "C" fn always_dirty_false(
11097        _ctx: *mut RuntimeContext,
11098        _closure: GcRef,
11099        _state: GcRef,
11100    ) -> GcRef {
11101        dirty_padded_false()
11102    }
11103
11104    /// `ClosureOracle::is_goal` must read the closure's `Bool` answer at a
11105    /// `Bool`'s width: the payload is **one** byte (`BoolPayload = u8`, and
11106    /// `BOOL` is built from it), so an eight-byte read would take seven further
11107    /// bytes from past the object.
11108    ///
11109    /// Every goal predicate of every goal-directed helper goes
11110    /// through that read, so this walk is the whole class: the goal answers
11111    /// `false` at the only reachable state, and the answer must be `None`.
11112    ///
11113    /// The `false` it answers is [`dirty_padded_false`], whose payload byte is
11114    /// `0x00` and whose next seven bytes are `0xFF`. That is what makes this a
11115    /// gate rather than a walk: an eight-byte read answers
11116    /// `0xFFFF_FFFF_FFFF_FF00`, which is "goal reached at the start state" and
11117    /// therefore `Some(0)`; a read at *any* offset past byte zero answers
11118    /// `0xFF`, likewise `Some(0)`. Only one byte at offset zero answers `None`,
11119    /// so the test fails if either the width or the offset is wrong. Asking the
11120    /// heap for this shape does not work — see [`DirtyPaddedBool`].
11121    ///
11122    /// [`read_scalar`] is what makes both mistakes unspellable at the call
11123    /// site, and `int_payload`'s width check — an ordinary branch, so it holds
11124    /// in every profile — is what stops the next site from making them;
11125    /// `read_scalar_answers_none_for_a_foreign_descriptor` pins the reader
11126    /// itself.
11127    #[test]
11128    fn a_graph_goal_predicate_reads_a_bool_at_a_bool_s_width() {
11129        // The fixture is only a gate while its padding is dirty: state that
11130        // here, so a later edit that zeroes it fails loudly rather than
11131        // silently turning this back into the walk it replaced.
11132        let fixture = dirty_padded_false();
11133        assert!(std::ptr::eq(fixture.descriptor(), &scalars::BOOL));
11134        assert_eq!(
11135            unsafe { read_scalar(fixture, scalars::BOOL_PAYLOAD) },
11136            Some(0u8),
11137            "the fixture is `false` at a Bool's width"
11138        );
11139        assert_ne!(
11140            unsafe { *fixture.payload::<i64>() },
11141            0,
11142            "…and non-zero at an Int's, which is what the wrong read consumed"
11143        );
11144
11145        let mut rt = Runtime::new();
11146        let ctx = wired_ctx(&mut rt);
11147        let answer = unsafe {
11148            let goal = praxis_alloc_closure(ctx, always_dirty_false as *const u8, 0);
11149            let neighbours = praxis_alloc_closure(ctx, no_neighbours as *const u8, 0);
11150            let start = praxis_alloc_int(ctx, 0);
11151            praxis_bfs_distance(ctx, start, neighbours, goal)
11152        };
11153        assert!(!rt.has_pending_fault(), "fault: {:?}", rt.fault());
11154        assert_eq!(answer.descriptor().id(), crate::enums::ENUM.id());
11155        assert_eq!(
11156            enum_tag_of(answer),
11157            crate::enums::OPTION_NONE_TAG as u32,
11158            "the goal answered `false` at every state, so no distance was found"
11159        );
11160        unsafe { drop_ctx(ctx) };
11161    }
11162
11163    /// The same walk against the immortal `false` every real program's closure
11164    /// answers — a companion, not a gate: a wrong-width read passes it, because
11165    /// the allocator leaves a `Bool`'s slack bytes zero. It rules out a reader
11166    /// that only handles the fixture correctly.
11167    #[test]
11168    fn a_graph_goal_predicate_that_is_false_everywhere_finds_nothing() {
11169        let mut rt = Runtime::new();
11170        let ctx = wired_ctx(&mut rt);
11171        let answer = unsafe {
11172            let goal = praxis_alloc_closure(ctx, always_false as *const u8, 0);
11173            let neighbours = praxis_alloc_closure(ctx, no_neighbours as *const u8, 0);
11174            let start = praxis_alloc_int(ctx, 0);
11175            praxis_bfs_distance(ctx, start, neighbours, goal)
11176        };
11177        assert!(!rt.has_pending_fault(), "fault: {:?}", rt.fault());
11178        assert_eq!(answer.descriptor().id(), crate::enums::ENUM.id());
11179        assert_eq!(enum_tag_of(answer), crate::enums::OPTION_NONE_TAG as u32);
11180        unsafe { drop_ctx(ctx) };
11181    }
11182
11183    /// The reader itself, both directions. `read_scalar` is what makes a
11184    /// wrong-type or wrong-width read unspellable at a call site, so its own
11185    /// contract is pinned here: the right type reads at the right width, and a
11186    /// foreign type answers `None` instead of reinterpreting the bytes.
11187    #[test]
11188    fn read_scalar_answers_none_for_a_foreign_descriptor() {
11189        let mut rt = Runtime::new();
11190        let ctx = wired_ctx(&mut rt);
11191        unsafe {
11192            let t = bool_ref(ctx, true);
11193            let f = bool_ref(ctx, false);
11194            assert_eq!(read_scalar(t, crate::scalars::BOOL_PAYLOAD), Some(1u8));
11195            assert_eq!(read_scalar(f, crate::scalars::BOOL_PAYLOAD), Some(0u8));
11196            // An `Int` is not a `Bool`, and the answer is absence rather than
11197            // the first byte of the `i64`.
11198            let n = praxis_alloc_int(ctx, 1);
11199            assert_eq!(read_scalar(n, crate::scalars::BOOL_PAYLOAD), None);
11200            assert_eq!(read_scalar(n, crate::scalars::INT_PAYLOAD), Some(1i64));
11201            drop_ctx(ctx);
11202        }
11203    }
11204
11205    /// The variant tag of an enum value, read the way the runtime's own
11206    /// `enum_format` reads it.
11207    fn enum_tag_of(value: GcRef) -> u32 {
11208        // SAFETY: the caller passes an ENUM-descriptor object.
11209        unsafe { (*(value.payload::<u8>() as *const crate::enums::EnumPayload)).tag }
11210    }
11211
11212    /// The same rule under a *tuple* static type:
11213    /// `Grid.find` answers `(Int, Int)`, so "nothing matched" cannot be the Unit
11214    /// sentinel wearing that type.
11215    #[test]
11216    fn absent_grid_find_does_not_return_an_untyped_unit_sentinel() {
11217        let mut rt = Runtime::new();
11218        let ctx = wired_ctx(&mut rt);
11219        let (missing, found);
11220        unsafe {
11221            let cell = praxis_alloc_int(ctx, 1);
11222            let sought = praxis_alloc_int(ctx, 2);
11223            let grid = rt.alloc_grid(&crate::scalars::INT, vec![cell], 1);
11224            missing = praxis_grid_find(ctx, grid, sought);
11225            let present = praxis_alloc_int(ctx, 1);
11226            found = praxis_grid_find(ctx, grid, present);
11227        }
11228
11229        assert_ne!(
11230            missing.descriptor().id(),
11231            crate::scalars::UNIT.id(),
11232            "Grid.find is statically point-typed; absence needs Option or a checked fault, not Unit"
11233        );
11234        assert_eq!(missing.descriptor().id(), crate::enums::ENUM.id());
11235        assert_eq!(enum_tag_of(missing), crate::enums::OPTION_NONE_TAG as u32);
11236        // …and a hit is `Some((x, y))`, still a real point inside the option.
11237        assert_eq!(enum_tag_of(found), crate::enums::OPTION_SOME_TAG as u32);
11238        let point = unsafe { praxis_enum_payload(ctx, found, 0) };
11239        assert_eq!(point.descriptor().id(), crate::tuples::TUPLE.id());
11240        unsafe { drop_ctx(ctx) };
11241    }
11242
11243    // --- GC pacing (§12.4, ADR-019) ----------------------------------------
11244    //
11245    // `maybe_collect` is the load-bearing mechanism for the shadow-stack
11246    // spill: the alloc wrappers call it so collection happens automatically
11247    // inside JIT'd code.
11248
11249    #[test]
11250    fn maybe_collect_skips_below_threshold() {
11251        // A fresh heap with a single small allocation is well under the 64 KiB
11252        // threshold, so `maybe_collect` must report no collection ran.
11253        let mut rt = Runtime::new();
11254        let ctx = wired_ctx(&mut rt);
11255        // SAFETY: ctx wired.
11256        unsafe {
11257            let _ = praxis_alloc_int(ctx, 1);
11258            // Nothing live matters here; we only ask whether collection *ran*.
11259            let roots = crate::roots::RuntimeRoots::from_context(ctx);
11260            let ran = rt.heap().maybe_collect(&roots);
11261            assert!(
11262                !ran,
11263                "a single small Int must not trip the 64 KiB threshold"
11264            );
11265        }
11266        unsafe { drop_ctx(ctx) };
11267    }
11268
11269    #[test]
11270    fn maybe_collect_runs_under_pressure() {
11271        // Allocating past the 64 KiB threshold collects on its own, with no
11272        // generated frame on the stack and no hand-written `maybe_collect`
11273        // call. The helper asserts it happens within 10,000 allocations.
11274        let mut rt = Runtime::new();
11275        let ctx = wired_ctx(&mut rt);
11276        // SAFETY: ctx wired.
11277        unsafe {
11278            let _ = allocate_until_automatic_collection(&rt, ctx);
11279            // After a collection the pacing counter resets, so an immediate
11280            // call (no new allocations) does not collect again.
11281            let roots = crate::roots::RuntimeRoots::from_context(ctx);
11282            assert!(
11283                !rt.heap().maybe_collect(&roots),
11284                "counter must reset after a collection"
11285            );
11286        }
11287        unsafe { drop_ctx(ctx) };
11288    }
11289
11290    #[test]
11291    fn checked_int_add_is_an_automatic_gc_safepoint() {
11292        let mut rt = Runtime::new();
11293        let ctx = wired_ctx(&mut rt);
11294        let collected;
11295        unsafe {
11296            // The *sum* has to be uninterned, not just the operands: what is
11297            // watched below is whether `praxis_int_add`'s result enters the live
11298            // registry, and an interned sum never does (see `UNINTERNED`).
11299            let lhs = praxis_alloc_int(ctx, UNINTERNED);
11300            let rhs = praxis_alloc_int(ctx, 22);
11301            let mut frame = push_frame(ctx, SlotCount::new(2).unwrap());
11302            frame.set(0, lhs);
11303            frame.set(1, rhs);
11304
11305            let mut before = rt.heap().stats().live_count;
11306            let mut observed = false;
11307            for _ in 0..10_000 {
11308                let _ = praxis_int_add(ctx, lhs, rhs);
11309                let after = rt.heap().stats().live_count;
11310                if after < before.saturating_add(1) {
11311                    observed = true;
11312                    break;
11313                }
11314                before = after;
11315            }
11316            collected = observed;
11317            drop(frame);
11318        }
11319        unsafe { drop_ctx(ctx) };
11320
11321        assert!(
11322            collected,
11323            "every allocating ABI wrapper must participate in automatic GC pacing"
11324        );
11325    }
11326
11327    #[test]
11328    fn automatic_gc_roots_the_ambient_input_buffer() {
11329        let mut rt = Runtime::new();
11330        let ctx = wired_ctx(&mut rt);
11331        let live_after_collection;
11332        unsafe {
11333            (*ctx).input_source = rt.alloc_text("input that main has not read yet");
11334            let frame = push_frame(ctx, SlotCount::new(0).unwrap());
11335            live_after_collection = allocate_until_automatic_collection(&rt, ctx);
11336            drop(frame);
11337        }
11338        unsafe { drop_ctx(ctx) };
11339
11340        assert!(
11341            live_after_collection >= 2,
11342            "the ambient input Text and the allocation returned after collection must both remain live"
11343        );
11344    }
11345
11346    #[test]
11347    fn automatic_gc_roots_parse_failure_partial_values() {
11348        let mut rt = Runtime::new();
11349        let ctx = wired_ctx(&mut rt);
11350        // Uninterned: what this test watches is a *registered* object surviving
11351        // a collection that roots only through `ParseDetail.partial`, and an
11352        // interned `Int` is never registered, so it would survive whether the
11353        // root set included the slot or not (see `UNINTERNED`).
11354        let partial = rt.alloc_int(UNINTERNED);
11355        rt.parse_detail_mut()
11356            .consider(ParseFail::here(0, "test").with_partial(Some(partial)), b"");
11357        let live_after_collection;
11358        unsafe {
11359            let frame = push_frame(ctx, SlotCount::new(0).unwrap());
11360            live_after_collection = allocate_until_automatic_collection(&rt, ctx);
11361            drop(frame);
11362        }
11363        unsafe { drop_ctx(ctx) };
11364
11365        assert!(
11366            live_after_collection >= 2,
11367            "ParseDetail.partial is runtime-owned and must be included in every automatic root set"
11368        );
11369    }
11370
11371    #[test]
11372    fn automatic_gc_roots_runtime_owned_crash_snapshots() {
11373        let mut rt = Runtime::new();
11374        let ctx = wired_ctx(&mut rt);
11375        // Uninterned, for `automatic_gc_roots_parse_failure_partial_values`'s
11376        // reason: the observable is a registered object surviving because the
11377        // snapshot rooted it.
11378        let captured = rt.alloc_int(UNINTERNED);
11379        let local_name = b"value";
11380        let meta = crate::debug::DebugLocalMeta {
11381            callee_name: std::ptr::null(),
11382            callee_name_len: 0,
11383            source_name: local_name.as_ptr(),
11384            name_len: local_name.len() as u32,
11385            symbol_id: 1,
11386            descriptor: &crate::scalars::INT as *const _,
11387            type_id: 0,
11388            kind: crate::debug::LOCAL_KIND_USER,
11389            span_start: 0,
11390            span_end: 0,
11391            slot_kind: crate::debug::DebugSlotKind::Reference,
11392        };
11393        let metas = [meta];
11394        let func_name = b"main";
11395        let func_meta = crate::debug::FunctionDebugMeta {
11396            func_name: func_name.as_ptr(),
11397            func_name_len: func_name.len() as u32,
11398            local_count: 1,
11399            locals: metas.as_ptr(),
11400            span_start: 0,
11401            span_end: 0,
11402        };
11403        let live_after_collection;
11404        // SAFETY: `ctx` is wired to `rt`; `func_meta`/`metas` outlive the guard,
11405        // and the snapshot is taken while the frame is still claimed — the
11406        // ordering a generated fault epilogue has (ADR-033 decision 1).
11407        unsafe {
11408            let mut debug_frame = crate::debug::push_frame(ctx, &func_meta);
11409            debug_frame.set(0, captured);
11410            crate::crash_snapshot::praxis_snapshot_debug_chain(ctx);
11411            drop(debug_frame);
11412            assert!(rt.crash_snapshot().is_some());
11413
11414            let shadow_frame = push_frame(ctx, SlotCount::new(0).unwrap());
11415            live_after_collection = allocate_until_automatic_collection(&rt, ctx);
11416            drop(shadow_frame);
11417        }
11418        unsafe { drop_ctx(ctx) };
11419
11420        assert!(
11421            live_after_collection >= 2,
11422            "a runtime-owned CrashSnapshot must root its copied local values during automatic GC"
11423        );
11424    }
11425
11426    #[test]
11427    fn nested_allocating_helpers_root_intermediate_results() {
11428        // `Grid.positions` builds its result Vec in a Rust local and fills it by
11429        // calling `alloc_point`, which allocates three times per point. Every
11430        // one of those is a safepoint, and the shadow stack only sees what
11431        // generated code spilled — this is all native code, so the result Vec,
11432        // the points already in it and the tuple `alloc_point` is midway
11433        // through filling are rooted by the helper's own `NativeScope`.
11434        //
11435        // This calls the real helper rather than inlining a sketch of it, and
11436        // reading the points back afterwards is what proves nothing was
11437        // reclaimed.
11438        // The grid is wide enough that the helper's own point allocations cross
11439        // the pacing threshold partway through the loop — the collection has to
11440        // happen *inside* the helper for this to test anything.
11441        const W: usize = 40;
11442        let mut rt = Runtime::new();
11443        let ctx = wired_ctx(&mut rt);
11444        let mut coords: Vec<(i64, i64)> = Vec::new();
11445        let collections_inside_the_helper;
11446        unsafe {
11447            let cells: Vec<GcRef> = (0..(W * W) as i64).map(|i| rt.alloc_int(i)).collect();
11448            let grid = rt.alloc_grid(&scalars::INT, cells, W);
11449            let mut frame = push_frame(ctx, SlotCount::new(1).unwrap());
11450            frame.set(0, grid);
11451
11452            let before = rt.heap().stats().live_count;
11453            let positions = praxis_grid_positions(ctx, grid);
11454            // Every point survived, so the live count only grew; a collection
11455            // that reclaimed the half-built result would show up as a drop.
11456            collections_inside_the_helper = rt.heap().stats().live_count > before;
11457
11458            let items = &(*positions.payload::<VecPayload>()).items;
11459            assert_eq!(items.len(), W * W, "one position per cell");
11460            for point in items {
11461                let tuple = &*point.payload::<crate::tuples::TuplePayload>();
11462                coords.push((int_payload(tuple.items[0]), int_payload(tuple.items[1])));
11463            }
11464
11465            drop(frame);
11466        }
11467        unsafe { drop_ctx(ctx) };
11468
11469        assert!(collections_inside_the_helper);
11470        let expected: Vec<(i64, i64)> = (0..W * W)
11471            .map(|i| ((i % W) as i64, (i / W) as i64))
11472            .collect();
11473        assert_eq!(
11474            coords, expected,
11475            "every point and coordinate the helper allocated must survive the \
11476             collections the helper itself triggers"
11477        );
11478    }
11479
11480    // --- null-context safety (defensive guards, §10.4 spirit) --------------
11481
11482    #[test]
11483    fn check_fault_on_null_context_is_zero() {
11484        // A null/unwired context must report no fault rather than dereferencing
11485        // the null pointer (the guard at `praxis_check_fault`).
11486        // SAFETY: passing a null context is the exact case the guard handles.
11487        assert_eq!(unsafe { praxis_check_fault(std::ptr::null_mut()) }, 0);
11488    }
11489
11490    // The inline prologue deliberately does not null-check the context
11491    // (ADR-101): the check would cost every call in the language, and
11492    // `Runtime::context` is the only producer of a context generated code is
11493    // handed. `RuntimeContext::placeholder` carries the obligation in its doc.
11494
11495    // --- a null element type stays unknown ---------------------------------
11496
11497    /// A collection built with no static element type must not claim to hold
11498    /// `Int`s, and what it holds must render as what it is.
11499    ///
11500    /// The codegen passes a **null** descriptor for `var c = Counter()` — its
11501    /// contract above `collection_element_descriptor_for` says so, and says
11502    /// every `praxis_*_new` wrapper reads it that way. Replacing the null with
11503    /// `&INT` is not a default but a false claim, and the label is dispatched
11504    /// through: a `Text` key would hash and print as an `i64`, a `Float`
11505    /// element as the integer its bits spell.
11506    ///
11507    /// Both halves are asserted because either alone passes a wrong fix: the
11508    /// absent label is the representation, the rendering is the answer.
11509    #[test]
11510    fn a_collection_with_no_static_element_type_does_not_claim_int() {
11511        let mut rt = Runtime::new();
11512        let ctx = wired_ctx(&mut rt);
11513        // SAFETY: `ctx` is wired; every argument below is a wrapper's own.
11514        unsafe {
11515            // Exactly what the codegen passes when the element type is still an
11516            // inference variable.
11517            let null = std::ptr::null::<TypeDescriptor>();
11518            let counter = praxis_counter_new(ctx, null);
11519            let set = praxis_set_new(ctx, null);
11520            let map = praxis_map_new(ctx, null);
11521            let min_heap = praxis_min_heap_new(ctx, null);
11522            let max_heap = praxis_max_heap_new(ctx, null);
11523
11524            assert!(
11525                counter_payload(counter).key().is_none(),
11526                "a Counter with no static key type must not claim one"
11527            );
11528            assert!(set_payload(set).element().is_none());
11529            assert!(map_payload(map).key().is_none());
11530            assert!(min_heap_payload(min_heap).element().is_none());
11531            assert!(max_heap_payload(max_heap).element().is_none());
11532
11533            // …and the values come back out as themselves. A `Text` key through
11534            // a `Counter`, a `Float` through a `MinHeap`: neither is an `Int`,
11535            // and a guessed `Int` label would print both as one.
11536            let key = praxis_alloc_text(ctx, "ab".as_ptr(), 2);
11537            praxis_counter_inc(ctx, counter, key);
11538            let keys = praxis_counter_keys(ctx, counter);
11539            let mut rendered = String::new();
11540            keys.format(&mut rendered);
11541            assert_eq!(rendered, "[ab]", "a Counter's keys are its keys");
11542
11543            let half = praxis_alloc_float(ctx, 1.5f64.to_bits() as i64);
11544            praxis_min_heap_push(ctx, min_heap, half);
11545            let mut rendered = String::new();
11546            min_heap.format(&mut rendered);
11547            assert_eq!(rendered, "[1.5]", "a MinHeap prints the elements it holds");
11548
11549            let member = praxis_alloc_text(ctx, "zz".as_ptr(), 2);
11550            praxis_set_insert(ctx, set, member);
11551            let items = praxis_set_items(ctx, set);
11552            let mut rendered = String::new();
11553            items.format(&mut rendered);
11554            assert_eq!(rendered, "[zz]");
11555        }
11556        // SAFETY: the context was leaked by `wired_ctx` and is unused after this.
11557        unsafe { drop_ctx(ctx) };
11558    }
11559
11560    /// A `Map` does not claim its values are `Int`s.
11561    ///
11562    /// `praxis_map_new` takes one descriptor — the key's — because the `MapNew`
11563    /// row carries one type argument, so the value slot starts **null**.
11564    /// Writing `INT` there would be a claim rather than a default, and it would
11565    /// be the same word as "unknown", so the adoption that follows could not
11566    /// tell a `Map` that really holds `Int`s from one that had never been told
11567    /// anything — and a `Map[Text, Text]`'s value would be read as an `i64`.
11568    #[test]
11569    fn a_map_does_not_claim_its_values_are_ints() {
11570        let mut rt = Runtime::new();
11571        let ctx = wired_ctx(&mut rt);
11572        // SAFETY: `ctx` is wired; every argument below is a wrapper's own.
11573        unsafe {
11574            let empty = praxis_map_new(ctx, &crate::text::TEXT);
11575            assert!(
11576                map_payload(empty).value().is_none(),
11577                "an empty Map has been told nothing about its values"
11578            );
11579            // …so the `Vec` its `values()` answers is not labelled `Int`
11580            // either. A guessed `Int` there would make an empty
11581            // `Map[Text, Text]`'s values unequal to an empty `Vec[Text]`,
11582            // because `vec_equals` compares element labels.
11583            let none_yet = praxis_map_values(ctx, empty);
11584            assert!(vec_payload(none_yet).element().is_none());
11585
11586            // The first insert is what the map learns from.
11587            let k = praxis_alloc_text(ctx, "k".as_ptr(), 1);
11588            let v = praxis_alloc_text(ctx, "vv".as_ptr(), 2);
11589            praxis_map_insert(ctx, empty, k, v);
11590            assert!(
11591                std::ptr::eq(map_payload(empty).value().unwrap(), &crate::text::TEXT),
11592                "a Map learns its value type from the first value inserted"
11593            );
11594            let mut rendered = String::new();
11595            praxis_map_values(ctx, empty).format(&mut rendered);
11596            assert_eq!(rendered, "[vv]");
11597
11598            // A `Map` that really does hold `Int`s says so — an assertion only
11599            // a null "unknown" makes possible, since a hardcoded `INT` would be
11600            // the same word as "never been told".
11601            let ints = praxis_map_new(ctx, &crate::text::TEXT);
11602            let ik = praxis_alloc_text(ctx, "n".as_ptr(), 1);
11603            praxis_map_insert(ctx, ints, ik, praxis_alloc_int(ctx, 7));
11604            assert!(std::ptr::eq(
11605                map_payload(ints).value().unwrap(),
11606                &scalars::INT
11607            ));
11608        }
11609        // SAFETY: the context was leaked by `wired_ctx` and is unused after this.
11610        unsafe { drop_ctx(ctx) };
11611    }
11612
11613    /// An unlearned label is not a label, so it cannot make two empty
11614    /// collections unequal.
11615    ///
11616    /// `same_element` is not pointer identity, because a never-inserted `Map`'s
11617    /// `values()` carries no label and an equally-typed empty `Vec[Int]`
11618    /// carries `Int`. ADR-066 decision 5 is the rule: a null slot means the
11619    /// *value's own* descriptor answers, and a collection with no label has no
11620    /// values, so nothing is left to disagree. Reinstating a guessed descriptor
11621    /// is the fix this rejects.
11622    ///
11623    /// The last case is the limit: two collections that have each been told
11624    /// their element type must still disagree when the types differ.
11625    #[test]
11626    fn an_unlearned_element_label_does_not_make_two_empty_collections_unequal() {
11627        use crate::collections::same_element;
11628        let int: *const crate::descriptor::TypeDescriptor = &scalars::INT;
11629        let text: *const crate::descriptor::TypeDescriptor = &crate::text::TEXT;
11630        let unlearned: *const crate::descriptor::TypeDescriptor = std::ptr::null();
11631
11632        assert!(same_element(unlearned, int), "no label agrees with `Int`");
11633        assert!(same_element(int, unlearned), "and in the other order");
11634        assert!(same_element(unlearned, unlearned));
11635        assert!(same_element(int, int));
11636        // The rule this must not weaken: two *learned* labels that differ are
11637        // two different collections, empty or not.
11638        assert!(!same_element(int, text));
11639
11640        // End to end: a `Map` never inserted into has no value label, and its
11641        // `values()` is an empty `Vec` that is equal to an empty `Vec` however
11642        // that one was labelled.
11643        let mut rt = Runtime::new();
11644        let ctx = wired_ctx(&mut rt);
11645        // SAFETY: `ctx` is wired; every argument below is a wrapper's own.
11646        unsafe {
11647            let never_inserted = praxis_map_new(ctx, &crate::text::TEXT);
11648            let unlabelled = praxis_map_values(ctx, never_inserted);
11649            assert!(vec_payload(unlabelled).element().is_none());
11650
11651            let labelled_ints = praxis_vec_new(ctx, &scalars::INT as *const _);
11652            assert!(
11653                praxis_struct_eq(ctx, unlabelled, labelled_ints) != 0,
11654                "an empty Map's values are an empty Vec[Int]"
11655            );
11656            assert!(
11657                praxis_struct_eq(ctx, labelled_ints, unlabelled) != 0,
11658                "and equality is symmetric"
11659            );
11660
11661            // A non-empty collection is still not an empty one: the length
11662            // check behind `same_element` is what answers, and it must.
11663            praxis_vec_push(ctx, labelled_ints, praxis_alloc_int(ctx, 1));
11664            assert!(praxis_struct_eq(ctx, unlabelled, labelled_ints) == 0);
11665        }
11666        // SAFETY: the context was leaked by `wired_ctx` and is unused after this.
11667        unsafe { drop_ctx(ctx) };
11668    }
11669
11670    // --- the manifest's fault column is checked against the code -----------
11671
11672    /// Every function defined in this file, as `(name, body)`.
11673    ///
11674    /// Line-based on purpose: a definition is a line whose first tokens are one
11675    /// of Rust's `fn` spellings, and its body runs to the line where the brace
11676    /// depth opened by that definition returns to zero. Anything cleverer would
11677    /// be a Rust parser, and anything looser — matching `fn` anywhere — reads
11678    /// the word out of doc comments and glues unrelated bodies together.
11679    fn functions_in_this_file() -> Vec<(String, String)> {
11680        functions_in(include_str!("abi.rs"))
11681    }
11682
11683    /// The code of one line, with any `//` comment removed.
11684    ///
11685    /// The sweep reads what the **compiler** sees, not what a reader wrote
11686    /// beside it. The fixed point matches `set_fault(` as a plain substring, so
11687    /// a comment inside a wrapper naming the helper would otherwise classify
11688    /// that wrapper as faulting. A sweep a comment can fool is a sweep that gets
11689    /// edited around rather than satisfied, which is the failure mode the whole
11690    /// invariant exists to prevent.
11691    ///
11692    /// A `//` inside a string or `char` literal is **not** a comment — `"//"`
11693    /// and `'/'` both occur in this file — so the scan tracks which it is in.
11694    /// It is not a Rust lexer: a raw string's hashes and a block comment are
11695    /// not modelled, because neither appears in a function body here and a
11696    /// half-lexer that claimed to be one would be worse than a stated
11697    /// limitation. It errs toward keeping code, never toward dropping it.
11698    fn code_only(line: &str) -> &str {
11699        let bytes = line.as_bytes();
11700        let (mut in_str, mut in_char, mut escaped) = (false, false, false);
11701        let mut i = 0;
11702        while i < bytes.len() {
11703            let c = bytes[i];
11704            if escaped {
11705                escaped = false;
11706            } else if c == b'\\' && (in_str || in_char) {
11707                escaped = true;
11708            } else if in_str {
11709                in_str = c != b'"';
11710            } else if in_char {
11711                in_char = c != b'\'';
11712            } else if c == b'"' {
11713                in_str = true;
11714            } else if c == b'\'' {
11715                // A lifetime (`'a`) is not a `char` literal; a `char` literal's
11716                // closing quote is at most three bytes away (`'\\n'`, `'\\''`).
11717                in_char = bytes[i + 1..].iter().take(4).any(|b| *b == b'\'');
11718            } else if c == b'/' && bytes.get(i + 1) == Some(&b'/') {
11719                return &line[..i];
11720            }
11721            i += 1;
11722        }
11723        line
11724    }
11725
11726    /// Every function defined in `src`, as `(name, code)` — the code only, with
11727    /// comments stripped by [`code_only`]. Braces are counted on the code too,
11728    /// so a comment holding an unbalanced brace cannot end a body early or run
11729    /// two bodies together.
11730    fn functions_in(src: &str) -> Vec<(String, String)> {
11731        const PREFIXES: [&str; 6] = [
11732            "fn ",
11733            "pub fn ",
11734            "pub(crate) fn ",
11735            "unsafe fn ",
11736            "pub unsafe fn ",
11737            "pub unsafe extern \"C\" fn ",
11738        ];
11739        let mut out: Vec<(String, String)> = Vec::new();
11740        // (name, body so far, brace depth, whether the body has opened at all —
11741        // a multi-line signature spends several lines at depth zero before its
11742        // `{`, and closing there would give every such wrapper a one-line body).
11743        let mut open: Option<(String, String, i32, bool)> = None;
11744        for raw in src.lines() {
11745            let line = code_only(raw);
11746            let depth_change = |l: &str| -> i32 {
11747                l.chars().filter(|c| *c == '{').count() as i32
11748                    - l.chars().filter(|c| *c == '}').count() as i32
11749            };
11750            if let Some((name, body, depth, opened)) = open.as_mut() {
11751                body.push_str(line);
11752                body.push('\n');
11753                *depth += depth_change(line);
11754                *opened |= line.contains('{');
11755                if *opened && *depth <= 0 {
11756                    out.push((std::mem::take(name), std::mem::take(body)));
11757                    open = None;
11758                }
11759                continue;
11760            }
11761            let trimmed = line.trim_start();
11762            let Some(rest) = PREFIXES
11763                .iter()
11764                .find_map(|p| trimmed.strip_prefix(p).filter(|_| trimmed.starts_with(p)))
11765            else {
11766                continue;
11767            };
11768            let name: String = rest
11769                .chars()
11770                .take_while(|c| c.is_alphanumeric() || *c == '_')
11771                .collect();
11772            if name.is_empty() {
11773                continue;
11774            }
11775            let depth = depth_change(line);
11776            let opened = line.contains('{');
11777            // A one-line definition has already opened and closed its body.
11778            if opened && depth <= 0 {
11779                out.push((name, line.to_string()));
11780            } else {
11781                open = Some((name, format!("{line}\n"), depth, opened));
11782            }
11783        }
11784        out
11785    }
11786
11787    /// A wrapper that can raise a fault says so in the manifest.
11788    ///
11789    /// A row declared `Allocates` makes `RuntimeSymbol::faults()` answer
11790    /// `false`, so no `CheckFault` follows the call. If such a wrapper can reach
11791    /// `set_fault` the consequence is not cosmetic: the operation is silently
11792    /// abandoned, the wrapper answers the Unit sentinel, and the fault is
11793    /// observed by some later unrelated check — at the wrong source location,
11794    /// after the program has computed and possibly printed an answer.
11795    ///
11796    /// A hand-corrected row drifts again, so this is the invariant instead: the
11797    /// file is read at compile time, each `praxis_*` wrapper's body is walked,
11798    /// and any body that can reach `set_fault` — directly or through a helper
11799    /// defined here, transitively — must belong to a symbol whose row says
11800    /// `faults()`.
11801    ///
11802    /// One direction only. A row may declare a fault the reader cannot see: the
11803    /// arithmetic wrappers are generated by `checked_int_binop!` and have no
11804    /// textual definition at all, and a future wrapper may fault through a
11805    /// helper in another module. Those are false negatives — this test is weaker
11806    /// than the truth, never stricter — and the direction it does check is the
11807    /// one that produces wrong answers.
11808    /// The fixed point of "can reach `set_fault`" over `defs`: a function
11809    /// faults if it calls `set_fault`, or calls something that does.
11810    fn faulting_functions(defs: &[(String, String)]) -> std::collections::HashSet<String> {
11811        let mut faulting: std::collections::HashSet<String> =
11812            ["set_fault".to_string()].into_iter().collect();
11813        loop {
11814            let mut grew = false;
11815            for (name, body) in defs {
11816                if faulting.contains(name) {
11817                    continue;
11818                }
11819                if faulting.iter().any(|f| body.contains(&format!("{f}("))) {
11820                    faulting.insert(name.clone());
11821                    grew = true;
11822                }
11823            }
11824            if !grew {
11825                break;
11826            }
11827        }
11828        faulting
11829    }
11830
11831    #[test]
11832    fn a_wrapper_that_can_raise_a_fault_declares_that_it_faults() {
11833        let defs = functions_in_this_file();
11834        let faulting = faulting_functions(&defs);
11835
11836        let mut checked = 0usize;
11837        for (name, _) in &defs {
11838            let Some(sym) = praxis_stdlib::abi::RuntimeSymbol::from_name(name) else {
11839                continue;
11840            };
11841            if !faulting.contains(name) {
11842                continue;
11843            }
11844            assert!(
11845                sym.faults(),
11846                "{name} can reach `set_fault`, but its manifest row says it \
11847                 cannot fault — so no `CheckFault` follows the call and the \
11848                 fault is observed somewhere else entirely"
11849            );
11850            checked += 1;
11851        }
11852        // If the scan stopped finding wrappers, the assertion above would be
11853        // vacuous and this test would pass while saying nothing.
11854        assert!(
11855            checked >= 20,
11856            "expected the fault-raising wrappers to be found; saw {checked}"
11857        );
11858        // These three reach `set_fault` only through a helper, so they are the
11859        // shape the transitive scan has to be able to see.
11860        for name in [
11861            "praxis_vec_push",
11862            "praxis_deque_push_front",
11863            "praxis_deque_push_back",
11864        ] {
11865            assert!(
11866                faulting.contains(name),
11867                "{name} reaches `set_fault` through `adopt_or_reject`; a scan \
11868                 that cannot see that cannot hold the invariant"
11869            );
11870        }
11871        // **`InvalidText` lives at exactly one site (ADR-111).**
11872        // `praxis_alloc_text` trusts its caller; the one caller holding raw host
11873        // bytes raises the fault instead. Asserting the destination is visible
11874        // to the scan is what distinguishes a relocated fault from a deleted
11875        // one — without it, deleting the validation outright would leave this
11876        // test just as green.
11877        assert!(
11878            faulting.contains("praxis_get_input"),
11879            "`praxis_get_input` validates the host's input and raises \
11880             `InvalidText` itself (ADR-111); a scan that cannot see that cannot \
11881             tell a relocated fault from a deleted one"
11882        );
11883        assert!(
11884            !faulting.contains("praxis_alloc_text"),
11885            "`praxis_alloc_text` reaches `set_fault` again. Its row is \
11886             `Effect::Allocates`, so nothing observes the fault — a violated \
11887             UTF-8 precondition aborts through `abi_guard!` instead (ADR-111)"
11888        );
11889    }
11890    /// **The sweep above reads code, not prose.** Its classification is a
11891    /// substring match, so without [`code_only`] a *comment* inside a wrapper
11892    /// naming the helper would classify that wrapper as faulting and fail the
11893    /// invariant for a wrapper that cannot fault. A sweep a comment can fool is
11894    /// a sweep that gets edited around rather than satisfied.
11895    ///
11896    /// Synthetic source, because the real file must not contain the shape: the
11897    /// point is that it may, safely.
11898    #[test]
11899    fn the_manifest_sweep_reads_code_and_not_comments() {
11900        let src = r#"
11901pub unsafe extern "C" fn praxis_pretend_pure(ctx: *mut RuntimeContext) -> GcRef {
11902    // It used to call set_fault(ctx, RaisedFault::TYPE_MISMATCH) here, and a
11903    // later edit removed the only path that could. Prose, not code. }
11904    let sep = "//";
11905    let slash = '/';
11906    let _ = (sep, slash);
11907    unit_sentinel(ctx)
11908}
11909
11910pub unsafe extern "C" fn praxis_pretend_faulting(ctx: *mut RuntimeContext) -> GcRef {
11911    set_fault(ctx, RaisedFault::TYPE_MISMATCH);
11912    unit_sentinel(ctx)
11913}
11914"#;
11915        let defs = functions_in(src);
11916        let names: Vec<&str> = defs.iter().map(|(n, _)| n.as_str()).collect();
11917        assert_eq!(names, ["praxis_pretend_pure", "praxis_pretend_faulting"]);
11918        // The unbalanced `}` in that comment must not close the body early:
11919        // braces are counted on the code too, so the whole function is read.
11920        assert!(
11921            defs[0].1.contains("unit_sentinel(ctx)"),
11922            "a brace inside a comment ended the body early: {:?}",
11923            defs[0].1
11924        );
11925
11926        let faulting = faulting_functions(&defs);
11927        assert!(
11928            !faulting.contains("praxis_pretend_pure"),
11929            "a comment naming `set_fault` is not a call to it"
11930        );
11931        assert!(
11932            faulting.contains("praxis_pretend_faulting"),
11933            "and a real call still is — stripping comments must not blind the sweep"
11934        );
11935    }
11936
11937    /// [`code_only`]'s own contract, both directions.
11938    #[test]
11939    fn code_only_keeps_a_slash_inside_a_literal() {
11940        assert_eq!(code_only("let x = 1; // two"), "let x = 1; ");
11941        assert_eq!(code_only(r#"let s = "a//b";"#), r#"let s = "a//b";"#);
11942        assert_eq!(code_only(r"let c = '/'; // gone"), r"let c = '/'; ");
11943        assert_eq!(
11944            code_only(r#"let e = "\"//"; // gone"#),
11945            r#"let e = "\"//"; "#
11946        );
11947        assert_eq!(code_only("    /// a doc comment"), "    ");
11948        assert_eq!(code_only("no comment here"), "no comment here");
11949        // A lifetime is not a `char` literal, so the comment after it is still
11950        // a comment.
11951        assert_eq!(
11952            code_only("fn f<'a>(x: &'a str) {} // gone"),
11953            "fn f<'a>(x: &'a str) {} "
11954        );
11955    }
11956
11957    // --- the panic backstop ------------------------------------------------
11958
11959    /// Every `#[unsafe(no_mangle)] extern "C"` function in this crate has its body
11960    /// inside `abi_guard!`.
11961    ///
11962    /// Per-wrapper totality is the contract: a wrapper validates its arguments
11963    /// and reports a bad one as a fault, so the guard never fires. This is the
11964    /// proof that the contract cannot be violated *silently* — a panic
11965    /// unwinding out of `extern "C"` into Cranelift frames is undefined
11966    /// behaviour, and the failure mode of forgetting is a corrupted process at
11967    /// some unrelated later point rather than a message.
11968    ///
11969    /// Read as source text on purpose. The property is "every entry point is
11970    /// wrapped", which is a property of the *set* of entry points; a test that
11971    /// called them one by one would be a test of the ones somebody remembered.
11972    ///
11973    /// **The file set is discovered, not declared.** A hand-written list of
11974    /// files would make the guarantee "every entry point in a file somebody
11975    /// remembered to list", and the `wrappers > 100` floor would still pass on
11976    /// the files that were listed. So the walk covers **every crate's `src/`**,
11977    /// not only this one: nothing says a future `#[unsafe(no_mangle)]` has to live
11978    /// here.
11979    #[test]
11980    fn every_no_mangle_wrapper_is_behind_the_panic_guard() {
11981        /// Every `.rs` file under `dir`, recursively, in a stable order.
11982        fn rust_sources(dir: &std::path::Path, out: &mut Vec<(String, String)>) {
11983            let entries =
11984                std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {}: {e}", dir.display()));
11985            let mut entries: Vec<_> = entries.map(|e| e.expect("dir entry").path()).collect();
11986            entries.sort();
11987            for path in entries {
11988                let name = path.file_name().unwrap_or_default().to_string_lossy();
11989                if name == "target" || name.starts_with('.') {
11990                    continue;
11991                }
11992                if path.is_dir() {
11993                    rust_sources(&path, out);
11994                } else if path.extension().is_some_and(|e| e == "rs") {
11995                    let text = std::fs::read_to_string(&path)
11996                        .unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
11997                    out.push((path.display().to_string(), text));
11998                }
11999            }
12000        }
12001
12002        // `crates/`, from this crate's manifest directory.
12003        let mut crates_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
12004        crates_dir.pop();
12005        let mut sources: Vec<(String, String)> = Vec::new();
12006        rust_sources(&crates_dir, &mut sources);
12007        // A guard against the walk silently covering nothing — a wrong root
12008        // would otherwise pass by finding no wrapper attribute at all.
12009        assert!(
12010            sources.len() > 50,
12011            "the walk of {} found only {} Rust files, so it is not reading the workspace",
12012            crates_dir.display(),
12013            sources.len()
12014        );
12015
12016        let mut wrappers = 0usize;
12017        let mut unguarded: Vec<String> = Vec::new();
12018        for (file, source) in &sources {
12019            let lines: Vec<&str> = source.lines().collect();
12020            for (n, line) in lines.iter().enumerate() {
12021                // Both spellings: `#[unsafe(no_mangle)]` is the edition-2024
12022                // form and the only one this crate uses, but matching the bare
12023                // attribute too keeps the scan from going blind if a wrapper is
12024                // ever pasted in from older code.
12025                if !matches!(line.trim(), "#[unsafe(no_mangle)]" | "#[no_mangle]") {
12026                    continue;
12027                }
12028                wrappers += 1;
12029                // Walk to the line that opens the body, then require the very
12030                // next non-blank line to be the guard.
12031                let mut k = n + 1;
12032                while k < lines.len() && !lines[k].trim_end().ends_with('{') {
12033                    k += 1;
12034                }
12035                // Inclusive of `k`: a one-line signature puts `fn name(` on the
12036                // very line that opens the body, so an exclusive range would
12037                // report `<unnamed>` in the message that tells someone which
12038                // wrapper they forgot.
12039                let name = lines[n..=k.min(lines.len() - 1)]
12040                    .iter()
12041                    .find_map(|l| l.split("fn ").nth(1))
12042                    .and_then(|l| l.split('(').next())
12043                    .unwrap_or("<unnamed>")
12044                    .trim()
12045                    .to_string();
12046                let opens_guard = lines
12047                    .get(k + 1)
12048                    .map(|l| l.trim_start().starts_with("abi_guard!("))
12049                    .unwrap_or(false);
12050                if !opens_guard {
12051                    unguarded.push(format!("{file}:{} {name}", n + 1));
12052                }
12053            }
12054        }
12055
12056        assert!(
12057            wrappers > 100,
12058            "the scan found only {wrappers} wrappers, so it is not reading the ABI surface"
12059        );
12060        assert!(
12061            unguarded.is_empty(),
12062            "these `extern \"C\"` entry points can let a panic unwind into generated frames: {unguarded:#?}"
12063        );
12064    }
12065
12066    /// The guard's own behaviour: a panic inside a wrapper becomes a fault with
12067    /// a message naming the wrapper, and the wrapper returns its defined dummy.
12068    ///
12069    /// `praxis_dbg` is the one wrapper that can be made to panic on demand
12070    /// without an invalid argument — it formats its value, and a `Text` whose
12071    /// payload is a live `Unit` is a descriptor/payload pairing no validation
12072    /// catches. Every *reachable* panic is a bug to fix in the wrapper; this
12073    /// test is about what happens when one is missed.
12074    #[test]
12075    fn a_panic_inside_a_wrapper_becomes_a_fault_and_a_defined_dummy() {
12076        let value = {
12077            abi_guard!(
12078                "praxis_test_panics",
12079                std::ptr::null_mut::<RuntimeContext>(),
12080                {
12081                    #[allow(unreachable_code)]
12082                    {
12083                        if std::hint::black_box(false) {
12084                            panic!("this is the guard under test");
12085                        }
12086                        7i64
12087                    }
12088                }
12089            )
12090        };
12091        assert_eq!(value, 7, "the guard is transparent when nothing panics");
12092
12093        // A **faulting** wrapper: its call sites can carry a `CheckFault`, so
12094        // generated code observes the fault before it looks at the value, and
12095        // the defined dummy is the right answer. The name has to be a real
12096        // manifest symbol — see `panic_fault_is_observable`, which is what
12097        // decides whether the dummy is returned at all.
12098        let mut runtime = crate::Runtime::new();
12099        let mut ctx = runtime.context();
12100        let previous = std::panic::take_hook();
12101        std::panic::set_hook(Box::new(|_| {}));
12102        let dummy: GcRef = abi_guard!("praxis_run_parser", &mut ctx as *mut RuntimeContext, {
12103            panic!("a wrapper that forgot to be total");
12104        });
12105        std::panic::set_hook(previous);
12106
12107        assert_eq!(
12108            runtime.fault(),
12109            crate::FaultKind::Panic,
12110            "an escaped panic is a fault, not an unwind into generated code"
12111        );
12112        assert!(
12113            runtime
12114                .fault_message()
12115                .is_some_and(|m| m.contains("praxis_run_parser")),
12116            "the fault names the wrapper it escaped, which a bare kind could not"
12117        );
12118        assert_eq!(
12119            dummy.descriptor().id(),
12120            crate::scalars::UNIT.id(),
12121            "the dummy is the Unit sentinel §10.4 already specifies"
12122        );
12123    }
12124
12125    /// **The dummy is only returned where the fault will be seen.**
12126    ///
12127    /// Generated code tests the fault slot only where MIR emitted a
12128    /// `CheckFault`, and **`praxis_mir::verify` is what makes that true of a
12129    /// non-faulting wrapper** — its `RedundantFaultCheck` rule rejects a check
12130    /// after an instruction that cannot fault (ADR-088). So for the wrappers the
12131    /// manifest declares non-faulting there is no check, and returning
12132    /// `unit_sentinel` there would hand a `Unit` into a slot generated code
12133    /// believes holds a Record, a Tuple or a closure — a descriptor/payload
12134    /// confusion introduced by the backstop meant to prevent worse. Those abort
12135    /// instead.
12136    ///
12137    /// This test states the classification. The abort itself cannot be asserted
12138    /// in-process, which is exactly why the rule has to be a total function of
12139    /// the manifest rather than a case-by-case judgement.
12140    #[test]
12141    fn a_panic_dummy_is_only_returned_where_a_fault_check_can_follow() {
12142        use praxis_stdlib::abi::RuntimeSymbol;
12143
12144        let mut pure = 0usize;
12145        let mut faulting = 0usize;
12146        for symbol in RuntimeSymbol::ALL.iter().copied() {
12147            let observable = panic_fault_is_observable(symbol.name());
12148            assert_eq!(
12149                observable,
12150                symbol.faults(),
12151                "`{}` is declared {:?}; the panic dummy must be returned iff a \
12152                 fault check can follow it",
12153                symbol.name(),
12154                symbol.sig().effect
12155            );
12156            if symbol.faults() {
12157                faulting += 1;
12158            } else {
12159                pure += 1;
12160            }
12161        }
12162        assert!(
12163            pure > 0 && faulting > 0,
12164            "the manifest must contain both classes for this rule to mean anything \
12165             ({pure} non-faulting, {faulting} faulting)"
12166        );
12167
12168        // Every `#[unsafe(no_mangle)]` wrapper in this crate is manifested, so the
12169        // unobservable case left is a name that is not a wrapper at all.
12170        assert!(
12171            !panic_fault_is_observable("praxis_not_a_wrapper_at_all"),
12172            "an unknown name is never treated as observable"
12173        );
12174    }
12175
12176    // ---- Process input (§7.10) ----
12177
12178    /// A reader that answers nothing. A `fn` and not a closure because
12179    /// [`crate::input::InputReader`] is a plain `fn` pointer.
12180    fn no_bytes() -> Vec<u8> {
12181        Vec::new()
12182    }
12183
12184    /// Read the bytes behind a `Text` `GcRef`.
12185    ///
12186    /// # Safety
12187    /// `r` must be a live `Text`.
12188    unsafe fn text_bytes_of(r: GcRef) -> &'static [u8] {
12189        // SAFETY: the caller guarantees `r` is a live Text, so its payload is a
12190        // validly-linked `TextPayload`.
12191        unsafe { crate::text::text_bytes(r.payload::<crate::text::TextPayload>() as *const _) }
12192    }
12193
12194    /// A reader that answers zero bytes has given *empty input*, not no input,
12195    /// so its answer is installed as `input_source` whatever its length.
12196    ///
12197    /// Allocating the buffer only `if !bytes.is_empty()` would leave empty
12198    /// standard input at the immortal Unit, so `praxis_run_parser`'s §6.3
12199    /// descriptor guard would fault *before* the parser ran — a `ParseFailed`
12200    /// with no input span, no `expected` and no `actual`, which is none of the
12201    /// six fields §7.11 says a mismatch carries. A fault raised before any
12202    /// buffer exists cannot carry them; the buffer is what makes the diagnostic
12203    /// possible at all (ADR-087).
12204    #[test]
12205    fn a_reader_that_answers_zero_bytes_installs_an_empty_text() {
12206        let mut rt = Runtime::new();
12207        let ctx = wired_ctx(&mut rt);
12208        crate::input::install_input_reader(no_bytes);
12209        // SAFETY: ctx is wired to rt and live for this call.
12210        let source = unsafe { praxis_get_input(ctx) };
12211        assert_eq!(
12212            source.descriptor().id(),
12213            crate::text::TEXT.id(),
12214            "a zero-byte answer is still an input buffer"
12215        );
12216        // SAFETY: the assertion above proves `source` is a Text.
12217        assert!(
12218            unsafe { text_bytes_of(source) }.is_empty(),
12219            "and the buffer holds exactly what the reader answered"
12220        );
12221        // SAFETY: ctx is wired to rt and live for this call.
12222        assert_eq!(
12223            unsafe { (*ctx).input_source }.as_ptr(),
12224            source.as_ptr(),
12225            "the buffer is installed, not merely returned — §7.10's later \
12226             `read`s reuse it"
12227        );
12228        // SAFETY: ctx came from `wired_ctx` and is not used again.
12229        unsafe { drop_ctx(ctx) };
12230    }
12231
12232    /// **A mutation companion, not a gate.**
12233    ///
12234    /// The cheapest wrong repair is to allocate a `Text` unconditionally in
12235    /// `praxis_get_input`, which passes the gate above and quietly deletes the
12236    /// one state the §6.3 descriptor guard exists for. A host that installs
12237    /// **neither** a buffer nor a reader — every JIT test, every embedder — must
12238    /// still reach `praxis_run_parser` with the Unit source, because
12239    /// `adv_read_against_non_text_input_faults_cleanly` in the codegen crate's
12240    /// `jit.rs` is the probe that a `read` there faults instead of
12241    /// reinterpreting Unit's payload as a `TextPayload` and segfaulting.
12242    ///
12243    /// That is the boundary ADR-087 draws: a reader answering zero bytes is a
12244    /// program state (empty input); no reader at all is a host state (no input),
12245    /// and no `praxis run` reaches it.
12246    #[test]
12247    fn a_host_that_installs_no_reader_keeps_the_unit_source() {
12248        let mut rt = Runtime::new();
12249        let ctx = wired_ctx(&mut rt);
12250        crate::input::clear_input_reader();
12251        // SAFETY: ctx is wired to rt and live for these calls.
12252        let before = unsafe { (*ctx).input_source };
12253        // SAFETY: as above.
12254        let source = unsafe { praxis_get_input(ctx) };
12255        assert_eq!(
12256            source.as_ptr(),
12257            before.as_ptr(),
12258            "with no reader installed there is nothing to call and nothing to \
12259             install; `input_source` is answered untouched"
12260        );
12261        assert_ne!(
12262            source.descriptor().id(),
12263            crate::text::TEXT.id(),
12264            "and it is still the Unit the §6.3 guard is the net under"
12265        );
12266        // SAFETY: ctx came from `wired_ctx` and is not used again.
12267        unsafe { drop_ctx(ctx) };
12268    }
12269
12270    /// **The guard must not report a parse that never ran.**
12271    ///
12272    /// `praxis_run_parser` returns early for a non-Text `input` (§6.3) —
12273    /// `run_plan` would otherwise reinterpret the payload as a `TextPayload` —
12274    /// and that early return must still perform the `clear_parse_detail` every
12275    /// other entry into the parser performs. Without it, a host reaching the
12276    /// guard after an earlier mismatch reports *that* mismatch's offset and
12277    /// expectation for a parse that never started.
12278    ///
12279    /// Not reachable end to end: a fault is terminal within one `praxis run`,
12280    /// so the shape is an embedder calling `main` twice (or the crash debugger's
12281    /// `restart`). This test pins it at the level where the hazard exists.
12282    ///
12283    /// Fabricating a `ParseFail` here instead would be worse than clearing: with
12284    /// no buffer there is no input span, and an invented `expected` would make
12285    /// an embedder's host bug read as a parse failure at an offset that does not
12286    /// exist.
12287    #[test]
12288    fn the_non_text_guard_does_not_report_a_previous_parses_failure() {
12289        let mut rt = Runtime::new();
12290        let ctx = wired_ctx(&mut rt);
12291        rt.parse_detail_mut()
12292            .consider(ParseFail::here(7, "int"), b"0123456789");
12293        assert!(rt.parse_detail().is_set(), "the seed is in place");
12294        // SAFETY: ctx is wired to rt; the plan index is never read, because the
12295        // descriptor guard returns before it.
12296        unsafe {
12297            let plan = praxis_alloc_int(ctx, 1);
12298            let unit = (*ctx).unit_ref;
12299            let result = praxis_run_parser(ctx, plan, unit);
12300            assert_eq!(
12301                result.descriptor().id(),
12302                crate::scalars::UNIT.id(),
12303                "the guard answers the sentinel"
12304            );
12305        }
12306        assert!(rt.has_pending_fault());
12307        assert_eq!(rt.fault(), FaultKind::ParseFailed);
12308        assert!(
12309            !rt.parse_detail().is_set(),
12310            "the §6.3 guard runs no parse, so it has no detail to report — and \
12311             it must not report the previous parse's"
12312        );
12313        // SAFETY: ctx came from `wired_ctx` and is not used again.
12314        unsafe { drop_ctx(ctx) };
12315    }
12316}
12317
12318#[cfg(test)]
12319mod growth_charging_tests {
12320    //! **Every wrapper that can grow a collection's buffer charges the pacer**
12321    //! (ADR-121). See [`super::charge_growth`] for why.
12322    //!
12323    //! The values pushed are all inside `small_int`'s interned range, and that
12324    //! is the whole design of these tests rather than a convenience: an interned
12325    //! `Int` is an immortal the allocator never charges for, so the *only* thing
12326    //! that can move `bytes_since_collect` here is the spine. Push
12327    //! `UNINTERNED + i` instead and every one of these passes whether or not the
12328    //! growth is charged, because the elements would be paying for it.
12329
12330    use super::tests::{drop_ctx, wired_ctx};
12331    use super::*;
12332    use crate::Runtime;
12333
12334    /// Reset the counter, run `body`, and answer what it charged.
12335    fn charged_by(rt: &Runtime, body: impl FnOnce()) -> usize {
12336        // A collection zeroes the counter, so take a reading either side and
12337        // require the run not to have collected; the pushes below are far too
12338        // few to reach any threshold.
12339        let before = rt.heap().bytes_since_collect();
12340        body();
12341        rt.heap().bytes_since_collect().saturating_sub(before)
12342    }
12343
12344    /// Enough pushes that amortized doubling must have reallocated at least
12345    /// once, whatever the initial capacity is.
12346    const PUSHES: i64 = 256;
12347
12348    macro_rules! charges_its_spine {
12349        ($name:ident, $make:expr_2021, $push:expr_2021) => {
12350            #[test]
12351            fn $name() {
12352                let mut rt = Runtime::new();
12353                let ctx = wired_ctx(&mut rt);
12354                // SAFETY: `ctx` is wired to `rt` for the whole test.
12355                unsafe {
12356                    let subject = $make(ctx);
12357                    let charged = charged_by(&rt, || {
12358                        for i in 0..PUSHES {
12359                            $push(ctx, subject, i);
12360                        }
12361                    });
12362                    assert!(
12363                        charged > 0,
12364                        "growing this collection charged the pacer nothing, so a \
12365                         program whose memory is this buffer would never collect \
12366                         (ADR-121); every value pushed is an interned immortal, so \
12367                         the spine is the only thing that could have charged"
12368                    );
12369                    drop_ctx(ctx);
12370                }
12371            }
12372        };
12373    }
12374
12375    charges_its_spine!(
12376        vec_push_charges_its_spine,
12377        |c| praxis_vec_new(c, &crate::scalars::INT),
12378        |c, s, i| { praxis_vec_push(c, s, praxis_alloc_int(c, i)) }
12379    );
12380    charges_its_spine!(
12381        deque_push_back_charges_its_spine,
12382        |c| praxis_deque_new(c, &crate::scalars::INT),
12383        |c, s, i| praxis_deque_push_back(c, s, praxis_alloc_int(c, i))
12384    );
12385    charges_its_spine!(
12386        deque_push_front_charges_its_spine,
12387        |c| praxis_deque_new(c, &crate::scalars::INT),
12388        |c, s, i| praxis_deque_push_front(c, s, praxis_alloc_int(c, i))
12389    );
12390    charges_its_spine!(
12391        map_insert_charges_its_spine,
12392        |c| praxis_map_new(c, &crate::scalars::INT),
12393        |c, s, i| praxis_map_insert(c, s, praxis_alloc_int(c, i), praxis_alloc_int(c, i))
12394    );
12395    charges_its_spine!(
12396        set_insert_charges_its_spine,
12397        |c| praxis_set_new(c, &crate::scalars::INT),
12398        |c, s, i| praxis_set_insert(c, s, praxis_alloc_int(c, i))
12399    );
12400    charges_its_spine!(
12401        counter_set_charges_its_spine,
12402        |c| praxis_counter_new(c, &crate::scalars::INT),
12403        |c, s, i| praxis_counter_set(c, s, praxis_alloc_int(c, i), praxis_alloc_int(c, i))
12404    );
12405    charges_its_spine!(
12406        bitset_insert_charges_its_spine,
12407        |c| praxis_bitset_new(c),
12408        |c, s, i| praxis_bitset_insert(c, s, praxis_alloc_int(c, i))
12409    );
12410    charges_its_spine!(
12411        max_heap_push_charges_its_spine,
12412        |c| praxis_max_heap_new(c, &crate::scalars::INT),
12413        |c, s, i| praxis_max_heap_push(c, s, praxis_alloc_int(c, i))
12414    );
12415    charges_its_spine!(
12416        min_heap_push_charges_its_spine,
12417        |c| praxis_min_heap_new(c, &crate::scalars::INT),
12418        |c, s, i| praxis_min_heap_push(c, s, praxis_alloc_int(c, i))
12419    );
12420}