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::DequeIsEmpty => praxis_deque_is_empty as *const (),
163        RuntimeSymbol::DequeLen => praxis_deque_len as *const (),
164        RuntimeSymbol::DequeNew => praxis_deque_new as *const (),
165        RuntimeSymbol::DequePopBack => praxis_deque_pop_back as *const (),
166        RuntimeSymbol::DequePopFront => praxis_deque_pop_front as *const (),
167        RuntimeSymbol::DequePushBack => praxis_deque_push_back as *const (),
168        RuntimeSymbol::DequePushFront => praxis_deque_push_front as *const (),
169        RuntimeSymbol::Dbg => praxis_dbg as *const (),
170        RuntimeSymbol::Dfs => praxis_dfs as *const (),
171        RuntimeSymbol::DfsDistance => praxis_dfs_distance as *const (),
172        RuntimeSymbol::DfsPath => praxis_dfs_path as *const (),
173        RuntimeSymbol::Dijkstra => praxis_dijkstra as *const (),
174        RuntimeSymbol::DijkstraDistance => praxis_dijkstra_distance as *const (),
175        RuntimeSymbol::DijkstraPath => praxis_dijkstra_path as *const (),
176        RuntimeSymbol::EnumPayload => praxis_enum_payload as *const (),
177        RuntimeSymbol::EnumSetPayload => praxis_enum_set_payload as *const (),
178        RuntimeSymbol::EnumTag => praxis_enum_tag as *const (),
179        RuntimeSymbol::FloatAbs => praxis_float_abs as *const (),
180        RuntimeSymbol::FloatCeil => praxis_float_ceil as *const (),
181        RuntimeSymbol::FloatE => praxis_float_e as *const (),
182        RuntimeSymbol::FloatFloor => praxis_float_floor as *const (),
183        RuntimeSymbol::FloatIsInfinite => praxis_float_is_infinite as *const (),
184        RuntimeSymbol::FloatIsNan => praxis_float_is_nan as *const (),
185        RuntimeSymbol::FloatLoad => praxis_float_load as *const (),
186        RuntimeSymbol::FloatMax => praxis_float_max as *const (),
187        RuntimeSymbol::FloatMin => praxis_float_min as *const (),
188        RuntimeSymbol::FloatPi => praxis_float_pi as *const (),
189        RuntimeSymbol::FloatRound => praxis_float_round as *const (),
190        RuntimeSymbol::FloatSign => praxis_float_sign as *const (),
191        RuntimeSymbol::FloatSqrt => praxis_float_sqrt as *const (),
192        RuntimeSymbol::FloatToInt => praxis_float_to_int as *const (),
193        RuntimeSymbol::FloatToText => praxis_float_to_text as *const (),
194        RuntimeSymbol::FloodFill => praxis_flood_fill as *const (),
195        RuntimeSymbol::GetInput => praxis_get_input as *const (),
196        RuntimeSymbol::GridAround4 => praxis_grid_around4 as *const (),
197        RuntimeSymbol::GridAround8 => praxis_grid_around8 as *const (),
198        RuntimeSymbol::GridCells => praxis_grid_cells as *const (),
199        RuntimeSymbol::GridColumn => praxis_grid_column as *const (),
200        RuntimeSymbol::GridContains => praxis_grid_contains as *const (),
201        RuntimeSymbol::GridCount4 => praxis_grid_count4 as *const (),
202        RuntimeSymbol::GridCount4Where => praxis_grid_count4_where as *const (),
203        RuntimeSymbol::GridCount8 => praxis_grid_count8 as *const (),
204        RuntimeSymbol::GridCount8Where => praxis_grid_count8_where as *const (),
205        RuntimeSymbol::GridFind => praxis_grid_find as *const (),
206        RuntimeSymbol::GridFindAll => praxis_grid_find_all as *const (),
207        RuntimeSymbol::GridGet => praxis_grid_get as *const (),
208        RuntimeSymbol::GridHeight => praxis_grid_height as *const (),
209        RuntimeSymbol::GridNeighbors4 => praxis_grid_neighbors4 as *const (),
210        RuntimeSymbol::GridNeighbors8 => praxis_grid_neighbors8 as *const (),
211        RuntimeSymbol::GridFilled => praxis_grid_filled as *const (),
212        RuntimeSymbol::GridNew => praxis_grid_new as *const (),
213        RuntimeSymbol::GridPositions => praxis_grid_positions as *const (),
214        RuntimeSymbol::GridRotateLeft => praxis_grid_rotate_left as *const (),
215        RuntimeSymbol::GridRotateRight => praxis_grid_rotate_right as *const (),
216        RuntimeSymbol::GridRow => praxis_grid_row as *const (),
217        RuntimeSymbol::GridSet => praxis_grid_set as *const (),
218        RuntimeSymbol::GridTranspose => praxis_grid_transpose as *const (),
219        RuntimeSymbol::GridWidth => praxis_grid_width as *const (),
220        RuntimeSymbol::IntAbs => praxis_int_abs as *const (),
221        RuntimeSymbol::IntAdd => praxis_int_add as *const (),
222        RuntimeSymbol::IntCheckedAdd => praxis_int_checked_add as *const (),
223        RuntimeSymbol::IntCheckedMul => praxis_int_checked_mul as *const (),
224        RuntimeSymbol::IntCheckedSub => praxis_int_checked_sub as *const (),
225        RuntimeSymbol::IntClamp => praxis_int_clamp as *const (),
226        RuntimeSymbol::IntDiv => praxis_int_div as *const (),
227        RuntimeSymbol::IntEq => praxis_int_eq as *const (),
228        RuntimeSymbol::IntGcd => praxis_int_gcd as *const (),
229        RuntimeSymbol::IntGe => praxis_int_ge as *const (),
230        RuntimeSymbol::IntGt => praxis_int_gt as *const (),
231        RuntimeSymbol::IntLcm => praxis_int_lcm as *const (),
232        RuntimeSymbol::IntLe => praxis_int_le as *const (),
233        RuntimeSymbol::IntLoad => praxis_int_load as *const (),
234        RuntimeSymbol::IntLt => praxis_int_lt as *const (),
235        RuntimeSymbol::IntMax => praxis_int_max as *const (),
236        RuntimeSymbol::IntMin => praxis_int_min as *const (),
237        RuntimeSymbol::IntMul => praxis_int_mul as *const (),
238        RuntimeSymbol::IntNe => praxis_int_ne as *const (),
239        RuntimeSymbol::IntNeg => praxis_int_neg as *const (),
240        RuntimeSymbol::IntRem => praxis_int_rem as *const (),
241        RuntimeSymbol::IntSaturatingAdd => praxis_int_saturating_add as *const (),
242        RuntimeSymbol::IntSaturatingMul => praxis_int_saturating_mul as *const (),
243        RuntimeSymbol::IntSaturatingSub => praxis_int_saturating_sub as *const (),
244        RuntimeSymbol::IntSign => praxis_int_sign as *const (),
245        RuntimeSymbol::IntSub => praxis_int_sub as *const (),
246        RuntimeSymbol::IntToChar => praxis_int_to_char as *const (),
247        RuntimeSymbol::IntToFloat => praxis_int_to_float as *const (),
248        RuntimeSymbol::IntToText => praxis_int_to_text as *const (),
249        RuntimeSymbol::IntWrappingAdd => praxis_int_wrapping_add as *const (),
250        RuntimeSymbol::IntWrappingMul => praxis_int_wrapping_mul as *const (),
251        RuntimeSymbol::IntWrappingSub => praxis_int_wrapping_sub as *const (),
252        RuntimeSymbol::MapContains => praxis_map_contains as *const (),
253        RuntimeSymbol::RangeGet => praxis_range_get as *const (),
254        RuntimeSymbol::RangeLen => praxis_range_len as *const (),
255        RuntimeSymbol::RangeNew => praxis_range_new as *const (),
256        RuntimeSymbol::RangeNewInclusive => praxis_range_new_inclusive as *const (),
257        RuntimeSymbol::MapGet => praxis_map_get as *const (),
258        RuntimeSymbol::MapIndex => praxis_map_index as *const (),
259        RuntimeSymbol::MapInsert => praxis_map_insert as *const (),
260        RuntimeSymbol::MapIsEmpty => praxis_map_is_empty as *const (),
261        RuntimeSymbol::MapKeys => praxis_map_keys as *const (),
262        RuntimeSymbol::MapLen => praxis_map_len as *const (),
263        RuntimeSymbol::MapNew => praxis_map_new as *const (),
264        RuntimeSymbol::MapRemove => praxis_map_remove as *const (),
265        RuntimeSymbol::MapUpdateMax => praxis_map_update_max as *const (),
266        RuntimeSymbol::MapUpdateMin => praxis_map_update_min as *const (),
267        RuntimeSymbol::MapValues => praxis_map_values as *const (),
268        RuntimeSymbol::MaxHeapIsEmpty => praxis_max_heap_is_empty as *const (),
269        RuntimeSymbol::MaxHeapItems => praxis_max_heap_items as *const (),
270        RuntimeSymbol::MaxHeapLen => praxis_max_heap_len as *const (),
271        RuntimeSymbol::MaxHeapNew => praxis_max_heap_new as *const (),
272        RuntimeSymbol::MaxHeapPeek => praxis_max_heap_peek as *const (),
273        RuntimeSymbol::MaxHeapPop => praxis_max_heap_pop as *const (),
274        RuntimeSymbol::MaxHeapPush => praxis_max_heap_push as *const (),
275        RuntimeSymbol::MinHeapIsEmpty => praxis_min_heap_is_empty as *const (),
276        RuntimeSymbol::MinHeapItems => praxis_min_heap_items as *const (),
277        RuntimeSymbol::MinHeapLen => praxis_min_heap_len as *const (),
278        RuntimeSymbol::MinHeapNew => praxis_min_heap_new as *const (),
279        RuntimeSymbol::MinHeapPeek => praxis_min_heap_peek as *const (),
280        RuntimeSymbol::MinHeapPop => praxis_min_heap_pop as *const (),
281        RuntimeSymbol::MinHeapPush => praxis_min_heap_push as *const (),
282        RuntimeSymbol::Panic => praxis_panic as *const (),
283        RuntimeSymbol::RaiseDivByZeroIf => praxis_raise_div_by_zero_if as *const (),
284        RuntimeSymbol::RaiseEmptyCollection => praxis_raise_empty_collection as *const (),
285        RuntimeSymbol::RaiseIntOverflowIf => praxis_raise_int_overflow_if as *const (),
286        RuntimeSymbol::RaiseStackOverflow => praxis_raise_stack_overflow as *const (),
287        RuntimeSymbol::RecordField => praxis_record_field as *const (),
288        RuntimeSymbol::RecordSetField => praxis_record_set_field as *const (),
289        RuntimeSymbol::RunParser => praxis_run_parser as *const (),
290        RuntimeSymbol::SetContains => praxis_set_contains as *const (),
291        RuntimeSymbol::SetInsert => praxis_set_insert as *const (),
292        RuntimeSymbol::SetIsEmpty => praxis_set_is_empty as *const (),
293        RuntimeSymbol::SetItems => praxis_set_items as *const (),
294        RuntimeSymbol::SetLen => praxis_set_len as *const (),
295        RuntimeSymbol::SetNew => praxis_set_new as *const (),
296        RuntimeSymbol::SetRemove => praxis_set_remove as *const (),
297        RuntimeSymbol::SnapshotDebugChain => {
298            crate::crash_snapshot::praxis_snapshot_debug_chain as *const ()
299        }
300        RuntimeSymbol::StructEq => praxis_struct_eq as *const (),
301        RuntimeSymbol::TextConcat => praxis_text_concat as *const (),
302        RuntimeSymbol::TextGet => praxis_text_get as *const (),
303        RuntimeSymbol::TextFloat => praxis_text_float as *const (),
304        RuntimeSymbol::TextInt => praxis_text_int as *const (),
305        RuntimeSymbol::TextIsEmpty => praxis_text_is_empty as *const (),
306        RuntimeSymbol::TextLen => praxis_text_len as *const (),
307        RuntimeSymbol::TupleGet => praxis_tuple_get as *const (),
308        RuntimeSymbol::TupleSet => praxis_tuple_set as *const (),
309        RuntimeSymbol::ValueCmp => praxis_value_cmp as *const (),
310        RuntimeSymbol::ValueToText => praxis_value_to_text as *const (),
311        RuntimeSymbol::VarCellGet => praxis_var_cell_get as *const (),
312        RuntimeSymbol::VarCellSet => praxis_var_cell_set as *const (),
313        RuntimeSymbol::VecFrequencies => praxis_vec_frequencies as *const (),
314        RuntimeSymbol::VecGet => praxis_vec_get as *const (),
315        RuntimeSymbol::VecSet => praxis_vec_set as *const (),
316        RuntimeSymbol::VecIsEmpty => praxis_vec_is_empty as *const (),
317        RuntimeSymbol::VecJoin => praxis_vec_join as *const (),
318        RuntimeSymbol::VecLen => praxis_vec_len as *const (),
319        RuntimeSymbol::VecChunks => praxis_vec_chunks as *const (),
320        RuntimeSymbol::VecFilled => praxis_vec_filled as *const (),
321        RuntimeSymbol::VecNew => praxis_vec_new as *const (),
322        RuntimeSymbol::VecPush => praxis_vec_push as *const (),
323        RuntimeSymbol::VecReversed => praxis_vec_reversed as *const (),
324        RuntimeSymbol::VecSorted => praxis_vec_sorted as *const (),
325        RuntimeSymbol::VecSortedByKey => praxis_vec_sorted_by_key as *const (),
326        RuntimeSymbol::VecToText => praxis_vec_to_text as *const (),
327        RuntimeSymbol::VecUnique => praxis_vec_unique as *const (),
328        RuntimeSymbol::VecWindows => praxis_vec_windows as *const (),
329        RuntimeSymbol::WriteStdout => praxis_write_stdout as *const (),
330    };
331    ptr as *const u8
332}
333
334// ---------------------------------------------------------------------------
335// The panic backstop (§9.2, §10.4)
336// ---------------------------------------------------------------------------
337
338/// The defined dummy a wrapper returns when it has raised a fault and has no
339/// real answer (§10.4).
340///
341/// A wrapper's return type is part of the ABI, so "return nothing" is not
342/// available; every type generated code can receive needs a value that is safe
343/// to hold and never read. `GcRef` is `NonNull`, so its dummy is the context's
344/// `Unit` — the same sentinel the fault epilogue returns — and integer zero
345/// would be an invalid reference, not a dummy.
346pub(crate) trait AbiSentinel {
347    /// # Safety
348    /// `ctx` must be null or point at a live, wired `RuntimeContext`.
349    unsafe fn sentinel(ctx: *mut RuntimeContext) -> Self;
350}
351
352impl AbiSentinel for () {
353    unsafe fn sentinel(_ctx: *mut RuntimeContext) {}
354}
355
356impl AbiSentinel for i64 {
357    unsafe fn sentinel(_ctx: *mut RuntimeContext) -> i64 {
358        0
359    }
360}
361
362impl AbiSentinel for GcRef {
363    unsafe fn sentinel(ctx: *mut RuntimeContext) -> GcRef {
364        // SAFETY: the caller guarantees a live, wired context. A null one
365        // cannot produce a `GcRef` at all, and `abi_panic_escaped` refuses to
366        // reach here with one.
367        unsafe { unit_sentinel(ctx) }
368    }
369}
370
371impl<T> AbiSentinel for *mut T {
372    unsafe fn sentinel(_ctx: *mut RuntimeContext) -> *mut T {
373        std::ptr::null_mut()
374    }
375}
376
377impl<T> AbiSentinel for *const T {
378    unsafe fn sentinel(_ctx: *mut RuntimeContext) -> *const T {
379        std::ptr::null()
380    }
381}
382
383/// Translate a panic that reached an `extern "C"` boundary into a fault, and
384/// return the boundary's defined dummy.
385///
386/// **This must never fire.** Totality is the contract — a wrapper validates its
387/// arguments and reports a bad one as a fault — and this exists because a
388/// contract that cannot be checked is a hope. A Rust panic unwinding out of
389/// `extern "C"` into Cranelift frames is undefined behaviour; the guard turns
390/// the one outcome nobody can reason about into the one §10.4 already
391/// specifies, and does it uniformly so no future wrapper has to remember.
392///
393/// The kind is [`FaultKind::Panic`](crate::FaultKind::Panic), with a message
394/// naming the wrapper. It is deliberately not a new `FaultKind::Internal`: a
395/// new kind is a `#[repr(C)]` layout change that costs an ABI bump (ADR-075),
396/// and `Panic` plus a message that names the function carries strictly more
397/// information for the crash report (§9.4) than a bare kind would.
398///
399/// # Safety
400/// `ctx` must be null or point at a live, wired `RuntimeContext`.
401#[cold]
402#[inline(never)]
403pub(crate) unsafe fn abi_panic_escaped<T: AbiSentinel>(
404    ctx: *mut RuntimeContext,
405    wrapper: &'static str,
406) -> T {
407    if ctx.is_null() {
408        // There is no fault slot to write and no `Unit` to return. Aborting is
409        // the only defined answer left, and it is still better than unwinding
410        // into generated frames.
411        std::process::abort();
412    }
413    // SAFETY: the caller guarantees a live, wired context.
414    unsafe { set_fault(ctx, RaisedFault::PANIC) };
415    let message = format!("internal error: a panic escaped the runtime wrapper `{wrapper}`");
416    // SAFETY: as above.
417    unsafe { set_fault_message(ctx, message.clone()) };
418    if !panic_fault_is_observable(wrapper) {
419        // **The dummy has to be unreachable where nobody will look at the
420        // fault.** Generated code tests the fault slot only where MIR emitted
421        // a `CheckFault`, and MIR emits one only after a call it classifies as
422        // faultable — so for a wrapper the manifest declares non-faulting there
423        // is *no* check by construction, and returning `unit_sentinel` would
424        // hand a `Unit` into a slot generated code believes holds a Record, a
425        // Tuple or a closure. Aborting with the message is the only answer that
426        // does not introduce a descriptor/payload confusion.
427        eprintln!("{message}");
428        std::process::abort();
429    }
430    // SAFETY: as above.
431    unsafe { T::sentinel(ctx) }
432}
433
434/// Whether generated code can be expected to observe a `Panic` fault raised by
435/// `wrapper` — i.e. whether the wrapper's defined dummy is ever consumed under
436/// a fault check rather than as a value.
437///
438/// The manifest is the authority: a symbol declared [`Effect::Pure`] or
439/// [`Effect::Allocates`] cannot be followed by a `CheckFault`. **That is
440/// `praxis_mir::verify`'s rule, not a claim restated here** — its
441/// `RedundantFaultCheck` variant rejects a check after an instruction that
442/// cannot fault, so this function's premise is enforced rather than assumed
443/// (ADR-088). A wrapper the manifest does not name at all is in the same
444/// position and is treated the same way.
445///
446/// The converse is *not* claimed here: a declared-faulting wrapper's call sites
447/// are MIR's business. What this rules out is the class where the check is
448/// impossible.
449fn panic_fault_is_observable(wrapper: &str) -> bool {
450    praxis_stdlib::abi::RuntimeSymbol::from_name(wrapper).is_some_and(|s| s.faults())
451}
452
453/// Wrap an `extern "C"` wrapper's body so a panic becomes a fault.
454///
455/// Every `#[unsafe(no_mangle)] extern "C" fn` in this crate has its body inside
456/// these, and `every_no_mangle_wrapper_is_behind_the_panic_guard` is the test
457/// that keeps it that way — a new wrapper that forgets is a failing test rather
458/// than a latent abort.
459macro_rules! abi_guard {
460    ($wrapper:expr_2021, $ctx:expr_2021, $body:block) => {{
461        // `AssertUnwindSafe`: the body's captures are the wrapper's own
462        // arguments, which are `Copy` C types, and the fault protocol is how
463        // the runtime already communicates a half-finished operation.
464        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || $body)) {
465            Ok(value) => value,
466            // SAFETY: `ctx` is the wrapper's own context argument, whose
467            // validity every wrapper's `# Safety` section already requires.
468            Err(_) => unsafe { crate::abi::abi_panic_escaped($ctx, $wrapper) },
469        }
470    }};
471}
472
473pub(crate) use abi_guard;
474
475// ---------------------------------------------------------------------------
476// Internals the wrappers share.
477// ---------------------------------------------------------------------------
478
479/// Raise `fault` on `ctx`'s fault slot (§10.4). Does nothing if the context's
480/// fault pointer is null (a misuse, but never panics across the ABI).
481///
482/// Takes a [`RaisedFault`], not a `FaultKind`: every raise names a kind that
483/// describes it, and "no fault" is not spellable here.
484unsafe fn set_fault(ctx: *mut RuntimeContext, fault: RaisedFault) {
485    if let Some(slot) = unsafe { (*ctx).pending_fault.as_mut() } {
486        slot.set(fault);
487    }
488}
489
490/// Record `text` as the message the fault about to be raised carries (§9.1).
491/// Does nothing if the context's message slot is null (a host that wired no
492/// runtime), so a `panic` still faults even where nothing can render its words.
493unsafe fn set_fault_message(ctx: *mut RuntimeContext, text: String) {
494    if let Some(slot) = unsafe { (*ctx).fault_message.as_mut() } {
495        slot.set(text);
496    }
497}
498
499/// The heap pointer out of a context, or null.
500#[inline]
501unsafe fn heap<'a>(ctx: *mut RuntimeContext) -> &'a Heap {
502    // SAFETY: the caller guarantees `ctx` points at a live, wired context whose
503    // `heap` field references a valid `Heap` for the duration of the call.
504    unsafe { &*(*ctx).heap }
505}
506
507#[inline]
508/// Charge the pacer for a collection's buffer growing (ADR-121).
509///
510/// `before` and `after` are the same payload's `owned_bytes()`, read either
511/// side of a mutation that may reallocate. Nothing is charged when the buffer
512/// did not grow, which is the overwhelmingly common case: amortized doubling
513/// means a `push` reallocates once every *n* pushes, so this is a compare and a
514/// not-taken branch on the hot path.
515///
516/// # Why every growing wrapper has to call this
517///
518/// `Heap::alloc_raw` charges `stride + owned_bytes_of(payload)` once, at
519/// construction. Leaving later growth uncharged relies on the elements
520/// themselves being paced allocations, so that the residual under-count is only
521/// the spine — and scalar promotion deletes exactly those element allocations,
522/// so an allocation-light program that grows a large buffer paces nothing.
523/// Uncharged, `bfs` runs 6 collections instead of 41 and reaches a peak
524/// resident set of 224 MiB against 61 (ADR-121).
525///
526/// So the rule is: **a wrapper that can grow a buffer charges the growth.** The
527/// `growth_charging_tests` module below has one case per such wrapper, because
528/// the failure mode is silent — a program that simply stops collecting, which
529/// reads as a leak nobody connects to the wrapper that was added.
530fn charge_growth(ctx: *mut RuntimeContext, before: usize, after: usize) {
531    let Some(grown) = after.checked_sub(before).filter(|g| *g != 0) else {
532        return;
533    };
534    if ctx.is_null() {
535        return;
536    }
537    // SAFETY: every caller is inside `abi_guard!`, which established that `ctx`
538    // is live and wired; the null check above covers the guard's own edge.
539    unsafe { heap(ctx).charge_owned_growth(grown) };
540}
541
542/// Trigger a collection on allocation pressure, rooting from the context
543/// (§12.4, ADR-019, ADR-101). Called by every allocating `praxis_*` wrapper.
544/// Safe to call with a null/unwired context (no-op).
545///
546/// The roots are every **strong** arm of
547/// [`RuntimeRoots`](crate::roots::RuntimeRoots) — the shadow stack, the ambient
548/// input buffer, a parse failure's partial value, a runtime-owned crash
549/// snapshot, and the native root store. All five, not the shadow stack alone:
550/// host-driven allocation and the parser interpreter push no shadow frame, and
551/// the other four owners are reachable regardless. The debug arm is
552/// deliberately *weak* (ADR-106): it names storage without keeping it alive, so
553/// it is cleared after the sweep rather than traced here.
554unsafe fn maybe_collect(ctx: *mut RuntimeContext) {
555    if ctx.is_null() {
556        return;
557    }
558    // SAFETY: ctx is live and wired.
559    let roots = unsafe { crate::roots::RuntimeRoots::from_context(ctx) };
560    unsafe { heap(ctx).maybe_collect(&roots) };
561}
562
563/// Pace the collector and mint the token one allocation needs.
564///
565/// Every `praxis_*` wrapper reaches the heap through [`gc_alloc`] or
566/// [`gc_alloc_owned`], which call this — and even a wrapper that reached
567/// `Heap::alloc` directly would have to come through here, because the token
568/// has no other producer. That is the whole point: an allocation that skipped
569/// the pacer would let a program whose pressure comes from `Text`, `.len()` or
570/// checked arithmetic run arbitrarily long without the collector ever being
571/// offered a turn.
572///
573/// # Safety
574/// `ctx` must point at a live, wired `RuntimeContext`. Every allocating
575/// wrapper already requires this to reach the heap at all.
576#[inline]
577unsafe fn safepoint<'a>(ctx: *mut RuntimeContext) -> (&'a Heap, Safepoint<'a>) {
578    // SAFETY: caller upholds ctx validity.
579    let h = unsafe { heap(ctx) };
580    // SAFETY: as above; the roots are read out of the same live context.
581    let roots = unsafe { crate::roots::RuntimeRoots::from_context(ctx) };
582    let sp = h.pace(&roots);
583    (h, sp)
584}
585
586/// Pace the collector, then allocate a `Copy` payload (§12.4).
587///
588/// The descriptor arrives as a [`Payload<T>`] — `scalars::INT_PAYLOAD`, not
589/// `&scalars::INT` — so `value`'s Rust type is checked against it here, at the
590/// call: a value of the wrong type is an `E0308`, and an *untyped* literal
591/// infers as the payload type instead of defaulting to `i32`. A bare descriptor
592/// reference with `T` free would let a width mismatch reach the heap and abort
593/// the process from inside `extern "C"`, which §10.4 forbids.
594///
595/// # Safety
596/// `ctx` must be live and wired.
597#[inline]
598unsafe fn gc_alloc<T: Copy>(ctx: *mut RuntimeContext, payload: Payload<T>, value: T) -> GcRef {
599    // SAFETY: caller upholds ctx validity.
600    let (h, sp) = unsafe { safepoint(ctx) };
601    h.alloc(sp, payload, value)
602}
603
604/// Pace the collector, then allocate a payload that owns Rust resources.
605///
606/// [`gc_alloc`]'s counterpart for the payloads no [`Payload<T>`] can describe.
607/// The type is named **once**, as `P`: [`Heap::alloc_payload`] derives the size,
608/// the alignment and the write from it, so no wrapper restates all three and
609/// keeps them in agreement by hand.
610///
611/// The payload arrives as a producer rather than a value, and that is
612/// load-bearing: `init` runs *after* [`safepoint`] has given the collector its
613/// turn, so a payload built out of bare `GcRef`s — `vec![fill; n]` in
614/// [`praxis_vec_filled`] and [`praxis_grid_filled`] — is never a `Vec<GcRef>`
615/// live across a collection with no root set able to see it.
616///
617/// # Safety
618/// `ctx` must be live and wired, and `descriptor` must be `P`'s own descriptor
619/// ([`Heap::alloc_payload`]'s contract).
620#[inline]
621unsafe fn gc_alloc_owned<P>(
622    ctx: *mut RuntimeContext,
623    descriptor: &'static TypeDescriptor,
624    init: impl FnOnce() -> P,
625) -> GcRef {
626    // SAFETY: caller upholds ctx validity.
627    let (h, sp) = unsafe { safepoint(ctx) };
628    // `init()` is evaluated here, downstream of the safepoint above — see the
629    // ordering note in this function's doc.
630    // SAFETY: forwarded from this function's contract.
631    unsafe { h.alloc_payload(sp, descriptor, init()) }
632}
633
634/// The immortal `Bool` for `value`, off the context's cached singletons.
635///
636/// Never an allocation: there are exactly two `Bool` values and the runtime
637/// minted both at startup, so every comparison, `contains` and `is_empty` a
638/// program evaluates answers with a singleton rather than consuming arena
639/// storage permanently. It is also what makes those manifest rows honestly
640/// `Effect::Pure`: nothing here can collect, so the call site is not a
641/// safepoint.
642///
643/// # Safety
644/// `ctx` must point at a live, wired `RuntimeContext`.
645#[inline]
646unsafe fn bool_ref(ctx: *mut RuntimeContext, value: bool) -> GcRef {
647    // SAFETY: caller upholds ctx validity.
648    let c = unsafe { &*ctx };
649    if value { c.true_ref } else { c.false_ref }
650}
651
652/// The `Int` for `value`: the interned immortal when it is small
653/// ([`crate::small_int`]), a fresh allocation otherwise.
654///
655/// [`bool_ref`]'s shape, one step less absolute: `Bool` has two values so it is
656/// always the singleton, while `Int` has a *range* that is interned and an
657/// unbounded remainder that is not. Every wrapper that answers an `Int` reaches
658/// the heap through here, so the interning covers not just literals but
659/// `Vec.len()`, a `Counter` bump, an enum tag, a comparison's index and the
660/// result of arithmetic — which is where most of a real program's small `Int`s
661/// come from.
662///
663/// # It paces even when it does not allocate, and that is deliberate
664///
665/// The manifest declares `VecLen`, `MapLen`, `EnumTag`, `TextLen`, `CounterGet`
666/// and two dozen more `Effect::Allocates`, which is generated code's contract
667/// that the call site is a GC safepoint. If this returned before [`safepoint`],
668/// a loop whose only allocations were small `Int`s would never offer the
669/// collector a turn — the collector's *only* trigger is the pacing counter, and
670/// nothing else in such a loop touches it. So the token is minted and then
671/// dropped: [`Safepoint`] is `#[must_use]`, so `drop(sp)` is the honest spelling
672/// of "the collector got its turn and we allocated nothing", and it is a
673/// compile error to forget which of the two happened.
674///
675/// The interned path therefore costs a threshold compare and a range test rather
676/// than an allocation. `Inst::ConstGc` is what removes even that, but only for a
677/// *literal*, where the compiler knows the value and no manifest row applies.
678///
679/// # Safety
680/// `ctx` must point at a live, wired `RuntimeContext`.
681#[inline]
682unsafe fn int_ref(ctx: *mut RuntimeContext, value: i64) -> GcRef {
683    // SAFETY: caller upholds ctx validity.
684    let (h, sp) = unsafe { safepoint(ctx) };
685    match crate::small_int::index_of(value) {
686        Some(i) => {
687            drop(sp);
688            // SAFETY: `index_of` bounds `i` by `SMALL_INT_COUNT`, and
689            // `Runtime::context` points `small_ints` at a table of exactly that
690            // length whose slot `i` holds `SMALL_INT_MIN + i`.
691            unsafe { *(*ctx).small_ints.add(i) }
692        }
693        None => h.alloc(sp, scalars::INT_PAYLOAD, value),
694    }
695}
696
697/// The `Char` for `code`: the interned immortal when it is ASCII
698/// ([`crate::small_char`]), a fresh allocation otherwise.
699///
700/// [`int_ref`]'s shape and its argument (ADR-107). Every wrapper that answers a
701/// `Char` reaches the heap through here — [`checked_alloc_char`], which is both
702/// `praxis_alloc_char` and `praxis_int_to_char`; [`praxis_text_get`], which is
703/// both `t[i]` and every step of `for c in t`; and [`default_cell`], which is a
704/// `Grid[Char]`'s fill. `praxis_text_get` is the one that matters for real code:
705/// an AoC-shaped program that walks a line of text would otherwise box a fresh
706/// object per character read, and every character of such a line is ASCII.
707///
708/// # It paces even when it does not allocate
709///
710/// The manifest declares `TextGet`, `AllocChar` and `IntToChar`
711/// `Effect::AllocatesAndFaults`, which is generated code's contract that the call
712/// site is a GC safepoint. If this returned before [`safepoint`], `for c in text`
713/// over an ASCII line would never offer the collector a turn — the collector's
714/// *only* trigger is the pacing counter, and a loop that reads characters and
715/// compares them touches nothing else that would bump it. So the token is minted
716/// and then dropped: [`Safepoint`] is `#[must_use]`, so `drop(sp)` is the honest
717/// spelling of "the collector got its turn and we allocated nothing", and it is a
718/// compile error to forget which of the two happened. Pinned by
719/// `char_ref_paces_the_collector_even_when_it_answers_from_the_table`.
720///
721/// Unlike `int_ref` there is no `Inst::ConstGc` that removes even the pacing
722/// check: that instruction exists for a *literal*, and the language has no
723/// character literal (ADR-107 Decision 2).
724///
725/// # Safety
726/// `ctx` must point at a live, wired `RuntimeContext`, and `code` must be a valid
727/// Unicode scalar value — every caller has already established this, either by
728/// [`checked_alloc_char`]'s range check or by starting from a Rust `char`.
729#[inline]
730unsafe fn char_ref(ctx: *mut RuntimeContext, code: u32) -> GcRef {
731    debug_assert!(
732        crate::scalars::is_valid_char(code),
733        "char_ref's callers validate first"
734    );
735    // SAFETY: caller upholds ctx validity.
736    let (h, sp) = unsafe { safepoint(ctx) };
737    match crate::small_char::index_of(code) {
738        Some(i) => {
739            drop(sp);
740            // SAFETY: `index_of` bounds `i` by `SMALL_CHAR_COUNT`, and
741            // `Runtime::context` points `small_chars` at a table of exactly that
742            // length whose slot `i` holds code point `i`.
743            unsafe { *(*ctx).small_chars.add(i) }
744        }
745        None => h.alloc(sp, scalars::CHAR_PAYLOAD, code),
746    }
747}
748
749/// A fresh owned `Text` holding `s`.
750///
751/// [`bool_ref`]/[`int_ref`]/[`char_ref`]'s place in the file but not their
752/// shape: there is nothing interned to answer from — every `Text` is a distinct
753/// object — so this always allocates, and always paces.
754///
755/// `impl Into<Box<str>>` is what lets every text-producing wrapper share this
756/// one allocation without a defensive `.clone()`: a `String` from a renderer, a
757/// `Box<str>` the caller already built ([`praxis_alloc_text`]), a `&str`.
758///
759/// # Safety
760/// `ctx` must point at a live, wired `RuntimeContext`.
761#[inline]
762unsafe fn text_ref(ctx: *mut RuntimeContext, s: impl Into<Box<str>>) -> GcRef {
763    // SAFETY: caller upholds ctx validity; `TextPayload` is `TEXT`'s payload type.
764    unsafe {
765        gc_alloc_owned(ctx, &crate::text::TEXT, || {
766            crate::text::TextPayload::owned(s)
767        })
768    }
769}
770
771/// Read `r`'s payload through a [`Payload`] handle, first checking that `r`
772/// really is that handle's type.
773///
774/// This is the reader to reach for whenever a wrapper receives a `GcRef` it did
775/// not itself allocate — a value handed back by a program's closure, most of
776/// all. Two mistakes are impossible through it: reading a value of the wrong
777/// *type* (the identity check answers `None`, and the caller decides whether
778/// that is a `TypeMismatch` fault), and reading the right type at the wrong
779/// *width* (the width is `size_of::<T>()`, which [`Payload::new`] proved is the
780/// descriptor's width when the handle was declared). Reading a one-byte `Bool`
781/// payload with an eight-byte `int_payload` is the class it rules out.
782///
783/// # Safety
784/// `r` must be a valid `GcRef` into a live heap.
785#[inline]
786unsafe fn read_scalar<T: Copy>(r: GcRef, handle: crate::descriptor::Payload<T>) -> Option<T> {
787    if !std::ptr::eq(r.descriptor(), handle.descriptor()) {
788        return None;
789    }
790    // SAFETY: the identity check proves `r`'s payload is this handle's type, and
791    // the handle's own construction proved `T` is that type's layout.
792    Some(unsafe { handle.read(r.payload::<u8>()) })
793}
794
795/// Read the `i64` payload of an `Int` `GcRef`. Used by every arithmetic wrapper.
796///
797/// Prefer [`read_scalar`] for any value whose type is not already established:
798/// this reads eight bytes, and the descriptor check is all that stands between
799/// it and a narrower payload.
800///
801/// # The width check is a branch, not a `debug_assert`
802///
803/// A `debug_assert` is not a bound — it compiles out of a release build, leaving
804/// an eight-byte read against a descriptor that may be narrower or zero bytes
805/// wide, so the two profiles would answer differently and the wrong one is the
806/// one users get. As an ordinary branch it holds in every profile: the read
807/// cannot happen. What happens instead is ADR-080's defined panic path —
808/// `abi_guard` catches it, raises `RaisedFault::PANIC` with a message naming
809/// the wrapper, and either faults into the crash debugger (a wrapper the
810/// manifest declares faultable) or prints that message and aborts (one it does
811/// not, which is `praxis_int_load`'s case). The guard is a memory-safety check
812/// on a raw read, not a stand-in for a type system.
813///
814/// This stays `-> i64` rather than becoming fallible: sixty-odd wrappers read
815/// through it, and a `ctx`-threading signature change is a larger edit than the
816/// one memory safety needs.
817#[inline]
818unsafe fn int_payload(r: GcRef) -> i64 {
819    // SAFETY: `read_scalar` proves `r`'s descriptor *is* `INT` before reading,
820    // so the eight bytes are in bounds and are an `i64`. The compiler only emits
821    // these calls with Int-typed operands, and a fault that would feed a
822    // non-`Int` (the Unit sentinel, say) into an arithmetic wrapper is diverted
823    // by `Inst::CheckFault` before it gets here (§10.4).
824    unsafe { read_scalar(r, scalars::INT_PAYLOAD) }
825        .unwrap_or_else(|| scalar_type_mismatch("int_payload", "Int", r.descriptor().name))
826}
827
828/// The refusal every scalar reader shares, out of line so the check costs a
829/// never-taken branch on the hot path.
830///
831/// `#[cold]` and `#[inline(never)]` are what let the check be unconditional, and
832/// it must be unconditional: a `debug_assert` compiles out, so a release build
833/// would do an out-of-bounds heap read where a debug build aborted.
834///
835/// A panic here is ADR-080's defined path: `abi_guard!` catches it, raises
836/// `RaisedFault::PANIC` naming the wrapper, and either faults into the crash
837/// debugger or prints the message and aborts. That is the backstop, not the
838/// primary defence — a raw scalar read must prove its own width whatever the
839/// type system believes.
840#[cold]
841#[inline(never)]
842fn scalar_type_mismatch(what: &'static str, want: &'static str, found: &'static str) -> ! {
843    panic!("{what} wants a `{want}` payload; this value is a `{found}` (REP-56)");
844}
845
846/// [`praxis_alloc_text`]'s refusal when its buffer is not UTF-8 — a violated
847/// precondition, not a runtime condition (ADR-111).
848///
849/// **Why this is a panic and not a fault.** The precondition is the one that is
850/// actually true: the compiler's bytes are a Rust `&str` unbroken from
851/// `Lit::Text(String)` through `AllocKind::Text { value: String }` to
852/// `Generation::alloc_str`, and the one caller in this crate that holds raw
853/// *host* bytes — [`praxis_get_input`] — validates them itself and raises
854/// `InvalidText` there, where the `read` can observe it. Spelling it as a fault
855/// instead would cost a `CheckFault` after every text literal for a fault no
856/// generated call site can raise.
857///
858/// **Why the `from_utf8` call above stays, in every profile.** This is
859/// [`scalar_type_mismatch`]'s argument verbatim and it is why that function is
860/// the neighbour: a `debug_assert` is not a bound, because it compiles out of a
861/// release build. What would be left in release is a `Box<str>` built from bytes
862/// that are not UTF-8, which [`crate::text::text_str`] later hands out as a
863/// `&str` — so the two profiles would answer differently and the wrong one is
864/// the one users get. `from_utf8_unchecked` is the same hole with the check
865/// deleted rather than compiled out. The unconditional branch costs a
866/// never-taken jump to this cold callee, which is the price ADR-102 §1 already
867/// established for the inline scalar loads.
868///
869/// **It must not reach `set_fault`, and that is enforced.**
870/// `a_wrapper_that_can_raise_a_fault_declares_that_it_faults` computes a textual
871/// fixed point over this file: a body that can reach `set_fault`, directly or
872/// through a helper defined here, must belong to a symbol whose manifest row
873/// says it faults. `AllocText`'s row is `Effect::Allocates`, so a refusal
874/// spelled as a fault would fail that test.
875///
876/// The end-to-end path on a violation is ADR-080's: panic → `abi_guard!`
877/// catches → `panic_fault_is_observable("praxis_alloc_text")` reads the
878/// `Allocates` row and answers `false` → the message is printed and the process
879/// aborts. That is the same outcome `praxis_int_load` gives a wrong descriptor,
880/// and it falls out of the row change with no code of its own.
881#[cold]
882#[inline(never)]
883fn text_bytes_are_not_utf8(len: usize) -> ! {
884    panic!(
885        "praxis_alloc_text was handed {len} bytes that are not valid UTF-8; its \
886         `# Safety` contract requires them to be (ADR-111). A host with untrusted \
887         bytes must validate them first, as `praxis_get_input` does."
888    );
889}
890
891/// The Unit `GcRef` returned on fault paths as the "defined dummy" (§10.4).
892/// Reads the cached immortal `unit_ref` from the context, which is stable for
893/// the program's lifetime.
894#[inline]
895unsafe fn unit_sentinel(ctx: *mut RuntimeContext) -> GcRef {
896    unsafe { (*ctx).unit_ref }
897}
898
899/// The slot a source index `i` names in a container of `len` elements, or
900/// `None` when it is outside `0..len` (§9.2, §11.1).
901///
902/// **The one place a source index becomes a `usize`.** The `< 0` test has to
903/// run before the cast: a negative `i64` casts to a value near `usize::MAX` and
904/// would sail straight past a bare length comparison. Every accessor shares this
905/// one copy, so no single site can drop the guard invisibly. The construction
906/// side guards the same hazard with a named
907/// [`GridExtent`](crate::collections::GridExtent).
908///
909/// `Vec` and `Deque` indexing is one language rule applied to two containers,
910/// so they share this rather than each spelling it out.
911fn linear_index(i: i64, len: usize) -> Option<usize> {
912    if i < 0 {
913        return None;
914    }
915    // Non-negative above, so the cast is exact.
916    let i = i as usize;
917    (i < len).then_some(i)
918}
919
920/// The row-major slot `(x, y)` names in a `width × height` grid, or `None` when
921/// either axis falls outside it.
922///
923/// Both axes go through [`linear_index`], so the 2-D rule is the 1-D rule twice
924/// and the signed-to-`usize` cast still exists in exactly one place. The
925/// product cannot overflow: `y < height` and `x < width` with
926/// `height = items.len() / width`, so the result is below `items.len()`.
927fn cell_index(x: i64, y: i64, width: usize, height: usize) -> Option<usize> {
928    let x = linear_index(x, width)?;
929    let y = linear_index(y, height)?;
930    Some(y * width + x)
931}
932
933/// [`linear_index`], raising `IndexOutOfBounds` when the index is out of range.
934/// `None` means the fault is already set and the caller owes only its sentinel
935/// return.
936///
937/// The raising wrapper is deliberately separate from the pure predicate,
938/// because not every bounds question may fault. [`praxis_grid_contains`] is
939/// declared `Pure` in the ABI manifest while `GridGet` is `Faults`, and MIR's
940/// `RedundantFaultCheck` emits no `CheckFault` after a `Pure` call — so a fault
941/// raised on every legitimate `false` would sit pending until some later check
942/// mistook it for its own. Bounds-testing sites take [`cell_index`] /
943/// [`linear_index`]; only sites the manifest says can fault take these.
944///
945/// # Safety
946/// `ctx` must be live and wired.
947unsafe fn checked_index(ctx: *mut RuntimeContext, i: i64, len: usize) -> Option<usize> {
948    let idx = linear_index(i, len);
949    if idx.is_none() {
950        unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
951    }
952    idx
953}
954
955/// [`cell_index`], raising `IndexOutOfBounds` when `(x, y)` is off the grid.
956/// See [`checked_index`] for why the raising and the predicate are two
957/// functions.
958///
959/// # Safety
960/// `ctx` must be live and wired.
961unsafe fn checked_cell(
962    ctx: *mut RuntimeContext,
963    x: i64,
964    y: i64,
965    width: usize,
966    height: usize,
967) -> Option<usize> {
968    let idx = cell_index(x, y, width, height);
969    if idx.is_none() {
970        unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
971    }
972    idx
973}
974
975// ---------------------------------------------------------------------------
976// Allocation wrappers.
977// ---------------------------------------------------------------------------
978
979/// The `Int` for `value` (§4.3, §11.1) — the interned immortal when it is small
980/// ([`crate::small_int`]), a fresh box otherwise.
981///
982/// The row stays `Effect::Allocates`, not `Pure` as `AllocBool`'s is: this
983/// wrapper still allocates for an out-of-range value, so the call site is still
984/// a GC safepoint and generated code must still spill its roots across it. The
985/// interning is invisible to the caller by design.
986///
987/// # Safety
988/// `ctx` must point at a live, wired `RuntimeContext` whose `heap` is valid.
989#[unsafe(no_mangle)]
990pub unsafe extern "C" fn praxis_alloc_int(ctx: *mut RuntimeContext, value: i64) -> GcRef {
991    abi_guard!("praxis_alloc_int", ctx, {
992        // `int_ref` paces first, rooted at the whole `RuntimeRoots`. The new object
993        // is not yet a root, but it is returned by value to the caller, which spills
994        // it — so it is safe across this collection (the *previous* allocation's
995        // result was already spilled by the backend before this wrapper was called).
996        // SAFETY: caller upholds the ctx/heap validity.
997        unsafe { int_ref(ctx, value) }
998    })
999}
1000
1001/// Allocate a boxed `Bool` from a 0/1 value (§4.3). Returns the immortal
1002/// singleton, never a fresh allocation.
1003///
1004/// # Safety
1005/// `ctx` must point at a live, wired `RuntimeContext`.
1006#[unsafe(no_mangle)]
1007pub unsafe extern "C" fn praxis_alloc_bool(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1008    abi_guard!("praxis_alloc_bool", ctx, {
1009        // There are two `Bool` values, and the runtime allocated both at startup.
1010        // `value != 0` is true; `0` is false.
1011        // SAFETY: caller upholds ctx validity.
1012        let c = unsafe { &*ctx };
1013        if value != 0 { c.true_ref } else { c.false_ref }
1014    })
1015}
1016
1017/// Allocate the `Unit` singleton (§4.3).
1018///
1019/// # Safety
1020/// `ctx` must point at a live, wired `RuntimeContext`.
1021#[unsafe(no_mangle)]
1022pub unsafe extern "C" fn praxis_alloc_unit(ctx: *mut RuntimeContext) -> GcRef {
1023    abi_guard!("praxis_alloc_unit", ctx, {
1024        // The one `Unit` value, cached on the context for the fault path.
1025        // SAFETY: caller upholds ctx validity.
1026        unsafe { (*ctx).unit_ref }
1027    })
1028}
1029
1030/// Allocate a boxed `Char` from a Unicode scalar value (§4.3). The `value`
1031/// is the `u32` code point carried as `i64` (the uniform scalar ABI width). If
1032/// the code point is not a valid scalar, the fault is set and the Unit sentinel
1033/// is returned (no panic crosses the ABI).
1034///
1035/// # Safety
1036/// `ctx` must point at a live, wired `RuntimeContext`.
1037#[unsafe(no_mangle)]
1038pub unsafe extern "C" fn praxis_alloc_char(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1039    abi_guard!("praxis_alloc_char", ctx, {
1040        // SAFETY: caller upholds ctx/heap validity.
1041        unsafe { checked_alloc_char(ctx, value) }
1042    })
1043}
1044
1045/// Box an `i64` as a `Char`, or raise `InvalidChar` and answer the Unit sentinel.
1046///
1047/// The one place the `i64`-to-code-point rule is enforced, because there are two
1048/// doors into it — `praxis_alloc_char` (the parser and codegen's `AllocKind::Char`)
1049/// and `praxis_int_to_char` (`Int.to_char()`, ADR-086) — and a rule stated at both
1050/// goes stale at one.
1051///
1052/// **Range-check before narrowing.** `value as u32` truncates, so
1053/// `0x1_0000_0041` would silently become `'A'`. The scalar ABI is 64 bits wide;
1054/// a code point is not, and the conversion has to say so rather than wrap. The
1055/// surrogate range is rejected for the same reason: `char::from_u32` is what
1056/// decides, not a width.
1057///
1058/// # Safety
1059/// `ctx` must point at a live, wired `RuntimeContext`.
1060unsafe fn checked_alloc_char(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1061    let Ok(code) = u32::try_from(value) else {
1062        unsafe { set_fault(ctx, RaisedFault::INVALID_CHAR) };
1063        return unsafe { unit_sentinel(ctx) };
1064    };
1065    if !crate::scalars::is_valid_char(code) {
1066        unsafe { set_fault(ctx, RaisedFault::INVALID_CHAR) };
1067        return unsafe { unit_sentinel(ctx) };
1068    }
1069    // SAFETY: caller upholds ctx/heap validity; code is a validated scalar.
1070    unsafe { char_ref(ctx, code) }
1071}
1072
1073/// Allocate an owned `Text` from a UTF-8 byte buffer (§4.3, ADR-013).
1074///
1075/// **UTF-8 is the caller's precondition, and this wrapper cannot fault**
1076/// (ADR-111). Its row is `Effect::Allocates`, so `Inst::Alloc { AllocKind::Text }`
1077/// is followed by no `CheckFault` — `praxis_mir::verify` rejects one — and a
1078/// `Text` literal in a loop is hoisted into the preheader like a `Float` one
1079/// (ADR-108 §3). Handing this bytes that are not UTF-8 is a violated contract,
1080/// not a runtime condition, and it aborts through `text_bytes_are_not_utf8`
1081/// (whose doc carries the argument) rather than raising `InvalidText`.
1082///
1083/// A host that holds *untrusted* bytes validates them before calling. There is
1084/// exactly one such caller in this crate — [`praxis_get_input`], whose row is
1085/// `AllocatesAndFaults` — and it raises `InvalidText` itself, so the fault a
1086/// `read` can observe still lands at the `read` (§4.3, §7.10).
1087///
1088/// # Safety
1089/// `ctx` must point at a live, wired `RuntimeContext`; `bytes` must point at
1090/// `len` valid UTF-8 bytes that remain valid for the duration of the call.
1091#[unsafe(no_mangle)]
1092pub unsafe extern "C" fn praxis_alloc_text(
1093    ctx: *mut RuntimeContext,
1094    bytes: *const u8,
1095    len: usize,
1096) -> GcRef {
1097    abi_guard!("praxis_alloc_text", ctx, {
1098        let slice = if bytes.is_null() || len == 0 {
1099            &[]
1100        } else {
1101            // SAFETY: caller guarantees `bytes..bytes+len` is a valid, UTF-8 buffer.
1102            unsafe { std::slice::from_raw_parts(bytes, len) }
1103        };
1104        // The check is unconditional in every profile: it is the backstop on a
1105        // raw read, the same standing `read_scalar` has, and its argument is
1106        // written out at `text_bytes_are_not_utf8`. A violation refuses rather
1107        // than recovering lossily behind a fault nobody at a generated call site
1108        // could observe (ADR-111).
1109        let owned: Box<str> = match std::str::from_utf8(slice) {
1110            Ok(s) => s.into(),
1111            Err(_) => text_bytes_are_not_utf8(len),
1112        };
1113        // SAFETY: ctx/heap valid.
1114        unsafe { text_ref(ctx, owned) }
1115    })
1116}
1117
1118// ---------------------------------------------------------------------------
1119// Scalar extraction / materialization.
1120// ---------------------------------------------------------------------------
1121
1122/// Read the `i64` payload of an `Int` `GcRef` (§10.3 transient scalar).
1123///
1124/// # Safety
1125/// `r` must be a valid `Int` `GcRef`.
1126#[unsafe(no_mangle)]
1127pub unsafe extern "C" fn praxis_int_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1128    abi_guard!("praxis_int_load", _ctx, {
1129        // SAFETY: caller guarantees `r` is an Int.
1130        unsafe { int_payload(r) }
1131    })
1132}
1133
1134/// Read a `Bool` payload as 0/1 (§10.3 transient scalar).
1135///
1136/// # Safety
1137/// `r` must be a valid `Bool` `GcRef`.
1138#[unsafe(no_mangle)]
1139pub unsafe extern "C" fn praxis_bool_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1140    abi_guard!("praxis_bool_load", _ctx, {
1141        // Read the byte, then decide — never `*r.payload::<bool>()`. A Rust
1142        // `bool` whose byte is not 0 or 1 is an *invalid value*, and
1143        // materializing one is undefined behaviour whatever the read's bounds
1144        // are; `BoolPayload` is a `u8` precisely so the runtime never has to.
1145        // SAFETY: `read_scalar` bounds the read against `r`'s own descriptor.
1146        let byte = unsafe { read_scalar(r, scalars::BOOL_PAYLOAD) }.unwrap_or_else(|| {
1147            scalar_type_mismatch("praxis_bool_load", "Bool", r.descriptor().name)
1148        });
1149        i64::from(byte != 0)
1150    })
1151}
1152
1153/// Read a `Char` payload as its `u32` code point widened to `i64` (§4.3).
1154///
1155/// # Safety
1156/// `r` must be a valid `Char` `GcRef`.
1157#[unsafe(no_mangle)]
1158pub unsafe extern "C" fn praxis_char_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1159    abi_guard!("praxis_char_load", _ctx, {
1160        // SAFETY: `read_scalar` bounds the read against `r`'s own descriptor.
1161        let code = unsafe { read_scalar(r, scalars::CHAR_PAYLOAD) }.unwrap_or_else(|| {
1162            scalar_type_mismatch("praxis_char_load", "Char", r.descriptor().name)
1163        });
1164        i64::from(code)
1165    })
1166}
1167
1168/// Allocate a boxed `Float` from an `i64` carrying the IEEE-754 binary64 bit
1169/// pattern (§4.3, §4.12). The uniform scalar ABI carries every payload as
1170/// `i64`; a float is transported as `f64::to_bits()` and reassembled here.
1171///
1172/// # Safety
1173/// `ctx` must point at a live, wired `RuntimeContext`.
1174#[unsafe(no_mangle)]
1175pub unsafe extern "C" fn praxis_alloc_float(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1176    abi_guard!("praxis_alloc_float", ctx, {
1177        let f = f64::from_bits(value as u64);
1178        // SAFETY: caller upholds ctx/heap validity; all f64 values are valid Floats.
1179        unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, f) }
1180    })
1181}
1182
1183/// Read a `Float` payload as its IEEE-754 bit pattern widened to `i64`
1184/// (§10.3 transient scalar). Generated code keeps floats in the uniform `i64`
1185/// scalar channel; the bit pattern is reassembled into an `f64` only at the
1186/// point of an arithmetic/comparison instruction.
1187///
1188/// # Safety
1189/// `r` must be a valid `Float` `GcRef`.
1190#[unsafe(no_mangle)]
1191pub unsafe extern "C" fn praxis_float_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1192    abi_guard!("praxis_float_load", _ctx, {
1193        // Through `float_payload`, which goes through `read_scalar`: the read
1194        // proves its own width rather than taking the caller's word for it.
1195        //
1196        // It matters more since ADR-102: generated code reads a `Float`
1197        // payload inline behind a descriptor check, and this wrapper is the
1198        // cold path that check branches to. If it read unchecked, the two
1199        // would disagree about what a wrong descriptor means — the inline
1200        // path would refuse and the fallback would read anyway.
1201        //
1202        // SAFETY: caller guarantees `r` is a valid `GcRef`; `float_payload`
1203        // proves it is a `Float` before reading.
1204        unsafe { float_payload(r) }.to_bits() as i64
1205    })
1206}
1207
1208// ---------------------------------------------------------------------------
1209// Float conversion & methods (§4.12). Float arithmetic never faults (IEEE-754
1210// produces inf/nan); only the narrowing `to_int` conversion does.
1211// ---------------------------------------------------------------------------
1212
1213/// Read a `Float` payload as an `f64` (private helper).
1214///
1215/// # Safety
1216/// `r` must be a valid `Float` `GcRef`.
1217unsafe fn float_payload(r: GcRef) -> f64 {
1218    // SAFETY: `read_scalar` proves `r`'s descriptor is `FLOAT` before reading.
1219    unsafe { read_scalar(r, scalars::FLOAT_PAYLOAD) }
1220        .unwrap_or_else(|| scalar_type_mismatch("float_payload", "Float", r.descriptor().name))
1221}
1222
1223/// Widen an `Int` to a `Float` (§4.12). Never faults — every `i64` is exactly
1224/// representable as an `f64`? No: integers above 2^53 lose precision, but the
1225/// conversion is still total and well-defined (rounds to nearest). This is the
1226/// explicit widening method `Int.to_float()`.
1227///
1228/// # Safety
1229/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1230#[unsafe(no_mangle)]
1231pub unsafe extern "C" fn praxis_int_to_float(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1232    abi_guard!("praxis_int_to_float", ctx, {
1233        let i = unsafe { int_payload(r) };
1234        // SAFETY: ctx/heap valid; every widened int is a valid Float payload.
1235        unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, i as f64) }
1236    })
1237}
1238
1239/// `Char.to_int()` — the Unicode scalar value, as an `Int` (ADR-086). Never
1240/// faults: every valid scalar fits an `i64`.
1241///
1242/// This reads through [`read_scalar`] with the `Char` handle rather than
1243/// `int_payload`, because a `Char` payload is **four** bytes and an `i64` read
1244/// would take eight of them.
1245///
1246/// # Safety
1247/// `ctx` must be live and wired; `r` must be a valid `Char` `GcRef`.
1248#[unsafe(no_mangle)]
1249pub unsafe extern "C" fn praxis_char_to_int(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1250    abi_guard!("praxis_char_to_int", ctx, {
1251        // SAFETY: caller guarantees `r` is a valid `GcRef`; `read_scalar` proves
1252        // the descriptor is `CHAR` before reading its four bytes.
1253        let code = unsafe { read_scalar(r, scalars::CHAR_PAYLOAD) }.unwrap_or_else(|| {
1254            scalar_type_mismatch("praxis_char_to_int", "Char", r.descriptor().name)
1255        });
1256        // SAFETY: ctx/heap valid; every scalar value is a valid Int payload.
1257        unsafe { int_ref(ctx, i64::from(code)) }
1258    })
1259}
1260
1261/// `Int.to_char()` — the `Char` with this Unicode scalar value (ADR-086).
1262/// Faults (`InvalidChar`) on a negative value, one above `0x10FFFF`, or one in
1263/// the surrogate range: those are not scalar values and have no `Char`.
1264///
1265/// It is `Char.to_int()`'s partial half exactly as `Float.to_int()` is
1266/// `Int.to_float()`'s — the narrowing direction is the one that can fail. The
1267/// check lives in [`checked_alloc_char`] and is not restated here.
1268///
1269/// # Safety
1270/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1271#[unsafe(no_mangle)]
1272pub unsafe extern "C" fn praxis_int_to_char(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1273    abi_guard!("praxis_int_to_char", ctx, {
1274        let value = unsafe { int_payload(r) };
1275        // SAFETY: caller upholds ctx/heap validity.
1276        unsafe { checked_alloc_char(ctx, value) }
1277    })
1278}
1279
1280/// Narrow a `Float` to an `Int` by truncating toward zero (§4.12). Faults
1281/// (`FloatToInt`) on NaN, ±infinity, or a finite value outside the signed
1282/// 64-bit range — these have no exact `Int` representation. On fault, sets
1283/// `pending_fault` and returns the Unit sentinel (no panic crosses the ABI).
1284///
1285/// # Safety
1286/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1287#[unsafe(no_mangle)]
1288pub unsafe extern "C" fn praxis_float_to_int(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1289    abi_guard!("praxis_float_to_int", ctx, {
1290        let f = unsafe { float_payload(r) };
1291        // NaN, infinities, and out-of-range finite values are not exactly
1292        // representable as i64. Rust's `as i64` saturates (inf→i64::MAX,
1293        // -inf→i64::MIN, nan→0), which would silently produce a plausible-but-wrong
1294        // value; per §4.12 these cases fault instead.
1295        if f.is_nan() || f.is_infinite() || f < i64::MIN as f64 || f >= i64::MAX as f64 {
1296            unsafe { set_fault(ctx, RaisedFault::FLOAT_TO_INT) };
1297            return unsafe { unit_sentinel(ctx) };
1298        }
1299        // The range check above bounds f to (-2^63, 2^63); truncation toward zero is
1300        // then exact for every representable integer and inexact-but-safe for the
1301        // fractional part (which is discarded).
1302        // SAFETY: ctx/heap valid; the value is in i64 range.
1303        unsafe { int_ref(ctx, f as i64) }
1304    })
1305}
1306
1307/// Re-box a `Float` after a pure transform (no fault possible). Used by
1308/// `abs`/`sqrt`/`floor`/`ceil`/`round`/`sign`.
1309unsafe fn rebox_float(ctx: *mut RuntimeContext, out: f64) -> GcRef {
1310    // SAFETY: ctx/heap valid; every f64 is a valid Float payload.
1311    unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, out) }
1312}
1313
1314/// `Float.abs()` — absolute value (§4.12).
1315///
1316/// # Safety
1317/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1318#[unsafe(no_mangle)]
1319pub unsafe extern "C" fn praxis_float_abs(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1320    abi_guard!("praxis_float_abs", ctx, {
1321        let f = unsafe { float_payload(r) };
1322        unsafe { rebox_float(ctx, f.abs()) }
1323    })
1324}
1325
1326/// `Float.sqrt()` — square root (§4.12). Negative inputs yield NaN (IEEE-754);
1327/// this never faults.
1328///
1329/// # Safety
1330/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1331#[unsafe(no_mangle)]
1332pub unsafe extern "C" fn praxis_float_sqrt(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1333    abi_guard!("praxis_float_sqrt", ctx, {
1334        let f = unsafe { float_payload(r) };
1335        unsafe { rebox_float(ctx, f.sqrt()) }
1336    })
1337}
1338
1339/// `Float.floor()` — round toward negative infinity (§4.12).
1340///
1341/// # Safety
1342/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1343#[unsafe(no_mangle)]
1344pub unsafe extern "C" fn praxis_float_floor(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1345    abi_guard!("praxis_float_floor", ctx, {
1346        let f = unsafe { float_payload(r) };
1347        unsafe { rebox_float(ctx, f.floor()) }
1348    })
1349}
1350
1351/// `Float.ceil()` — round toward positive infinity (§4.12).
1352///
1353/// # Safety
1354/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1355#[unsafe(no_mangle)]
1356pub unsafe extern "C" fn praxis_float_ceil(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1357    abi_guard!("praxis_float_ceil", ctx, {
1358        let f = unsafe { float_payload(r) };
1359        unsafe { rebox_float(ctx, f.ceil()) }
1360    })
1361}
1362
1363/// `Float.round()` — round half away from zero (§4.12, matches Rust's `f64::round`).
1364///
1365/// # Safety
1366/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1367#[unsafe(no_mangle)]
1368pub unsafe extern "C" fn praxis_float_round(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1369    abi_guard!("praxis_float_round", ctx, {
1370        let f = unsafe { float_payload(r) };
1371        unsafe { rebox_float(ctx, f.round()) }
1372    })
1373}
1374
1375/// `Float.sign()` — sign as -1.0 / 0.0 / 1.0 (§4.12). NaN yields NaN.
1376///
1377/// Not `f64::signum`: that returns `1.0` for `+0.0` and `-1.0` for `-0.0`,
1378/// because it reports the IEEE *sign bit*, not the sign of the value. Zero has
1379/// no sign in the sense `sign()` documents, so both zeros yield `0.0`.
1380///
1381/// # Safety
1382/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1383#[unsafe(no_mangle)]
1384pub unsafe extern "C" fn praxis_float_sign(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1385    abi_guard!("praxis_float_sign", ctx, {
1386        let f = unsafe { float_payload(r) };
1387        let sign = if f.is_nan() || f == 0.0 {
1388            // `f == 0.0` is true for both `+0.0` and `-0.0`; NaN falls through as
1389            // itself, which is what §4.12 specifies.
1390            f
1391        } else if f > 0.0 {
1392            1.0
1393        } else {
1394            -1.0
1395        };
1396        unsafe { rebox_float(ctx, sign) }
1397    })
1398}
1399
1400/// `Float.is_nan()` — true iff NaN (§4.12).
1401///
1402/// # Safety
1403/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1404#[unsafe(no_mangle)]
1405pub unsafe extern "C" fn praxis_float_is_nan(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1406    abi_guard!("praxis_float_is_nan", ctx, {
1407        let result = unsafe { float_payload(r) }.is_nan();
1408        // SAFETY: ctx valid; Bool immortal path.
1409        unsafe { bool_ref(ctx, result) }
1410    })
1411}
1412
1413/// `Float.is_infinite()` — true iff ±infinity (§4.12).
1414///
1415/// # Safety
1416/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1417#[unsafe(no_mangle)]
1418pub unsafe extern "C" fn praxis_float_is_infinite(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1419    abi_guard!("praxis_float_is_infinite", ctx, {
1420        let result = unsafe { float_payload(r) }.is_infinite();
1421        // SAFETY: ctx valid; Bool immortal path.
1422        unsafe { bool_ref(ctx, result) }
1423    })
1424}
1425
1426/// `Float.min(other)` — the smaller of two floats (§4.12). Per IEEE-754 /
1427/// Rust's `f64::min`: if either operand is NaN, returns the other (NaN only
1428/// propagates when both are NaN). `-0.0` is less than `+0.0`.
1429///
1430/// # Safety
1431/// `ctx` must be live and wired; both operands must be valid `Float` `GcRef`s.
1432#[unsafe(no_mangle)]
1433pub unsafe extern "C" fn praxis_float_min(
1434    ctx: *mut RuntimeContext,
1435    lhs: GcRef,
1436    rhs: GcRef,
1437) -> GcRef {
1438    abi_guard!("praxis_float_min", ctx, {
1439        let a = unsafe { float_payload(lhs) };
1440        let b = unsafe { float_payload(rhs) };
1441        unsafe { rebox_float(ctx, a.min(b)) }
1442    })
1443}
1444
1445/// `Float.max(other)` — the larger of two floats (§4.12). See `praxis_float_min`
1446/// for NaN handling.
1447///
1448/// # Safety
1449/// `ctx` must be live and wired; both operands must be valid `Float` `GcRef`s.
1450#[unsafe(no_mangle)]
1451pub unsafe extern "C" fn praxis_float_max(
1452    ctx: *mut RuntimeContext,
1453    lhs: GcRef,
1454    rhs: GcRef,
1455) -> GcRef {
1456    abi_guard!("praxis_float_max", ctx, {
1457        let a = unsafe { float_payload(lhs) };
1458        let b = unsafe { float_payload(rhs) };
1459        unsafe { rebox_float(ctx, a.max(b)) }
1460    })
1461}
1462
1463/// `Float.to_text()` — the same text `out()` writes, which is the shortest form
1464/// that reads back as the same Praxis `Float` (§4.12, ADR-083).
1465///
1466/// It goes through `scalars::write_float` rather than restating the rule,
1467/// because `to_text()` and `out()` disagreeing is a defect in itself: a program
1468/// that prints a value and a program that builds a string from it must produce
1469/// the same characters.
1470///
1471/// # Safety
1472/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1473#[unsafe(no_mangle)]
1474pub unsafe extern "C" fn praxis_float_to_text(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1475    abi_guard!("praxis_float_to_text", ctx, {
1476        let f = unsafe { float_payload(r) };
1477        let mut s = String::new();
1478        scalars::write_float(&mut s, f);
1479        // SAFETY: `s` is valid UTF-8 for the duration of the call; ctx/heap valid.
1480        unsafe { text_ref(ctx, s) }
1481    })
1482}
1483
1484/// `Int.to_text()` — the same digits `out()` writes (ADR-143).
1485///
1486/// It goes through `scalars::write_int` rather than restating the rendering,
1487/// because `to_text()` and `out()` disagreeing is a defect in itself: a program
1488/// that prints a value and a program that builds a string from it must produce
1489/// the same characters. That is the guarantee, and the shared writer is what
1490/// makes it structural rather than a thing a test happens to check.
1491///
1492/// Never faults: every `i64` renders, `i64::MIN` included.
1493///
1494/// # Safety
1495/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1496#[unsafe(no_mangle)]
1497pub unsafe extern "C" fn praxis_int_to_text(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1498    abi_guard!("praxis_int_to_text", ctx, {
1499        let v = unsafe { int_payload(r) };
1500        let mut s = String::new();
1501        scalars::write_int(&mut s, v);
1502        // SAFETY: `s` is valid UTF-8; ctx/heap valid.
1503        unsafe { text_ref(ctx, s) }
1504    })
1505}
1506
1507/// `Char.to_text()` — the one-character `Text` holding this scalar, which is the
1508/// same character `out()` writes (ADR-143).
1509///
1510/// Shares `scalars::write_char` with the descriptor's `format` callback for
1511/// [`praxis_int_to_text`]'s reason. Never faults: a `CharPayload` is a validated
1512/// Unicode scalar value by construction (ADR-086).
1513///
1514/// Reads through [`read_scalar`] with the `Char` handle rather than
1515/// `int_payload`, because a `Char` payload is **four** bytes and an `i64` read
1516/// would take eight of them — the same care [`praxis_char_to_int`] takes.
1517///
1518/// # Safety
1519/// `ctx` must be live and wired; `r` must be a valid `Char` `GcRef`.
1520#[unsafe(no_mangle)]
1521pub unsafe extern "C" fn praxis_char_to_text(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1522    abi_guard!("praxis_char_to_text", ctx, {
1523        // SAFETY: caller guarantees `r` is a valid `GcRef`; `read_scalar` proves
1524        // the descriptor is `CHAR` before reading its four bytes.
1525        let code = unsafe { read_scalar(r, scalars::CHAR_PAYLOAD) }.unwrap_or_else(|| {
1526            scalar_type_mismatch("praxis_char_to_text", "Char", r.descriptor().name)
1527        });
1528        let mut s = String::new();
1529        scalars::write_char(&mut s, code);
1530        // SAFETY: `s` is valid UTF-8; ctx/heap valid.
1531        unsafe { text_ref(ctx, s) }
1532    })
1533}
1534
1535/// `pi()` — the constant π as a `Float` (§4.12 prelude free function).
1536///
1537/// # Safety
1538/// `ctx` must be live and wired.
1539#[unsafe(no_mangle)]
1540pub unsafe extern "C" fn praxis_float_pi(ctx: *mut RuntimeContext) -> GcRef {
1541    abi_guard!("praxis_float_pi", ctx, {
1542        // SAFETY: ctx/heap valid.
1543        unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, core::f64::consts::PI) }
1544    })
1545}
1546
1547/// `e()` — Euler's number as a `Float` (§4.12 prelude free function).
1548///
1549/// # Safety
1550/// `ctx` must be live and wired.
1551#[unsafe(no_mangle)]
1552pub unsafe extern "C" fn praxis_float_e(ctx: *mut RuntimeContext) -> GcRef {
1553    abi_guard!("praxis_float_e", ctx, {
1554        // SAFETY: ctx/heap valid.
1555        unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, core::f64::consts::E) }
1556    })
1557}
1558
1559// ---------------------------------------------------------------------------
1560// Checked arithmetic (§4.12). All fault rather than panic (§10.4).
1561// ---------------------------------------------------------------------------
1562
1563macro_rules! checked_int_binop {
1564    ($name:ident, $op:tt, $fault:expr_2021) => {
1565        #[doc = concat!("Checked `Int ", stringify!($op), "` (§4.12). On fault sets `pending_fault` and returns Unit.")]
1566        ///
1567        /// # Safety
1568        /// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1569        #[unsafe(no_mangle)]
1570        pub unsafe extern "C" fn $name(
1571            ctx: *mut RuntimeContext,
1572            lhs: GcRef,
1573            rhs: GcRef,
1574        ) -> GcRef {
1575            abi_guard!(stringify!($name), ctx, {
1576                let a = unsafe { int_payload(lhs) };
1577                let b = unsafe { int_payload(rhs) };
1578                match a.$op(b) {
1579                    Some(result) => unsafe { int_ref(ctx, result) },
1580                    None => {
1581                        unsafe { set_fault(ctx, $fault) };
1582                        unsafe { unit_sentinel(ctx) }
1583                    }
1584                }
1585            })
1586        }
1587    };
1588}
1589
1590checked_int_binop!(praxis_int_add, checked_add, RaisedFault::INT_OVERFLOW);
1591checked_int_binop!(praxis_int_sub, checked_sub, RaisedFault::INT_OVERFLOW);
1592checked_int_binop!(praxis_int_mul, checked_mul, RaisedFault::INT_OVERFLOW);
1593
1594/// Checked `Int` division (§4.12). Faults on division by zero, and on overflow
1595/// (`Int::MIN / -1`, the one signed-division case that overflows §4.12).
1596///
1597/// # Safety
1598/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1599#[unsafe(no_mangle)]
1600pub unsafe extern "C" fn praxis_int_div(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
1601    abi_guard!("praxis_int_div", ctx, {
1602        let a = unsafe { int_payload(lhs) };
1603        let b = unsafe { int_payload(rhs) };
1604        if b == 0 {
1605            unsafe { set_fault(ctx, RaisedFault::DIV_BY_ZERO) };
1606            return unsafe { unit_sentinel(ctx) };
1607        }
1608        // `i64::MIN / -1` is the sole overflowing signed division: the mathematical
1609        // result (+2^63) is not representable, and the raw `/` panics on overflow in
1610        // debug builds (violating the no-panic-across-the-ABI rule, §10.4). Treat it
1611        // as checked-arithmetic overflow per §4.12.
1612        if a == i64::MIN && b == -1 {
1613            unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1614            return unsafe { unit_sentinel(ctx) };
1615        }
1616        // Division truncates toward zero (Rust's `i64::div_euclid` rounds differently;
1617        // Praxis follows C/Rust integer division semantics toward zero).
1618        unsafe { int_ref(ctx, a / b) }
1619    })
1620}
1621
1622/// Checked `Int` remainder (§4.12). Faults on division by zero, and on overflow
1623/// (`Int::MIN % -1`, whose result is not representable under the §4.12 rule).
1624///
1625/// # Safety
1626/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1627#[unsafe(no_mangle)]
1628pub unsafe extern "C" fn praxis_int_rem(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
1629    abi_guard!("praxis_int_rem", ctx, {
1630        let a = unsafe { int_payload(lhs) };
1631        let b = unsafe { int_payload(rhs) };
1632        if b == 0 {
1633            unsafe { set_fault(ctx, RaisedFault::DIV_BY_ZERO) };
1634            return unsafe { unit_sentinel(ctx) };
1635        }
1636        // `i64::MIN % -1`: the remainder is 0 mathematically, but the raw `%` traps
1637        // on this exact case in debug builds because the corresponding quotient
1638        // overflows. Guard it for the same no-panic reason as `praxis_int_div`.
1639        if a == i64::MIN && b == -1 {
1640            unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1641            return unsafe { unit_sentinel(ctx) };
1642        }
1643        unsafe { int_ref(ctx, a % b) }
1644    })
1645}
1646
1647/// Negate an `Int` (§4.12). Faults on overflow (`Int::MIN`).
1648///
1649/// # Safety
1650/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1651#[unsafe(no_mangle)]
1652pub unsafe extern "C" fn praxis_int_neg(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1653    abi_guard!("praxis_int_neg", ctx, {
1654        let a = unsafe { int_payload(r) };
1655        match a.checked_neg() {
1656            Some(result) => unsafe { int_ref(ctx, result) },
1657            None => {
1658                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1659                unsafe { unit_sentinel(ctx) }
1660            }
1661        }
1662    })
1663}
1664
1665// ---------------------------------------------------------------------------
1666// §4.12's explicit overflow alternatives: three modes —
1667// `wrapping_`, `saturating_`, `checked_` — over `add`, `sub` and `mul`.
1668//
1669// §4.12 states the family and its two closures (no `_div`/`_rem`, no
1670// `_neg`/`_abs`) and is the only place that rule is written; the catalog test
1671// `the_overflow_alternative_family_is_three_modes_over_three_operators` is what
1672// enforces it. Do not restate it here.
1673//
1674// **None of the nine can fault, and that is the whole point of them** — their
1675// manifest rows are `Allocates`, so ADR-088's verifier rule means no
1676// `CheckFault` follows the call. They allocate, like every other wrapper that
1677// answers a fresh number.
1678// ---------------------------------------------------------------------------
1679
1680/// `a.wrapping_add(b)` (§4.12): two's-complement wraparound instead of a fault.
1681///
1682/// # Safety
1683/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1684#[unsafe(no_mangle)]
1685pub unsafe extern "C" fn praxis_int_wrapping_add(
1686    ctx: *mut RuntimeContext,
1687    a: GcRef,
1688    b: GcRef,
1689) -> GcRef {
1690    abi_guard!("praxis_int_wrapping_add", ctx, {
1691        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1692        unsafe { int_ref(ctx, x.wrapping_add(y)) }
1693    })
1694}
1695
1696/// `a.saturating_add(b)` (§4.12): clamp to `Int`'s ends instead of faulting.
1697///
1698/// # Safety
1699/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1700#[unsafe(no_mangle)]
1701pub unsafe extern "C" fn praxis_int_saturating_add(
1702    ctx: *mut RuntimeContext,
1703    a: GcRef,
1704    b: GcRef,
1705) -> GcRef {
1706    abi_guard!("praxis_int_saturating_add", ctx, {
1707        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1708        unsafe { int_ref(ctx, x.saturating_add(y)) }
1709    })
1710}
1711
1712/// `a.checked_add(b)` (§4.12): `Option[Int]` — `None` where the checked `+`
1713/// would fault.
1714///
1715/// It answers a real `Option` (ADR-076): the absence is the *answer* here, not
1716/// an error channel, which is exactly §4.7's distinction.
1717///
1718/// # Safety
1719/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1720#[unsafe(no_mangle)]
1721pub unsafe extern "C" fn praxis_int_checked_add(
1722    ctx: *mut RuntimeContext,
1723    a: GcRef,
1724    b: GcRef,
1725) -> GcRef {
1726    abi_guard!("praxis_int_checked_add", ctx, {
1727        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1728        match x.checked_add(y) {
1729            Some(sum) => unsafe {
1730                let scope = NativeScope::new(ctx);
1731                let boxed = int_ref(ctx, sum);
1732                let rooted = scope.root(boxed);
1733                option_some(ctx, rooted.get())
1734            },
1735            None => unsafe { option_none(ctx) },
1736        }
1737    })
1738}
1739
1740/// `a.wrapping_sub(b)` (§4.12): two's-complement wraparound instead of a fault.
1741///
1742/// # Safety
1743/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1744#[unsafe(no_mangle)]
1745pub unsafe extern "C" fn praxis_int_wrapping_sub(
1746    ctx: *mut RuntimeContext,
1747    a: GcRef,
1748    b: GcRef,
1749) -> GcRef {
1750    abi_guard!("praxis_int_wrapping_sub", ctx, {
1751        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1752        unsafe { int_ref(ctx, x.wrapping_sub(y)) }
1753    })
1754}
1755
1756/// `a.saturating_sub(b)` (§4.12): clamp to `Int`'s ends instead of faulting.
1757///
1758/// # Safety
1759/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1760#[unsafe(no_mangle)]
1761pub unsafe extern "C" fn praxis_int_saturating_sub(
1762    ctx: *mut RuntimeContext,
1763    a: GcRef,
1764    b: GcRef,
1765) -> GcRef {
1766    abi_guard!("praxis_int_saturating_sub", ctx, {
1767        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1768        unsafe { int_ref(ctx, x.saturating_sub(y)) }
1769    })
1770}
1771
1772/// `a.checked_sub(b)` (§4.12): `Option[Int]` — `None` where the checked `-`
1773/// would fault.
1774///
1775/// # Safety
1776/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1777#[unsafe(no_mangle)]
1778pub unsafe extern "C" fn praxis_int_checked_sub(
1779    ctx: *mut RuntimeContext,
1780    a: GcRef,
1781    b: GcRef,
1782) -> GcRef {
1783    abi_guard!("praxis_int_checked_sub", ctx, {
1784        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1785        match x.checked_sub(y) {
1786            Some(difference) => unsafe {
1787                let scope = NativeScope::new(ctx);
1788                let boxed = int_ref(ctx, difference);
1789                let rooted = scope.root(boxed);
1790                option_some(ctx, rooted.get())
1791            },
1792            None => unsafe { option_none(ctx) },
1793        }
1794    })
1795}
1796
1797/// `a.wrapping_mul(b)` (§4.12): two's-complement wraparound instead of a fault.
1798///
1799/// This is the one of the nine a program could not write for itself: with every
1800/// arithmetic operator checked and no bitwise operators in the language, there
1801/// is no in-language spelling of modular multiplication (§4.12).
1802///
1803/// # Safety
1804/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1805#[unsafe(no_mangle)]
1806pub unsafe extern "C" fn praxis_int_wrapping_mul(
1807    ctx: *mut RuntimeContext,
1808    a: GcRef,
1809    b: GcRef,
1810) -> GcRef {
1811    abi_guard!("praxis_int_wrapping_mul", ctx, {
1812        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1813        unsafe { int_ref(ctx, x.wrapping_mul(y)) }
1814    })
1815}
1816
1817/// `a.saturating_mul(b)` (§4.12): clamp to `Int`'s ends instead of faulting.
1818///
1819/// # Safety
1820/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1821#[unsafe(no_mangle)]
1822pub unsafe extern "C" fn praxis_int_saturating_mul(
1823    ctx: *mut RuntimeContext,
1824    a: GcRef,
1825    b: GcRef,
1826) -> GcRef {
1827    abi_guard!("praxis_int_saturating_mul", ctx, {
1828        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1829        unsafe { int_ref(ctx, x.saturating_mul(y)) }
1830    })
1831}
1832
1833/// `a.checked_mul(b)` (§4.12): `Option[Int]` — `None` where the checked `*`
1834/// would fault.
1835///
1836/// # Safety
1837/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1838#[unsafe(no_mangle)]
1839pub unsafe extern "C" fn praxis_int_checked_mul(
1840    ctx: *mut RuntimeContext,
1841    a: GcRef,
1842    b: GcRef,
1843) -> GcRef {
1844    abi_guard!("praxis_int_checked_mul", ctx, {
1845        let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1846        match x.checked_mul(y) {
1847            Some(product) => unsafe {
1848                let scope = NativeScope::new(ctx);
1849                let boxed = int_ref(ctx, product);
1850                let rooted = scope.root(boxed);
1851                option_some(ctx, rooted.get())
1852            },
1853            None => unsafe { option_none(ctx) },
1854        }
1855    })
1856}
1857
1858// ---------------------------------------------------------------------------
1859// The §16.1 numeric prelude helpers: `abs`, `sign`, `min`, `max`, `clamp`,
1860// `gcd`, `lcm`.
1861//
1862// All seven are monomorphic on `Int` (ADR-058), so every payload read here is
1863// an `Int` payload and no descriptor check is needed. `min`/`max`/`clamp` hand
1864// back one of the references they were given rather than allocating a copy:
1865// an `Int` object is immutable, so sharing it is what "the smaller of the two"
1866// means. The four that compute a *new* number allocate one, and the three that
1867// can leave the `Int` range fault rather than wrapping — `abs(Int::MIN)` has no
1868// positive counterpart, and `gcd`/`lcm` reach the same edge through it.
1869// ---------------------------------------------------------------------------
1870
1871/// `abs(n)` (§16.1). Faults on overflow: `Int::MIN` has no positive
1872/// counterpart, exactly as `praxis_int_neg` faults on it.
1873///
1874/// # Safety
1875/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1876#[unsafe(no_mangle)]
1877pub unsafe extern "C" fn praxis_int_abs(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1878    abi_guard!("praxis_int_abs", ctx, {
1879        let a = unsafe { int_payload(r) };
1880        match a.checked_abs() {
1881            Some(result) => unsafe { int_ref(ctx, result) },
1882            None => {
1883                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1884                unsafe { unit_sentinel(ctx) }
1885            }
1886        }
1887    })
1888}
1889
1890/// `sign(n)` (§16.1): `-1`, `0` or `1`. Total — every `Int`, `Int::MIN`
1891/// included, has a sign in range.
1892///
1893/// # Safety
1894/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1895#[unsafe(no_mangle)]
1896pub unsafe extern "C" fn praxis_int_sign(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1897    abi_guard!("praxis_int_sign", ctx, {
1898        let a = unsafe { int_payload(r) };
1899        unsafe { int_ref(ctx, a.signum()) }
1900    })
1901}
1902
1903/// `min(a, b)` (§16.1): the smaller operand, returned as **the reference that
1904/// was passed in**. Allocates nothing.
1905///
1906/// # Safety
1907/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1908#[unsafe(no_mangle)]
1909pub unsafe extern "C" fn praxis_int_min(
1910    _ctx: *mut RuntimeContext,
1911    lhs: GcRef,
1912    rhs: GcRef,
1913) -> GcRef {
1914    abi_guard!("praxis_int_min", _ctx, {
1915        let a = unsafe { int_payload(lhs) };
1916        let b = unsafe { int_payload(rhs) };
1917        if b < a { rhs } else { lhs }
1918    })
1919}
1920
1921/// `max(a, b)` (§16.1): the larger operand, returned as **the reference that
1922/// was passed in**. Allocates nothing.
1923///
1924/// # Safety
1925/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1926#[unsafe(no_mangle)]
1927pub unsafe extern "C" fn praxis_int_max(
1928    _ctx: *mut RuntimeContext,
1929    lhs: GcRef,
1930    rhs: GcRef,
1931) -> GcRef {
1932    abi_guard!("praxis_int_max", _ctx, {
1933        let a = unsafe { int_payload(lhs) };
1934        let b = unsafe { int_payload(rhs) };
1935        if b > a { rhs } else { lhs }
1936    })
1937}
1938
1939/// `clamp(value, low, high)` (§16.1): `value` confined to the inclusive range
1940/// `low..=high`, returned as one of the three references passed in.
1941///
1942/// **Faults when `low > high`.** The range is empty, so there is no value to
1943/// return and no answer that is not a guess — clamping to an empty range is a
1944/// mistake in the program, not in the data, and a mistake is reported rather
1945/// than answered with an invented number. (Rust's `Ord::clamp` panics on the
1946/// same input; a panic across `extern "C"` is what §10.4 forbids, so it is a
1947/// fault.) The kind is `EmptyRange` (ADR-058).
1948///
1949/// # Safety
1950/// `ctx` must be live and wired; all three operands must be valid `Int`
1951/// `GcRef`s.
1952#[unsafe(no_mangle)]
1953pub unsafe extern "C" fn praxis_int_clamp(
1954    ctx: *mut RuntimeContext,
1955    value: GcRef,
1956    low: GcRef,
1957    high: GcRef,
1958) -> GcRef {
1959    abi_guard!("praxis_int_clamp", ctx, {
1960        let v = unsafe { int_payload(value) };
1961        let lo = unsafe { int_payload(low) };
1962        let hi = unsafe { int_payload(high) };
1963        if lo > hi {
1964            unsafe { set_fault(ctx, RaisedFault::EMPTY_RANGE) };
1965            return unsafe { unit_sentinel(ctx) };
1966        }
1967        if v < lo {
1968            low
1969        } else if v > hi {
1970            high
1971        } else {
1972            value
1973        }
1974    })
1975}
1976
1977/// The non-negative greatest common divisor of two `i64`s, computed by
1978/// Euclid's algorithm **in `i128`** so that `Int::MIN`'s absolute value needs no
1979/// special case. Returns `None` only when the mathematical result is outside the
1980/// `Int` range, which happens for exactly one input pair:
1981/// `gcd(Int::MIN, Int::MIN)` is `2^63`.
1982///
1983/// `gcd(0, 0)` is `0` — the conventional answer, and the identity `gcd(n, 0) ==
1984/// abs(n)` extended to `n == 0`.
1985fn checked_gcd(a: i64, b: i64) -> Option<i64> {
1986    let mut x = (a as i128).abs();
1987    let mut y = (b as i128).abs();
1988    while y != 0 {
1989        let t = x % y;
1990        x = y;
1991        y = t;
1992    }
1993    i64::try_from(x).ok()
1994}
1995
1996/// `gcd(a, b)` (§16.1): the non-negative greatest common divisor. Faults on the
1997/// one pair whose result is out of range (`gcd(Int::MIN, Int::MIN)`).
1998///
1999/// # Safety
2000/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
2001#[unsafe(no_mangle)]
2002pub unsafe extern "C" fn praxis_int_gcd(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
2003    abi_guard!("praxis_int_gcd", ctx, {
2004        let a = unsafe { int_payload(lhs) };
2005        let b = unsafe { int_payload(rhs) };
2006        match checked_gcd(a, b) {
2007            Some(result) => unsafe { int_ref(ctx, result) },
2008            None => {
2009                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
2010                unsafe { unit_sentinel(ctx) }
2011            }
2012        }
2013    })
2014}
2015
2016/// `lcm(a, b)` (§16.1): the non-negative least common multiple, `0` when either
2017/// operand is `0`. Faults when the result does not fit an `Int` — which it
2018/// often does not, since the product of two large operands overflows long
2019/// before their multiple does.
2020///
2021/// # Safety
2022/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
2023#[unsafe(no_mangle)]
2024pub unsafe extern "C" fn praxis_int_lcm(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
2025    abi_guard!("praxis_int_lcm", ctx, {
2026        let a = unsafe { int_payload(lhs) };
2027        let b = unsafe { int_payload(rhs) };
2028        // `lcm(n, 0)` is 0 for every n: 0 is a multiple of everything, and dividing
2029        // by the gcd below would divide by zero when both are 0.
2030        if a == 0 || b == 0 {
2031            return unsafe { int_ref(ctx, 0i64) };
2032        }
2033        // |a / gcd * b| in i128, which cannot overflow: both operands fit i64, so
2034        // the product fits i128 with room to spare. The range check is the only
2035        // thing that can refuse.
2036        let result = checked_gcd(a, b)
2037            .map(|g| ((a as i128) / (g as i128) * (b as i128)).abs())
2038            .and_then(|m| i64::try_from(m).ok());
2039        match result {
2040            Some(result) => unsafe { int_ref(ctx, result) },
2041            None => {
2042                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
2043                unsafe { unit_sentinel(ctx) }
2044            }
2045        }
2046    })
2047}
2048
2049// ---------------------------------------------------------------------------
2050// Comparisons (yield a Bool GcRef).
2051// ---------------------------------------------------------------------------
2052
2053macro_rules! int_cmp {
2054    ($name:ident, $op:tt) => {
2055        #[doc = concat!(" `Int ", stringify!($op), "` comparison; returns a Bool GcRef (§4.12).")]
2056        ///
2057        /// # Safety
2058        /// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
2059        #[unsafe(no_mangle)]
2060        pub unsafe extern "C" fn $name(
2061            ctx: *mut RuntimeContext,
2062            lhs: GcRef,
2063            rhs: GcRef,
2064        ) -> GcRef {
2065            abi_guard!(stringify!($name), ctx, {
2066                let a = unsafe { int_payload(lhs) };
2067                let b = unsafe { int_payload(rhs) };
2068                let result = a $op b;
2069                // SAFETY: ctx/heap valid; Bool immortal path.
2070                unsafe { bool_ref(ctx, result) }
2071            })
2072        }
2073    };
2074}
2075
2076int_cmp!(praxis_int_eq, ==);
2077int_cmp!(praxis_int_ne, !=);
2078int_cmp!(praxis_int_lt, <);
2079int_cmp!(praxis_int_gt, >);
2080int_cmp!(praxis_int_le, <=);
2081int_cmp!(praxis_int_ge, >=);
2082
2083// ---------------------------------------------------------------------------
2084// Fault check.
2085// ---------------------------------------------------------------------------
2086
2087/// Return 1 if a fault is pending on `ctx`, else 0 (§10.4).
2088///
2089/// **Generated code does not call this.** An `Inst::CheckFault` is a load of
2090/// `ctx.pending_fault`, a load of the kind at
2091/// [`Fault::KIND_OFFSET`](crate::Fault::KIND_OFFSET) and a `brif` (ADR-102) —
2092/// the same question, without the call, the `catch_unwind` region and the
2093/// `Result` discriminant, on a path that runs once per faultable instruction.
2094///
2095/// The wrapper stays: it is the named ABI entry point for a host asking the
2096/// question from Rust (the JIT test harness does), it keeps its manifest row and
2097/// its address-table arm so `RuntimeSymbol` stays a bijection onto real
2098/// addresses, and deleting it would churn ADR-080's source-reading test for no
2099/// gain. Its two null tests are the difference between it and the inline form,
2100/// and they are why *this* is what a host with a possibly-unwired context calls.
2101///
2102/// # Safety
2103/// `ctx` must point at a live `RuntimeContext` (a null/unwired context reports
2104/// no fault rather than panicking).
2105#[unsafe(no_mangle)]
2106pub unsafe extern "C" fn praxis_check_fault(ctx: *mut RuntimeContext) -> i64 {
2107    abi_guard!("praxis_check_fault", ctx, {
2108        if ctx.is_null() {
2109            return 0;
2110        }
2111        if let Some(fault) = unsafe { (*ctx).pending_fault.as_ref() } {
2112            return fault.is_pending().into();
2113        }
2114        0
2115    })
2116}
2117
2118/// Stop the program at a `:bp` marker and show the host its frame chain (§9.8).
2119///
2120/// `span_start`/`span_end` are the marker's own source span, passed as
2121/// immediates: this is a call with no operands from the program, and boxing a
2122/// span so it could ride a `GcRef` argument would put an allocation at the one
2123/// site whose cost has to stay a single call.
2124///
2125/// Everything that makes a stop *not* a fault lives in
2126/// [`crate::breakpoint::stop`]: the host handler is given a snapshot and no
2127/// context, so it cannot allocate, cannot collect and cannot raise. That is what
2128/// lets this be declared [`Effect::Pure`](praxis_stdlib::abi::Effect::Pure), and
2129/// therefore what lets generated code emit no root spill before it and no fault
2130/// check after.
2131///
2132/// A program with no handler installed — every JIT test, every embedder that
2133/// wants none — finds nothing to call and returns.
2134///
2135/// # Safety
2136/// `ctx` must point at a live, wired `RuntimeContext` whose claimed debug frames
2137/// satisfy `copy_stack`'s contract, which every generated prologue establishes.
2138#[unsafe(no_mangle)]
2139pub unsafe extern "C" fn praxis_breakpoint(
2140    ctx: *mut RuntimeContext,
2141    span_start: u32,
2142    span_end: u32,
2143) {
2144    abi_guard!("praxis_breakpoint", ctx, {
2145        if ctx.is_null() {
2146            return;
2147        }
2148        // SAFETY: `ctx` is non-null and the caller guarantees it is live and
2149        // wired; the debug frames are the ones its prologue chain claimed.
2150        unsafe { crate::breakpoint::stop(ctx, (span_start, span_end)) };
2151    })
2152}
2153
2154/// Raise a [`FaultKind::StackOverflow`] fault on `ctx` (§9.2, §17.4). Called by
2155/// the generated prologue guard when `ctx.stack_left` is less than this frame's
2156/// [`frame_cost`](crate::frame_cost), so the host survives deep recursion
2157/// instead of overflowing the native stack. The prologue then unwinds to its
2158/// fault epilogue (pop frame + return Unit) — same path as any other fault.
2159///
2160/// # Safety
2161/// `ctx` must point at a live, wired `RuntimeContext`.
2162#[unsafe(no_mangle)]
2163pub unsafe extern "C" fn praxis_raise_stack_overflow(ctx: *mut RuntimeContext) {
2164    abi_guard!("praxis_raise_stack_overflow", ctx, {
2165        unsafe { set_fault(ctx, RaisedFault::STACK_OVERFLOW) };
2166    })
2167}
2168
2169/// Raise a [`FaultKind::EmptyCollection`] fault on `ctx` (§9.2).
2170///
2171/// `reduce`, `min_by` and `max_by` have no answer for an empty sequence: they
2172/// seed their accumulator from the first element, and there is no first
2173/// element. Handing back an unwritten accumulator slot would materialize
2174/// whatever the register held as a `GcRef` that is `NonNull` by type and
2175/// arbitrary in fact; this is the defined failure instead, and a fault is what
2176/// the other empty-collection accessors (`Deque.pop_front`, heap `pop`/`peek`)
2177/// already raise for the same reason.
2178///
2179/// Unconditional, unlike the two arithmetic raise wrappers: the emptiness test
2180/// is a branch generated code has to make anyway (the seen-flag gates the whole
2181/// sink), so there is no predicate worth passing. It returns the Unit sentinel
2182/// rather than nothing, so the MIR `Call` that emits it has an ordinary `Gc`
2183/// destination — a `Void` row would put the context pointer in a rootable slot.
2184///
2185/// # Safety
2186/// `ctx` must point at a live, wired `RuntimeContext`.
2187#[unsafe(no_mangle)]
2188pub unsafe extern "C" fn praxis_raise_empty_collection(ctx: *mut RuntimeContext) -> GcRef {
2189    abi_guard!("praxis_raise_empty_collection", ctx, {
2190        unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
2191        unsafe { unit_sentinel(ctx) }
2192    })
2193}
2194
2195/// Raise a [`FaultKind::IntOverflow`] fault on `ctx` iff `condition` is
2196/// non-zero (§4.12).
2197///
2198/// Generated code lowers `Int` arithmetic natively — `iadd`/`isub`/`imul` on
2199/// the raw scalar channel — and computes the overflow predicate inline. This is
2200/// how it reports one. It allocates nothing, so an arithmetic site is not a GC
2201/// safepoint and spills no roots.
2202///
2203/// **The call site branches; this is the cold path.** Calling unconditionally
2204/// and letting `condition` decide would keep arithmetic to a single basic
2205/// block, but a branch does not clobber registers and a call does, so it would
2206/// force a spill and reload of every live value around an arithmetic op that
2207/// never faults. The site is a `brif` to a cold block (ADR-102);
2208/// `raise_on_cold_path` in the backend carries the full argument.
2209///
2210/// The cold block passes a constant `1` — honest, since it is reached only when
2211/// the predicate held, and it keeps the test below a true statement rather than
2212/// dead code.
2213///
2214/// # Safety
2215/// `ctx` must point at a live, wired `RuntimeContext`.
2216#[unsafe(no_mangle)]
2217pub unsafe extern "C" fn praxis_raise_int_overflow_if(ctx: *mut RuntimeContext, condition: i64) {
2218    abi_guard!("praxis_raise_int_overflow_if", ctx, {
2219        if condition != 0 {
2220            unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
2221        }
2222    })
2223}
2224
2225/// Raise a [`FaultKind::DivByZero`] fault on `ctx` iff `condition` is non-zero
2226/// (§4.12). The division counterpart of [`praxis_raise_int_overflow_if`].
2227///
2228/// # Safety
2229/// `ctx` must point at a live, wired `RuntimeContext`.
2230#[unsafe(no_mangle)]
2231pub unsafe extern "C" fn praxis_raise_div_by_zero_if(ctx: *mut RuntimeContext, condition: i64) {
2232    abi_guard!("praxis_raise_div_by_zero_if", ctx, {
2233        if condition != 0 {
2234            unsafe { set_fault(ctx, RaisedFault::DIV_BY_ZERO) };
2235        }
2236    })
2237}
2238
2239// ---------------------------------------------------------------------------
2240// Collection payload accessors (§11.1, §11.4).
2241//
2242// Every collection below reaches its payload through a named accessor —
2243// `vec_payload`, `map_payload_mut`, … — and the name is the descriptor
2244// assertion the wrapper is making, which is why the nine kinds keep nine
2245// (shared, mut) pairs rather than calling `payload::<P>()` inline. The two
2246// casts underneath live here once, and each named accessor is the one line
2247// that spells its payload type.
2248// ---------------------------------------------------------------------------
2249
2250/// Read a `P` payload out of a `GcRef` as a shared ref.
2251///
2252/// # Safety
2253/// `r` must be a valid `GcRef` whose descriptor's payload type is `P`.
2254unsafe fn payload_ref<P>(r: GcRef) -> &'static P {
2255    // SAFETY: caller guarantees `r`'s payload is a `P`; the non-moving GC
2256    // (ADR-011) keeps the payload address stable for the object's lifetime. The
2257    // `'static` is unbounded because the raw FFI boundary has no lifetime to
2258    // carry; the caller (a wrapper that holds `ctx`) ensures the object outlives
2259    // the use.
2260    unsafe { &*r.payload::<P>() }
2261}
2262
2263/// Read a `P` payload out of a rooted `GcRef` as a mutable ref — the accessor
2264/// the wrappers that mutate in place go through (§11.1).
2265///
2266/// # Safety
2267/// `r` must be a valid `GcRef` whose descriptor's payload type is `P`, rooted
2268/// for `'s`.
2269unsafe fn payload_mut<'s, P>(r: Rooted<'s>) -> &'s mut P {
2270    // SAFETY: caller guarantees `r`'s payload is a `P`; the non-moving GC
2271    // (ADR-011) keeps the payload address stable for the object's lifetime, and
2272    // `Rooted` proves the object is in the collector's root set for `'s`, so a
2273    // collection triggered while this reference is held cannot reclaim what it
2274    // points at.
2275    unsafe { &mut *r.get().payload::<P>() }
2276}
2277
2278// ---------------------------------------------------------------------------
2279// Vec[T] collection methods (§11.1, §11.2, §11.5).
2280//
2281// `VecPayload` stores a growable [`ReprCVec<GcRef>`](crate::ReprCVec), so
2282// `push` mutates the existing payload in place and the receiver's `GcRef` stays
2283// valid across it. Per §11.5 reallocation safety, no interior pointer into that
2284// buffer is retained across a capacity-mutating op.
2285// ---------------------------------------------------------------------------
2286
2287/// Read the `VecPayload` out of a `GcRef` as a shared ref, asserting it is a Vec.
2288///
2289/// # Safety
2290/// `r` must be a valid `Vec` `GcRef`.
2291unsafe fn vec_payload(r: GcRef) -> &'static VecPayload {
2292    // SAFETY: caller guarantees `r` is a Vec; see `payload_ref`.
2293    unsafe { payload_ref::<VecPayload>(r) }
2294}
2295
2296/// Read the `VecPayload` out of a `GcRef` as a mutable ref, asserting it is a
2297/// Vec. Used by `push` to mutate the vector in place (§11.1).
2298///
2299/// # Safety
2300/// `r` must be a valid `Vec` `GcRef`, rooted for `'s`.
2301unsafe fn vec_payload_mut<'s>(r: Rooted<'s>) -> &'s mut VecPayload {
2302    // SAFETY: caller guarantees `r` is a Vec; see `payload_mut`.
2303    unsafe { payload_mut::<VecPayload>(r) }
2304}
2305
2306/// Build a `Vec[T]` holding `items`, with `element_descriptor` as its element
2307/// type — the shape every wrapper that answers with a collection needs.
2308///
2309/// The `Vec` is rooted across the pushes, which is the part worth having in one
2310/// place: `praxis_vec_new` allocates, and so may the caller's own iteration, so a
2311/// collection between the allocation and the last push would reclaim it.
2312///
2313/// `element_descriptor` may be **null**: the source collection's label is what
2314/// its own construction site knew, and that may have been nothing. A
2315/// `Vec`'s null means "empty" — `vec_format` reads it that way — so a null label
2316/// with members present would answer `[]`. The first member's own descriptor is
2317/// what the `Vec` adopts instead, which is exactly what `praxis_vec_push` does.
2318///
2319/// # Safety
2320/// `ctx` must be live and wired; `element_descriptor` must be a valid
2321/// `'static TypeDescriptor` or null; every item must be a valid `GcRef` whose
2322/// payload matches its own header.
2323unsafe fn vec_of(
2324    ctx: *mut RuntimeContext,
2325    element_descriptor: *const TypeDescriptor,
2326    items: impl Iterator<Item = GcRef>,
2327) -> GcRef {
2328    let items: Vec<GcRef> = items.collect();
2329    let element_descriptor = if element_descriptor.is_null() {
2330        items
2331            .first()
2332            .map_or(std::ptr::null(), |first| first.descriptor() as *const _)
2333    } else {
2334        element_descriptor
2335    };
2336    let result = unsafe { praxis_vec_new(ctx, element_descriptor) };
2337    let scope = unsafe { NativeScope::new(ctx) };
2338    let rp = unsafe { vec_payload_mut(scope.root(result)) };
2339    rp.items.extend(items);
2340    result
2341}
2342
2343/// Allocate a new empty `Vec[T]` with the given element descriptor (§11.2).
2344/// Returns a `GcRef` to a zero-length vector.
2345///
2346/// # Safety
2347/// `ctx` must be live and wired. `element_descriptor` must be a valid pointer to
2348/// a `'static TypeDescriptor`.
2349#[unsafe(no_mangle)]
2350pub unsafe extern "C" fn praxis_vec_new(
2351    ctx: *mut RuntimeContext,
2352    element_descriptor: *const TypeDescriptor,
2353) -> GcRef {
2354    abi_guard!("praxis_vec_new", ctx, {
2355        // A null descriptor is kept null: it means "the caller has no static
2356        // element type", which is a thing this payload can hold. Spelling it
2357        // `INT` instead would make an empty `Vec[Float]` claim to hold `Int`s.
2358        // SAFETY: VecPayload is VEC's payload type.
2359        unsafe {
2360            gc_alloc_owned(ctx, &crate::collections::VEC, || VecPayload {
2361                element_descriptor,
2362                items: ReprCVec::new(),
2363            })
2364        }
2365    })
2366}
2367
2368/// Allocate a `Vec[T]` of `count` slots, every one holding `fill` (ADR-146's
2369/// `Vec(n, fill)`).
2370///
2371/// Faults `InvalidSize` if `count` is negative or exceeds
2372/// [`VecExtent::MAX_ITEMS`](crate::collections::VecExtent::MAX_ITEMS): the count
2373/// arrives from source and would otherwise become a `usize` cast, where a
2374/// negative value lands near `usize::MAX` (ADR-041 decision 1).
2375///
2376/// Faults `TypeMismatch` if the caller declared an element type that the fill is
2377/// not, through the same [`adopt_or_reject`] `push` uses — a `Vec[Int]` filled
2378/// with a `Float` is a mislabelled element descriptor, and every later
2379/// `equals`/`hash`/`format` would read the payloads as the wrong type. A null
2380/// static descriptor adopts the fill's, which is what "the caller has no static
2381/// element type" already means here.
2382///
2383/// **`fill` is stored `count` times, not copied `count` times.** Every slot is
2384/// the same `GcRef`, so `Vec(3, Vec())` is three names for one inner `Vec`.
2385/// That is the language's existing reference semantics — `outer.push(a)` twice
2386/// aliases too — stated at a new site rather than a new rule (ADR-146 decision
2387/// 4).
2388///
2389/// `count` arrives boxed rather than as a `RawI64` like [`praxis_grid_new`]'s
2390/// extents: MIR lowers an argument expression to a `Gc` local, and unboxing it
2391/// there would cost an `ExtractScalar` and a second shape in the codegen's
2392/// allocation arm. `praxis_grid_new`'s two are `iconst` immediates with no local
2393/// to unbox, which is why the two wrappers differ.
2394///
2395/// # Safety
2396/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
2397/// a `'static TypeDescriptor` (or null); `count` must be a valid `Int` `GcRef`;
2398/// `fill` must be a valid `GcRef`.
2399#[unsafe(no_mangle)]
2400pub unsafe extern "C" fn praxis_vec_filled(
2401    ctx: *mut RuntimeContext,
2402    element_descriptor: *const TypeDescriptor,
2403    count: GcRef,
2404    fill: GcRef,
2405) -> GcRef {
2406    abi_guard!("praxis_vec_filled", ctx, {
2407        // SAFETY: caller guarantees `count` is a valid Int.
2408        let n = unsafe { int_payload(count) };
2409        let Some(extent) = crate::collections::VecExtent::new(n) else {
2410            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
2411            return unsafe { unit_sentinel(ctx) };
2412        };
2413        let mut descriptor = element_descriptor;
2414        if !unsafe { adopt_or_reject(ctx, &mut descriptor, fill) } {
2415            return unsafe { unit_sentinel(ctx) };
2416        }
2417        // `fill` is a bare `GcRef` argument, and `gc_alloc_owned` may collect.
2418        // Rooting it in a native scope is what keeps it addressable across the
2419        // allocation — the caller's shadow frame roots it up to the call, and
2420        // this roots it through it.
2421        let scope = unsafe { NativeScope::new(ctx) };
2422        let fill = scope.root(fill).get();
2423        // The items are built inside the initializer, which `gc_alloc_owned`
2424        // runs *after* the safepoint: no untraced `Vec<GcRef>` is ever live
2425        // across a collection.
2426        // SAFETY: VecPayload is VEC's payload type.
2427        unsafe {
2428            gc_alloc_owned(ctx, &crate::collections::VEC, || VecPayload {
2429                element_descriptor: descriptor,
2430                items: ReprCVec::from_vec(vec![fill; extent.len()]),
2431            })
2432        }
2433    })
2434}
2435
2436/// Allocate a nominal record (§4.5) with all fields initialized to Unit.
2437/// The `schema_ptr` points at a `'static RecordSchema` (built and leaked by the
2438/// codegen from the record def). Fields are filled in declaration order via
2439/// [`praxis_record_set_field`] after allocation. Returns the record `GcRef`.
2440///
2441/// # Safety
2442/// `ctx` must be live and wired; `schema_ptr` must be a valid `'static` pointer.
2443#[unsafe(no_mangle)]
2444pub unsafe extern "C" fn praxis_alloc_record(
2445    ctx: *mut RuntimeContext,
2446    schema_ptr: *const crate::records::RecordSchema,
2447) -> GcRef {
2448    abi_guard!("praxis_alloc_record", ctx, {
2449        if schema_ptr.is_null() {
2450            // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2451            // caller already guarantees.
2452            return unsafe { unit_sentinel(ctx) };
2453        }
2454        // SAFETY: caller guarantees schema_ptr is a valid 'static pointer.
2455        let schema = unsafe { &*schema_ptr };
2456        let arity = schema.fields.len();
2457        // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2458        // caller already guarantees.
2459        let unit = unsafe { unit_sentinel(ctx) };
2460        // SAFETY: RecordPayload is RECORD's payload type.
2461        // Every field slot starts as Unit (a valid GcRef), keeping the GC sound
2462        // before the caller fills them in via praxis_record_set_field.
2463        unsafe {
2464            gc_alloc_owned(ctx, &crate::records::RECORD, || {
2465                crate::records::RecordPayload {
2466                    schema: schema_ptr,
2467                    items: vec![unit; arity],
2468                }
2469            })
2470        }
2471    })
2472}
2473
2474/// Set field `idx` of `record` to `value` (§4.5). Used by the codegen to
2475/// fill in fields after [`praxis_alloc_record`]. Returns the record (the
2476/// receiver is mutated in place).
2477///
2478/// # Safety
2479/// `ctx` must be live; `record` must be a valid record `GcRef`; `idx` must be
2480/// in bounds.
2481#[unsafe(no_mangle)]
2482pub unsafe extern "C" fn praxis_record_set_field(
2483    ctx: *mut RuntimeContext,
2484    record: GcRef,
2485    idx: u32,
2486    value: GcRef,
2487) -> GcRef {
2488    abi_guard!("praxis_record_set_field", ctx, {
2489        let _ = ctx;
2490        // SAFETY: caller guarantees record is a valid record GcRef.
2491        let payload = record.payload::<u8>() as *mut crate::records::RecordPayload;
2492        // SAFETY: the payload is a RecordPayload for any RECORD-descriptor object.
2493        let rp = unsafe { &mut *payload };
2494        if let Some(slot) = rp.items.get_mut(idx as usize) {
2495            *slot = value;
2496        }
2497        record
2498    })
2499}
2500
2501/// Read field `idx` out of a record `GcRef` (§4.5). Returns the field's
2502/// `GcRef` value. Returns Unit if the record is malformed or the index is out
2503/// of bounds (defensive; the type checker prevents this in well-typed code).
2504///
2505/// # Safety
2506/// `ctx` must be live; `record` must be a valid record `GcRef`.
2507#[unsafe(no_mangle)]
2508pub unsafe extern "C" fn praxis_record_field(
2509    ctx: *mut RuntimeContext,
2510    record: GcRef,
2511    idx: u32,
2512) -> GcRef {
2513    abi_guard!("praxis_record_field", ctx, {
2514        // SAFETY: caller guarantees record is a valid record GcRef; the payload is
2515        // a RecordPayload for any RECORD-descriptor object.
2516        let payload = record.payload::<u8>() as *const crate::records::RecordPayload;
2517        let rp = unsafe { &*payload };
2518        rp.items
2519            .get(idx as usize)
2520            .copied()
2521            .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2522    })
2523}
2524
2525/// Allocate an enum value (§4.6) of the type `schema_ptr` describes, with
2526/// variant `tag` and every payload slot initialized to Unit. Payload values are
2527/// filled via [`praxis_enum_set_payload`] after allocation. Returns the enum
2528/// `GcRef`.
2529///
2530/// The arity is **read from the schema** rather than passed alongside it, as
2531/// [`praxis_alloc_tuple`] already does: a schema and an arity that disagree is
2532/// a state no caller can now reach. A null schema, or a tag the schema has no
2533/// variant for, allocates nothing and answers the Unit sentinel — the same
2534/// answer `praxis_alloc_tuple` gives a null schema.
2535///
2536/// # Safety
2537/// `ctx` must be live and wired; `schema_ptr` must be null or a valid
2538/// `'static` pointer.
2539#[unsafe(no_mangle)]
2540pub unsafe extern "C" fn praxis_alloc_enum(
2541    ctx: *mut RuntimeContext,
2542    schema_ptr: *const crate::enums::EnumSchema,
2543    tag: i64,
2544) -> GcRef {
2545    abi_guard!("praxis_alloc_enum", ctx, {
2546        if schema_ptr.is_null() || tag < 0 {
2547            return unsafe { unit_sentinel(ctx) };
2548        }
2549        // SAFETY: caller guarantees schema_ptr is a valid 'static pointer.
2550        let schema = unsafe { &*schema_ptr };
2551        if schema.variant_at(tag as usize).is_none() {
2552            return unsafe { unit_sentinel(ctx) };
2553        }
2554        let arity = schema.arity_of(tag as usize);
2555        let unit = unsafe { unit_sentinel(ctx) };
2556        let items = vec![unit; arity];
2557        // SAFETY: EnumPayload is ENUM's payload type.
2558        unsafe {
2559            gc_alloc_owned(ctx, &crate::enums::ENUM, || crate::enums::EnumPayload {
2560                schema: schema_ptr,
2561                tag: tag as u32,
2562                items,
2563            })
2564        }
2565    })
2566}
2567
2568/// Allocate `Some(value)` under the runtime's own [`option_schema`].
2569///
2570/// `value` is rooted across the enum allocation: the allocation is a safepoint,
2571/// and a bare `GcRef` argument is not in anyone's root set.
2572///
2573/// [`option_schema`]: crate::enums::option_schema
2574///
2575/// # Safety
2576/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
2577pub(crate) unsafe fn option_some(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
2578    // SAFETY: the caller upholds ctx/value validity.
2579    unsafe {
2580        let scope = NativeScope::new(ctx);
2581        let rooted = scope.root(value);
2582        let some = praxis_alloc_enum(
2583            ctx,
2584            crate::enums::option_schema(),
2585            crate::enums::OPTION_SOME_TAG,
2586        );
2587        praxis_enum_set_payload(ctx, some, 0, rooted.get());
2588        some
2589    }
2590}
2591
2592/// Allocate `None` under the runtime's own `option_schema`.
2593///
2594/// # Safety
2595/// `ctx` must be live and wired.
2596pub(crate) unsafe fn option_none(ctx: *mut RuntimeContext) -> GcRef {
2597    // SAFETY: the caller upholds ctx validity.
2598    unsafe {
2599        praxis_alloc_enum(
2600            ctx,
2601            crate::enums::option_schema(),
2602            crate::enums::OPTION_NONE_TAG,
2603        )
2604    }
2605}
2606
2607/// Set payload slot `idx` of `enum_value` to `value` (§4.6). Returns the
2608/// enum value (mutated in place).
2609///
2610/// # Safety
2611/// `ctx` must be live; `enum_value` must be a valid enum `GcRef`; `idx` in bounds.
2612#[unsafe(no_mangle)]
2613pub unsafe extern "C" fn praxis_enum_set_payload(
2614    ctx: *mut RuntimeContext,
2615    enum_value: GcRef,
2616    idx: i64,
2617    value: GcRef,
2618) -> GcRef {
2619    abi_guard!("praxis_enum_set_payload", ctx, {
2620        let _ = ctx;
2621        // SAFETY: caller guarantees enum_value is a valid enum GcRef.
2622        let payload = enum_value.payload::<u8>() as *mut crate::enums::EnumPayload;
2623        let ep = unsafe { &mut *payload };
2624        if let Some(slot) = ep.items.get_mut(idx as usize) {
2625            *slot = value;
2626        }
2627        enum_value
2628    })
2629}
2630
2631/// Read the variant tag (discriminant) of an enum value (§4.6). Returns the
2632/// tag as a boxed `Int` `GcRef` (the uniform ABI convention), so the `match`
2633/// lowering can extract the scalar and compare. Used by `match` to branch.
2634///
2635/// # Safety
2636/// `ctx` must be live; `enum_value` must be a valid enum `GcRef`.
2637#[unsafe(no_mangle)]
2638pub unsafe extern "C" fn praxis_enum_tag(ctx: *mut RuntimeContext, enum_value: GcRef) -> GcRef {
2639    abi_guard!("praxis_enum_tag", ctx, {
2640        // SAFETY: caller guarantees enum_value is a valid enum GcRef.
2641        // Read the tag BEFORE allocating — the alloc below may trigger GC, and
2642        // enum_value is not explicitly rooted (it's only in a Cranelift local).
2643        let payload = enum_value.payload::<u8>() as *const crate::enums::EnumPayload;
2644        let tag = unsafe { (*payload).tag as i64 };
2645        // SAFETY: alloc boxes the i64 into a fresh Int object. The tag value is
2646        // already in a register, so GC collecting enum_value here is safe.
2647        unsafe { int_ref(ctx, tag) }
2648    })
2649}
2650
2651/// Read payload slot `idx` of an enum value (§4.6). Returns the slot's
2652/// `GcRef`. Used by `match` to bind variant payload variables.
2653///
2654/// # Safety
2655/// `ctx` must be live; `enum_value` must be a valid enum `GcRef`; `idx` in bounds.
2656#[unsafe(no_mangle)]
2657pub unsafe extern "C" fn praxis_enum_payload(
2658    ctx: *mut RuntimeContext,
2659    enum_value: GcRef,
2660    idx: i64,
2661) -> GcRef {
2662    abi_guard!("praxis_enum_payload", ctx, {
2663        // SAFETY: caller guarantees enum_value is a valid enum GcRef.
2664        let payload = enum_value.payload::<u8>() as *const crate::enums::EnumPayload;
2665        let ep = unsafe { &*payload };
2666        ep.items
2667            .get(idx as usize)
2668            .copied()
2669            .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2670    })
2671}
2672
2673/// Allocate a tuple (§4.5 structural tuples) with all element slots
2674/// initialized to Unit. The `schema_ptr` points at a `'static TupleSchema`
2675/// (built and leaked by the codegen from the tuple's element-type sequence).
2676/// Elements are filled in positional order via [`praxis_tuple_set`] after
2677/// allocation. Returns the tuple `GcRef`.
2678///
2679/// # Safety
2680/// `ctx` must be live and wired; `schema_ptr` must be a valid `'static` pointer.
2681#[unsafe(no_mangle)]
2682pub unsafe extern "C" fn praxis_alloc_tuple(
2683    ctx: *mut RuntimeContext,
2684    schema_ptr: *const crate::tuples::TupleSchema,
2685) -> GcRef {
2686    abi_guard!("praxis_alloc_tuple", ctx, {
2687        if schema_ptr.is_null() {
2688            // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2689            // caller already guarantees.
2690            return unsafe { unit_sentinel(ctx) };
2691        }
2692        // SAFETY: caller guarantees schema_ptr is a valid 'static pointer.
2693        let schema = unsafe { &*schema_ptr };
2694        let arity = schema.descriptors.len();
2695        // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2696        // caller already guarantees.
2697        let unit = unsafe { unit_sentinel(ctx) };
2698        // SAFETY: TuplePayload is TUPLE's payload type.
2699        // Every element slot starts as Unit (a valid GcRef), keeping the GC sound
2700        // before the caller fills them in via praxis_tuple_set.
2701        unsafe {
2702            gc_alloc_owned(ctx, &crate::tuples::TUPLE, || crate::tuples::TuplePayload {
2703                schema: schema_ptr,
2704                items: vec![unit; arity],
2705            })
2706        }
2707    })
2708}
2709
2710/// Set element `idx` of `tuple` to `value` (§4.5). Used by the codegen to
2711/// fill in elements after [`praxis_alloc_tuple`]. Returns the tuple (the
2712/// receiver is mutated in place).
2713///
2714/// # Safety
2715/// `ctx` must be live; `tuple` must be a valid tuple `GcRef`; `idx` in bounds.
2716#[unsafe(no_mangle)]
2717pub unsafe extern "C" fn praxis_tuple_set(
2718    ctx: *mut RuntimeContext,
2719    tuple: GcRef,
2720    idx: i64,
2721    value: GcRef,
2722) -> GcRef {
2723    abi_guard!("praxis_tuple_set", ctx, {
2724        let _ = ctx;
2725        // SAFETY: caller guarantees tuple is a valid tuple GcRef.
2726        let payload = tuple.payload::<u8>() as *mut crate::tuples::TuplePayload;
2727        // SAFETY: the payload is a TuplePayload for any TUPLE-descriptor object.
2728        let tp = unsafe { &mut *payload };
2729        if let Some(slot) = tp.items.get_mut(idx as usize) {
2730            *slot = value;
2731        }
2732        tuple
2733    })
2734}
2735
2736/// Read element `idx` out of a tuple `GcRef` (§4.5). Returns the element's
2737/// `GcRef` value. Returns Unit if the tuple is malformed or the index is out of
2738/// bounds (defensive; the type checker prevents this in well-typed code).
2739///
2740/// # Safety
2741/// `ctx` must be live; `tuple` must be a valid tuple `GcRef`.
2742#[unsafe(no_mangle)]
2743pub unsafe extern "C" fn praxis_tuple_get(
2744    ctx: *mut RuntimeContext,
2745    tuple: GcRef,
2746    idx: i64,
2747) -> GcRef {
2748    abi_guard!("praxis_tuple_get", ctx, {
2749        // SAFETY: caller guarantees tuple is a valid tuple GcRef; the payload is a
2750        // TuplePayload for any TUPLE-descriptor object.
2751        let payload = tuple.payload::<u8>() as *const crate::tuples::TuplePayload;
2752        let tp = unsafe { &*payload };
2753        tp.items
2754            .get(idx as usize)
2755            .copied()
2756            .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2757    })
2758}
2759
2760/// Structural equality between two GC values (§5.5). Reads the descriptor
2761/// from `a` and dispatches to its `equals` callback, which recurses element/field
2762/// wise for composite types (records, tuples, enums, collections). Returns 1 for
2763/// equal, 0 for not equal. Returns 0 if `a`'s type is not equatable (functions
2764/// are never equatable, §5.5) — the type checker rejects this in well-typed code,
2765/// so this is defensive.
2766///
2767/// # Safety
2768/// `ctx` must be live and wired; `a` and `b` must be valid `GcRef`s of the same
2769/// type (the caller has already unified their types at compile time).
2770#[unsafe(no_mangle)]
2771pub unsafe extern "C" fn praxis_struct_eq(ctx: *mut RuntimeContext, a: GcRef, b: GcRef) -> i64 {
2772    abi_guard!("praxis_struct_eq", ctx, {
2773        let _ = ctx;
2774        // SAFETY: caller guarantees a is a valid GcRef; the descriptor header is
2775        // always present and its `equals` (if Some) is safe to call with a/b.
2776        let desc = a.descriptor();
2777        // Both operands must be the same runtime type before any callback runs
2778        // (ADR-045 decision 3). Well-typed code has unified them, so this is the
2779        // miscompile case — and a callback dispatched on a foreign layout is how a
2780        // type confusion becomes a wild read rather than a wrong answer.
2781        if !std::ptr::eq(desc, b.descriptor()) {
2782            return 0;
2783        }
2784        match desc.equals {
2785            // SAFETY: both a and b are values of desc's type (caller has type-checked
2786            // them equal); the equals callback is safe under that invariant.
2787            Some(eq) => {
2788                let pa = a.payload::<u8>() as *const u8;
2789                let pb = b.payload::<u8>() as *const u8;
2790                if unsafe { eq(pa, pb) } { 1 } else { 0 }
2791            }
2792            // Not equatable: treat as not-equal. The type checker rejects this in
2793            // well-typed code; the defensive default keeps runtime sound.
2794            None => 0,
2795        }
2796    })
2797}
2798
2799/// Order two GC values through their descriptor's `compare` callback (ADR-045).
2800/// Returns `-1`, `0` or `1` — the caller turns that into the `<`/`<=`/`>`/`>=`
2801/// it wanted by comparing against zero.
2802///
2803/// This is the ordering counterpart of [`praxis_struct_eq`], and it exists for
2804/// the same reason: a `Text` is a pointer-and-length structure, so ordering one
2805/// by loading its first eight payload bytes would compare *addresses*.
2806///
2807/// Raises `FaultKind::TypeMismatch` and answers `0` when the two operands are
2808/// not the same runtime type, or when the type has no `compare`. The type
2809/// checker rejects both in well-typed code (`Y006`), so reaching either is a
2810/// compiler bug — reported as a fault rather than a callback dispatched on a
2811/// foreign layout.
2812///
2813/// The second guard is a weak backstop, and deliberately named as one: ADR-138
2814/// populated `compare` on every type a `Map` key can be, including tuples and
2815/// records, so a *miscompile* that lowered `(1, 2) < (1, 3)` to this wrapper
2816/// would be answered rather than faulted. `capability::supports_ord` refuses it
2817/// at `praxis check`, so no well-typed program reaches here either way.
2818///
2819/// # Safety
2820/// `ctx` must be live and wired; `a` and `b` must be valid `GcRef`s.
2821#[unsafe(no_mangle)]
2822pub unsafe extern "C" fn praxis_value_cmp(ctx: *mut RuntimeContext, a: GcRef, b: GcRef) -> i64 {
2823    abi_guard!("praxis_value_cmp", ctx, {
2824        // SAFETY: caller guarantees both are valid GcRefs; every object carries a
2825        // descriptor in its header.
2826        let desc = a.descriptor();
2827        if !std::ptr::eq(desc, b.descriptor()) {
2828            unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
2829            return 0;
2830        }
2831        let Some(compare) = desc.compare else {
2832            unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
2833            return 0;
2834        };
2835        // SAFETY: both values carry `desc` (checked above), so both payloads are
2836        // values of its type.
2837        let ordering = unsafe {
2838            compare(
2839                a.payload::<u8>() as *const u8,
2840                b.payload::<u8>() as *const u8,
2841            )
2842        };
2843        match ordering {
2844            std::cmp::Ordering::Less => -1,
2845            std::cmp::Ordering::Equal => 0,
2846            std::cmp::Ordering::Greater => 1,
2847        }
2848    })
2849}
2850
2851/// Allocate a closure value (§4.10) with `fn_ptr` as its entry point and
2852/// `n_captures` environment slots initialized to Unit. Captures are filled via
2853/// [`praxis_closure_set_capture`] after allocation. Returns the closure `GcRef`.
2854///
2855/// # Safety
2856/// `ctx` must be live and wired; `fn_ptr` must be a valid JIT'd function pointer
2857/// whose calling convention matches `fn(ctx, params..., env...) -> i64`.
2858#[unsafe(no_mangle)]
2859pub unsafe extern "C" fn praxis_alloc_closure(
2860    ctx: *mut RuntimeContext,
2861    fn_ptr: *const u8,
2862    n_captures: i64,
2863) -> GcRef {
2864    abi_guard!("praxis_alloc_closure", ctx, {
2865        // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2866        // caller already guarantees.
2867        let unit = unsafe { unit_sentinel(ctx) };
2868        let env = vec![unit; n_captures as usize];
2869        // SAFETY: ClosurePayload is CLOSURE's payload type.
2870        unsafe {
2871            gc_alloc_owned(ctx, &crate::closures::CLOSURE, || {
2872                crate::closures::ClosurePayload { fn_ptr, env }
2873            })
2874        }
2875    })
2876}
2877
2878/// Set capture slot `idx` of `closure` to `value` (§4.10). Returns the
2879/// closure (mutated in place).
2880///
2881/// # Safety
2882/// `ctx` must be live; `closure` must be a valid closure `GcRef`; `idx` in bounds.
2883#[unsafe(no_mangle)]
2884pub unsafe extern "C" fn praxis_closure_set_capture(
2885    ctx: *mut RuntimeContext,
2886    closure: GcRef,
2887    idx: i64,
2888    value: GcRef,
2889) -> GcRef {
2890    abi_guard!("praxis_closure_set_capture", ctx, {
2891        let _ = ctx;
2892        // SAFETY: caller guarantees closure is a valid closure GcRef.
2893        let payload = closure.payload::<u8>() as *mut crate::closures::ClosurePayload;
2894        let cp = unsafe { &mut *payload };
2895        if let Some(slot) = cp.env.get_mut(idx as usize) {
2896            *slot = value;
2897        }
2898        closure
2899    })
2900}
2901
2902/// Read the function pointer out of a closure `GcRef` (§4.10). Used by the
2903/// indirect-call lowering to obtain the entry point before a native call.
2904///
2905/// `ctx` is accepted (and unused) for ABI uniformity with every other `praxis_*`
2906/// wrapper — generated code calls all wrappers as `fn(ctx, args...)`, so this
2907/// keeps the calling convention consistent. The returned `*const u8` is carried
2908/// as an `i64` (pointer-sized) back into the JIT'd code.
2909///
2910/// # Safety
2911/// `ctx` must be live; `closure` must be a valid closure `GcRef`.
2912#[unsafe(no_mangle)]
2913pub unsafe extern "C" fn praxis_closure_fn_ptr(
2914    ctx: *mut RuntimeContext,
2915    closure: GcRef,
2916) -> *const u8 {
2917    abi_guard!("praxis_closure_fn_ptr", ctx, {
2918        let _ = ctx;
2919        // SAFETY: caller guarantees closure is a valid closure GcRef.
2920        let payload = closure.payload::<u8>() as *const crate::closures::ClosurePayload;
2921        unsafe { (*payload).fn_ptr }
2922    })
2923}
2924
2925/// Read capture slot `idx` out of a closure `GcRef` (§4.10). Used by the
2926/// closure's synthetic function to load its captured values from the env.
2927///
2928/// # Safety
2929/// `ctx` must be live; `closure` must be a valid closure `GcRef`; `idx` in bounds.
2930#[unsafe(no_mangle)]
2931pub unsafe extern "C" fn praxis_closure_capture(
2932    ctx: *mut RuntimeContext,
2933    closure: GcRef,
2934    idx: i64,
2935) -> GcRef {
2936    abi_guard!("praxis_closure_capture", ctx, {
2937        // SAFETY: caller guarantees closure is a valid closure GcRef.
2938        let payload = closure.payload::<u8>() as *const crate::closures::ClosurePayload;
2939        let cp = unsafe { &*payload };
2940        cp.env
2941            .get(idx as usize)
2942            .copied()
2943            .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2944    })
2945}
2946
2947/// Allocate a `VarCell` holding `value` (§4.10). The cell is the shared
2948/// mutable storage for a captured `var` binding: the binding site and every
2949/// closure that captures the `var` refer to the same cell. Returns the cell
2950/// `GcRef`.
2951///
2952/// # Safety
2953/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
2954#[unsafe(no_mangle)]
2955pub unsafe extern "C" fn praxis_alloc_var_cell(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
2956    abi_guard!("praxis_alloc_var_cell", ctx, {
2957        // SAFETY: VarCellPayload is VAR_CELL's payload type.
2958        unsafe {
2959            gc_alloc_owned(ctx, &crate::var_cell::VAR_CELL, || {
2960                crate::var_cell::VarCellPayload { value }
2961            })
2962        }
2963    })
2964}
2965
2966/// Read the current value out of a `VarCell` (§4.10). Used by `Path`
2967/// reads of a captured `var` (the local holds the cell; this derefs it).
2968///
2969/// # Safety
2970/// `ctx` must be live; `cell` must be a valid `VarCell` `GcRef`.
2971#[unsafe(no_mangle)]
2972pub unsafe extern "C" fn praxis_var_cell_get(ctx: *mut RuntimeContext, cell: GcRef) -> GcRef {
2973    abi_guard!("praxis_var_cell_get", ctx, {
2974        let _ = ctx;
2975        // SAFETY: caller guarantees cell is a valid VarCell GcRef.
2976        let payload = cell.payload::<u8>() as *const crate::var_cell::VarCellPayload;
2977        unsafe { (*payload).value }
2978    })
2979}
2980
2981/// Store `value` into a `VarCell` (§4.10). Used by `Assign` to a
2982/// captured `var`. Returns the cell (mutated in place).
2983///
2984/// # Safety
2985/// `ctx` must be live; `cell` must be a valid `VarCell` `GcRef`; `value` valid.
2986#[unsafe(no_mangle)]
2987pub unsafe extern "C" fn praxis_var_cell_set(
2988    ctx: *mut RuntimeContext,
2989    cell: GcRef,
2990    value: GcRef,
2991) -> GcRef {
2992    abi_guard!("praxis_var_cell_set", ctx, {
2993        let _ = ctx;
2994        // SAFETY: caller guarantees cell is a valid VarCell GcRef.
2995        let payload = cell.payload::<u8>() as *mut crate::var_cell::VarCellPayload;
2996        unsafe {
2997            (*payload).value = value;
2998        }
2999        cell
3000    })
3001}
3002
3003/// Append `value` to `vec` in place (§11.1). Returns the Unit sentinel — the
3004/// receiver is mutated directly, so the caller's `GcRef` remains valid (the
3005/// `VecPayload` object does not move; only its internal buffer may grow).
3006///
3007/// # Safety
3008/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; `value`
3009/// must be a valid `GcRef` whose type matches the vector's element descriptor.
3010#[unsafe(no_mangle)]
3011pub unsafe extern "C" fn praxis_vec_push(
3012    ctx: *mut RuntimeContext,
3013    vec: GcRef,
3014    value: GcRef,
3015) -> GcRef {
3016    abi_guard!("praxis_vec_push", ctx, {
3017        // `push` may grow the Vec's backing buffer, which allocates Rust heap memory
3018        // (not GC memory). A GC collection during this would be safe (the vec
3019        // object is rooted by the caller's spilled `vec` local), but we trigger it
3020        // *before* the mutation to keep the rooting story simple: `value` is passed
3021        // by value and is not yet in the vec, so it must survive across this
3022        // collection via the caller's shadow frame.
3023        unsafe { maybe_collect(ctx) };
3024        // SAFETY: caller guarantees `vec` is a valid Vec.
3025        let scope = unsafe { NativeScope::new(ctx) };
3026        let p = unsafe { vec_payload_mut(scope.root(vec)) };
3027        // A vector that was never told its element type adopts the first pushed
3028        // value's — the `forall T. () -> Vec[T]` builtin leaves `T` generalized
3029        // until first use, so construction genuinely has nothing to record. A
3030        // vector that *was* told rejects a mismatch instead of retagging itself:
3031        // retagging would turn an explicitly typed `Vec[Int]` into a `Vec[Float]`
3032        // on one bad push, and every later `equals`/`hash`/`format` would then
3033        // read the remaining `Int` payloads as `f64`.
3034        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3035            return unsafe { unit_sentinel(ctx) };
3036        }
3037        // Charge the spine when it grows (ADR-121; see
3038        // `Heap::charge_owned_growth`). Measured either side of the mutation
3039        // through the payload's own `owned_bytes`, so the growth policy stays
3040        // `RawVec`'s and the size formula stays the descriptor's.
3041        let before = p.owned_bytes();
3042        p.items.push(value);
3043        charge_growth(ctx, before, p.owned_bytes());
3044        unsafe { unit_sentinel(ctx) }
3045    })
3046}
3047
3048/// Reconcile a collection's element descriptor with a value about to be stored
3049/// in it: adopt the value's descriptor if the collection has none, accept if
3050/// they agree, and raise `TypeMismatch` if they do not.
3051///
3052/// Returns whether the store may proceed. Descriptors are `static`, so pointer
3053/// identity is the authoritative test (ADR-038).
3054///
3055/// # Safety
3056/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
3057unsafe fn adopt_or_reject(
3058    ctx: *mut RuntimeContext,
3059    element_descriptor: &mut *const TypeDescriptor,
3060    value: GcRef,
3061) -> bool {
3062    let pushed = value.descriptor();
3063    if element_descriptor.is_null() {
3064        *element_descriptor = pushed;
3065        return true;
3066    }
3067    if std::ptr::eq(*element_descriptor, pushed) {
3068        return true;
3069    }
3070    unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3071    false
3072}
3073
3074/// The number of elements in `vec`, as a boxed `Int` (§11.1).
3075///
3076/// # Safety
3077/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3078#[unsafe(no_mangle)]
3079pub unsafe extern "C" fn praxis_vec_len(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3080    abi_guard!("praxis_vec_len", ctx, {
3081        // SAFETY: caller guarantees `vec` is a valid Vec.
3082        let p = unsafe { vec_payload(vec) };
3083        let len = p.items.len() as i64;
3084        // len allocates the returned Int, but the input vec is still live via `vec`.
3085        unsafe { int_ref(ctx, len) }
3086    })
3087}
3088
3089/// The element at `index`, or an `IndexOutOfBounds` fault if out of range
3090/// (§9.2, §11.1). Returns the Unit sentinel on fault.
3091///
3092/// # Safety
3093/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; `index`
3094/// must be a valid `Int` `GcRef`.
3095#[unsafe(no_mangle)]
3096pub unsafe extern "C" fn praxis_vec_get(
3097    ctx: *mut RuntimeContext,
3098    vec: GcRef,
3099    index: GcRef,
3100) -> GcRef {
3101    abi_guard!("praxis_vec_get", ctx, {
3102        // SAFETY: caller guarantees `vec` is a valid Vec.
3103        let p = unsafe { vec_payload(vec) };
3104        // SAFETY: caller guarantees `index` is a valid Int.
3105        let idx = unsafe { int_payload(index) };
3106        // SAFETY: `abi_guard!` established that `ctx` is live and wired.
3107        let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3108            return unsafe { unit_sentinel(ctx) };
3109        };
3110        // Return the element by value (a copy of the GcRef). No allocation, so no
3111        // collection is needed; the vec stays live via `vec`.
3112        p.items[idx]
3113    })
3114}
3115
3116/// Replace the element at `index`; faults `IndexOutOfBounds` if out of range
3117/// (§9.2, §11.1). Returns the Unit sentinel.
3118///
3119/// **Replaces, and never appends.** `v[v.len()] = x` is out of range rather than
3120/// a push, which is `praxis_vec_push`'s job: a store whose index decides between
3121/// the two operations makes an off-by-one grow the vector instead of reporting.
3122///
3123/// The element descriptor goes through the same [`adopt_or_reject`] every push
3124/// does, so a store into a vector that was never told its element type adopts
3125/// the first value's, and one into a `Vec[Int]` raises `TypeMismatch` rather
3126/// than retagging the collection — `push`'s rule, at the second door.
3127///
3128/// # Safety
3129/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; `index`
3130/// must be a valid `Int` `GcRef`; `value` must be a valid `GcRef`.
3131#[unsafe(no_mangle)]
3132pub unsafe extern "C" fn praxis_vec_set(
3133    ctx: *mut RuntimeContext,
3134    vec: GcRef,
3135    index: GcRef,
3136    value: GcRef,
3137) -> GcRef {
3138    abi_guard!("praxis_vec_set", ctx, {
3139        // SAFETY: caller guarantees `vec` is a valid Vec.
3140        let scope = unsafe { NativeScope::new(ctx) };
3141        let p = unsafe { vec_payload_mut(scope.root(vec)) };
3142        // SAFETY: caller guarantees `index` is a valid Int.
3143        let idx = unsafe { int_payload(index) };
3144        // SAFETY: `abi_guard!` established that `ctx` is live and wired.
3145        let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3146            return unsafe { unit_sentinel(ctx) };
3147        };
3148        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3149            return unsafe { unit_sentinel(ctx) };
3150        }
3151        // No allocation: the slot takes a `GcRef` the caller already holds.
3152        p.items[idx] = value;
3153        unsafe { unit_sentinel(ctx) }
3154    })
3155}
3156
3157/// True iff `vec` has no elements, as a boxed `Bool` (§11.1).
3158///
3159/// # Safety
3160/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3161#[unsafe(no_mangle)]
3162pub unsafe extern "C" fn praxis_vec_is_empty(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3163    abi_guard!("praxis_vec_is_empty", ctx, {
3164        // SAFETY: caller guarantees `vec` is a valid Vec.
3165        let p = unsafe { vec_payload(vec) };
3166        let empty = p.items.is_empty();
3167        // SAFETY: ctx/heap valid; Bool immortal path.
3168        unsafe { bool_ref(ctx, empty) }
3169    })
3170}
3171
3172// --- the §6.3 barrier combinators ------------------------------------------
3173//
3174// A *barrier* is a pipeline stage that cannot be fused into the loop feeding it
3175// because it needs the whole sequence before it can answer its first element:
3176// `sorted` has to see the largest element before it knows the smallest is first.
3177// So each is a real runtime call over a materialized `Vec` rather than an
3178// intrinsic the MIR fuser expands, and the fuser's own recognizer already knows
3179// to end a chain at one and start a fresh chain from its result.
3180//
3181// All three rebuild through [`vec_of`] rather than mutating the receiver, which
3182// is the shape `praxis_set_items` and `praxis_counter_keys` already use.
3183// `v.sorted()` is an expression, not a statement: §6.3 lists it beside `map` and
3184// `filter`, and a caller that also holds `v` must still see `v`'s own order.
3185
3186/// `v.sorted()` — the elements of `vec` in ascending order, as a **new** `Vec`
3187/// (§6.3). The receiver is not touched.
3188///
3189/// Ordering goes through the element descriptor's `compare` callback — the same
3190/// callback [`praxis_value_cmp`] uses, and for the same reason: a `Text` is a
3191/// pointer-and-length structure, so ordering one by its first eight payload
3192/// bytes compares *addresses*. That sorts `Vec[Int]` correctly and `Vec[Text]`
3193/// into allocation order, which is the failure that looks like it works.
3194///
3195/// The sort is **stable**, so equal elements keep their input order and the
3196/// answer is a function of the input alone.
3197///
3198/// Raises `FaultKind::TypeMismatch` and answers Unit when the elements are not
3199/// all one type, or when that type has no `compare`. The catalog row's `Ord`
3200/// bound (ADR-093, `Bound::Kind`) rejects both at `praxis check`, so reaching
3201/// either is a compiler bug — reported as a fault rather than as a callback
3202/// dispatched on a foreign layout. As [`praxis_value_cmp`], the second guard
3203/// covers fewer types since ADR-138 populated `compare` on the composites.
3204///
3205/// It is the callback a `Set` and a `Map` order their keys through too
3206/// (ADR-138), which is what makes `out(s)` and `out(s.sorted())` print one
3207/// sequence rather than one numeric and one lexicographic.
3208///
3209/// # Safety
3210/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3211#[unsafe(no_mangle)]
3212pub unsafe extern "C" fn praxis_vec_sorted(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3213    abi_guard!("praxis_vec_sorted", ctx, {
3214        // SAFETY: caller guarantees `vec` is a valid Vec.
3215        let p = unsafe { vec_payload(vec) };
3216        let mut items: Vec<GcRef> = p.items.to_vec();
3217        // Nothing to order, and nothing to check: a zero- or one-element Vec has
3218        // no pair to compare, so an empty `Vec[fn(Int) -> Int]` sorts rather than
3219        // faulting on a `compare` it would never have called.
3220        if items.len() > 1 {
3221            // The elements' *own* descriptors decide, not the Vec's label: the
3222            // label may be null (the construction site knew no element type)
3223            // while every member is a perfectly good `Text`.
3224            let desc = items[0].descriptor();
3225            if !items.iter().all(|i| std::ptr::eq(i.descriptor(), desc)) {
3226                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3227                return unsafe { unit_sentinel(ctx) };
3228            }
3229            let Some(compare) = desc.compare else {
3230                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3231                return unsafe { unit_sentinel(ctx) };
3232            };
3233            items.sort_by(|a, b| {
3234                // SAFETY: every element carries `desc` (checked above), so both
3235                // payloads are values of its type; the non-moving GC keeps them
3236                // stable, and `sort_by` allocates nothing that could collect.
3237                unsafe {
3238                    compare(
3239                        a.payload::<u8>() as *const u8,
3240                        b.payload::<u8>() as *const u8,
3241                    )
3242                }
3243            });
3244        }
3245        unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
3246    })
3247}
3248
3249/// `v.sorted_by_key(f)` — the elements of `vec` ordered by the key `f` extracts,
3250/// as a **new** `Vec` (§6.3, ADR-127 decision 5). The receiver is not touched.
3251///
3252/// # Why this row exists, and why it is not `sorted_by`
3253///
3254/// ADR-045 decided that no composite is orderable, so the moment a pipeline's
3255/// item is a pair — which is the moment its source is a `Map` or a `Counter` —
3256/// `sorted` is unavailable and "the five most common values" has no spelling.
3257/// The closure extracts an orderable key from an item that is not.
3258///
3259/// Not a `(T, T) -> Bool` comparator: `min_by`/`max_by` already own the
3260/// less-than-predicate shape, and a comparator is O(n log n) calls back into
3261/// JIT'd code where a key extractor is n.
3262///
3263/// **Decorate–sort–undecorate.** Every key is extracted once, up front, and the
3264/// sort orders the (key, element) pairs — which is what makes it n calls. The
3265/// keys are held in a `Vec<GcRef>` the collector cannot see, so they are rooted
3266/// in a native scope: extraction allocates, and a collection triggered by the
3267/// *next* call would otherwise free the key the previous one produced.
3268///
3269/// Ordering goes through the same `compare` callback [`praxis_value_cmp`] uses,
3270/// so the `Ord` bound the catalog row puts on the *key* is the rule this
3271/// enforces. The sort is **stable**, so items with equal keys keep their input
3272/// order and the answer is a function of the input alone.
3273///
3274/// Raises `FaultKind::TypeMismatch` and answers Unit when the keys are not all
3275/// one type, or when that type has no ordering; a fault the closure itself
3276/// raised stops the sort and is left for the call site's own check.
3277///
3278/// # Safety
3279/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `key`
3280/// a valid closure `GcRef`.
3281#[unsafe(no_mangle)]
3282pub unsafe extern "C" fn praxis_vec_sorted_by_key(
3283    ctx: *mut RuntimeContext,
3284    vec: GcRef,
3285    key: GcRef,
3286) -> GcRef {
3287    abi_guard!("praxis_vec_sorted_by_key", ctx, {
3288        let scope = unsafe { NativeScope::new(ctx) };
3289        // The receiver is rooted explicitly: its items are read into a Rust
3290        // `Vec` and held across one closure call *per element*, which is a far
3291        // longer window than a single-allocation wrapper's.
3292        let _receiver = scope.root(vec);
3293        // SAFETY: caller guarantees `vec` is a valid Vec.
3294        let p = unsafe { vec_payload(vec) };
3295        let element_descriptor = p.element_descriptor;
3296        let items: Vec<GcRef> = p.items.to_vec();
3297        for item in &items {
3298            scope.root(*item);
3299        }
3300        // Decorate: one call per element, keys rooted as they arrive.
3301        let mut decorated: Vec<(GcRef, GcRef)> = Vec::with_capacity(items.len());
3302        for item in items {
3303            let Some(k) = (unsafe { call_unary_closure(ctx, key, item) }) else {
3304                // The closure faulted (or is not a closure, which the type
3305                // checker already refused). Its answer is the Unit sentinel, so
3306                // sorting on it would order garbage; stop and leave the fault
3307                // for the call site.
3308                return unsafe { unit_sentinel(ctx) };
3309            };
3310            decorated.push((scope.root(k).get(), item));
3311        }
3312        if decorated.len() > 1 {
3313            // The keys' *own* descriptors decide, as `praxis_vec_sorted`'s
3314            // elements' do: the source Vec's label says nothing about them.
3315            let desc = decorated[0].0.descriptor();
3316            if !decorated
3317                .iter()
3318                .all(|(k, _)| std::ptr::eq(k.descriptor(), desc))
3319            {
3320                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3321                return unsafe { unit_sentinel(ctx) };
3322            }
3323            let Some(compare) = desc.compare else {
3324                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3325                return unsafe { unit_sentinel(ctx) };
3326            };
3327            decorated.sort_by(|(a, _), (b, _)| {
3328                // SAFETY: every key carries `desc` (checked above), so both
3329                // payloads are values of its type; the non-moving GC keeps them
3330                // stable, and `sort_by` allocates nothing that could collect.
3331                unsafe {
3332                    compare(
3333                        a.payload::<u8>() as *const u8,
3334                        b.payload::<u8>() as *const u8,
3335                    )
3336                }
3337            });
3338        }
3339        // Undecorate.
3340        unsafe {
3341            vec_of(
3342                ctx,
3343                element_descriptor,
3344                decorated.into_iter().map(|(_, item)| item),
3345            )
3346        }
3347    })
3348}
3349
3350/// Call a `(T) -> U` Praxis closure with one argument, or `None` if it faulted —
3351/// or if it is not a closure at all.
3352///
3353/// The descriptor is checked rather than assumed: the type checker says the
3354/// operand is a function and the only runtime representation of one is a closure
3355/// object, but the alternative to a `TypeMismatch` fault is transmuting whatever
3356/// the payload's first word happens to be into a function pointer and jumping to
3357/// it.
3358///
3359/// # Safety
3360/// `ctx` must be live and wired; `closure` and `arg` must be valid `GcRef`s.
3361unsafe fn call_unary_closure(
3362    ctx: *mut RuntimeContext,
3363    closure: GcRef,
3364    arg: GcRef,
3365) -> Option<GcRef> {
3366    if !std::ptr::eq(closure.descriptor(), &crate::closures::CLOSURE) {
3367        unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3368        return None;
3369    }
3370    // SAFETY: the descriptor check proves the payload is a `ClosurePayload`, so
3371    // `fn_ptr` is the entry point the codegen wrote there.
3372    let fn_ptr = unsafe { (*closure.payload::<crate::closures::ClosurePayload>()).fn_ptr };
3373    // A closure's entry point is `fn(ctx, closure_self, params...) -> GcRef`
3374    // (§4.10, Approach B): the closure value itself is a hidden first argument,
3375    // and the prologue loads its captures from it.
3376    //
3377    // SAFETY: `fn_ptr` is a finalized JIT entry whose parameter count is the one
3378    // the type checker enforced for this operand; every value crossing is a
3379    // `GcRef`, which is the ABI's only value kind.
3380    let result = unsafe {
3381        let f: unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef) -> GcRef =
3382            std::mem::transmute(fn_ptr);
3383        f(ctx, closure, arg)
3384    };
3385    // The closure ran arbitrary Praxis code and may have faulted; its result on
3386    // that path is the Unit sentinel.
3387    if unsafe { praxis_check_fault(ctx) } != 0 {
3388        return None;
3389    }
3390    Some(result)
3391}
3392
3393/// `v.unique()` — the elements of `vec` with later duplicates dropped, as a
3394/// **new** `Vec`, in first-occurrence order (§6.3). The receiver is not touched.
3395///
3396/// First-occurrence order rather than sorted-and-deduped: `unique` is listed
3397/// separately from `sorted` in §6.3, so composing them has to be the user's
3398/// choice, and an order that depends on a hash map's iteration would make the
3399/// same program answer differently on two runs — and here the order is the
3400/// program's *answer*, not only its printing.
3401///
3402/// Sameness is [`DynamicKey`]'s — the descriptor's `hash` and `equals`
3403/// callbacks, which is what "the same value" means everywhere else in this
3404/// runtime (§5.5, §11.3). The catalog row's `HashStable` bound is what keeps a
3405/// mutable element out; a key that can change after it is stored cannot be found
3406/// again.
3407///
3408/// # Safety
3409/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3410#[unsafe(no_mangle)]
3411pub unsafe extern "C" fn praxis_vec_unique(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3412    abi_guard!("praxis_vec_unique", ctx, {
3413        // SAFETY: caller guarantees `vec` is a valid Vec.
3414        let p = unsafe { vec_payload(vec) };
3415        let mut seen: std::collections::HashSet<DynamicKey> = std::collections::HashSet::new();
3416        let mut kept: Vec<GcRef> = Vec::new();
3417        for item in &p.items {
3418            if seen.insert(DynamicKey::new(*item)) {
3419                kept.push(*item);
3420            }
3421        }
3422        unsafe { vec_of(ctx, p.element_descriptor, kept.into_iter()) }
3423    })
3424}
3425
3426/// `v.reversed()` — the elements of `vec` in the opposite order, as a **new**
3427/// `Vec` (ADR-145). The receiver is not touched.
3428///
3429/// A barrier for `praxis_vec_sorted`'s reason and not a fused stage: reversal
3430/// cannot answer its first element until it has seen the last one.
3431///
3432/// It reads **no descriptor callback** — not `compare`, not `equals`, not
3433/// `hash` — so unlike `sorted` and `unique` there is no element it can be handed
3434/// that it cannot reverse, and its catalog row carries no capability bound. That
3435/// is why the manifest row is `Allocates` and there is no `TypeMismatch` path
3436/// here to read.
3437///
3438/// The element label is copied through unchanged, the null a construction site
3439/// that knew no element type leaves included.
3440///
3441/// # Safety
3442/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3443#[unsafe(no_mangle)]
3444pub unsafe extern "C" fn praxis_vec_reversed(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3445    abi_guard!("praxis_vec_reversed", ctx, {
3446        // SAFETY: caller guarantees `vec` is a valid Vec.
3447        let p = unsafe { vec_payload(vec) };
3448        let items: Vec<GcRef> = p.items.iter().rev().copied().collect();
3449        unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
3450    })
3451}
3452
3453/// The group size `chunks(n)` and `windows(n)` share, or `None` when `n` names no
3454/// group at all (ADR-149).
3455///
3456/// **The only thing either wrapper refuses.** A run of zero elements is not a
3457/// short run — chunking a non-empty sequence into them has no finite answer, and
3458/// sliding one along it has no useful one — and a negative run names nothing.
3459/// Every other `n` has an answer, including one larger than the receiver: a
3460/// `chunks` wider than the sequence is one short chunk, a `windows` wider than it
3461/// is no windows. So this returns an `Option` of a size rather than clamping to
3462/// one, and the two callers spell those two answers themselves.
3463///
3464/// There is no upper bound here and none is missing. Both results are *shorter*
3465/// than the receiver — one group per start position at most — so neither can
3466/// ask for an extent [`VecExtent`](crate::collections::VecExtent) would refuse,
3467/// which is the bound `praxis_vec_filled` needs and these do not.
3468fn group_size(n: i64) -> Option<usize> {
3469    if n <= 0 {
3470        return None;
3471    }
3472    usize::try_from(n).ok()
3473}
3474
3475/// The `Vec[Vec[T]]` both groupings answer, built from the half-open source
3476/// ranges `groups` names (ADR-149).
3477///
3478/// **The outer label is `collections::VEC` at every length, and it is *passed*
3479/// rather than inferred** (ADR-149 decision 1). Which label belongs there is not
3480/// this wrapper's choice — `outer.push(inner)` builds a `Vec[Vec[T]]` today and
3481/// `adopt_or_reject` labels it `VEC`, so anything else would disagree with
3482/// `push`. What is chosen here is only that it is written down: letting
3483/// [`vec_of`] infer it from the first group would answer `VEC` for
3484/// `[1].chunks(2)` and *null* for `[].chunks(2)` — one type with two labels, and
3485/// the null is the one `vec_format` renders as `[]`.
3486///
3487/// That is [`praxis_grid_positions`]'s argument, not a new one: it passes
3488/// `&tuples::TUPLE` for the same reason, and `Grid(0, 0, 1).positions()` is the
3489/// same empty case. Naming the label is what a wrapper does whenever its result's
3490/// element kind is not its receiver's.
3491///
3492/// The inner labels *are* the receiver's own, passed through unchanged the way
3493/// `praxis_vec_reversed` passes its one through, null included.
3494///
3495/// # Safety
3496/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; every
3497/// range `groups` yields must lie within its length.
3498unsafe fn vec_of_groups(
3499    ctx: *mut RuntimeContext,
3500    vec: GcRef,
3501    groups: impl Iterator<Item = (usize, usize)>,
3502) -> GcRef {
3503    // SAFETY: caller guarantees `vec` is a valid Vec.
3504    let element_descriptor = unsafe { vec_payload(vec) }.element_descriptor;
3505    let outer = unsafe { praxis_vec_new(ctx, &crate::collections::VEC as *const _) };
3506    let scope = unsafe { NativeScope::new(ctx) };
3507    let op = unsafe { vec_payload_mut(scope.root(outer)) };
3508    for (start, end) in groups {
3509        // The elements are read out of the receiver, which the caller's shadow
3510        // frame roots across this call, so the untraced `Vec<GcRef>` below holds
3511        // nothing a collection inside `vec_of` could reclaim — the receiver
3512        // holds every one of them too. That is `praxis_vec_unique`'s argument at
3513        // a second site, and it is why the *groups* are what need rooting and
3514        // the items are not: an inner `Vec` is reachable from nothing until it
3515        // is pushed, which is why it is pushed before the next one is built.
3516        //
3517        // SAFETY: caller guarantees `vec` is a valid Vec and that `start..end`
3518        // lies within its length.
3519        let items: Vec<GcRef> = unsafe { vec_payload(vec) }.items[start..end].to_vec();
3520        let inner = unsafe { vec_of(ctx, element_descriptor, items.into_iter()) };
3521        op.items.push(inner);
3522    }
3523    outer
3524}
3525
3526/// `seq.chunks(n)` — these elements in consecutive non-overlapping runs of `n`,
3527/// the last short if the length does not divide (ADR-149). The receiver is not
3528/// touched.
3529///
3530/// `[1, 2, 3, 4, 5].chunks(2)` is `[[1, 2], [3, 4], [5]]`. An empty receiver
3531/// answers `[]` at any size, and an `n` at or above the length answers one chunk
3532/// holding everything.
3533///
3534/// Raises `FaultKind::InvalidSize` and answers Unit when `n` is not positive —
3535/// [`group_size`] has the reason, and it is the wrapper's whole faulting
3536/// surface. It reads **no descriptor callback**, so unlike `sorted` there is no
3537/// element it can be handed that it cannot group.
3538///
3539/// # Safety
3540/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `n` a
3541/// valid `Int` `GcRef`.
3542#[unsafe(no_mangle)]
3543pub unsafe extern "C" fn praxis_vec_chunks(
3544    ctx: *mut RuntimeContext,
3545    vec: GcRef,
3546    n: GcRef,
3547) -> GcRef {
3548    abi_guard!("praxis_vec_chunks", ctx, {
3549        // SAFETY: caller guarantees `n` is a valid Int.
3550        let Some(size) = group_size(unsafe { int_payload(n) }) else {
3551            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
3552            return unsafe { unit_sentinel(ctx) };
3553        };
3554        // SAFETY: caller guarantees `vec` is a valid Vec.
3555        let len = unsafe { vec_payload(vec) }.items.len();
3556        // Every `size`th position starts a chunk; the last one stops at the end
3557        // rather than past it, which is the short tail.
3558        let groups = (0..len)
3559            .step_by(size)
3560            .map(move |s| (s, (s + size).min(len)));
3561        unsafe { vec_of_groups(ctx, vec, groups) }
3562    })
3563}
3564
3565/// `seq.windows(n)` — every consecutive run of exactly `n`, each starting one
3566/// element after the last (ADR-149). The receiver is not touched.
3567///
3568/// `[1, 2, 3, 4].windows(2)` is `[[1, 2], [2, 3], [3, 4]]`. Elements are shared,
3569/// not copied: the `2` in the first window and the `2` in the second are one
3570/// object, which is the language's reference semantics rather than a rule of
3571/// this wrapper.
3572///
3573/// **A window that does not fit is dropped rather than shortened**, which is the
3574/// one place this and [`praxis_vec_chunks`] answer differently: `[1, 2].windows(5)`
3575/// is `[]`, because a run of five is a run of five. It is not the fault below
3576/// arriving late — "which runs of five are there" has an answer for a sequence
3577/// of two, and that answer is none.
3578///
3579/// Raises `FaultKind::InvalidSize` and answers Unit when `n` is not positive,
3580/// for [`praxis_vec_chunks`]'s reason.
3581///
3582/// # Safety
3583/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `n` a
3584/// valid `Int` `GcRef`.
3585#[unsafe(no_mangle)]
3586pub unsafe extern "C" fn praxis_vec_windows(
3587    ctx: *mut RuntimeContext,
3588    vec: GcRef,
3589    n: GcRef,
3590) -> GcRef {
3591    abi_guard!("praxis_vec_windows", ctx, {
3592        // SAFETY: caller guarantees `n` is a valid Int.
3593        let Some(size) = group_size(unsafe { int_payload(n) }) else {
3594            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
3595            return unsafe { unit_sentinel(ctx) };
3596        };
3597        // SAFETY: caller guarantees `vec` is a valid Vec.
3598        let len = unsafe { vec_payload(vec) }.items.len();
3599        // Written as a subtraction guarded by its own comparison rather than a
3600        // `saturating_sub`: `len - size` saturating to zero would answer *one*
3601        // window for a receiver too short to hold any, and the empty answer is
3602        // the whole point of the branch.
3603        let starts = if size <= len { len - size + 1 } else { 0 };
3604        let groups = (0..starts).map(move |s| (s, s + size));
3605        unsafe { vec_of_groups(ctx, vec, groups) }
3606    })
3607}
3608
3609/// `seq.join(sep)` — these `Text` elements concatenated with `sep` between them
3610/// (ADR-144). An empty sequence answers `""`; a one-element sequence answers
3611/// that element's characters and no separator.
3612///
3613/// Raises `FaultKind::TypeMismatch` and answers Unit when an element is not a
3614/// `Text`. The catalog row bounds the item to `Text`, so reaching that is a
3615/// compiler bug — reported the way `praxis_vec_sorted` reports its own, rather
3616/// than reading a foreign payload as a pointer-and-length pair.
3617///
3618/// This does **not** render: a `Vec[Int]` is refused at `praxis check` rather
3619/// than stringified here, which is what keeps `join` from being a back door
3620/// around ADR-143's decision about which types have a `to_text`.
3621///
3622/// # Safety
3623/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `sep` a
3624/// valid `Text` `GcRef`.
3625#[unsafe(no_mangle)]
3626pub unsafe extern "C" fn praxis_vec_join(
3627    ctx: *mut RuntimeContext,
3628    vec: GcRef,
3629    sep: GcRef,
3630) -> GcRef {
3631    abi_guard!("praxis_vec_join", ctx, {
3632        // SAFETY: caller guarantees `vec` is a valid Vec and `sep` a valid Text.
3633        let p = unsafe { vec_payload(vec) };
3634        if !p
3635            .items
3636            .iter()
3637            .all(|item| std::ptr::eq(item.descriptor(), &crate::text::TEXT))
3638        {
3639            unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3640            return unsafe { unit_sentinel(ctx) };
3641        }
3642        let separator = unsafe { text_str(sep) };
3643        let mut joined = String::new();
3644        for (i, item) in p.items.iter().enumerate() {
3645            if i > 0 {
3646                joined.push_str(separator);
3647            }
3648            // SAFETY: the loop above proved every element's descriptor is TEXT.
3649            joined.push_str(unsafe { text_str(*item) });
3650        }
3651        // SAFETY: `joined` is valid UTF-8; ctx/heap valid.
3652        unsafe { text_ref(ctx, joined) }
3653    })
3654}
3655
3656/// `chars.to_text()` — these `Char`s as one `Text`, with nothing between them
3657/// (ADR-144). The inverse of walking a `Text`, and what renders a `Grid` row
3658/// back as the line it was read from.
3659///
3660/// Each code point is read through [`read_scalar`] with the `Char` handle, never
3661/// a bare payload read: the payload is **four** bytes and an `i64` read would
3662/// take eight of them. A foreign element is `TypeMismatch` and the Unit
3663/// sentinel, for [`praxis_vec_join`]'s reason.
3664///
3665/// # Safety
3666/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3667#[unsafe(no_mangle)]
3668pub unsafe extern "C" fn praxis_vec_to_text(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3669    abi_guard!("praxis_vec_to_text", ctx, {
3670        // SAFETY: caller guarantees `vec` is a valid Vec.
3671        let p = unsafe { vec_payload(vec) };
3672        let mut rendered = String::new();
3673        for item in &p.items {
3674            // SAFETY: `read_scalar` proves the descriptor is `CHAR` before
3675            // reading its four bytes, and answers `None` otherwise.
3676            let Some(code) = (unsafe { read_scalar(*item, scalars::CHAR_PAYLOAD) }) else {
3677                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3678                return unsafe { unit_sentinel(ctx) };
3679            };
3680            // The descriptor's own writer, so a line rebuilt from a `Grid` row
3681            // holds the characters `out` would have written one at a time.
3682            scalars::write_char(&mut rendered, code);
3683        }
3684        // SAFETY: `rendered` is valid UTF-8; ctx/heap valid.
3685        unsafe { text_ref(ctx, rendered) }
3686    })
3687}
3688
3689/// `v.frequencies()` — a `Counter[T]` holding how many times each element of
3690/// `vec` occurs (§6.3, §6.2).
3691///
3692/// The first combinator whose result is a **keyed** collection, which is why the
3693/// catalog row carries a `HashStable` bound of its own:
3694/// `require_collection_invariants` is applied to a method's *receiver*, and the
3695/// receiver here is an ordinary `Vec` that may legitimately hold anything. It is
3696/// the result that has a key rule.
3697///
3698/// The counter's key descriptor is the source `Vec`'s element label, which may
3699/// be null when the construction site knew no element type — the same null
3700/// [`praxis_counter_new`] already accepts and means "not told yet".
3701///
3702/// # Safety
3703/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3704#[unsafe(no_mangle)]
3705pub unsafe extern "C" fn praxis_vec_frequencies(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3706    abi_guard!("praxis_vec_frequencies", ctx, {
3707        let scope = unsafe { NativeScope::new(ctx) };
3708        // The receiver is rooted **explicitly**, unlike the one-allocation
3709        // wrappers around it. The keys below are `GcRef`s into this `Vec`'s
3710        // items and they are held across one allocation *per distinct element*,
3711        // which is a much longer window than `praxis_set_items`' single
3712        // `vec_of`; relying on the caller's shadow frame alone for that long is
3713        // an assumption worth not making.
3714        let _receiver = scope.root(vec);
3715        // SAFETY: caller guarantees `vec` is a valid Vec.
3716        let p = unsafe { vec_payload(vec) };
3717        // Count first, with no allocation at all, so the tally cannot be
3718        // disturbed by a collection mid-loop. The tally is a `Vec` with a side
3719        // index rather than a bare map so the counts come out in
3720        // first-occurrence order, which makes the *allocation* order a function
3721        // of the input; the `Counter` itself is unordered either way.
3722        let mut counts: Vec<(DynamicKey, i64)> = Vec::new();
3723        let mut index: std::collections::HashMap<DynamicKey, usize> =
3724            std::collections::HashMap::new();
3725        for item in &p.items {
3726            let key = DynamicKey::new(*item);
3727            match index.get(&key) {
3728                Some(at) => counts[*at].1 += 1,
3729                None => {
3730                    index.insert(key, counts.len());
3731                    counts.push((key, 1));
3732                }
3733            }
3734        }
3735        let counter = unsafe { praxis_counter_new(ctx, p.element_descriptor) };
3736        let rooted = scope.root(counter);
3737        for (key, count) in counts {
3738            // Allocate first, then take the payload borrow — the boxed `Int`
3739            // allocation can collect, and the counter has to be reachable
3740            // through the native root store rather than through a `&mut` this
3741            // frame is holding across it.
3742            let boxed = unsafe { int_ref(ctx, count) };
3743            unsafe { counter_payload_mut(rooted) }
3744                .entries
3745                .insert(key, boxed);
3746        }
3747        counter
3748    })
3749}
3750
3751// ---------------------------------------------------------------------------
3752// Deque[T] methods (§6.1). Mirrors the Vec surface but adds the
3753// front/back distinction: `push_front`/`push_back`/`pop_front`/`pop_back`.
3754// `pop_*` fault on an empty deque (§9.1 `EmptyCollection`).
3755// ---------------------------------------------------------------------------
3756
3757use crate::collections::DequePayload;
3758
3759/// Read the `DequePayload` out of a `GcRef` as a shared ref, asserting Deque.
3760///
3761/// # Safety
3762/// `r` must be a valid `Deque` `GcRef`.
3763unsafe fn deque_payload(r: GcRef) -> &'static DequePayload {
3764    // SAFETY: caller guarantees `r` is a Deque; see `payload_ref`.
3765    unsafe { payload_ref::<DequePayload>(r) }
3766}
3767
3768/// Read the `DequePayload` out of a `GcRef` as a mutable ref, asserting Deque.
3769///
3770/// # Safety
3771/// `r` must be a valid `Deque` `GcRef`, rooted for `'s`.
3772unsafe fn deque_payload_mut<'s>(r: Rooted<'s>) -> &'s mut DequePayload {
3773    // SAFETY: caller guarantees `r` is a Deque; see `payload_mut`.
3774    unsafe { payload_mut::<DequePayload>(r) }
3775}
3776
3777/// Allocate a new empty `Deque[T]` with the given element descriptor (§11.2).
3778/// A null descriptor stays null — "not told yet" — exactly as `praxis_vec_new`.
3779///
3780/// # Safety
3781/// `ctx` must be live and wired. `element_descriptor` must be a valid pointer to
3782/// a `'static TypeDescriptor` (or null).
3783#[unsafe(no_mangle)]
3784pub unsafe extern "C" fn praxis_deque_new(
3785    ctx: *mut RuntimeContext,
3786    element_descriptor: *const TypeDescriptor,
3787) -> GcRef {
3788    abi_guard!("praxis_deque_new", ctx, {
3789        // SAFETY: DequePayload is DEQUE's payload type.
3790        unsafe {
3791            gc_alloc_owned(ctx, &crate::collections::DEQUE, || DequePayload {
3792                element_descriptor,
3793                items: std::collections::VecDeque::new(),
3794            })
3795        }
3796    })
3797}
3798
3799/// Prepend `value` to the front of `deque`; returns Unit (§6.1).
3800///
3801/// # Safety
3802/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
3803/// `value` must be a valid `GcRef`.
3804#[unsafe(no_mangle)]
3805pub unsafe extern "C" fn praxis_deque_push_front(
3806    ctx: *mut RuntimeContext,
3807    deque: GcRef,
3808    value: GcRef,
3809) -> GcRef {
3810    abi_guard!("praxis_deque_push_front", ctx, {
3811        unsafe { maybe_collect(ctx) };
3812        let scope = unsafe { NativeScope::new(ctx) };
3813        let p = unsafe { deque_payload_mut(scope.root(deque)) };
3814        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3815            return unsafe { unit_sentinel(ctx) };
3816        }
3817        let before = p.owned_bytes();
3818        p.items.push_front(value);
3819        charge_growth(ctx, before, p.owned_bytes());
3820        unsafe { unit_sentinel(ctx) }
3821    })
3822}
3823
3824/// Append `value` to the back of `deque`; returns Unit (§6.1).
3825///
3826/// # Safety
3827/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
3828/// `value` must be a valid `GcRef`.
3829#[unsafe(no_mangle)]
3830pub unsafe extern "C" fn praxis_deque_push_back(
3831    ctx: *mut RuntimeContext,
3832    deque: GcRef,
3833    value: GcRef,
3834) -> GcRef {
3835    abi_guard!("praxis_deque_push_back", ctx, {
3836        unsafe { maybe_collect(ctx) };
3837        let scope = unsafe { NativeScope::new(ctx) };
3838        let p = unsafe { deque_payload_mut(scope.root(deque)) };
3839        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3840            return unsafe { unit_sentinel(ctx) };
3841        }
3842        let before = p.owned_bytes();
3843        p.items.push_back(value);
3844        charge_growth(ctx, before, p.owned_bytes());
3845        unsafe { unit_sentinel(ctx) }
3846    })
3847}
3848
3849/// Remove and return the front element; faults `EmptyCollection` if empty.
3850///
3851/// # Safety
3852/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
3853#[unsafe(no_mangle)]
3854pub unsafe extern "C" fn praxis_deque_pop_front(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
3855    abi_guard!("praxis_deque_pop_front", ctx, {
3856        // No allocation in the common case, but `pop_front` on a VecDeque does not
3857        // allocate Rust heap, so no collection is needed; `deque` stays live.
3858        let scope = unsafe { NativeScope::new(ctx) };
3859        let p = unsafe { deque_payload_mut(scope.root(deque)) };
3860        match p.items.pop_front() {
3861            Some(v) => v,
3862            None => {
3863                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
3864                unsafe { unit_sentinel(ctx) }
3865            }
3866        }
3867    })
3868}
3869
3870/// Remove and return the back element; faults `EmptyCollection` if empty.
3871///
3872/// # Safety
3873/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
3874#[unsafe(no_mangle)]
3875pub unsafe extern "C" fn praxis_deque_pop_back(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
3876    abi_guard!("praxis_deque_pop_back", ctx, {
3877        let scope = unsafe { NativeScope::new(ctx) };
3878        let p = unsafe { deque_payload_mut(scope.root(deque)) };
3879        match p.items.pop_back() {
3880            Some(v) => v,
3881            None => {
3882                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
3883                unsafe { unit_sentinel(ctx) }
3884            }
3885        }
3886    })
3887}
3888
3889/// The number of elements in `deque`, as a boxed `Int` (§6.1).
3890///
3891/// # Safety
3892/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
3893#[unsafe(no_mangle)]
3894pub unsafe extern "C" fn praxis_deque_len(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
3895    abi_guard!("praxis_deque_len", ctx, {
3896        let p = unsafe { deque_payload(deque) };
3897        let len = p.items.len() as i64;
3898        unsafe { int_ref(ctx, len) }
3899    })
3900}
3901
3902/// The element at `index` (0-based from the front); faults `IndexOutOfBounds`.
3903///
3904/// # Safety
3905/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
3906/// `index` must be a valid `Int` `GcRef`.
3907#[unsafe(no_mangle)]
3908pub unsafe extern "C" fn praxis_deque_get(
3909    ctx: *mut RuntimeContext,
3910    deque: GcRef,
3911    index: GcRef,
3912) -> GcRef {
3913    abi_guard!("praxis_deque_get", ctx, {
3914        let p = unsafe { deque_payload(deque) };
3915        let idx = unsafe { int_payload(index) };
3916        let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3917            return unsafe { unit_sentinel(ctx) };
3918        };
3919        p.items[idx]
3920    })
3921}
3922
3923/// Replace the element at `index` (0-based from the front); faults
3924/// `IndexOutOfBounds` if out of range. Returns the Unit sentinel.
3925///
3926/// A replacement and never an insertion, and the element descriptor is
3927/// reconciled the same way, for [`praxis_vec_set`]'s reasons.
3928///
3929/// # Safety
3930/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
3931/// `index` must be a valid `Int` `GcRef`; `value` must be a valid `GcRef`.
3932#[unsafe(no_mangle)]
3933pub unsafe extern "C" fn praxis_deque_set(
3934    ctx: *mut RuntimeContext,
3935    deque: GcRef,
3936    index: GcRef,
3937    value: GcRef,
3938) -> GcRef {
3939    abi_guard!("praxis_deque_set", ctx, {
3940        let scope = unsafe { NativeScope::new(ctx) };
3941        let p = unsafe { deque_payload_mut(scope.root(deque)) };
3942        let idx = unsafe { int_payload(index) };
3943        let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3944            return unsafe { unit_sentinel(ctx) };
3945        };
3946        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3947            return unsafe { unit_sentinel(ctx) };
3948        }
3949        p.items[idx] = value;
3950        unsafe { unit_sentinel(ctx) }
3951    })
3952}
3953
3954/// True iff `deque` has no elements, as a boxed `Bool` (§6.1).
3955///
3956/// # Safety
3957/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
3958#[unsafe(no_mangle)]
3959pub unsafe extern "C" fn praxis_deque_is_empty(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
3960    abi_guard!("praxis_deque_is_empty", ctx, {
3961        let p = unsafe { deque_payload(deque) };
3962        let empty = p.items.is_empty();
3963        unsafe { bool_ref(ctx, empty) }
3964    })
3965}
3966
3967// ---------------------------------------------------------------------------
3968// Map[K, V] / Set[T] / Counter[T] (§6.1, §11.3).
3969//
3970// All three reuse Rust hash collections behind opaque GC objects. Keys are
3971// wrapped in `DynamicKey`, which delegates Rust `Hash`/`Eq` to the descriptor's
3972// structural callbacks — this is what makes tuples/records/enums/nested
3973// collections work as keys (§19.7 criterion). Counter's absent keys read as
3974// zero (§6.2); `min=`/`max=` update a map entry in place (§6.2).
3975// ---------------------------------------------------------------------------
3976
3977use crate::maps::{CounterPayload, MapPayload, SetPayload};
3978
3979/// Read a `MapPayload` as a shared ref. See `payload_ref` for the safety model.
3980unsafe fn map_payload(r: GcRef) -> &'static MapPayload {
3981    unsafe { payload_ref::<MapPayload>(r) }
3982}
3983
3984unsafe fn map_payload_mut<'s>(r: Rooted<'s>) -> &'s mut MapPayload {
3985    unsafe { payload_mut::<MapPayload>(r) }
3986}
3987
3988unsafe fn set_payload(r: GcRef) -> &'static SetPayload {
3989    unsafe { payload_ref::<SetPayload>(r) }
3990}
3991
3992unsafe fn set_payload_mut<'s>(r: Rooted<'s>) -> &'s mut SetPayload {
3993    unsafe { payload_mut::<SetPayload>(r) }
3994}
3995
3996unsafe fn counter_payload(r: GcRef) -> &'static CounterPayload {
3997    unsafe { payload_ref::<CounterPayload>(r) }
3998}
3999
4000unsafe fn counter_payload_mut<'s>(r: Rooted<'s>) -> &'s mut CounterPayload {
4001    unsafe { payload_mut::<CounterPayload>(r) }
4002}
4003
4004/// Allocate an empty `Map[K, V]`. `key_descriptor` is the key type the
4005/// construction site knew, or **null** when it knew none — which is kept null,
4006/// the way `praxis_vec_new` keeps it. Spelling an unknown type `INT` is a claim,
4007/// and every reader that believed it would read the wrong type.
4008///
4009/// # Safety
4010/// `ctx` must be live and wired. `key_descriptor` must be a valid pointer to a
4011/// `'static TypeDescriptor` (or null).
4012#[unsafe(no_mangle)]
4013pub unsafe extern "C" fn praxis_map_new(
4014    ctx: *mut RuntimeContext,
4015    key_descriptor: *const TypeDescriptor,
4016) -> GcRef {
4017    abi_guard!("praxis_map_new", ctx, {
4018        // The `Map` row carries one type argument, so the value type never reaches
4019        // this wrapper at all — it is unknown here by construction, and says so.
4020        // `praxis_map_insert` adopts the first inserted value's own descriptor,
4021        // which is how a `Vec` learns its element type.
4022        let value_descriptor: *const TypeDescriptor = std::ptr::null();
4023        // SAFETY: MapPayload is MAP's payload type.
4024        unsafe {
4025            gc_alloc_owned(ctx, &crate::maps::MAP, || MapPayload {
4026                key_descriptor,
4027                value_descriptor,
4028                entries: std::collections::HashMap::new(),
4029            })
4030        }
4031    })
4032}
4033
4034/// Insert `(key, value)` into `map`, replacing any prior value; returns Unit.
4035///
4036/// # Safety
4037/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`; `key` and
4038/// `value` must be valid `GcRef`s.
4039#[unsafe(no_mangle)]
4040pub unsafe extern "C" fn praxis_map_insert(
4041    ctx: *mut RuntimeContext,
4042    map: GcRef,
4043    key: GcRef,
4044    value: GcRef,
4045) -> GcRef {
4046    abi_guard!("praxis_map_insert", ctx, {
4047        unsafe { maybe_collect(ctx) };
4048        let scope = unsafe { NativeScope::new(ctx) };
4049        let p = unsafe { map_payload_mut(scope.root(map)) };
4050        // Learn the value type from the first value inserted, the way a `Vec`
4051        // learns its element type from the first `push`. Null is the encoding of
4052        // "never been told", so it is distinguishable from a `Map` that really
4053        // holds `Int`s.
4054        //
4055        // A later value of a different type un-learns it rather than faulting: the
4056        // type checker makes a `Map` homogeneous, so this is unreachable for a
4057        // well-typed program, and `praxis_map_insert` is a non-faulting row (its
4058        // caller emits no fault check). Null is now representable and means "the
4059        // value's own descriptor answers", so forgetting is the safe direction.
4060        let val_desc = value.descriptor();
4061        match p.value() {
4062            None => p.value_descriptor = val_desc,
4063            Some(known) if !std::ptr::eq(known, val_desc) => {
4064                p.value_descriptor = std::ptr::null();
4065            }
4066            Some(_) => {}
4067        }
4068        let before = p.owned_bytes();
4069        p.entries.insert(DynamicKey::new(key), value);
4070        charge_growth(ctx, before, p.owned_bytes());
4071        unsafe { unit_sentinel(ctx) }
4072    })
4073}
4074
4075/// `Some(value)` for `key`, or `None` if absent (§4.7, §5.7).
4076///
4077/// §5.7 writes the signature `Map[K,V].get(K) -> Option[V]` and §4.7 opens
4078/// "Option[T] represents normal domain-level absence. It is not an error
4079/// channel." Answering the Unit sentinel under a `V` static type instead would
4080/// hand the program a value it could not distinguish from a real one without
4081/// `contains`, while the type system insisted it was a `V`.
4082///
4083/// The `Option` is built through the runtime's own `option_schema`, whose
4084/// `Some` slot is unknown — `V` is learned from the value found, never from a
4085/// static type — and which `EnumSchema::same_type` therefore recognizes as the
4086/// same type as the codegen's `Option[Int]`. That is what lets the result match
4087/// against arms the program wrote.
4088///
4089/// # Safety
4090/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`; `key`
4091/// must be a valid `GcRef`.
4092#[unsafe(no_mangle)]
4093pub unsafe extern "C" fn praxis_map_get(ctx: *mut RuntimeContext, map: GcRef, key: GcRef) -> GcRef {
4094    abi_guard!("praxis_map_get", ctx, {
4095        let found = {
4096            let p = unsafe { map_payload(map) };
4097            p.entries.get(&DynamicKey::new(key)).copied()
4098        };
4099        match found {
4100            Some(v) => unsafe { option_some(ctx, v) },
4101            None => unsafe { option_none(ctx) },
4102        }
4103    })
4104}
4105
4106/// `map[key]` (§4.7): the value for `key`, **faulting** if it is absent.
4107///
4108/// A different wrapper from [`praxis_map_get`] because the two answers are the
4109/// language's own choice, not an implementation detail: §4.7 says "indexing a
4110/// missing map key faults instead of returning an option… the user chooses
4111/// between explicit absence with `.get` and assertion-like access with
4112/// indexing". Sharing one wrapper would take that choice away from the user.
4113///
4114/// The fault is [`FaultKind::IndexOutOfBounds`](crate::FaultKind::IndexOutOfBounds)
4115/// — an index the collection does not hold, which is what its doc already
4116/// describes. A dedicated `MissingKey` kind would read better, and adding one is
4117/// a `#[repr(C)]` change that costs an ABI bump (ADR-075).
4118///
4119/// # Safety
4120/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`; `key`
4121/// must be a valid `GcRef`.
4122#[unsafe(no_mangle)]
4123pub unsafe extern "C" fn praxis_map_index(
4124    ctx: *mut RuntimeContext,
4125    map: GcRef,
4126    key: GcRef,
4127) -> GcRef {
4128    abi_guard!("praxis_map_index", ctx, {
4129        let p = unsafe { map_payload(map) };
4130        match p.entries.get(&DynamicKey::new(key)) {
4131            Some(v) => *v,
4132            None => {
4133                unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
4134                unsafe { unit_sentinel(ctx) }
4135            }
4136        }
4137    })
4138}
4139
4140/// True iff `key` is present, as a boxed Bool.
4141///
4142/// # Safety
4143/// `ctx` must be live and wired; `map` and `key` must be valid `GcRef`s.
4144#[unsafe(no_mangle)]
4145pub unsafe extern "C" fn praxis_map_contains(
4146    ctx: *mut RuntimeContext,
4147    map: GcRef,
4148    key: GcRef,
4149) -> GcRef {
4150    abi_guard!("praxis_map_contains", ctx, {
4151        let p = unsafe { map_payload(map) };
4152        let present = p.entries.contains_key(&DynamicKey::new(key));
4153        unsafe { bool_ref(ctx, present) }
4154    })
4155}
4156
4157/// Remove `key`; returns Unit (the removed value, if any, is dropped).
4158///
4159/// # Safety
4160/// `ctx` must be live and wired; `map` and `key` must be valid `GcRef`s.
4161#[unsafe(no_mangle)]
4162pub unsafe extern "C" fn praxis_map_remove(
4163    ctx: *mut RuntimeContext,
4164    map: GcRef,
4165    key: GcRef,
4166) -> GcRef {
4167    abi_guard!("praxis_map_remove", ctx, {
4168        let scope = unsafe { NativeScope::new(ctx) };
4169        let p = unsafe { map_payload_mut(scope.root(map)) };
4170        p.entries.remove(&DynamicKey::new(key));
4171        unsafe { unit_sentinel(ctx) }
4172    })
4173}
4174
4175/// The number of entries, as a boxed Int.
4176///
4177/// # Safety
4178/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4179#[unsafe(no_mangle)]
4180pub unsafe extern "C" fn praxis_map_len(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4181    abi_guard!("praxis_map_len", ctx, {
4182        let p = unsafe { map_payload(map) };
4183        unsafe { int_ref(ctx, p.entries.len() as i64) }
4184    })
4185}
4186
4187/// `m.keys()` — every key, as a `Vec[K]`. Ordered like
4188/// [`praxis_counter_keys`], and index-aligned with [`praxis_map_values`].
4189///
4190/// This and `values()` are the only way to enumerate a `Map`: `for kv in m` has
4191/// no lowering.
4192///
4193/// # Safety
4194/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4195#[unsafe(no_mangle)]
4196pub unsafe extern "C" fn praxis_map_keys(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4197    abi_guard!("praxis_map_keys", ctx, {
4198        let key_desc = unsafe { map_payload(map) }.key_descriptor;
4199        let rows = unsafe { crate::maps::ordered_entries(&map_payload(map).entries) };
4200        unsafe { vec_of(ctx, key_desc, rows.into_iter().map(|(k, _)| k)) }
4201    })
4202}
4203
4204/// `m.values()` — every value, as a `Vec[V]`. See [`praxis_map_keys`].
4205///
4206/// # Safety
4207/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4208#[unsafe(no_mangle)]
4209pub unsafe extern "C" fn praxis_map_values(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4210    abi_guard!("praxis_map_values", ctx, {
4211        let val_desc = unsafe { map_payload(map) }.value_descriptor;
4212        let rows = unsafe { crate::maps::ordered_entries(&map_payload(map).entries) };
4213        unsafe { vec_of(ctx, val_desc, rows.into_iter().map(|(_, v)| v)) }
4214    })
4215}
4216
4217/// True iff the map is empty, as a boxed Bool.
4218///
4219/// # Safety
4220/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4221#[unsafe(no_mangle)]
4222pub unsafe extern "C" fn praxis_map_is_empty(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4223    abi_guard!("praxis_map_is_empty", ctx, {
4224        let p = unsafe { map_payload(map) };
4225        unsafe { bool_ref(ctx, p.entries.is_empty()) }
4226    })
4227}
4228
4229/// `distance[key] min= candidate` (§6.2): keep the smaller value, or insert if
4230/// absent (an absent entry accepts the first value). The value must support
4231/// ordering (Int); returns Unit.
4232///
4233/// # Safety
4234/// `ctx` must be live and wired; `map`, `key`, `value` must be valid `GcRef`s
4235/// and `value` must be an `Int`.
4236#[unsafe(no_mangle)]
4237pub unsafe extern "C" fn praxis_map_update_min(
4238    ctx: *mut RuntimeContext,
4239    map: GcRef,
4240    key: GcRef,
4241    value: GcRef,
4242) -> GcRef {
4243    abi_guard!("praxis_map_update_min", ctx, {
4244        unsafe { maybe_collect(ctx) };
4245        let scope = unsafe { NativeScope::new(ctx) };
4246        let p = unsafe { map_payload_mut(scope.root(map)) };
4247        let cand = unsafe { int_payload(value) };
4248        match p.entries.get_mut(&DynamicKey::new(key)) {
4249            Some(existing) => {
4250                let cur = unsafe { int_payload(*existing) };
4251                if cand < cur {
4252                    *existing = value;
4253                }
4254            }
4255            None => {
4256                p.entries.insert(DynamicKey::new(key), value);
4257            }
4258        }
4259        unsafe { unit_sentinel(ctx) }
4260    })
4261}
4262
4263/// `best[key] max= score` (§6.2): keep the larger value, or insert if absent.
4264///
4265/// # Safety
4266/// `ctx` must be live and wired; `map`, `key`, `value` must be valid `GcRef`s
4267/// and `value` must be an `Int`.
4268#[unsafe(no_mangle)]
4269pub unsafe extern "C" fn praxis_map_update_max(
4270    ctx: *mut RuntimeContext,
4271    map: GcRef,
4272    key: GcRef,
4273    value: GcRef,
4274) -> GcRef {
4275    abi_guard!("praxis_map_update_max", ctx, {
4276        unsafe { maybe_collect(ctx) };
4277        let scope = unsafe { NativeScope::new(ctx) };
4278        let p = unsafe { map_payload_mut(scope.root(map)) };
4279        let cand = unsafe { int_payload(value) };
4280        match p.entries.get_mut(&DynamicKey::new(key)) {
4281            Some(existing) => {
4282                let cur = unsafe { int_payload(*existing) };
4283                if cand > cur {
4284                    *existing = value;
4285                }
4286            }
4287            None => {
4288                p.entries.insert(DynamicKey::new(key), value);
4289            }
4290        }
4291        unsafe { unit_sentinel(ctx) }
4292    })
4293}
4294
4295// --- Set[T] -----------------------------------------------------------------
4296
4297/// Allocate an empty `Set[T]`. `element_descriptor` is the element type the
4298/// construction site knew, or **null** when it knew none — kept null.
4299///
4300/// # Safety
4301/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
4302/// a `'static TypeDescriptor` (or null).
4303#[unsafe(no_mangle)]
4304pub unsafe extern "C" fn praxis_set_new(
4305    ctx: *mut RuntimeContext,
4306    element_descriptor: *const TypeDescriptor,
4307) -> GcRef {
4308    abi_guard!("praxis_set_new", ctx, {
4309        // SAFETY: SetPayload is SET's payload type.
4310        unsafe {
4311            gc_alloc_owned(ctx, &crate::maps::SET, || SetPayload {
4312                element_descriptor,
4313                entries: std::collections::HashSet::new(),
4314            })
4315        }
4316    })
4317}
4318
4319/// Insert `value` into `set`; returns Unit.
4320///
4321/// # Safety
4322/// `ctx` must be live and wired; `set` and `value` must be valid `GcRef`s.
4323#[unsafe(no_mangle)]
4324pub unsafe extern "C" fn praxis_set_insert(
4325    ctx: *mut RuntimeContext,
4326    set: GcRef,
4327    value: GcRef,
4328) -> GcRef {
4329    abi_guard!("praxis_set_insert", ctx, {
4330        unsafe { maybe_collect(ctx) };
4331        let scope = unsafe { NativeScope::new(ctx) };
4332        let p = unsafe { set_payload_mut(scope.root(set)) };
4333        let before = p.owned_bytes();
4334        p.entries.insert(DynamicKey::new(value));
4335        charge_growth(ctx, before, p.owned_bytes());
4336        unsafe { unit_sentinel(ctx) }
4337    })
4338}
4339
4340/// Remove `value` from `set`; returns Unit.
4341///
4342/// # Safety
4343/// `ctx` must be live and wired; `set` and `value` must be valid `GcRef`s.
4344#[unsafe(no_mangle)]
4345pub unsafe extern "C" fn praxis_set_remove(
4346    ctx: *mut RuntimeContext,
4347    set: GcRef,
4348    value: GcRef,
4349) -> GcRef {
4350    abi_guard!("praxis_set_remove", ctx, {
4351        let scope = unsafe { NativeScope::new(ctx) };
4352        let p = unsafe { set_payload_mut(scope.root(set)) };
4353        p.entries.remove(&DynamicKey::new(value));
4354        unsafe { unit_sentinel(ctx) }
4355    })
4356}
4357
4358/// True iff `value` is in the set, as a boxed Bool.
4359///
4360/// # Safety
4361/// `ctx` must be live and wired; `set` and `value` must be valid `GcRef`s.
4362#[unsafe(no_mangle)]
4363pub unsafe extern "C" fn praxis_set_contains(
4364    ctx: *mut RuntimeContext,
4365    set: GcRef,
4366    value: GcRef,
4367) -> GcRef {
4368    abi_guard!("praxis_set_contains", ctx, {
4369        let p = unsafe { set_payload(set) };
4370        let present = p.entries.contains(&DynamicKey::new(value));
4371        unsafe { bool_ref(ctx, present) }
4372    })
4373}
4374
4375/// The number of elements, as a boxed Int.
4376///
4377/// # Safety
4378/// `ctx` must be live and wired; `set` must be a valid `Set` `GcRef`.
4379#[unsafe(no_mangle)]
4380pub unsafe extern "C" fn praxis_set_len(ctx: *mut RuntimeContext, set: GcRef) -> GcRef {
4381    abi_guard!("praxis_set_len", ctx, {
4382        let p = unsafe { set_payload(set) };
4383        unsafe { int_ref(ctx, p.entries.len() as i64) }
4384    })
4385}
4386
4387/// True iff the set is empty, as a boxed Bool.
4388///
4389/// # Safety
4390/// `ctx` must be live and wired; `set` must be a valid `Set` `GcRef`.
4391#[unsafe(no_mangle)]
4392pub unsafe extern "C" fn praxis_set_is_empty(ctx: *mut RuntimeContext, set: GcRef) -> GcRef {
4393    abi_guard!("praxis_set_is_empty", ctx, {
4394        let p = unsafe { set_payload(set) };
4395        unsafe { bool_ref(ctx, p.entries.is_empty()) }
4396    })
4397}
4398
4399/// Every member, as a `Vec[T]` in [`crate::maps::ordered_members`] order — the
4400/// snapshot `for x in s` iterates (ADR-066).
4401///
4402/// There is no `praxis_set_get`, and this is why: a `HashSet` has no nth member,
4403/// so an indexed accessor would be a linear scan per step and the loop would be
4404/// quadratic. The snapshot is one pass, and it is what makes the order
4405/// deterministic — which for `for` is the program's *answer* and not only its
4406/// printing.
4407///
4408/// # Safety
4409/// `ctx` must be live and wired; `set` must be a valid `Set` `GcRef`.
4410#[unsafe(no_mangle)]
4411pub unsafe extern "C" fn praxis_set_items(ctx: *mut RuntimeContext, set: GcRef) -> GcRef {
4412    abi_guard!("praxis_set_items", ctx, {
4413        let elem_desc = unsafe { set_payload(set) }.element_descriptor;
4414        let members = unsafe { crate::maps::ordered_members(&set_payload(set).entries) };
4415        unsafe { vec_of(ctx, elem_desc, members.into_iter()) }
4416    })
4417}
4418
4419// --- Counter[T] -------------------------------------------------------------
4420
4421/// Allocate an empty `Counter[T]`. `key_descriptor` is the key type the
4422/// construction site knew, or **null** when it knew none — kept null.
4423///
4424/// # Safety
4425/// `ctx` must be live and wired; `key_descriptor` must be a valid pointer to a
4426/// `'static TypeDescriptor` (or null).
4427#[unsafe(no_mangle)]
4428pub unsafe extern "C" fn praxis_counter_new(
4429    ctx: *mut RuntimeContext,
4430    key_descriptor: *const TypeDescriptor,
4431) -> GcRef {
4432    abi_guard!("praxis_counter_new", ctx, {
4433        // SAFETY: CounterPayload is COUNTER's payload type.
4434        unsafe {
4435            gc_alloc_owned(ctx, &crate::maps::COUNTER, || CounterPayload {
4436                key_descriptor,
4437                entries: std::collections::HashMap::new(),
4438            })
4439        }
4440    })
4441}
4442
4443/// The count for `key`, or zero if absent (§6.2: "absent values read as zero").
4444/// Never faults. Returns a boxed Int.
4445///
4446/// # Safety
4447/// `ctx` must be live and wired; `counter` and `key` must be valid `GcRef`s.
4448#[unsafe(no_mangle)]
4449pub unsafe extern "C" fn praxis_counter_get(
4450    ctx: *mut RuntimeContext,
4451    counter: GcRef,
4452    key: GcRef,
4453) -> GcRef {
4454    abi_guard!("praxis_counter_get", ctx, {
4455        let p = unsafe { counter_payload(counter) };
4456        let count = match p.entries.get(&DynamicKey::new(key)) {
4457            Some(v) => unsafe { int_payload(*v) },
4458            None => 0, // §6.2: absent reads as zero.
4459        };
4460        unsafe { int_ref(ctx, count) }
4461    })
4462}
4463
4464/// Increment the count for `key` by one (inserting 1 if absent); returns Unit.
4465///
4466/// # Safety
4467/// `ctx` must be live and wired; `counter` and `key` must be valid `GcRef`s.
4468#[unsafe(no_mangle)]
4469pub unsafe extern "C" fn praxis_counter_inc(
4470    ctx: *mut RuntimeContext,
4471    counter: GcRef,
4472    key: GcRef,
4473) -> GcRef {
4474    abi_guard!("praxis_counter_inc", ctx, {
4475        let scope = unsafe { NativeScope::new(ctx) };
4476        let p = unsafe { counter_payload_mut(scope.root(counter)) };
4477        let dk = DynamicKey::new(key);
4478        match p.entries.get_mut(&dk) {
4479            Some(v) => {
4480                let cur = unsafe { int_payload(*v) };
4481                // Checked, like every other integer computation in this file
4482                // (§4.12): a raw `cur + 1` panics across `extern "C"` in debug — the
4483                // non-unwinding panic §10.4 forbids — and wraps to `i64::MIN` in
4484                // release, which is a silently wrong count. A `Counter`'s values are
4485                // set to arbitrary `Int`s by `c[k] = n`, so this is reachable from
4486                // source and not only from `i64::MAX` increments.
4487                let Some(next) = cur.checked_add(1) else {
4488                    unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
4489                    return unsafe { unit_sentinel(ctx) };
4490                };
4491                // SAFETY: ctx is wired; alloc a fresh Int for the incremented value.
4492                *v = unsafe { int_ref(ctx, next) };
4493            }
4494            None => {
4495                let one = unsafe { int_ref(ctx, 1_i64) };
4496                p.entries.insert(dk, one);
4497            }
4498        }
4499        unsafe { unit_sentinel(ctx) }
4500    })
4501}
4502
4503/// `counts[key] = value` (§6.2): set the count for `key`, replacing any prior
4504/// one; returns Unit.
4505///
4506/// [`praxis_counter_inc`] adds exactly one, so it cannot express
4507/// `counts[key] += n` or `counts[key] = n`. A subscript assignment is a
4508/// read-modify-write over the pair (`praxis_counter_get`, this), which is what
4509/// makes every assignment operator work on a `Counter` rather than only `+= 1`.
4510///
4511/// # Safety
4512/// `ctx` must be live and wired; `counter` and `key` must be valid `GcRef`s and
4513/// `value` must be an `Int`.
4514#[unsafe(no_mangle)]
4515pub unsafe extern "C" fn praxis_counter_set(
4516    ctx: *mut RuntimeContext,
4517    counter: GcRef,
4518    key: GcRef,
4519    value: GcRef,
4520) -> GcRef {
4521    abi_guard!("praxis_counter_set", ctx, {
4522        unsafe { maybe_collect(ctx) };
4523        let scope = unsafe { NativeScope::new(ctx) };
4524        let p = unsafe { counter_payload_mut(scope.root(counter)) };
4525        let before = p.owned_bytes();
4526        p.entries.insert(DynamicKey::new(key), value);
4527        charge_growth(ctx, before, p.owned_bytes());
4528        unsafe { unit_sentinel(ctx) }
4529    })
4530}
4531
4532/// `c.keys()` — every key, as a `Vec[T]`.
4533///
4534/// Ordered by the key's own `compare` (ADR-138), so it is the *same* order
4535/// [`praxis_counter_values`] uses and the two are index-aligned. A `HashMap`'s
4536/// own order is randomized per process, so returning it would make the same
4537/// program answer differently on two runs — and here the order is the program's
4538/// *answer*, not only its printing.
4539///
4540/// # Safety
4541/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4542#[unsafe(no_mangle)]
4543pub unsafe extern "C" fn praxis_counter_keys(ctx: *mut RuntimeContext, counter: GcRef) -> GcRef {
4544    abi_guard!("praxis_counter_keys", ctx, {
4545        let key_desc = unsafe { counter_payload(counter) }.key_descriptor;
4546        let rows = unsafe { crate::maps::ordered_entries(&counter_payload(counter).entries) };
4547        unsafe { vec_of(ctx, key_desc, rows.into_iter().map(|(k, _)| k)) }
4548    })
4549}
4550
4551/// `c.values()` — every count, as a `Vec[Int]`.
4552///
4553/// §3.3's representative program is `counts.values().count(|n| n >= 2)`. Ordered
4554/// like [`praxis_counter_keys`]; see it for why the order is fixed.
4555///
4556/// # Safety
4557/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4558#[unsafe(no_mangle)]
4559pub unsafe extern "C" fn praxis_counter_values(ctx: *mut RuntimeContext, counter: GcRef) -> GcRef {
4560    abi_guard!("praxis_counter_values", ctx, {
4561        let rows = unsafe { crate::maps::ordered_entries(&counter_payload(counter).entries) };
4562        unsafe { vec_of(ctx, &scalars::INT, rows.into_iter().map(|(_, v)| v)) }
4563    })
4564}
4565
4566/// The number of distinct keys, as a boxed Int.
4567///
4568/// # Safety
4569/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4570#[unsafe(no_mangle)]
4571pub unsafe extern "C" fn praxis_counter_len(ctx: *mut RuntimeContext, counter: GcRef) -> GcRef {
4572    abi_guard!("praxis_counter_len", ctx, {
4573        let p = unsafe { counter_payload(counter) };
4574        unsafe { int_ref(ctx, p.entries.len() as i64) }
4575    })
4576}
4577
4578/// True iff the counter has no keys, as a boxed Bool.
4579///
4580/// # Safety
4581/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4582#[unsafe(no_mangle)]
4583pub unsafe extern "C" fn praxis_counter_is_empty(
4584    ctx: *mut RuntimeContext,
4585    counter: GcRef,
4586) -> GcRef {
4587    abi_guard!("praxis_counter_is_empty", ctx, {
4588        let p = unsafe { counter_payload(counter) };
4589        unsafe { bool_ref(ctx, p.entries.is_empty()) }
4590    })
4591}
4592
4593// ---------------------------------------------------------------------------
4594// MinHeap[T] / MaxHeap[T] (§6.1, §11.2).
4595//
4596// `MaxHeap` maps directly to Rust's max-`BinaryHeap`; `MinHeap` wraps entries in
4597// `Reverse` so the smallest surfaces first. `pop`/`peek` fault `EmptyCollection`
4598// on an empty heap.
4599// ---------------------------------------------------------------------------
4600
4601use crate::heaps::{HeapEntry, MaxHeapPayload, MinHeapPayload};
4602use std::collections::BinaryHeap;
4603
4604unsafe fn max_heap_payload_mut<'s>(r: Rooted<'s>) -> &'s mut MaxHeapPayload {
4605    unsafe { payload_mut::<MaxHeapPayload>(r) }
4606}
4607
4608unsafe fn max_heap_payload(r: GcRef) -> &'static MaxHeapPayload {
4609    unsafe { payload_ref::<MaxHeapPayload>(r) }
4610}
4611
4612unsafe fn min_heap_payload_mut<'s>(r: Rooted<'s>) -> &'s mut MinHeapPayload {
4613    unsafe { payload_mut::<MinHeapPayload>(r) }
4614}
4615
4616unsafe fn min_heap_payload(r: GcRef) -> &'static MinHeapPayload {
4617    unsafe { payload_ref::<MinHeapPayload>(r) }
4618}
4619
4620/// Allocate an empty `MaxHeap[T]`. A null `element_descriptor` — the codegen's
4621/// "no static element type" — is kept null.
4622///
4623/// # Safety
4624/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
4625/// a `'static TypeDescriptor` (or null).
4626#[unsafe(no_mangle)]
4627pub unsafe extern "C" fn praxis_max_heap_new(
4628    ctx: *mut RuntimeContext,
4629    element_descriptor: *const TypeDescriptor,
4630) -> GcRef {
4631    abi_guard!("praxis_max_heap_new", ctx, {
4632        // SAFETY: MaxHeapPayload is MAX_HEAP's payload type.
4633        unsafe {
4634            gc_alloc_owned(ctx, &crate::heaps::MAX_HEAP, || MaxHeapPayload {
4635                element_descriptor,
4636                items: BinaryHeap::new(),
4637            })
4638        }
4639    })
4640}
4641
4642/// Push `value` onto the max-heap; returns Unit.
4643///
4644/// # Safety
4645/// `ctx` must be live and wired; `heap` and `value` must be valid `GcRef`s.
4646#[unsafe(no_mangle)]
4647pub unsafe extern "C" fn praxis_max_heap_push(
4648    ctx: *mut RuntimeContext,
4649    heap_ref: GcRef,
4650    value: GcRef,
4651) -> GcRef {
4652    abi_guard!("praxis_max_heap_push", ctx, {
4653        unsafe { maybe_collect(ctx) };
4654        let scope = unsafe { NativeScope::new(ctx) };
4655        let p = unsafe { max_heap_payload_mut(scope.root(heap_ref)) };
4656        let before = p.owned_bytes();
4657        p.items.push(HeapEntry {
4658            value,
4659            descriptor: value.descriptor(),
4660        });
4661        charge_growth(ctx, before, p.owned_bytes());
4662        unsafe { unit_sentinel(ctx) }
4663    })
4664}
4665
4666/// Remove and return the largest element; faults `EmptyCollection` if empty.
4667///
4668/// # Safety
4669/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4670#[unsafe(no_mangle)]
4671pub unsafe extern "C" fn praxis_max_heap_pop(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4672    abi_guard!("praxis_max_heap_pop", ctx, {
4673        let scope = unsafe { NativeScope::new(ctx) };
4674        let p = unsafe { max_heap_payload_mut(scope.root(heap_ref)) };
4675        match p.items.pop() {
4676            Some(e) => e.value,
4677            None => {
4678                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4679                unsafe { unit_sentinel(ctx) }
4680            }
4681        }
4682    })
4683}
4684
4685/// The largest element without removing it; faults `EmptyCollection` if empty.
4686///
4687/// # Safety
4688/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4689#[unsafe(no_mangle)]
4690pub unsafe extern "C" fn praxis_max_heap_peek(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4691    abi_guard!("praxis_max_heap_peek", ctx, {
4692        let p = unsafe { max_heap_payload(heap_ref) };
4693        match p.items.peek() {
4694            Some(e) => e.value,
4695            None => {
4696                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4697                unsafe { unit_sentinel(ctx) }
4698            }
4699        }
4700    })
4701}
4702
4703/// The number of elements, as a boxed Int.
4704///
4705/// # Safety
4706/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4707#[unsafe(no_mangle)]
4708pub unsafe extern "C" fn praxis_max_heap_len(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4709    abi_guard!("praxis_max_heap_len", ctx, {
4710        let p = unsafe { max_heap_payload(heap_ref) };
4711        unsafe { int_ref(ctx, p.items.len() as i64) }
4712    })
4713}
4714
4715/// Every element, as a `Vec[T]` in [`crate::heaps::in_pop_order`] — the snapshot
4716/// `for x in h` iterates (ADR-066). The heap is **not** drained.
4717///
4718/// A heap's backing array is heap-ordered only at its root, so an indexed
4719/// accessor over it would answer in insertion-history order — reading the array
4720/// as if it were a `Vec`'s.
4721///
4722/// # Safety
4723/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4724#[unsafe(no_mangle)]
4725pub unsafe extern "C" fn praxis_max_heap_items(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4726    abi_guard!("praxis_max_heap_items", ctx, {
4727        let p = unsafe { max_heap_payload(heap_ref) };
4728        let items = crate::heaps::in_pop_order(&p.items, |e| e.value);
4729        unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
4730    })
4731}
4732
4733/// True iff the heap is empty, as a boxed Bool.
4734///
4735/// # Safety
4736/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4737#[unsafe(no_mangle)]
4738pub unsafe extern "C" fn praxis_max_heap_is_empty(
4739    ctx: *mut RuntimeContext,
4740    heap_ref: GcRef,
4741) -> GcRef {
4742    abi_guard!("praxis_max_heap_is_empty", ctx, {
4743        let p = unsafe { max_heap_payload(heap_ref) };
4744        unsafe { bool_ref(ctx, p.items.is_empty()) }
4745    })
4746}
4747
4748// --- MinHeap (mirrors MaxHeap with Reverse wrapping) -----------------------
4749
4750/// Allocate an empty `MinHeap[T]`. A null `element_descriptor` — the codegen's
4751/// "no static element type" — is kept null.
4752///
4753/// # Safety
4754/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
4755/// a `'static TypeDescriptor` (or null).
4756#[unsafe(no_mangle)]
4757pub unsafe extern "C" fn praxis_min_heap_new(
4758    ctx: *mut RuntimeContext,
4759    element_descriptor: *const TypeDescriptor,
4760) -> GcRef {
4761    abi_guard!("praxis_min_heap_new", ctx, {
4762        // SAFETY: MinHeapPayload is MIN_HEAP's payload type.
4763        unsafe {
4764            gc_alloc_owned(ctx, &crate::heaps::MIN_HEAP, || MinHeapPayload {
4765                element_descriptor,
4766                items: BinaryHeap::new(),
4767            })
4768        }
4769    })
4770}
4771
4772/// Push `value` onto the min-heap; returns Unit.
4773///
4774/// # Safety
4775/// `ctx` must be live and wired; `heap` and `value` must be valid `GcRef`s.
4776#[unsafe(no_mangle)]
4777pub unsafe extern "C" fn praxis_min_heap_push(
4778    ctx: *mut RuntimeContext,
4779    heap_ref: GcRef,
4780    value: GcRef,
4781) -> GcRef {
4782    abi_guard!("praxis_min_heap_push", ctx, {
4783        unsafe { maybe_collect(ctx) };
4784        let scope = unsafe { NativeScope::new(ctx) };
4785        let p = unsafe { min_heap_payload_mut(scope.root(heap_ref)) };
4786        let before = p.owned_bytes();
4787        p.items.push(std::cmp::Reverse(HeapEntry {
4788            value,
4789            descriptor: value.descriptor(),
4790        }));
4791        charge_growth(ctx, before, p.owned_bytes());
4792        unsafe { unit_sentinel(ctx) }
4793    })
4794}
4795
4796/// Remove and return the smallest element; faults `EmptyCollection` if empty.
4797///
4798/// # Safety
4799/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
4800#[unsafe(no_mangle)]
4801pub unsafe extern "C" fn praxis_min_heap_pop(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4802    abi_guard!("praxis_min_heap_pop", ctx, {
4803        let scope = unsafe { NativeScope::new(ctx) };
4804        let p = unsafe { min_heap_payload_mut(scope.root(heap_ref)) };
4805        match p.items.pop() {
4806            Some(e) => e.0.value,
4807            None => {
4808                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4809                unsafe { unit_sentinel(ctx) }
4810            }
4811        }
4812    })
4813}
4814
4815/// The smallest element without removing it; faults `EmptyCollection` if empty.
4816///
4817/// # Safety
4818/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
4819#[unsafe(no_mangle)]
4820pub unsafe extern "C" fn praxis_min_heap_peek(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4821    abi_guard!("praxis_min_heap_peek", ctx, {
4822        let p = unsafe { min_heap_payload(heap_ref) };
4823        match p.items.peek() {
4824            Some(e) => e.0.value,
4825            None => {
4826                unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4827                unsafe { unit_sentinel(ctx) }
4828            }
4829        }
4830    })
4831}
4832
4833/// The number of elements, as a boxed Int.
4834///
4835/// # Safety
4836/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
4837#[unsafe(no_mangle)]
4838pub unsafe extern "C" fn praxis_min_heap_len(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4839    abi_guard!("praxis_min_heap_len", ctx, {
4840        let p = unsafe { min_heap_payload(heap_ref) };
4841        unsafe { int_ref(ctx, p.items.len() as i64) }
4842    })
4843}
4844
4845/// Every element, as a `Vec[T]` in [`crate::heaps::in_pop_order`] — ascending,
4846/// because the stored entry is a `Reverse<HeapEntry>`. See
4847/// [`praxis_max_heap_items`].
4848///
4849/// # Safety
4850/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
4851#[unsafe(no_mangle)]
4852pub unsafe extern "C" fn praxis_min_heap_items(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4853    abi_guard!("praxis_min_heap_items", ctx, {
4854        let p = unsafe { min_heap_payload(heap_ref) };
4855        let items = crate::heaps::in_pop_order(&p.items, |e| e.0.value);
4856        unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
4857    })
4858}
4859
4860/// True iff the heap is empty, as a boxed Bool.
4861///
4862/// # Safety
4863/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
4864#[unsafe(no_mangle)]
4865pub unsafe extern "C" fn praxis_min_heap_is_empty(
4866    ctx: *mut RuntimeContext,
4867    heap_ref: GcRef,
4868) -> GcRef {
4869    abi_guard!("praxis_min_heap_is_empty", ctx, {
4870        let p = unsafe { min_heap_payload(heap_ref) };
4871        unsafe { bool_ref(ctx, p.items.is_empty()) }
4872    })
4873}
4874
4875// ---------------------------------------------------------------------------
4876// BitSet (§6.1). A compact set of non-negative integers.
4877// ---------------------------------------------------------------------------
4878
4879use crate::bitset::{BitIndex, BitSetPayload};
4880
4881unsafe fn bitset_payload(r: GcRef) -> &'static BitSetPayload {
4882    unsafe { payload_ref::<BitSetPayload>(r) }
4883}
4884
4885unsafe fn bitset_payload_mut<'s>(r: Rooted<'s>) -> &'s mut BitSetPayload {
4886    unsafe { payload_mut::<BitSetPayload>(r) }
4887}
4888
4889/// Allocate an empty `BitSet` (§6.1). Nullary — no element descriptor.
4890///
4891/// # Safety
4892/// `ctx` must be live and wired.
4893#[unsafe(no_mangle)]
4894pub unsafe extern "C" fn praxis_bitset_new(ctx: *mut RuntimeContext) -> GcRef {
4895    abi_guard!("praxis_bitset_new", ctx, {
4896        // SAFETY: BitSetPayload is BITSET's payload type.
4897        unsafe {
4898            gc_alloc_owned(ctx, &crate::bitset::BITSET, || BitSetPayload {
4899                words: ReprCVec::new(),
4900            })
4901        }
4902    })
4903}
4904
4905/// Set bit `value`; returns Unit. Faults `InvalidSize` if `value` is negative
4906/// or above [`BitIndex::MAX`] — a member this set cannot hold.
4907///
4908/// # Safety
4909/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`; `value`
4910/// must be a valid `Int` `GcRef`.
4911#[unsafe(no_mangle)]
4912pub unsafe extern "C" fn praxis_bitset_insert(
4913    ctx: *mut RuntimeContext,
4914    bs: GcRef,
4915    value: GcRef,
4916) -> GcRef {
4917    abi_guard!("praxis_bitset_insert", ctx, {
4918        unsafe { maybe_collect(ctx) };
4919        let scope = unsafe { NativeScope::new(ctx) };
4920        let p = unsafe { bitset_payload_mut(scope.root(bs)) };
4921        let i = unsafe { int_payload(value) };
4922        // An insert that cannot be honoured is a fault, not a silent no-op: the
4923        // caller asked the set to contain something, and it will not.
4924        let Some(index) = BitIndex::new(i) else {
4925            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
4926            return unsafe { unit_sentinel(ctx) };
4927        };
4928        // A `BitSet` grows its word vector to reach the index, so an insert far
4929        // past the current high-water is a large uncharged allocation — the
4930        // shape `bfs` has, one visited-set per search.
4931        let before = p.owned_bytes();
4932        p.insert(index);
4933        charge_growth(ctx, before, p.owned_bytes());
4934        unsafe { unit_sentinel(ctx) }
4935    })
4936}
4937
4938/// Clear bit `value`; returns Unit. A value the set cannot hold is a value it
4939/// does not contain, so removing one is a no-op rather than a fault.
4940///
4941/// # Safety
4942/// `ctx` must be live and wired; `bs` and `value` must be valid `GcRef`s.
4943#[unsafe(no_mangle)]
4944pub unsafe extern "C" fn praxis_bitset_remove(
4945    ctx: *mut RuntimeContext,
4946    bs: GcRef,
4947    value: GcRef,
4948) -> GcRef {
4949    abi_guard!("praxis_bitset_remove", ctx, {
4950        let scope = unsafe { NativeScope::new(ctx) };
4951        let p = unsafe { bitset_payload_mut(scope.root(bs)) };
4952        let i = unsafe { int_payload(value) };
4953        if let Some(index) = BitIndex::new(i) {
4954            p.remove(index);
4955        }
4956        unsafe { unit_sentinel(ctx) }
4957    })
4958}
4959
4960/// True iff bit `value` is set, as a raw `0`/`1` in the scalar channel. A value
4961/// the set cannot hold is simply absent — the query is total.
4962///
4963/// **It answers an `i64` and not a boxed `Bool` (ADR-118 decision 6.)** A boxed
4964/// answer would be unboxed again on the next instruction — `if bs.contains(x)`
4965/// as a `Materialize{Bool}`, an `ExtractScalar{Bool}` and then the branch that
4966/// wanted the predicate. `praxis_struct_eq` and `praxis_value_cmp` answer the
4967/// scalar channel for the same reason, and MIR carries this one as
4968/// [`Inst::BitsetContains`](praxis_mir::Inst::BitsetContains) — a
4969/// `Scalar(Bool)` result, and, because it neither allocates nor faults, not a
4970/// GC safepoint.
4971///
4972/// `0` and `1` and nothing else, which is what the `Bool` payload byte holds
4973/// and what `emit_inline_bool` re-boxes with a `!= 0` test.
4974///
4975/// # Safety
4976/// `ctx` must be live and wired; `bs` and `value` must be valid `GcRef`s.
4977#[unsafe(no_mangle)]
4978pub unsafe extern "C" fn praxis_bitset_contains(
4979    ctx: *mut RuntimeContext,
4980    bs: GcRef,
4981    value: GcRef,
4982) -> i64 {
4983    abi_guard!("praxis_bitset_contains", ctx, {
4984        let p = unsafe { bitset_payload(bs) };
4985        let i = unsafe { int_payload(value) };
4986        let present = BitIndex::new(i).is_some_and(|index| p.contains(index));
4987        i64::from(present)
4988    })
4989}
4990
4991/// The number of set bits, as a boxed Int.
4992///
4993/// # Safety
4994/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`.
4995#[unsafe(no_mangle)]
4996pub unsafe extern "C" fn praxis_bitset_len(ctx: *mut RuntimeContext, bs: GcRef) -> GcRef {
4997    abi_guard!("praxis_bitset_len", ctx, {
4998        let p = unsafe { bitset_payload(bs) };
4999        unsafe { int_ref(ctx, p.count() as i64) }
5000    })
5001}
5002
5003/// Every member, as a `Vec[Int]` **ascending** — the snapshot `for i in b`
5004/// iterates (ADR-066).
5005///
5006/// This is the one iterable whose members are not objects: they are bit
5007/// positions, so each one is boxed here rather than copied from the payload.
5008///
5009/// # Safety
5010/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`.
5011#[unsafe(no_mangle)]
5012pub unsafe extern "C" fn praxis_bitset_items(ctx: *mut RuntimeContext, bs: GcRef) -> GcRef {
5013    abi_guard!("praxis_bitset_items", ctx, {
5014        // The members are read out before the first allocation: `vec_of` allocates
5015        // per element, and a collection during the walk would move nothing here
5016        // (the bits are not objects) but would leave the borrow of the payload
5017        // spanning a safepoint, which is not allowed.
5018        let members: Vec<i64> = unsafe { bitset_payload(bs) }.members().collect();
5019        let result = unsafe { praxis_vec_new(ctx, &scalars::INT as *const _) };
5020        let scope = unsafe { NativeScope::new(ctx) };
5021        let rooted = scope.root(result);
5022        for value in members {
5023            let boxed = unsafe { int_ref(ctx, value) };
5024            unsafe { vec_payload_mut(rooted) }.items.push(boxed);
5025        }
5026        result
5027    })
5028}
5029
5030/// True iff the bitset is empty, as a boxed Bool.
5031///
5032/// # Safety
5033/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`.
5034#[unsafe(no_mangle)]
5035pub unsafe extern "C" fn praxis_bitset_is_empty(ctx: *mut RuntimeContext, bs: GcRef) -> GcRef {
5036    abi_guard!("praxis_bitset_is_empty", ctx, {
5037        let p = unsafe { bitset_payload(bs) };
5038        unsafe { bool_ref(ctx, p.count() == 0) }
5039    })
5040}
5041
5042// ---------------------------------------------------------------------------
5043// Grid[T] methods (§6.4). `GridPayload` is a row-major `Vec<GcRef>` plus a
5044// width. Coordinates are (x, y) with x rightward, y downward (§6.4). Indexing
5045// stays behind runtime wrappers (§11.5 realloc safety).
5046// ---------------------------------------------------------------------------
5047
5048use crate::collections::{GridExtent, GridPayload};
5049
5050unsafe fn grid_payload(r: GcRef) -> &'static GridPayload {
5051    unsafe { payload_ref::<GridPayload>(r) }
5052}
5053
5054unsafe fn grid_payload_mut<'s>(r: Rooted<'s>) -> &'s mut GridPayload {
5055    unsafe { payload_mut::<GridPayload>(r) }
5056}
5057
5058/// Allocate a `(x, y)` point tuple from two `i64` coordinates. The schema is
5059/// the cached `(Int, Int)` point schema; elements are filled via
5060/// `praxis_tuple_set`. Returns the point `GcRef`.
5061///
5062/// Three allocations, and each one may collect: the tuple must survive the two
5063/// coordinate allocations, and the x coordinate must survive the y's. Nothing
5064/// generated is on the stack here — the caller is a runtime helper — so the
5065/// only thing that can root them is a native scope.
5066unsafe fn alloc_point(ctx: *mut RuntimeContext, x: i64, y: i64) -> GcRef {
5067    let scope = unsafe { NativeScope::new(ctx) };
5068    let schema = crate::tuples::point_schema();
5069    let schema_ptr = schema as *const crate::tuples::TupleSchema;
5070    let tup = scope.root(unsafe { praxis_alloc_tuple(ctx, schema_ptr) });
5071    let x_ref = scope.root(unsafe { int_ref(ctx, x) });
5072    unsafe { praxis_tuple_set(ctx, tup.get(), 0, x_ref.get()) };
5073    let y_ref = unsafe { int_ref(ctx, y) };
5074    unsafe { praxis_tuple_set(ctx, tup.get(), 1, y_ref) };
5075    tup.get()
5076}
5077
5078/// The (x, y) coordinates of a flat `idx` in a grid of `width`.
5079fn grid_xy(idx: usize, width: usize) -> (i64, i64) {
5080    ((idx % width) as i64, (idx / width) as i64)
5081}
5082
5083/// Read the two coordinates out of a `(Int, Int)` point tuple.
5084///
5085/// The inverse of [`alloc_point`], and the one place the grid wrappers unpack a
5086/// point: every `Grid` method taking a position takes it as this tuple, so
5087/// there is one shape to read and no reason for two readings of it.
5088///
5089/// # Safety
5090/// `point` must be a valid `(Int, Int)` tuple `GcRef` — which the type checker
5091/// guarantees for every catalog row whose parameter is `Tuple[Int, Int]`.
5092unsafe fn point_xy(point: GcRef) -> (i64, i64) {
5093    let tp = point.payload::<crate::tuples::TuplePayload>() as *const crate::tuples::TuplePayload;
5094    // SAFETY: caller guarantees `point` is a tuple, so its payload is a
5095    // `TuplePayload`, and a `(Int, Int)` shape has both slots filled with `Int`s.
5096    let pt = unsafe { &*tp };
5097    unsafe { (int_payload(pt.items[0]), int_payload(pt.items[1])) }
5098}
5099
5100/// The height (row count) of a grid: `items.len() / width`, or 0 if width is 0
5101/// (avoids division by zero on a degenerate empty grid).
5102fn grid_height(items_len: usize, width: usize) -> usize {
5103    items_len.checked_div(width).unwrap_or(0)
5104}
5105
5106/// The in-bounds neighbour at `(px + dx, py + dy)`, or `None` if it falls
5107/// outside a `width × height` grid.
5108///
5109/// The offsets are `checked_add` because `px`/`py` come out of a user-supplied
5110/// point tuple, so `(i64::MAX, 0).neighbors4()` would otherwise overflow the
5111/// addition and panic *inside* `extern "C"`. A coordinate that overflows is
5112/// outside every grid — `GridExtent` bounds the extents far below `i64::MAX` —
5113/// so "outside" is the whole answer, not a special case.
5114fn grid_neighbor(
5115    px: i64,
5116    py: i64,
5117    dx: i64,
5118    dy: i64,
5119    width: usize,
5120    height: usize,
5121) -> Option<(i64, i64)> {
5122    let nx = px.checked_add(dx)?;
5123    let ny = py.checked_add(dy)?;
5124    // Both non-negative below, so the casts are exact.
5125    (nx >= 0 && ny >= 0 && (nx as usize) < width && (ny as usize) < height).then_some((nx, ny))
5126}
5127
5128/// The zero value of the type `descriptor` names, or `None` if that type has no
5129/// natural default.
5130///
5131/// Only the scalars and `Unit` have one. A `Grid[Vec[Int]](3, 3)` would need
5132/// nine distinct empty vectors and, worse, no way to know their element type —
5133/// so it is refused rather than filled with something of the wrong type. A null
5134/// descriptor means the caller never said what the cells are, which is likewise
5135/// nothing this can invent.
5136///
5137/// # Safety
5138/// `ctx` must be live and wired.
5139unsafe fn default_cell(
5140    ctx: *mut RuntimeContext,
5141    descriptor: *const TypeDescriptor,
5142) -> Option<GcRef> {
5143    use crate::descriptor::BuiltinTypeId as B;
5144    // SAFETY: a non-null descriptor is a valid `&'static`.
5145    let builtin = unsafe { descriptor.as_ref() }?.as_builtin()?;
5146    unsafe {
5147        match builtin {
5148            B::Unit => Some(unit_sentinel(ctx)),
5149            B::Bool => Some(bool_ref(ctx, false)),
5150            B::Int => Some(int_ref(ctx, 0_i64)),
5151            B::Byte => Some(gc_alloc(ctx, scalars::BYTE_PAYLOAD, 0_u8)),
5152            // `0_u32`, not `'\0'`: a `Char`'s payload is the scalar *value*,
5153            // and a Rust `char` only fits because it shares `u32`'s layout. NUL
5154            // is inside the interned range, so this is the immortal, like the
5155            // `Int` arm above.
5156            B::Char => Some(char_ref(ctx, 0_u32)),
5157            B::Float => Some(gc_alloc(ctx, scalars::FLOAT_PAYLOAD, 0.0_f64)),
5158            // `(null, 0)` meets `praxis_alloc_text`'s UTF-8 precondition
5159            // trivially: the wrapper's own `bytes.is_null() || len == 0` branch
5160            // turns it into the empty slice, and the empty slice is UTF-8.
5161            // The precondition is load-bearing (ADR-111): a violation here
5162            // would abort, not fault. `alloc_text_empty_string_round_trips`
5163            // pins the branch this depends on.
5164            B::Text => Some(praxis_alloc_text(ctx, std::ptr::null(), 0)),
5165            // A composite has no zero value the runtime can invent: a
5166            // `Grid[Vec[Int]]` must be filled by the program that knows what its
5167            // cells are.
5168            B::Vec
5169            | B::Deque
5170            | B::Grid
5171            | B::Map
5172            | B::Set
5173            | B::Counter
5174            | B::MinHeap
5175            | B::MaxHeap
5176            | B::BitSet
5177            // A `Range`'s zero value would be a pair of bounds nobody chose;
5178            // `0..0` is *a* range but it is not "the empty one" in any sense a
5179            // `Grid[Range]` cell wants.
5180            | B::Range
5181            | B::Tuple
5182            | B::Record
5183            | B::Enum
5184            | B::Closure
5185            | B::VarCell => None,
5186        }
5187    }
5188}
5189
5190/// Allocate an empty `Grid[T]` with the given element descriptor, width, and
5191/// height, all cells initialized to the cell type's zero value. (The input parser also constructs
5192/// grids directly; this wrapper is for source `Grid[T]()` + a follow-up fill.)
5193///
5194/// Faults `InvalidSize` if either extent is negative or the grid would exceed
5195/// [`GridExtent::MAX_CELLS`] — the sizes arrive from source, where a negative
5196/// value would otherwise land near `usize::MAX` on the cast.
5197///
5198/// # Safety
5199/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
5200/// a `'static TypeDescriptor` (or null).
5201#[unsafe(no_mangle)]
5202pub unsafe extern "C" fn praxis_grid_new(
5203    ctx: *mut RuntimeContext,
5204    element_descriptor: *const TypeDescriptor,
5205    width: i64,
5206    height: i64,
5207) -> GcRef {
5208    abi_guard!("praxis_grid_new", ctx, {
5209        let Some(extent) = GridExtent::new(width, height) else {
5210            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
5211            return unsafe { unit_sentinel(ctx) };
5212        };
5213        // Every cell of a `Grid[T]` must *be* a `T`. Filling with the Unit sentinel
5214        // under a `T` element descriptor is the same lie as a mislabelled element
5215        // descriptor, one level down: `get`, `format`, `equals` and `hash` all
5216        // dispatch `T`'s callbacks against a zero-sized Unit payload.
5217        let cells = if extent.cells() == 0 {
5218            Vec::new()
5219        } else {
5220            let Some(fill) = (unsafe { default_cell(ctx, element_descriptor) }) else {
5221                unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
5222                return unsafe { unit_sentinel(ctx) };
5223            };
5224            vec![fill; extent.cells()]
5225        };
5226        // SAFETY: GridPayload is GRID's payload type.
5227        unsafe {
5228            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
5229                element_descriptor,
5230                items: cells,
5231                width: extent.width(),
5232            })
5233        }
5234    })
5235}
5236
5237/// Allocate a `Grid[T]` of `width` × `height` cells, every one holding `fill`
5238/// (ADR-146's `Grid(w, h, fill)`) — the working grid an algorithm allocates for
5239/// itself: an occupancy board, a visited mask, a distance table.
5240///
5241/// Faults `InvalidSize` on the extents [`praxis_grid_new`] refuses, through the
5242/// same [`GridExtent::new`], since this is the very allocation ADR-041 was
5243/// written about and a fill changes nothing about the arithmetic.
5244///
5245/// **It does not call [`default_cell`], and that is the whole difference.**
5246/// `praxis_grid_new` has to invent a zero value for the cell type and has none
5247/// for a composite, so it raises `TypeMismatch` for a `Grid[Vec[Int]]` rather
5248/// than filling it with Unit sentinels under a `Vec` descriptor. An explicit
5249/// fill removes the question — the caller supplied a value of the cell type —
5250/// so a grid of collections is constructible here and not there. The descriptor
5251/// is still reconciled through [`adopt_or_reject`], so a *declared* cell type
5252/// the fill does not match is `TypeMismatch` rather than a silent retag.
5253///
5254/// Every cell is the same `GcRef`, exactly as [`praxis_vec_filled`]'s are; see
5255/// its comment for why that is the language's existing rule rather than a new
5256/// one. The extents arrive boxed for the reason stated there too.
5257///
5258/// # Safety
5259/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
5260/// a `'static TypeDescriptor` (or null); `width` and `height` must be valid
5261/// `Int` `GcRef`s; `fill` must be a valid `GcRef`.
5262#[unsafe(no_mangle)]
5263pub unsafe extern "C" fn praxis_grid_filled(
5264    ctx: *mut RuntimeContext,
5265    element_descriptor: *const TypeDescriptor,
5266    width: GcRef,
5267    height: GcRef,
5268    fill: GcRef,
5269) -> GcRef {
5270    abi_guard!("praxis_grid_filled", ctx, {
5271        // SAFETY: caller guarantees `width` and `height` are valid Ints.
5272        let (w, h) = unsafe { (int_payload(width), int_payload(height)) };
5273        let Some(extent) = GridExtent::new(w, h) else {
5274            unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
5275            return unsafe { unit_sentinel(ctx) };
5276        };
5277        let mut descriptor = element_descriptor;
5278        if !unsafe { adopt_or_reject(ctx, &mut descriptor, fill) } {
5279            return unsafe { unit_sentinel(ctx) };
5280        }
5281        let scope = unsafe { NativeScope::new(ctx) };
5282        let fill = scope.root(fill).get();
5283        // The cells are built inside the initializer, which `gc_alloc_owned` runs
5284        // *after* the safepoint — `praxis_vec_filled`'s rule, for the same
5285        // untraced `Vec<GcRef>`.
5286        // SAFETY: GridPayload is GRID's payload type.
5287        unsafe {
5288            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
5289                element_descriptor: descriptor,
5290                items: vec![fill; extent.cells()],
5291                width: extent.width(),
5292            })
5293        }
5294    })
5295}
5296
5297/// The grid width (number of columns), as a boxed Int.
5298///
5299/// # Safety
5300/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5301#[unsafe(no_mangle)]
5302pub unsafe extern "C" fn praxis_grid_width(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5303    abi_guard!("praxis_grid_width", ctx, {
5304        let p = unsafe { grid_payload(grid) };
5305        unsafe { int_ref(ctx, p.width as i64) }
5306    })
5307}
5308
5309/// The grid height (number of rows), as a boxed Int.
5310///
5311/// # Safety
5312/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5313#[unsafe(no_mangle)]
5314pub unsafe extern "C" fn praxis_grid_height(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5315    abi_guard!("praxis_grid_height", ctx, {
5316        let p = unsafe { grid_payload(grid) };
5317        // height = items.len() / width.
5318        let height = grid_height(p.items.len(), p.width);
5319        unsafe { int_ref(ctx, height as i64) }
5320    })
5321}
5322
5323/// The cell at `(x, y)`; faults `IndexOutOfBounds` if out of range.
5324///
5325/// # Safety
5326/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`/`y`
5327/// must be valid `Int` `GcRef`s.
5328#[unsafe(no_mangle)]
5329pub unsafe extern "C" fn praxis_grid_get(
5330    ctx: *mut RuntimeContext,
5331    grid: GcRef,
5332    x: GcRef,
5333    y: GcRef,
5334) -> GcRef {
5335    abi_guard!("praxis_grid_get", ctx, {
5336        let p = unsafe { grid_payload(grid) };
5337        let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5338        let height = grid_height(p.items.len(), p.width);
5339        let Some(idx) = (unsafe { checked_cell(ctx, xi, yi, p.width, height) }) else {
5340            return unsafe { unit_sentinel(ctx) };
5341        };
5342        p.items[idx]
5343    })
5344}
5345
5346/// Set the cell at `(x, y)`; faults `IndexOutOfBounds` if out of range.
5347///
5348/// # Safety
5349/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`/`y`
5350/// must be valid `Int` `GcRef`s; `value` must be a valid `GcRef`.
5351#[unsafe(no_mangle)]
5352pub unsafe extern "C" fn praxis_grid_set(
5353    ctx: *mut RuntimeContext,
5354    grid: GcRef,
5355    x: GcRef,
5356    y: GcRef,
5357    value: GcRef,
5358) -> GcRef {
5359    abi_guard!("praxis_grid_set", ctx, {
5360        let scope = unsafe { NativeScope::new(ctx) };
5361        let p = unsafe { grid_payload_mut(scope.root(grid)) };
5362        let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5363        let height = grid_height(p.items.len(), p.width);
5364        let Some(idx) = (unsafe { checked_cell(ctx, xi, yi, p.width, height) }) else {
5365            return unsafe { unit_sentinel(ctx) };
5366        };
5367        if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
5368            return unsafe { unit_sentinel(ctx) };
5369        }
5370        p.items[idx] = value;
5371        unsafe { unit_sentinel(ctx) }
5372    })
5373}
5374
5375/// True iff `(x, y)` is within the grid, as a boxed Bool.
5376///
5377/// # Safety
5378/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`/`y`
5379/// must be valid `Int` `GcRef`s.
5380#[unsafe(no_mangle)]
5381pub unsafe extern "C" fn praxis_grid_contains(
5382    ctx: *mut RuntimeContext,
5383    grid: GcRef,
5384    x: GcRef,
5385    y: GcRef,
5386) -> GcRef {
5387    abi_guard!("praxis_grid_contains", ctx, {
5388        let p = unsafe { grid_payload(grid) };
5389        let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5390        let height = grid_height(p.items.len(), p.width);
5391        // The **pure** [`cell_index`], never `checked_cell`: this wrapper's
5392        // manifest row is `Pure`, so generated code emits no `CheckFault` after
5393        // it and a fault raised on every legitimate `false` would sit pending
5394        // until an unrelated check picked it up.
5395        let inside = cell_index(xi, yi, p.width, height).is_some();
5396        unsafe { bool_ref(ctx, inside) }
5397    })
5398}
5399
5400/// The 4 orthogonal neighbors of `point` that lie inside the grid, as a `Vec`.
5401///
5402/// # Safety
5403/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5404#[unsafe(no_mangle)]
5405pub unsafe extern "C" fn praxis_grid_neighbors4(
5406    ctx: *mut RuntimeContext,
5407    grid: GcRef,
5408    point: GcRef,
5409) -> GcRef {
5410    abi_guard!("praxis_grid_neighbors4", ctx, {
5411        let p = unsafe { grid_payload(grid) };
5412        let (px, py) = unsafe { point_xy(point) };
5413        let height = grid_height(p.items.len(), p.width);
5414        let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
5415        let scope = unsafe { NativeScope::new(ctx) };
5416        let rp = unsafe { vec_payload_mut(scope.root(result)) };
5417        for (dx, dy) in [(0i64, -1), (0, 1), (-1, 0), (1, 0)] {
5418            if let Some((nx, ny)) = grid_neighbor(px, py, dx, dy, p.width, height) {
5419                let pt_ref = unsafe { alloc_point(ctx, nx, ny) };
5420                rp.items.push(pt_ref);
5421            }
5422        }
5423        result
5424    })
5425}
5426
5427/// The 8 neighbors of `point` that lie inside the grid, as a `Vec`.
5428///
5429/// # Safety
5430/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5431#[unsafe(no_mangle)]
5432pub unsafe extern "C" fn praxis_grid_neighbors8(
5433    ctx: *mut RuntimeContext,
5434    grid: GcRef,
5435    point: GcRef,
5436) -> GcRef {
5437    abi_guard!("praxis_grid_neighbors8", ctx, {
5438        let p = unsafe { grid_payload(grid) };
5439        let (px, py) = unsafe { point_xy(point) };
5440        let height = grid_height(p.items.len(), p.width);
5441        let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
5442        let scope = unsafe { NativeScope::new(ctx) };
5443        let rp = unsafe { vec_payload_mut(scope.root(result)) };
5444        for dy in -1i64..=1 {
5445            for dx in -1i64..=1 {
5446                if dx == 0 && dy == 0 {
5447                    continue;
5448                }
5449                if let Some((nx, ny)) = grid_neighbor(px, py, dx, dy, p.width, height) {
5450                    let pt_ref = unsafe { alloc_point(ctx, nx, ny) };
5451                    rp.items.push(pt_ref);
5452                }
5453            }
5454        }
5455        result
5456    })
5457}
5458
5459/// Build one neighbourhood record: a field per direction, in `directions`
5460/// order, each `Some((x, y))` or `None`.
5461///
5462/// # What the field order means, and what it costs to get wrong
5463///
5464/// Slot *i* of the record is `directions[i]`, and the reader's slot index comes
5465/// from the *static* type — the catalog's `Around4`/`Around8` row. The two
5466/// orders are required to agree and nothing derives one from the other, so
5467/// `around_schemas_match_the_catalog` asserts it: a mismatch is a field read
5468/// that quietly answers the wrong direction.
5469///
5470/// # Rooting
5471///
5472/// Every field costs up to four allocations (two `Int`s, a point tuple, a
5473/// `Some`), each a safepoint, so the record is rooted for the whole loop. Each
5474/// field's own value is stored the moment it exists — `praxis_record_set_field`
5475/// allocates nothing — so no second reference is ever live across a safepoint.
5476///
5477/// # Safety
5478/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef` and
5479/// `point` a valid `(Int, Int)` tuple.
5480unsafe fn grid_around(
5481    ctx: *mut RuntimeContext,
5482    grid: GcRef,
5483    point: GcRef,
5484    schema: &'static crate::records::RecordSchema,
5485    directions: &'static [crate::records::Direction],
5486) -> GcRef {
5487    // SAFETY: the caller upholds every argument's validity.
5488    let (width, height) = unsafe {
5489        let p = grid_payload(grid);
5490        (p.width, grid_height(p.items.len(), p.width))
5491    };
5492    let (px, py) = unsafe { point_xy(point) };
5493    let scope = unsafe { NativeScope::new(ctx) };
5494    let record = scope.root(unsafe { praxis_alloc_record(ctx, schema) });
5495    for (i, d) in directions.iter().enumerate() {
5496        let field = match grid_neighbor(px, py, d.dx, d.dy, width, height) {
5497            // `option_some` roots the point across the enum allocation.
5498            Some((nx, ny)) => unsafe { option_some(ctx, alloc_point(ctx, nx, ny)) },
5499            None => unsafe { option_none(ctx) },
5500        };
5501        unsafe { praxis_record_set_field(ctx, record.get(), i as u32, field) };
5502    }
5503    record.get()
5504}
5505
5506/// The four orthogonal neighbours of `point` as an `Around4` record — `up`,
5507/// `left`, `right`, `down`, each `Some((x, y))` or `None` (§6.4).
5508///
5509/// **Not a shorter `neighbors4`.** That wrapper answers a `Vec` clipped to what
5510/// is in bounds, which is what a graph walk wants and what
5511/// `bfs(start, |p| g.neighbors4(p))` passes; it throws away *which* direction
5512/// each neighbour was, and off the edge of the grid it throws away that there
5513/// was a direction at all. This answers exactly that, and the `None` is the
5514/// half a `Vec` cannot carry.
5515///
5516/// # Safety
5517/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5518#[unsafe(no_mangle)]
5519pub unsafe extern "C" fn praxis_grid_around4(
5520    ctx: *mut RuntimeContext,
5521    grid: GcRef,
5522    point: GcRef,
5523) -> GcRef {
5524    abi_guard!("praxis_grid_around4", ctx, {
5525        unsafe {
5526            grid_around(
5527                ctx,
5528                grid,
5529                point,
5530                crate::records::around4_schema(),
5531                crate::records::AROUND4_DIRECTIONS,
5532            )
5533        }
5534    })
5535}
5536
5537/// All eight neighbours of `point` as an `Around8` record, in reading order —
5538/// `up_left`, `up`, `up_right`, `left`, `right`, `down_left`, `down`,
5539/// `down_right` (§6.4). See [`praxis_grid_around4`].
5540///
5541/// # Safety
5542/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5543#[unsafe(no_mangle)]
5544pub unsafe extern "C" fn praxis_grid_around8(
5545    ctx: *mut RuntimeContext,
5546    grid: GcRef,
5547    point: GcRef,
5548) -> GcRef {
5549    abi_guard!("praxis_grid_around8", ctx, {
5550        unsafe {
5551            grid_around(
5552                ctx,
5553                grid,
5554                point,
5555                crate::records::around8_schema(),
5556                crate::records::AROUND8_DIRECTIONS,
5557            )
5558        }
5559    })
5560}
5561
5562/// How many of `point`'s in-bounds neighbours in `directions` hold a cell equal
5563/// to `value`.
5564///
5565/// Equality is the value's own descriptor callback — the same path
5566/// `praxis_grid_find` and `praxis_grid_find_all` take, so "equals" means one
5567/// thing across every `Grid` row (§5.5).
5568///
5569/// A direction that leaves the grid has no cell, so it is not counted. Nothing
5570/// allocates until the answer is boxed, so `grid` needs no root.
5571///
5572/// # Safety
5573/// `ctx` must be live and wired; `grid`, `point` and `value` must be valid
5574/// `GcRef`s.
5575unsafe fn grid_count_equal(
5576    ctx: *mut RuntimeContext,
5577    grid: GcRef,
5578    point: GcRef,
5579    value: GcRef,
5580    directions: &'static [crate::records::Direction],
5581) -> GcRef {
5582    // SAFETY: the caller upholds every argument's validity.
5583    let p = unsafe { grid_payload(grid) };
5584    let height = grid_height(p.items.len(), p.width);
5585    let (px, py) = unsafe { point_xy(point) };
5586    let eq = value.descriptor().equals;
5587    let mut n = 0_i64;
5588    for d in directions {
5589        let Some((nx, ny)) = grid_neighbor(px, py, d.dx, d.dy, p.width, height) else {
5590            continue;
5591        };
5592        let cell = p.items[ny as usize * p.width + nx as usize];
5593        let matches = match eq {
5594            // SAFETY: `equals` came off `value`'s descriptor, and the grid's
5595            // cells are values of the element type the catalog row unified
5596            // `value` with.
5597            Some(equals) => unsafe {
5598                equals(
5599                    cell.payload::<u8>() as *const u8,
5600                    value.payload::<u8>() as *const u8,
5601                )
5602            },
5603            None => cell == value,
5604        };
5605        n += i64::from(matches);
5606    }
5607    unsafe { int_ref(ctx, n) }
5608}
5609
5610/// `g.count4(p, v)` — how many of the four orthogonal in-bounds neighbours hold
5611/// `v` (§6.4).
5612///
5613/// # Safety
5614/// `ctx` must be live and wired; `grid`, `point` and `value` must be valid
5615/// `GcRef`s.
5616#[unsafe(no_mangle)]
5617pub unsafe extern "C" fn praxis_grid_count4(
5618    ctx: *mut RuntimeContext,
5619    grid: GcRef,
5620    point: GcRef,
5621    value: GcRef,
5622) -> GcRef {
5623    abi_guard!("praxis_grid_count4", ctx, {
5624        unsafe { grid_count_equal(ctx, grid, point, value, crate::records::AROUND4_DIRECTIONS) }
5625    })
5626}
5627
5628/// `g.count8(p, v)` — how many of the eight in-bounds neighbours hold `v`
5629/// (§6.4).
5630///
5631/// # Safety
5632/// `ctx` must be live and wired; `grid`, `point` and `value` must be valid
5633/// `GcRef`s.
5634#[unsafe(no_mangle)]
5635pub unsafe extern "C" fn praxis_grid_count8(
5636    ctx: *mut RuntimeContext,
5637    grid: GcRef,
5638    point: GcRef,
5639    value: GcRef,
5640) -> GcRef {
5641    abi_guard!("praxis_grid_count8", ctx, {
5642        unsafe { grid_count_equal(ctx, grid, point, value, crate::records::AROUND8_DIRECTIONS) }
5643    })
5644}
5645
5646/// How many of `point`'s in-bounds neighbours in `directions` hold a cell the
5647/// closure accepts.
5648///
5649/// A direction that leaves the grid has no cell, so the closure is not called
5650/// for it — the predicate never sees a position that is not on the grid.
5651///
5652/// # Rooting, and what a faulting closure does
5653///
5654/// The closure runs arbitrary Praxis code between iterations, so it allocates
5655/// and it collects. Every cell the loop will hand it is read out of the grid
5656/// and rooted **before the first call**: `grid_payload` hands back a borrow of
5657/// a heap object, and a collection triggered by call *i* would otherwise be
5658/// free to reclaim the cell call *i + 1* is about to receive.
5659///
5660/// A fault stops the count and answers the Unit sentinel, exactly as
5661/// `praxis_vec_sorted_by_key` does: the call site's own fault check is what
5662/// reports, and a half-finished count is not an answer.
5663///
5664/// # Safety
5665/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s and
5666/// `pred` a valid closure `GcRef`.
5667unsafe fn grid_count_where(
5668    ctx: *mut RuntimeContext,
5669    grid: GcRef,
5670    point: GcRef,
5671    pred: GcRef,
5672    directions: &'static [crate::records::Direction],
5673) -> GcRef {
5674    let scope = unsafe { NativeScope::new(ctx) };
5675    // SAFETY: the caller upholds every argument's validity.
5676    let cells: Vec<GcRef> = unsafe {
5677        let p = grid_payload(grid);
5678        let height = grid_height(p.items.len(), p.width);
5679        let (px, py) = point_xy(point);
5680        directions
5681            .iter()
5682            .filter_map(|d| grid_neighbor(px, py, d.dx, d.dy, p.width, height))
5683            .map(|(nx, ny)| {
5684                scope
5685                    .root(p.items[ny as usize * p.width + nx as usize])
5686                    .get()
5687            })
5688            .collect()
5689    };
5690    let mut n = 0_i64;
5691    for cell in cells {
5692        let Some(answer) = (unsafe { call_unary_closure(ctx, pred, cell) }) else {
5693            // The closure faulted (or is not a closure, which the type checker
5694            // already refused). Leave the fault for the call site's check.
5695            return unsafe { unit_sentinel(ctx) };
5696        };
5697        // A `Bool`'s payload is **one byte**, and `read_scalar` takes the width
5698        // from `BOOL_PAYLOAD`'s own type after checking the descriptor — so a
5699        // closure that answered something else is a `TypeMismatch` rather than
5700        // seven bytes of uninitialized alignment padding.
5701        // SAFETY: `answer` is the `GcRef` the call just produced.
5702        let Some(byte) = (unsafe { read_scalar(answer, scalars::BOOL_PAYLOAD) }) else {
5703            unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
5704            return unsafe { unit_sentinel(ctx) };
5705        };
5706        n += i64::from(byte != 0);
5707    }
5708    unsafe { int_ref(ctx, n) }
5709}
5710
5711/// `g.count4_where(p, f)` — how many of the four orthogonal in-bounds
5712/// neighbours hold a cell `f` accepts (§6.4).
5713///
5714/// # Safety
5715/// `ctx` must be live and wired; `grid`, `point` and `pred` must be valid
5716/// `GcRef`s.
5717#[unsafe(no_mangle)]
5718pub unsafe extern "C" fn praxis_grid_count4_where(
5719    ctx: *mut RuntimeContext,
5720    grid: GcRef,
5721    point: GcRef,
5722    pred: GcRef,
5723) -> GcRef {
5724    abi_guard!("praxis_grid_count4_where", ctx, {
5725        unsafe { grid_count_where(ctx, grid, point, pred, crate::records::AROUND4_DIRECTIONS) }
5726    })
5727}
5728
5729/// `g.count8_where(p, f)` — how many of the eight in-bounds neighbours hold a
5730/// cell `f` accepts (§6.4).
5731///
5732/// # Safety
5733/// `ctx` must be live and wired; `grid`, `point` and `pred` must be valid
5734/// `GcRef`s.
5735#[unsafe(no_mangle)]
5736pub unsafe extern "C" fn praxis_grid_count8_where(
5737    ctx: *mut RuntimeContext,
5738    grid: GcRef,
5739    point: GcRef,
5740    pred: GcRef,
5741) -> GcRef {
5742    abi_guard!("praxis_grid_count8_where", ctx, {
5743        unsafe { grid_count_where(ctx, grid, point, pred, crate::records::AROUND8_DIRECTIONS) }
5744    })
5745}
5746
5747/// All `(x, y)` positions in row-major order, as a `Vec`.
5748///
5749/// # Safety
5750/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5751#[unsafe(no_mangle)]
5752pub unsafe extern "C" fn praxis_grid_positions(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5753    abi_guard!("praxis_grid_positions", ctx, {
5754        unsafe { maybe_collect(ctx) };
5755        let p = unsafe { grid_payload(grid) };
5756        let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
5757        let scope = unsafe { NativeScope::new(ctx) };
5758        let rp = unsafe { vec_payload_mut(scope.root(result)) };
5759        for i in 0..p.items.len() {
5760            let (x, y) = grid_xy(i, p.width);
5761            rp.items.push(unsafe { alloc_point(ctx, x, y) });
5762        }
5763        result
5764    })
5765}
5766
5767/// All cells in row-major order, as a `Vec`.
5768///
5769/// # Safety
5770/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5771#[unsafe(no_mangle)]
5772pub unsafe extern "C" fn praxis_grid_cells(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5773    abi_guard!("praxis_grid_cells", ctx, {
5774        let p = unsafe { grid_payload(grid) };
5775        let result = unsafe { praxis_vec_new(ctx, p.element_descriptor) };
5776        let scope = unsafe { NativeScope::new(ctx) };
5777        let rp = unsafe { vec_payload_mut(scope.root(result)) };
5778        for cell in p.items.iter() {
5779            rp.items.push(*cell);
5780        }
5781        result
5782    })
5783}
5784
5785/// Row `y` as a `Vec`; faults `IndexOutOfBounds` if out of range.
5786///
5787/// # Safety
5788/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `y`
5789/// must be a valid `Int` `GcRef`.
5790#[unsafe(no_mangle)]
5791pub unsafe extern "C" fn praxis_grid_row(ctx: *mut RuntimeContext, grid: GcRef, y: GcRef) -> GcRef {
5792    abi_guard!("praxis_grid_row", ctx, {
5793        let p = unsafe { grid_payload(grid) };
5794        let yi = unsafe { int_payload(y) };
5795        let height = grid_height(p.items.len(), p.width);
5796        // One axis of [`cell_index`]'s rule: a row is bounded by the height
5797        // alone, and every `x` in it is in range by construction.
5798        let Some(row) = (unsafe { checked_index(ctx, yi, height) }) else {
5799            return unsafe { unit_sentinel(ctx) };
5800        };
5801        let start = row * p.width;
5802        let result = unsafe { praxis_vec_new(ctx, p.element_descriptor) };
5803        let scope = unsafe { NativeScope::new(ctx) };
5804        let rp = unsafe { vec_payload_mut(scope.root(result)) };
5805        for x in 0..p.width {
5806            rp.items.push(p.items[start + x]);
5807        }
5808        result
5809    })
5810}
5811
5812/// Column `x` as a `Vec`; faults `IndexOutOfBounds` if out of range.
5813///
5814/// # Safety
5815/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`
5816/// must be a valid `Int` `GcRef`.
5817#[unsafe(no_mangle)]
5818pub unsafe extern "C" fn praxis_grid_column(
5819    ctx: *mut RuntimeContext,
5820    grid: GcRef,
5821    x: GcRef,
5822) -> GcRef {
5823    abi_guard!("praxis_grid_column", ctx, {
5824        let p = unsafe { grid_payload(grid) };
5825        let xi = unsafe { int_payload(x) };
5826        // The other axis: a column is bounded by the width alone, and the
5827        // stride below walks only the rows that exist.
5828        let Some(col) = (unsafe { checked_index(ctx, xi, p.width) }) else {
5829            return unsafe { unit_sentinel(ctx) };
5830        };
5831        let result = unsafe { praxis_vec_new(ctx, p.element_descriptor) };
5832        let scope = unsafe { NativeScope::new(ctx) };
5833        let rp = unsafe { vec_payload_mut(scope.root(result)) };
5834        let mut idx = col;
5835        while idx < p.items.len() {
5836            rp.items.push(p.items[idx]);
5837            idx += p.width;
5838        }
5839        result
5840    })
5841}
5842
5843/// `Some((x, y))` for the first position whose cell equals `value`, or `None`
5844/// (§4.7).
5845///
5846/// An `Option` rather than a sentinel: the Unit sentinel under a `(Int, Int)`
5847/// static type is indistinguishable from a real answer. `find_all` needs no
5848/// equivalent — a `Vec` already encodes "nothing matched" as emptiness.
5849///
5850/// # Safety
5851/// `ctx` must be live and wired; `grid` and `value` must be valid `GcRef`s.
5852#[unsafe(no_mangle)]
5853pub unsafe extern "C" fn praxis_grid_find(
5854    ctx: *mut RuntimeContext,
5855    grid: GcRef,
5856    value: GcRef,
5857) -> GcRef {
5858    abi_guard!("praxis_grid_find", ctx, {
5859        let p = unsafe { grid_payload(grid) };
5860        let val_desc = value.descriptor();
5861        let eq = val_desc.equals;
5862        for (i, cell) in p.items.iter().enumerate() {
5863            let matches = match eq {
5864                Some(equals) => {
5865                    let a = cell.payload::<u8>() as *const u8;
5866                    let b = value.payload::<u8>() as *const u8;
5867                    unsafe { equals(a, b) }
5868                }
5869                None => *cell == value,
5870            };
5871            if matches {
5872                let (x, y) = grid_xy(i, p.width);
5873                // `option_some` roots the point across the enum allocation.
5874                return unsafe { option_some(ctx, alloc_point(ctx, x, y)) };
5875            }
5876        }
5877        unsafe { option_none(ctx) }
5878    })
5879}
5880
5881/// All `(x, y)` positions whose cell equals `value`, as a `Vec`.
5882///
5883/// # Safety
5884/// `ctx` must be live and wired; `grid` and `value` must be valid `GcRef`s.
5885#[unsafe(no_mangle)]
5886pub unsafe extern "C" fn praxis_grid_find_all(
5887    ctx: *mut RuntimeContext,
5888    grid: GcRef,
5889    value: GcRef,
5890) -> GcRef {
5891    abi_guard!("praxis_grid_find_all", ctx, {
5892        unsafe { maybe_collect(ctx) };
5893        let p = unsafe { grid_payload(grid) };
5894        let val_desc = value.descriptor();
5895        let eq = val_desc.equals;
5896        let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
5897        let scope = unsafe { NativeScope::new(ctx) };
5898        let rp = unsafe { vec_payload_mut(scope.root(result)) };
5899        for (i, cell) in p.items.iter().enumerate() {
5900            let matches = match eq {
5901                Some(equals) => {
5902                    let a = cell.payload::<u8>() as *const u8;
5903                    let b = value.payload::<u8>() as *const u8;
5904                    unsafe { equals(a, b) }
5905                }
5906                None => *cell == value,
5907            };
5908            if matches {
5909                let (x, y) = grid_xy(i, p.width);
5910                rp.items.push(unsafe { alloc_point(ctx, x, y) });
5911            }
5912        }
5913        result
5914    })
5915}
5916
5917/// A transposed copy of the grid (rows ↔ columns), as a new `Grid`.
5918///
5919/// # Safety
5920/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5921#[unsafe(no_mangle)]
5922pub unsafe extern "C" fn praxis_grid_transpose(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5923    abi_guard!("praxis_grid_transpose", ctx, {
5924        let p = unsafe { grid_payload(grid) };
5925        let height = grid_height(p.items.len(), p.width);
5926        let new_width = height;
5927        let new_height = p.width;
5928        let mut cells = Vec::with_capacity(p.items.len());
5929        for y in 0..new_height {
5930            for x in 0..new_width {
5931                // new[x,y] = old[y,x]
5932                cells.push(p.items[x * p.width + y]);
5933            }
5934        }
5935        let _ = ctx;
5936        // SAFETY: GridPayload is GRID's payload type.
5937        unsafe {
5938            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
5939                element_descriptor: p.element_descriptor,
5940                items: cells,
5941                width: new_width,
5942            })
5943        }
5944    })
5945}
5946
5947/// A copy of the grid rotated 90° left (counter-clockwise), as a new `Grid`.
5948///
5949/// # Safety
5950/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5951#[unsafe(no_mangle)]
5952pub unsafe extern "C" fn praxis_grid_rotate_left(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5953    abi_guard!("praxis_grid_rotate_left", ctx, {
5954        let p = unsafe { grid_payload(grid) };
5955        let height = grid_height(p.items.len(), p.width);
5956        // Rotate left (90° CCW): result is H×W (width=height, height=width).
5957        // With x rightward and y downward, turning counter-clockwise carries the
5958        // *rightmost* column to the top row, top-to-bottom:
5959        // result[x, y] = original[width-1-y, x], for x in 0..height, y in 0..width.
5960        let new_width = height;
5961        let new_height = p.width;
5962        let mut cells = Vec::with_capacity(p.items.len());
5963        for y in 0..new_height {
5964            for x in 0..new_width {
5965                let ox = p.width - 1 - y;
5966                let oy = x;
5967                cells.push(p.items[oy * p.width + ox]);
5968            }
5969        }
5970        // SAFETY: GridPayload is GRID's payload type.
5971        unsafe {
5972            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
5973                element_descriptor: p.element_descriptor,
5974                items: cells,
5975                width: new_width,
5976            })
5977        }
5978    })
5979}
5980
5981/// A copy of the grid rotated 90° right (clockwise), as a new `Grid`.
5982///
5983/// # Safety
5984/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5985#[unsafe(no_mangle)]
5986pub unsafe extern "C" fn praxis_grid_rotate_right(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5987    abi_guard!("praxis_grid_rotate_right", ctx, {
5988        let p = unsafe { grid_payload(grid) };
5989        let height = grid_height(p.items.len(), p.width);
5990        // Rotate right (90° CW): result is H×W (width=height, height=width).
5991        // With x rightward and y downward, turning clockwise carries the *leftmost*
5992        // column to the top row, bottom-to-top:
5993        // result[x, y] = original[y, height-1-x], for x in 0..height, y in 0..width.
5994        let new_width = height;
5995        let new_height = p.width;
5996        let mut cells = Vec::with_capacity(p.items.len());
5997        for y in 0..new_height {
5998            for x in 0..new_width {
5999                let ox = y;
6000                let oy = height - 1 - x;
6001                cells.push(p.items[oy * p.width + ox]);
6002            }
6003        }
6004        // SAFETY: GridPayload is GRID's payload type.
6005        unsafe {
6006            gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
6007                element_descriptor: p.element_descriptor,
6008                items: cells,
6009                width: new_width,
6010            })
6011        }
6012    })
6013}
6014
6015// ---------------------------------------------------------------------------
6016// Text methods (§4.3).
6017//
6018// `Text` is an immutable UTF-8 payload (`Box<str>`). The methods are pure
6019// (no allocation beyond the result object) and never fault.
6020// ---------------------------------------------------------------------------
6021
6022/// Read the `Text` payload of a `GcRef` as a `&str`, following slice owners.
6023///
6024/// # Safety
6025/// `r` must be a valid `Text` `GcRef`. Non-moving GC keeps it stable.
6026unsafe fn text_str(r: GcRef) -> &'static str {
6027    // SAFETY: caller guarantees `r` is Text; payload is a TextPayload.
6028    let payload = r.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload;
6029    unsafe { crate::text::text_str(payload) }
6030}
6031
6032/// The `Text` payload behind a `GcRef`.
6033///
6034/// # Safety
6035/// `r` must be a valid `Text` `GcRef`. Non-moving GC keeps it stable.
6036#[inline]
6037unsafe fn text_payload(r: GcRef) -> *const crate::text::TextPayload {
6038    r.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload
6039}
6040
6041/// The number of Unicode scalar values (chars) in `text`, as a boxed `Int`.
6042///
6043/// **O(1) after the text or its owner has been counted once** (ADR-115). The
6044/// count is cached rather than recomputed as `text_str(text).chars().count()`,
6045/// which is two passes over every byte — `text_str` re-validates the UTF-8 the
6046/// payload is already known to hold, and `chars().count()` then decodes it —
6047/// and this is called *once per iteration* of `for c in t`, because `lower_for`
6048/// puts the plan's `len` call in the loop **header**
6049/// (`praxis-mir/src/build.rs`, `lower_for`).
6050///
6051/// # Safety
6052/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6053#[unsafe(no_mangle)]
6054pub unsafe extern "C" fn praxis_text_len(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6055    abi_guard!("praxis_text_len", ctx, {
6056        // SAFETY: caller guarantees `text` is Text.
6057        let len = unsafe { crate::text::text_char_count(text_payload(text)) } as i64;
6058        unsafe { int_ref(ctx, len) }
6059    })
6060}
6061
6062/// The whole of `text`, trimmed, if `run` accepts all of it — the shared half of
6063/// [`praxis_text_int`] and [`praxis_text_float`] (ADR-136).
6064///
6065/// **`run` is the input parser's own scanner** (`parser::take_int_run`,
6066/// `parser::take_float_run`), and that is the point rather than a convenience.
6067/// `parse(t, int)` and `t.int()` are two spellings of "read a number out of
6068/// text", and a program that gets different answers from them has found a defect
6069/// in one of them. Sharing the scanner makes the disagreement unrepresentable.
6070///
6071/// The difference between the method and the atomic is *how much* must match,
6072/// not what: an atomic stops where its run stops and hands the rest of the line
6073/// to the template, and a method has no rest to hand anywhere — so a run that
6074/// covers less than the whole trimmed text is `None`. That is what makes
6075/// `"1 2"`, `"12abc"` and `"1."` rejections rather than partial answers.
6076///
6077/// Trimming is the one liberty taken, and it is what makes a line read off input
6078/// usable without a second call.
6079fn whole_trimmed(s: &str, run: fn(&[u8]) -> (&str, usize)) -> Option<&str> {
6080    let trimmed = s.trim();
6081    let (text, len) = run(trimmed.as_bytes());
6082    (!text.is_empty() && len == trimmed.len()).then_some(trimmed)
6083}
6084
6085/// The `Int` `text` spells, as `Some(n)`, or `None` when it spells no `Int`
6086/// (ADR-136).
6087///
6088/// `Y001`'s help on `var count: Int = raw` names `.int()`, so this is the method
6089/// that help sends the reader to.
6090///
6091/// `Option[Int]` and not `Int`, for §4.7's reason: a text that is not a number
6092/// is *absence*, not a fault. Input arrives as text and is routinely not what
6093/// the program hoped, so a panicking conversion would make `"abc".int()` a crash
6094/// the program has no way to prevent — where `read lines(int)`, the other half
6095/// of that help, reports at the parser and never produces the value at all.
6096///
6097/// The accepted spelling is **§7.4's `int` atomic** over the whole trimmed text:
6098/// an optional `-` and then digits (see [`whole_trimmed`]). `"1 2"`, `"0x10"`,
6099/// `"1_000"`, `"+5"` and `""` are all `None`, and so is a value outside `Int`'s
6100/// range — for the reason `Y013` exists: a saturated answer is a number nobody
6101/// wrote.
6102///
6103/// # Safety
6104/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6105#[unsafe(no_mangle)]
6106pub unsafe extern "C" fn praxis_text_int(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6107    abi_guard!("praxis_text_int", ctx, {
6108        // SAFETY: caller guarantees `text` is Text.
6109        let s = unsafe { text_str(text) };
6110        match whole_trimmed(s, crate::parser::take_int_run).and_then(|t| t.parse::<i64>().ok()) {
6111            // SAFETY: `ctx` is live and wired; `int_ref` allocates the payload
6112            // and `option_some` roots it across the enum allocation.
6113            Some(n) => unsafe {
6114                let boxed = int_ref(ctx, n);
6115                option_some(ctx, boxed)
6116            },
6117            // SAFETY: `ctx` is live and wired.
6118            None => unsafe { option_none(ctx) },
6119        }
6120    })
6121}
6122
6123/// The `Float` `text` spells, as `Some(x)`, or `None` when it spells no `Float`
6124/// (ADR-136).
6125///
6126/// [`praxis_text_int`]'s twin, over §7.4's `float` atomic: an optional sign,
6127/// digits, an optional `.` **with** a fraction, and an optional complete
6128/// exponent. `"1.5"`, `"-2"`, `"+5.0"` and `"1e10"` are values; `"1."`, `"1e"`,
6129/// `"inf"`, `"nan"` and `""` are `None`, because none of them is a token the
6130/// input parser reads either.
6131///
6132/// `inf` and `nan` are the answer worth stating: Rust's `f64::from_str` accepts
6133/// both, §7.4's `float` accepts neither, and a method that took them would be a
6134/// second opinion about what a number is. `Float` still *has* those values —
6135/// `1.0 / 0.0` is one — and `Float.to_text()` prints them; what has no spelling
6136/// is reading one back out of arbitrary text.
6137///
6138/// The leading `+` this accepts and [`praxis_text_int`] does not is §7.4's own
6139/// asymmetry, carried over rather than papered over: changing an atomic's
6140/// accepted set is a change to the input language.
6141///
6142/// # Safety
6143/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6144#[unsafe(no_mangle)]
6145pub unsafe extern "C" fn praxis_text_float(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6146    abi_guard!("praxis_text_float", ctx, {
6147        // SAFETY: caller guarantees `text` is Text.
6148        let s = unsafe { text_str(text) };
6149        match whole_trimmed(s, crate::parser::take_float_run).and_then(|t| t.parse::<f64>().ok()) {
6150            // SAFETY: `ctx` is live and wired. `praxis_alloc_float` takes the
6151            // bit pattern the uniform scalar ABI carries (§4.3), and
6152            // `option_some` roots the box across the enum allocation.
6153            Some(x) => unsafe {
6154                let boxed = praxis_alloc_float(ctx, x.to_bits() as i64);
6155                option_some(ctx, boxed)
6156            },
6157            // SAFETY: `ctx` is live and wired.
6158            None => unsafe { option_none(ctx) },
6159        }
6160    })
6161}
6162
6163/// True iff `text` has no chars, as a boxed `Bool`.
6164///
6165/// Asks the bytes rather than a `&str`: `text_str` validates the whole payload
6166/// to hand back a `&str`, which would make an O(1) question O(n) (ADR-115). A
6167/// text is empty iff it has no bytes — no scalar encodes to zero of them.
6168///
6169/// # Safety
6170/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6171#[unsafe(no_mangle)]
6172pub unsafe extern "C" fn praxis_text_is_empty(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6173    abi_guard!("praxis_text_is_empty", ctx, {
6174        // SAFETY: caller guarantees `text` is Text.
6175        let empty = unsafe { crate::text::text_bytes(text_payload(text)) }.is_empty();
6176        // SAFETY: ctx/heap valid; Bool immortal path.
6177        unsafe { bool_ref(ctx, empty) }
6178    })
6179}
6180
6181/// `a + b` on two `Text`s — a new owned `Text` holding their concatenation
6182/// (ADR-085).
6183///
6184/// Declared `Allocates` rather than `AllocatesAndFaults`, which is
6185/// `praxis_float_to_text`'s row and for the same reason: both payloads are
6186/// UTF-8 by construction, so their concatenation is too, and there is nothing
6187/// for the `InvalidText` fault to check. Since ADR-111 `praxis_alloc_text` is
6188/// `Allocates` on the same footing — every wrapper here trusts its caller about
6189/// encoding, and the one place that cannot (`praxis_get_input`, which holds the
6190/// host's raw bytes) validates and faults there.
6191///
6192/// The result is `Owned` and never a `Slice`: a concatenation has no single
6193/// owner to point into, and a slice of one would be a lie about its extent.
6194///
6195/// # Safety
6196/// `ctx` must be live and wired; `a` and `b` must be valid `Text` `GcRef`s.
6197#[unsafe(no_mangle)]
6198pub unsafe extern "C" fn praxis_text_concat(ctx: *mut RuntimeContext, a: GcRef, b: GcRef) -> GcRef {
6199    abi_guard!("praxis_text_concat", ctx, {
6200        // SAFETY: caller guarantees both are Text.
6201        let left = unsafe { text_str(a) };
6202        let right = unsafe { text_str(b) };
6203        let mut joined = String::with_capacity(left.len() + right.len());
6204        joined.push_str(left);
6205        joined.push_str(right);
6206        // SAFETY: TextPayload matches TEXT's size/align and is fully initialized.
6207        unsafe { text_ref(ctx, joined) }
6208    })
6209}
6210
6211/// Render `value` into a fresh `Text`, **exactly as `out` renders it** (§8.1,
6212/// ADR-147).
6213///
6214/// This is the whole of an interpolation hole. `"{v}"` on a `Vec[Int]` is
6215/// `[1, 2, 3]` because this function and [`praxis_write_stdout`] are the same
6216/// two lines with a different destination: both call [`GcRef::format`], which
6217/// dispatches through the value's type descriptor. There is no second renderer
6218/// here and there must never be one — writing a `write!` inline instead of
6219/// calling `format` is the mistake this wrapper exists to make unnecessary, and
6220/// it is the mistake ADR-143 decision 2 records for the three scalar rows.
6221///
6222/// That is also why a hole may hold **any** type (ADR-147 decision 2). Every
6223/// `GcRef` has a descriptor and every descriptor has a `format` callback, so
6224/// there is no value this can be handed that it cannot render — which is what
6225/// lets inference impose no requirement on a hole at all.
6226///
6227/// Declared `Allocates`, never `AllocatesAndFaults`: nothing above can fail, and
6228/// a `String` built by `format` is valid UTF-8 by construction, so there is
6229/// nothing for an `InvalidText` fault to check. That is `praxis_text_concat`'s
6230/// row exactly.
6231///
6232/// # Safety
6233/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6234#[unsafe(no_mangle)]
6235pub unsafe extern "C" fn praxis_value_to_text(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6236    abi_guard!("praxis_value_to_text", ctx, {
6237        let mut s = String::new();
6238        value.format(&mut s);
6239        // SAFETY: `s` is valid UTF-8; ctx/heap valid.
6240        unsafe { text_ref(ctx, s) }
6241    })
6242}
6243
6244/// The `Char` at `index`, or an `IndexOutOfBounds` fault if out of range
6245/// (ADR-086). `index` counts Unicode scalar values, not bytes.
6246///
6247/// # Safety
6248/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`; `index`
6249/// must be a valid `Int` `GcRef`.
6250#[unsafe(no_mangle)]
6251pub unsafe extern "C" fn praxis_text_get(
6252    ctx: *mut RuntimeContext,
6253    text: GcRef,
6254    index: GcRef,
6255) -> GcRef {
6256    abi_guard!("praxis_text_get", ctx, {
6257        // SAFETY: caller guarantees `text` is Text.
6258        let payload = unsafe { text_payload(text) };
6259        // SAFETY: caller guarantees `index` is a valid Int.
6260        let idx = unsafe { int_payload(index) };
6261        if idx < 0 {
6262            unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6263            return unsafe { unit_sentinel(ctx) };
6264        }
6265        // **The byte index is the character index exactly when every scalar is
6266        // one byte, and `text_ascii_bytes` answers that in O(1)** (ADR-115).
6267        // The fallback is `chars().nth(i)`, which is O(i): a multi-byte text
6268        // has no random access without either a wider representation or a
6269        // cursor, and ADR-115 declines the cursor with its arithmetic. `idx` is
6270        // non-negative above, so the `as usize` cannot wrap.
6271        // SAFETY: caller guarantees `text` is Text.
6272        if let Some(bytes) = unsafe { crate::text::text_ascii_bytes(payload) } {
6273            return match bytes.get(idx as usize) {
6274                // One-byte scalars are exactly the ASCII range, so the byte
6275                // *is* the code point (§4.3, ADR-086).
6276                Some(&b) => unsafe { char_ref(ctx, u32::from(b)) },
6277                None => {
6278                    unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6279                    unsafe { unit_sentinel(ctx) }
6280                }
6281            };
6282        }
6283        // SAFETY: caller guarantees `text` is Text.
6284        let s = unsafe { text_str(text) };
6285        match s.chars().nth(idx as usize) {
6286            Some(ch) => {
6287                // No validity check, and none belongs here: `ch` is a Rust `char`,
6288                // so `ch as u32` is a valid Unicode scalar by construction. The
6289                // check `praxis_int_to_char` needs is for the values that did not
6290                // come from one — which is why this goes to `char_ref` directly
6291                // rather than through `checked_alloc_char`.
6292                //
6293                // This is the interning's largest site (ADR-107): the same call
6294                // is `t[i]` and every step of `for c in t` (the `iter_plan`
6295                // lowering), so a program that walks a line of ASCII text would
6296                // otherwise box one object per character.
6297                unsafe { char_ref(ctx, ch as u32) }
6298            }
6299            None => {
6300                unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6301                unsafe { unit_sentinel(ctx) }
6302            }
6303        }
6304    })
6305}
6306
6307// ---------------------------------------------------------------------------
6308// `out(...)` — write a value to stdout followed by a newline (§16.1).
6309// ---------------------------------------------------------------------------
6310
6311/// Format `value` through its descriptor and write it to stdout followed by a
6312/// newline. Returns the Unit sentinel (§4.3), matching `out`'s `(T) -> Unit`
6313/// type. Never faults.
6314///
6315/// # Safety
6316/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6317#[unsafe(no_mangle)]
6318pub unsafe extern "C" fn praxis_write_stdout(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6319    abi_guard!("praxis_write_stdout", ctx, {
6320        use std::io::Write;
6321        let mut out = String::new();
6322        value.format(&mut out);
6323        let _ = std::io::stdout().write_all(out.as_bytes());
6324        let _ = std::io::stdout().write_all(b"\n");
6325        // `out` is `(T) -> Unit`: return the Unit sentinel so a Unit-typed value
6326        // flows out, not the printed argument (which would otherwise leak as the
6327        // function's result and be printed a second time by the host).
6328        unsafe { unit_sentinel(ctx) }
6329    })
6330}
6331
6332// ---------------------------------------------------------------------------
6333// `dbg(...)`, `panic(...)`, `assert(...)` — the rest of §16.1's control names.
6334// ---------------------------------------------------------------------------
6335
6336/// Format `value` through its descriptor, write it to stderr followed by a
6337/// newline, and hand **the same reference back** (§8.1). `dbg` is `forall T.
6338/// (T) -> T`, so it can be wrapped around any subexpression without changing
6339/// what the program computes. Never faults, never allocates.
6340///
6341/// # Safety
6342/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6343#[unsafe(no_mangle)]
6344pub unsafe extern "C" fn praxis_dbg(_ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6345    abi_guard!("praxis_dbg", _ctx, {
6346        use std::io::Write;
6347        let mut rendered = String::new();
6348        value.format(&mut rendered);
6349        let _ = std::io::stderr().write_all(rendered.as_bytes());
6350        let _ = std::io::stderr().write_all(b"\n");
6351        value
6352    })
6353}
6354
6355/// Record `value` as the fault message and raise [`FaultKind::Panic`] (§9.1).
6356///
6357/// The message is rendered **here**, through the value's descriptor, exactly as
6358/// `out` renders its argument. It has to be: the host reads the message after
6359/// the heap the `GcRef` points into has been torn down, so a stored reference
6360/// would outlive what it names.
6361///
6362/// Returns the Unit sentinel. `panic` is `forall T. (T) -> Never`, so no caller
6363/// can use the result — but the ABI returns a `GcRef` on every path, and a
6364/// fault epilogue needs a defined value to carry out (§10.4).
6365///
6366/// # Safety
6367/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6368#[unsafe(no_mangle)]
6369pub unsafe extern "C" fn praxis_panic(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6370    abi_guard!("praxis_panic", ctx, {
6371        let mut message = String::new();
6372        value.format(&mut message);
6373        unsafe { set_fault_message(ctx, message) };
6374        unsafe { set_fault(ctx, RaisedFault::PANIC) };
6375        unsafe { unit_sentinel(ctx) }
6376    })
6377}
6378
6379/// Raise [`FaultKind::AssertFailed`] when `condition` is false (§9.1), and do
6380/// nothing at all when it is true.
6381///
6382/// `assert` is `(Bool) -> Unit`, so the argument is one of the two `Bool`
6383/// immortals and reading its payload needs no descriptor check.
6384///
6385/// It sets **no** message: `assert` takes a condition and nothing else, so the
6386/// only text available would restate the fault kind. `panic` is the name that
6387/// carries words.
6388///
6389/// # Safety
6390/// `ctx` must be live and wired; `condition` must be a valid `Bool` `GcRef`.
6391#[unsafe(no_mangle)]
6392pub unsafe extern "C" fn praxis_assert(ctx: *mut RuntimeContext, condition: GcRef) -> GcRef {
6393    abi_guard!("praxis_assert", ctx, {
6394        // SAFETY: `assert`'s scheme is `(Bool) -> Unit`, so the argument is a Bool.
6395        if !unsafe { crate::immortal::read_bool(condition) } {
6396            unsafe { set_fault(ctx, RaisedFault::ASSERT_FAILED) };
6397        }
6398        unsafe { unit_sentinel(ctx) }
6399    })
6400}
6401
6402// ---------------------------------------------------------------------------
6403// `Range` (§4.11, ADR-059).
6404//
6405// `a..b` and `a..=b` are two symbols rather than one symbol with a flag: the
6406// choice is already a syntactic fact the MIR builder holds, and a boolean
6407// smuggled through an `i64` parameter would have 2^64 spellings for two states.
6408// Both bounds arrive as `Int` `GcRef`s, because a bound is an arbitrary
6409// expression and every other wrapper takes its operands boxed.
6410// ---------------------------------------------------------------------------
6411
6412/// Build the half-open range `start..end` (§4.11). A descending range is
6413/// **empty** — [`RangeVal::new`](crate::range::RangeVal::new) normalizes it, so
6414/// no range with a negative length exists.
6415///
6416/// # Safety
6417/// `ctx` must be live and wired; both bounds must be valid `Int` `GcRef`s.
6418#[unsafe(no_mangle)]
6419pub unsafe extern "C" fn praxis_range_new(
6420    ctx: *mut RuntimeContext,
6421    start: GcRef,
6422    end: GcRef,
6423) -> GcRef {
6424    abi_guard!("praxis_range_new", ctx, {
6425        let a = unsafe { int_payload(start) };
6426        let b = unsafe { int_payload(end) };
6427        unsafe {
6428            gc_alloc(
6429                ctx,
6430                crate::range::RANGE_PAYLOAD,
6431                crate::range::RangeVal::new(a, b),
6432            )
6433        }
6434    })
6435}
6436
6437/// Build the inclusive range `start..=end` (§4.11).
6438///
6439/// # Safety
6440/// `ctx` must be live and wired; both bounds must be valid `Int` `GcRef`s.
6441#[unsafe(no_mangle)]
6442pub unsafe extern "C" fn praxis_range_new_inclusive(
6443    ctx: *mut RuntimeContext,
6444    start: GcRef,
6445    end: GcRef,
6446) -> GcRef {
6447    abi_guard!("praxis_range_new_inclusive", ctx, {
6448        let a = unsafe { int_payload(start) };
6449        let b = unsafe { int_payload(end) };
6450        unsafe {
6451            gc_alloc(
6452                ctx,
6453                crate::range::RANGE_PAYLOAD,
6454                crate::range::RangeVal::new_inclusive(a, b),
6455            )
6456        }
6457    })
6458}
6459
6460/// The number of integers in a range (§4.11) — what a `for` loop reads to
6461/// bound itself.
6462///
6463/// **Faults when the count does not fit an `Int`.** Only the very widest ranges
6464/// reach it (`Int::MIN..Int::MAX` holds `2^64 - 1` integers), and reporting a
6465/// wrapped negative length instead would be a `for` loop that ran zero times
6466/// over every integer there is.
6467///
6468/// The kind is `IntOverflow`, which is what `gcd`, `lcm` and A\*'s path cost
6469/// already answer for a result with no `Int`. It is deliberately not
6470/// `EmptyRange`: the range this fires on is the *fullest* one there is, so that
6471/// message would lie about it (ADR-059, ADR-075).
6472///
6473/// # Safety
6474/// `ctx` must be live and wired; `r` must be a valid `Range` `GcRef`.
6475#[unsafe(no_mangle)]
6476pub unsafe extern "C" fn praxis_range_len(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
6477    abi_guard!("praxis_range_len", ctx, {
6478        // SAFETY: the compiler only emits this with a Range-typed operand.
6479        let range = unsafe { &*r.payload::<crate::range::RangeVal>() };
6480        match i64::try_from(range.len()) {
6481            Ok(len) => unsafe { int_ref(ctx, len) },
6482            Err(_) => {
6483                unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
6484                unsafe { unit_sentinel(ctx) }
6485            }
6486        }
6487    })
6488}
6489
6490/// The `index`-th integer of a range (§4.11). Faults when `index` is outside
6491/// it, exactly as `Vec.get` does.
6492///
6493/// # Safety
6494/// `ctx` must be live and wired; `r` must be a valid `Range` `GcRef` and
6495/// `index` a valid `Int` one.
6496#[unsafe(no_mangle)]
6497pub unsafe extern "C" fn praxis_range_get(
6498    ctx: *mut RuntimeContext,
6499    r: GcRef,
6500    index: GcRef,
6501) -> GcRef {
6502    abi_guard!("praxis_range_get", ctx, {
6503        // SAFETY: the compiler only emits this with a Range-typed receiver.
6504        let range = unsafe { &*r.payload::<crate::range::RangeVal>() };
6505        let i = unsafe { int_payload(index) };
6506        match range.get(i) {
6507            Some(value) => unsafe { int_ref(ctx, value) },
6508            None => {
6509                unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6510                unsafe { unit_sentinel(ctx) }
6511            }
6512        }
6513    })
6514}
6515
6516// ---------------------------------------------------------------------------
6517// Input parser (§7).
6518//
6519// `read` / `parse` lower to runtime calls that fetch the input buffer and run
6520// a compiled parser plan against it. The plan is compiled at HIR time and
6521// registered in a global slab; its index is passed as a boxed Int.
6522// ---------------------------------------------------------------------------
6523
6524/// Return the process-input source buffer (§7.10), reading it the **first**
6525/// time a program asks.
6526///
6527/// A `read` lowers to this call and then to `praxis_run_parser`, so this is
6528/// where §7.10's "the first `read` lazily reads standard input once" happens.
6529/// The host installs a [`crate::input::InputReader`] rather than a buffer; it
6530/// is called at most once — [`crate::input::take_input_reader`] removes it, so
6531/// "once" is structural rather than a flag — and the result is installed as
6532/// `input_source`, which every later `read` reuses.
6533///
6534/// Nothing before a program's first `read` touches the host's input. Reading it
6535/// up front would make a program with no `read` in it still consume standard
6536/// input, so `praxis run` against an open pipe would block forever.
6537///
6538/// A host that installs no reader — every JIT test, and the crash debugger's
6539/// re-run path, which installs the buffer directly to keep re-runs identical
6540/// (§9.7) — reaches the plain `input_source` read below.
6541///
6542/// **A reader that answers zero bytes has given empty input, not no input.** Its
6543/// answer is installed as `input_source` whatever its length, so `read` runs
6544/// against a zero-length buffer and the parser constructors answer from their own
6545/// rules — `lines(int)` over it is `[]` by `split_lines`'s rule, and one that
6546/// requires content faults at `0..0` naming what it expected. That is what §7.11
6547/// asks a mismatch to carry, and a fault raised before any buffer existed can
6548/// carry none of it: it has no input span to name. A zero-byte `--input` file is
6549/// the same decision, made at `praxis-cli/src/run.rs` (ADR-087).
6550///
6551/// The one remaining Unit-source state belongs to a host that installs **neither**
6552/// a buffer nor a reader — every JIT test, every embedder. `praxis_run_parser`'s
6553/// descriptor guard (§6.3) is what keeps that state survivable; no `praxis run`
6554/// reaches it.
6555///
6556/// **This wrapper owns the UTF-8 judgement, and it is the only producer of
6557/// [`FaultKind::InvalidText`](crate::FaultKind::InvalidText)** (ADR-111). A
6558/// reader's bytes are the host's, not the compiler's, so they are checked here
6559/// and `INVALID_TEXT` is raised here — where `lower_read`'s `CheckFault` makes
6560/// it divert at the `read`. Raising it inside `praxis_alloc_text` instead would
6561/// cost a check after every text *literal* for a fault a literal cannot
6562/// produce; that wrapper trusts its caller, and this is the caller that has to
6563/// earn the trust.
6564///
6565/// `praxis run` cannot reach the fault: `lazy_stdin::read` goes through
6566/// `std::io::read_to_string` and exits 2 on non-UTF-8 stdin before the runtime
6567/// sees a byte. An embedder installing its own reader can.
6568///
6569/// # Safety
6570/// `ctx` must be live and wired.
6571#[unsafe(no_mangle)]
6572pub unsafe extern "C" fn praxis_get_input(ctx: *mut RuntimeContext) -> GcRef {
6573    abi_guard!("praxis_get_input", ctx, {
6574        if let Some(read) = crate::input::take_input_reader() {
6575            let bytes = read();
6576            // **This is the one place in the runtime that holds raw host bytes,
6577            // so it is the one place the UTF-8 judgement §4.3 assigns belongs**
6578            // (ADR-111). Here the fault is real: a host's `InputReader` is
6579            // infallible about I/O by design (`crate::input`) and says nothing
6580            // about encoding, so these bytes are exactly as trustworthy as the
6581            // host. `GetInput`'s row is `AllocatesAndFaults` and `lower_read`
6582            // emits the check, so `InvalidText` diverts *at the `read`*.
6583            //
6584            // The Unit sentinel is the defined dummy (§10.4); `input_source`
6585            // holds it until a buffer is installed, so answering it below is
6586            // the same value by a shorter route.
6587            let Ok(text) = std::str::from_utf8(&bytes) else {
6588                unsafe { set_fault(ctx, RaisedFault::INVALID_TEXT) };
6589                return unsafe { (*ctx).input_source };
6590            };
6591            // **The validation is strictly before the allocation, and must
6592            // stay there.** SAFETY: `text` borrows a live, initialized buffer
6593            // for this call, and `ctx` is the caller's live context. The result
6594            // is stored into `input_source` — a root (`RuntimeRoots`) — with no
6595            // allocation in between, so the collection this allocation paces
6596            // cannot reclaim it. `praxis_alloc_text` takes `&[]` for
6597            // `len == 0`, so the empty answer needs no special case here and
6598            // must not get one.
6599            let text = unsafe { praxis_alloc_text(ctx, text.as_ptr(), text.len()) };
6600            unsafe { (*ctx).input_source = text };
6601        }
6602        unsafe { (*ctx).input_source }
6603    })
6604}
6605
6606/// Run a compiled parser plan against `input`, returning the parsed result as a
6607/// `GcRef` (§7.1). `plan_index_gc` is a boxed `Int` whose payload is the
6608/// plan's index in the HIR's global slab.
6609///
6610/// On a parse mismatch (or a non-Text `input`), sets `FaultKind::ParseFailed`
6611/// and returns the Unit sentinel (§7.11). No Rust panic crosses the ABI.
6612///
6613/// The non-Text guard is load-bearing (§6.3 host-safety gap): the parser
6614/// interpreter reinterprets `input`'s payload as a `TextPayload`, so a non-Text
6615/// `input` (e.g. the default Unit singleton when no input buffer was installed)
6616/// would be dereferenced as a Text buffer and segfault. Both `read` (whose
6617/// `input` comes from `praxis_get_input`) and `parse(text, expr)` (whose `input`
6618/// is an arbitrary expression) funnel through here, so guarding at this ABI
6619/// boundary closes the gap regardless of how the input was produced.
6620///
6621/// The guard **clears** the parse detail and records none of its own. It runs no
6622/// parse, so it has nothing to report — and fabricating a [`ParseFail`] there
6623/// would be worse than silence: with no buffer there is no input span, and an
6624/// invented `expected` would make an embedder's host bug read as a parse failure
6625/// at an offset that does not exist. Clearing is also what stops it reporting a
6626/// *previous* parse's offset: this is the one entry into the parser that does
6627/// not go through `run_plan`'s own clear.
6628///
6629/// # Safety
6630/// `ctx` must be live and wired; `plan_index_gc` must be a valid `Int`; `input`
6631/// must be a valid `GcRef` (any descriptor — a non-Text descriptor faults cleanly
6632/// rather than dereferencing garbage).
6633#[unsafe(no_mangle)]
6634pub unsafe extern "C" fn praxis_run_parser(
6635    ctx: *mut RuntimeContext,
6636    plan_index_gc: GcRef,
6637    input: GcRef,
6638) -> GcRef {
6639    abi_guard!("praxis_run_parser", ctx, {
6640        // Guard the parser interpreter against a non-Text input (§6.3). Reaching
6641        // `run_plan` with a non-Text payload would reinterpret foreign bytes as a
6642        // TextPayload and segfault; fault cleanly instead.
6643        if input.descriptor().id() != crate::text::TEXT.id() {
6644            unsafe { crate::parser::clear_parse_detail(ctx) };
6645            unsafe { set_fault(ctx, RaisedFault::PARSE_FAILED) };
6646            return unsafe { unit_sentinel(ctx) };
6647        }
6648        let idx = unsafe { int_payload(plan_index_gc) };
6649        // Delegate to the parser interpreter. It validates the id, reads the
6650        // plan from the process-wide arena, runs it against the input bytes, and
6651        // allocates the result.
6652        // SAFETY: `ctx` is the wrapper's own context argument, and `input` was
6653        // checked to carry a Text payload above.
6654        match unsafe { crate::parser::run_plan_by_id(ctx, idx, input) } {
6655            Some(result) => result,
6656            None => {
6657                // A `None` return means the value named no registered plan (out of
6658                // range, negative, or zero) or the interpreter was not linked.
6659                // Treat as a parse fault.
6660                unsafe { set_fault(ctx, RaisedFault::PARSE_FAILED) };
6661                unsafe { unit_sentinel(ctx) }
6662            }
6663        }
6664    })
6665}
6666
6667// ---------------------------------------------------------------------------
6668// Graph helpers (§6.5, ADR-060).
6669//
6670// Twelve prelude names whose graph is a closure: the caller passes a start
6671// state and a function from a state to its neighbours, and the wrapper walks
6672// whatever that function describes. `crate::graph` owns the walks and never
6673// touches a closure; `ClosureOracle` below is the one thing that does.
6674//
6675// The bare name is the whole walk, `_distance` is the number of the cheapest
6676// route to a goal, and `_path` is the route. The two goal-directed forms of a
6677// family share one walk and project the `cost` or the `states` out of the one
6678// `graph::Route` it answers, so they can never disagree about which route was
6679// found.
6680// ---------------------------------------------------------------------------
6681
6682/// A [`GraphOracle`](crate::graph::GraphOracle) backed by the closures a
6683/// program passed, with every state it is handed rooted in a native frame.
6684///
6685/// The scope is what makes the walks safe: a state lives in a Rust visited set
6686/// or queue, which the collector cannot see, and every closure call may
6687/// allocate. `retain` roots each state the moment the walk decides to remember
6688/// it, so a collection triggered inside the *next* call finds it.
6689struct ClosureOracle<'s, 'c> {
6690    ctx: *mut RuntimeContext,
6691    scope: &'s NativeScope<'c>,
6692    /// `(T) -> Vec[T]`.
6693    neighbours: GcRef,
6694    /// `(T, T) -> Int`, or the Unit sentinel for a helper that has no weights.
6695    weight: GcRef,
6696    /// `(T) -> Int`, or the Unit sentinel for a helper with no heuristic.
6697    heuristic: GcRef,
6698    /// `(T) -> Bool`, or the Unit sentinel for a helper with no goal test.
6699    goal: GcRef,
6700}
6701
6702impl ClosureOracle<'_, '_> {
6703    /// Call `closure` with `args`, or `Err` if it faulted — or if it is not a
6704    /// closure at all.
6705    ///
6706    /// The type checker says every one of these operands is a function, and the
6707    /// only runtime representation of a function value is a closure object. The
6708    /// descriptor is checked anyway: the alternative to a `TypeMismatch` fault
6709    /// is transmuting whatever the payload's first word happens to be into a
6710    /// function pointer and jumping to it.
6711    unsafe fn call(
6712        &mut self,
6713        closure: GcRef,
6714        args: &[GcRef],
6715    ) -> Result<GcRef, crate::graph::Aborted> {
6716        if !std::ptr::eq(closure.descriptor(), &crate::closures::CLOSURE) {
6717            return Err(self.abort(crate::context::FaultKind::TypeMismatch));
6718        }
6719        // SAFETY: the descriptor check above proves the payload is a
6720        // `ClosurePayload`, so `fn_ptr` is the entry point the codegen wrote
6721        // there (`praxis_alloc_closure`).
6722        let fn_ptr = unsafe { (*closure.payload::<crate::closures::ClosurePayload>()).fn_ptr };
6723        // A closure's entry point is `fn(ctx, closure_self, params...) -> GcRef`
6724        // (§4.10, Approach B): the closure value itself is a hidden first
6725        // explicit argument, and the prologue loads its captures from it. The
6726        // arity is fixed by the helper's signature, which inference has already
6727        // checked, so only the shapes the six helpers use exist here.
6728        let result = match args {
6729            // SAFETY: `fn_ptr` is a finalized JIT entry whose parameter count is
6730            // the one the type checker enforced for this operand; every value
6731            // crossing is a `GcRef`, which is the ABI's only value kind.
6732            [a] => unsafe {
6733                let f: unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef) -> GcRef =
6734                    std::mem::transmute(fn_ptr);
6735                f(self.ctx, closure, *a)
6736            },
6737            // SAFETY: as above, at the two-parameter shape.
6738            [a, b] => unsafe {
6739                let f: unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef, GcRef) -> GcRef =
6740                    std::mem::transmute(fn_ptr);
6741                f(self.ctx, closure, *a, *b)
6742            },
6743            // Unreachable: `GraphParam` has no shape with another arity, and the
6744            // match on it in `seed_builtin_schemes` is exhaustive. Faulting is
6745            // still the only safe answer, because the alternative is calling
6746            // with the wrong number of arguments.
6747            _ => return Err(self.abort(crate::context::FaultKind::TypeMismatch)),
6748        };
6749        // The closure ran arbitrary Praxis code and may have faulted. Its result
6750        // on that path is the Unit sentinel, so continuing would walk a graph of
6751        // Units; stop instead, leaving the fault for the call site's own check.
6752        if unsafe { praxis_check_fault(self.ctx) } != 0 {
6753            return Err(crate::graph::Aborted);
6754        }
6755        Ok(self.scope.root(result).get())
6756    }
6757
6758    /// The `i64` inside a boxed `Int` a closure returned, or a fault if it is
6759    /// not one.
6760    unsafe fn int_result(&mut self, value: GcRef) -> Result<i64, crate::graph::Aborted> {
6761        if !std::ptr::eq(value.descriptor(), &scalars::INT) {
6762            return Err(self.abort(crate::context::FaultKind::TypeMismatch));
6763        }
6764        Ok(unsafe { int_payload(value) })
6765    }
6766}
6767
6768impl crate::graph::GraphOracle for ClosureOracle<'_, '_> {
6769    fn neighbours(&mut self, state: GcRef) -> Result<Vec<GcRef>, crate::graph::Aborted> {
6770        // SAFETY: `ctx` is live for the wrapper's duration and `neighbours` is
6771        // the operand the type checker typed `(T) -> Vec[T]`.
6772        let result = unsafe { self.call(self.neighbours, &[state])? };
6773        if !std::ptr::eq(result.descriptor(), &crate::collections::VEC) {
6774            return Err(self.abort(crate::context::FaultKind::TypeMismatch));
6775        }
6776        // SAFETY: the descriptor check proves the payload is a `VecPayload`, and
6777        // the result is rooted by `call`, so reading its items cannot race a
6778        // collection — nothing allocates between here and the copy.
6779        let items = unsafe { (*result.payload::<VecPayload>()).items.to_vec() };
6780        for item in &items {
6781            self.scope.root(*item);
6782        }
6783        Ok(items)
6784    }
6785
6786    fn weight(&mut self, from: GcRef, to: GcRef) -> Result<i64, crate::graph::Aborted> {
6787        // SAFETY: as above, at the `(T, T) -> Int` operand.
6788        let result = unsafe { self.call(self.weight, &[from, to])? };
6789        // SAFETY: `result` is a live, rooted `GcRef`.
6790        unsafe { self.int_result(result) }
6791    }
6792
6793    fn heuristic(&mut self, state: GcRef) -> Result<i64, crate::graph::Aborted> {
6794        // SAFETY: as above, at the `(T) -> Int` operand.
6795        let result = unsafe { self.call(self.heuristic, &[state])? };
6796        // SAFETY: `result` is a live, rooted `GcRef`.
6797        unsafe { self.int_result(result) }
6798    }
6799
6800    fn is_goal(&mut self, state: GcRef) -> Result<bool, crate::graph::Aborted> {
6801        // SAFETY: as above, at the `(T) -> Bool` operand.
6802        let result = unsafe { self.call(self.goal, &[state])? };
6803        // A `Bool`'s payload is **one byte**. Reading it as an `i64` would take
6804        // seven further bytes of the block's alignment padding, which the bump
6805        // allocator never initialized. `read_scalar` checks the descriptor and
6806        // takes the width from `BOOL_PAYLOAD`'s own type, so neither half is
6807        // written here.
6808        //
6809        // SAFETY: `result` is a `GcRef` the oracle's own call just produced.
6810        match unsafe { read_scalar(result, scalars::BOOL_PAYLOAD) } {
6811            Some(b) => Ok(b != 0),
6812            None => Err(self.abort(crate::context::FaultKind::TypeMismatch)),
6813        }
6814    }
6815
6816    fn retain(&mut self, state: GcRef) {
6817        self.scope.root(state);
6818    }
6819
6820    fn abort(&mut self, kind: crate::context::FaultKind) -> crate::graph::Aborted {
6821        if let Some(fault) = RaisedFault::new(kind) {
6822            // SAFETY: `ctx` is live and wired for the wrapper's duration.
6823            unsafe { set_fault(self.ctx, fault) };
6824        }
6825        crate::graph::Aborted
6826    }
6827}
6828
6829/// The descriptor every state in this walk shares: the start state's own.
6830///
6831/// The type checker guarantees one state type per call, and a `GcRef` carries
6832/// its descriptor in its header — so the start state is the authority on what
6833/// the result collection holds, and no separate type argument has to cross the
6834/// ABI.
6835#[inline]
6836fn state_descriptor(start: GcRef) -> *const TypeDescriptor {
6837    start.descriptor() as *const TypeDescriptor
6838}
6839
6840/// Build a `Vec[T]` holding `states`, in order.
6841///
6842/// # Safety
6843/// `ctx` must be live and wired; every state must be a valid, rooted `GcRef`.
6844unsafe fn states_as_vec(
6845    ctx: *mut RuntimeContext,
6846    element: *const TypeDescriptor,
6847    states: &[GcRef],
6848) -> GcRef {
6849    let result = unsafe { praxis_vec_new(ctx, element) };
6850    let scope = unsafe { NativeScope::new(ctx) };
6851    let rooted = scope.root(result);
6852    // SAFETY: `result` is the `Vec` just allocated, and `rooted` proves it is in
6853    // the collector's root set for the borrow.
6854    let payload = unsafe { vec_payload_mut(rooted) };
6855    payload.items.extend_from_slice(states);
6856    result
6857}
6858
6859/// `bfs(start, neighbours)` — every reachable state, in breadth-first order
6860/// (§6.5).
6861///
6862/// # Safety
6863/// `ctx` must be live and wired; `start` must be a valid `GcRef` and
6864/// `neighbours` a closure value of type `(T) -> Vec[T]`.
6865#[unsafe(no_mangle)]
6866pub unsafe extern "C" fn praxis_bfs(
6867    ctx: *mut RuntimeContext,
6868    start: GcRef,
6869    neighbours: GcRef,
6870) -> GcRef {
6871    abi_guard!("praxis_bfs", ctx, {
6872        // SAFETY: the caller upholds ctx/operand validity.
6873        unsafe {
6874            let scope = NativeScope::new(ctx);
6875            let mut oracle = ClosureOracle {
6876                ctx,
6877                scope: &scope,
6878                neighbours,
6879                weight: unit_sentinel(ctx),
6880                heuristic: unit_sentinel(ctx),
6881                goal: unit_sentinel(ctx),
6882            };
6883            match crate::graph::bfs_order(&mut oracle, start) {
6884                Ok(states) => states_as_vec(ctx, state_descriptor(start), &states),
6885                Err(_) => unit_sentinel(ctx),
6886            }
6887        }
6888    })
6889}
6890
6891/// `dfs(start, neighbours)` — every reachable state, in depth-first pre-order
6892/// (§6.5).
6893///
6894/// # Safety
6895/// As [`praxis_bfs`].
6896#[unsafe(no_mangle)]
6897pub unsafe extern "C" fn praxis_dfs(
6898    ctx: *mut RuntimeContext,
6899    start: GcRef,
6900    neighbours: GcRef,
6901) -> GcRef {
6902    abi_guard!("praxis_dfs", ctx, {
6903        // SAFETY: the caller upholds ctx/operand validity.
6904        unsafe {
6905            let scope = NativeScope::new(ctx);
6906            let mut oracle = ClosureOracle {
6907                ctx,
6908                scope: &scope,
6909                neighbours,
6910                weight: unit_sentinel(ctx),
6911                heuristic: unit_sentinel(ctx),
6912                goal: unit_sentinel(ctx),
6913            };
6914            match crate::graph::dfs_order(&mut oracle, start) {
6915                Ok(states) => states_as_vec(ctx, state_descriptor(start), &states),
6916                Err(_) => unit_sentinel(ctx),
6917            }
6918        }
6919    })
6920}
6921
6922/// `flood_fill(start, neighbours)` — every reachable state, as a `Set` (§6.5).
6923///
6924/// # Safety
6925/// As [`praxis_bfs`].
6926#[unsafe(no_mangle)]
6927pub unsafe extern "C" fn praxis_flood_fill(
6928    ctx: *mut RuntimeContext,
6929    start: GcRef,
6930    neighbours: GcRef,
6931) -> GcRef {
6932    abi_guard!("praxis_flood_fill", ctx, {
6933        // SAFETY: the caller upholds ctx/operand validity.
6934        unsafe {
6935            let scope = NativeScope::new(ctx);
6936            let mut oracle = ClosureOracle {
6937                ctx,
6938                scope: &scope,
6939                neighbours,
6940                weight: unit_sentinel(ctx),
6941                heuristic: unit_sentinel(ctx),
6942                goal: unit_sentinel(ctx),
6943            };
6944            let states = match crate::graph::reachable(&mut oracle, start) {
6945                Ok(states) => states,
6946                Err(_) => return unit_sentinel(ctx),
6947            };
6948            let result = praxis_set_new(ctx, state_descriptor(start));
6949            let rooted = scope.root(result);
6950            let payload = set_payload_mut(rooted);
6951            for state in states {
6952                payload.entries.insert(DynamicKey::new(state));
6953            }
6954            result
6955        }
6956    })
6957}
6958
6959/// `bfs_distance(start, neighbours, is_goal)` — the fewest steps to a goal, or
6960/// `None` (§6.5).
6961///
6962/// # Safety
6963/// `ctx` must be live and wired; `start` must be a valid `GcRef`, `neighbours` a
6964/// `(T) -> Vec[T]` closure and `goal` a `(T) -> Bool` closure.
6965#[unsafe(no_mangle)]
6966pub unsafe extern "C" fn praxis_bfs_distance(
6967    ctx: *mut RuntimeContext,
6968    start: GcRef,
6969    neighbours: GcRef,
6970    goal: GcRef,
6971) -> GcRef {
6972    abi_guard!("praxis_bfs_distance", ctx, {
6973        // SAFETY: the caller upholds ctx/operand validity.
6974        unsafe {
6975            let scope = NativeScope::new(ctx);
6976            let mut oracle = ClosureOracle {
6977                ctx,
6978                scope: &scope,
6979                neighbours,
6980                weight: unit_sentinel(ctx),
6981                heuristic: unit_sentinel(ctx),
6982                goal,
6983            };
6984            match crate::graph::bfs_route(&mut oracle, start) {
6985                Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
6986                Err(_) => unit_sentinel(ctx),
6987            }
6988        }
6989    })
6990}
6991
6992/// `bfs_path(start, neighbours, is_goal)` — a shortest route to a goal, from
6993/// the start to the goal inclusive, or `None` (§6.5).
6994///
6995/// The same walk [`praxis_bfs_distance`] runs; this projects the route out of
6996/// its answer where that one projects the number.
6997///
6998/// # Safety
6999/// As [`praxis_bfs_distance`].
7000#[unsafe(no_mangle)]
7001pub unsafe extern "C" fn praxis_bfs_path(
7002    ctx: *mut RuntimeContext,
7003    start: GcRef,
7004    neighbours: GcRef,
7005    goal: GcRef,
7006) -> GcRef {
7007    abi_guard!("praxis_bfs_path", ctx, {
7008        // SAFETY: the caller upholds ctx/operand validity.
7009        unsafe {
7010            let scope = NativeScope::new(ctx);
7011            let mut oracle = ClosureOracle {
7012                ctx,
7013                scope: &scope,
7014                neighbours,
7015                weight: unit_sentinel(ctx),
7016                heuristic: unit_sentinel(ctx),
7017                goal,
7018            };
7019            match crate::graph::bfs_route(&mut oracle, start) {
7020                Ok(route) => states_as_optional_vec(
7021                    ctx,
7022                    state_descriptor(start),
7023                    route.as_ref().map(|r| r.states.as_slice()),
7024                ),
7025                Err(_) => unit_sentinel(ctx),
7026            }
7027        }
7028    })
7029}
7030
7031/// `dfs_distance(start, neighbours, is_goal)` — the number of edges on the
7032/// route depth-first search found to a goal, or `None` (§6.5).
7033///
7034/// Depth-first arrives by the route it descended into first, which need not be
7035/// a short one, so this and [`praxis_bfs_distance`] answer different numbers on
7036/// the same graph.
7037///
7038/// # Safety
7039/// As [`praxis_bfs_distance`].
7040#[unsafe(no_mangle)]
7041pub unsafe extern "C" fn praxis_dfs_distance(
7042    ctx: *mut RuntimeContext,
7043    start: GcRef,
7044    neighbours: GcRef,
7045    goal: GcRef,
7046) -> GcRef {
7047    abi_guard!("praxis_dfs_distance", ctx, {
7048        // SAFETY: the caller upholds ctx/operand validity.
7049        unsafe {
7050            let scope = NativeScope::new(ctx);
7051            let mut oracle = ClosureOracle {
7052                ctx,
7053                scope: &scope,
7054                neighbours,
7055                weight: unit_sentinel(ctx),
7056                heuristic: unit_sentinel(ctx),
7057                goal,
7058            };
7059            match crate::graph::dfs_route(&mut oracle, start) {
7060                Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7061                Err(_) => unit_sentinel(ctx),
7062            }
7063        }
7064    })
7065}
7066
7067/// `dfs_path(start, neighbours, is_goal)` — the route depth-first search found
7068/// to a goal, from the start to the goal inclusive, or `None` (§6.5).
7069///
7070/// # Safety
7071/// As [`praxis_bfs_distance`].
7072#[unsafe(no_mangle)]
7073pub unsafe extern "C" fn praxis_dfs_path(
7074    ctx: *mut RuntimeContext,
7075    start: GcRef,
7076    neighbours: GcRef,
7077    goal: GcRef,
7078) -> GcRef {
7079    abi_guard!("praxis_dfs_path", ctx, {
7080        // SAFETY: the caller upholds ctx/operand validity.
7081        unsafe {
7082            let scope = NativeScope::new(ctx);
7083            let mut oracle = ClosureOracle {
7084                ctx,
7085                scope: &scope,
7086                neighbours,
7087                weight: unit_sentinel(ctx),
7088                heuristic: unit_sentinel(ctx),
7089                goal,
7090            };
7091            match crate::graph::dfs_route(&mut oracle, start) {
7092                Ok(route) => states_as_optional_vec(
7093                    ctx,
7094                    state_descriptor(start),
7095                    route.as_ref().map(|r| r.states.as_slice()),
7096                ),
7097                Err(_) => unit_sentinel(ctx),
7098            }
7099        }
7100    })
7101}
7102
7103/// `dijkstra(start, neighbours, weight)` — the least cost to every reachable
7104/// state, as a `Map[T, Int]` (§6.5).
7105///
7106/// # Safety
7107/// `ctx` must be live and wired; `start` must be a valid `GcRef`, `neighbours` a
7108/// `(T) -> Vec[T]` closure and `weight` a `(T, T) -> Int` closure.
7109#[unsafe(no_mangle)]
7110pub unsafe extern "C" fn praxis_dijkstra(
7111    ctx: *mut RuntimeContext,
7112    start: GcRef,
7113    neighbours: GcRef,
7114    weight: GcRef,
7115) -> GcRef {
7116    abi_guard!("praxis_dijkstra", ctx, {
7117        // SAFETY: the caller upholds ctx/operand validity.
7118        unsafe {
7119            let scope = NativeScope::new(ctx);
7120            let mut oracle = ClosureOracle {
7121                ctx,
7122                scope: &scope,
7123                neighbours,
7124                weight,
7125                heuristic: unit_sentinel(ctx),
7126                goal: unit_sentinel(ctx),
7127            };
7128            let costs = match crate::graph::dijkstra_costs(&mut oracle, start) {
7129                Ok(costs) => costs,
7130                Err(_) => return unit_sentinel(ctx),
7131            };
7132            let result = scope.root(praxis_map_new(ctx, state_descriptor(start)));
7133            for (state, cost) in costs {
7134                // Each boxed cost is allocated *before* the payload borrow: an
7135                // allocation while a `&mut MapPayload` is live is what `Rooted`
7136                // exists to make impossible, and taking the borrow per entry is
7137                // what keeps that true.
7138                let boxed = scope.root(int_ref(ctx, cost));
7139                map_payload_mut(result)
7140                    .entries
7141                    .insert(DynamicKey::new(state), boxed.get());
7142            }
7143            result.get()
7144        }
7145    })
7146}
7147
7148/// `dijkstra_distance(start, neighbours, weight, is_goal)` — the cost of the
7149/// cheapest route to a goal, or `None` (§6.5).
7150///
7151/// The search [`praxis_dijkstra`] runs, stopped at the first goal it settles,
7152/// so it makes the same two refusals: a negative edge weight and a cost with no
7153/// `Int`.
7154///
7155/// # Safety
7156/// `ctx` must be live and wired; `start` must be a valid `GcRef`, `neighbours` a
7157/// `(T) -> Vec[T]` closure, `weight` a `(T, T) -> Int` closure and `goal` a
7158/// `(T) -> Bool` closure.
7159#[unsafe(no_mangle)]
7160pub unsafe extern "C" fn praxis_dijkstra_distance(
7161    ctx: *mut RuntimeContext,
7162    start: GcRef,
7163    neighbours: GcRef,
7164    weight: GcRef,
7165    goal: GcRef,
7166) -> GcRef {
7167    abi_guard!("praxis_dijkstra_distance", ctx, {
7168        // SAFETY: the caller upholds ctx/operand validity.
7169        unsafe {
7170            let scope = NativeScope::new(ctx);
7171            let mut oracle = ClosureOracle {
7172                ctx,
7173                scope: &scope,
7174                neighbours,
7175                weight,
7176                heuristic: unit_sentinel(ctx),
7177                goal,
7178            };
7179            match crate::graph::dijkstra_route(&mut oracle, start) {
7180                Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7181                Err(_) => unit_sentinel(ctx),
7182            }
7183        }
7184    })
7185}
7186
7187/// `dijkstra_path(start, neighbours, weight, is_goal)` — the cheapest route to
7188/// a goal, from the start to the goal inclusive, or `None` (§6.5).
7189///
7190/// # Safety
7191/// As [`praxis_dijkstra_distance`].
7192#[unsafe(no_mangle)]
7193pub unsafe extern "C" fn praxis_dijkstra_path(
7194    ctx: *mut RuntimeContext,
7195    start: GcRef,
7196    neighbours: GcRef,
7197    weight: GcRef,
7198    goal: GcRef,
7199) -> GcRef {
7200    abi_guard!("praxis_dijkstra_path", ctx, {
7201        // SAFETY: the caller upholds ctx/operand validity.
7202        unsafe {
7203            let scope = NativeScope::new(ctx);
7204            let mut oracle = ClosureOracle {
7205                ctx,
7206                scope: &scope,
7207                neighbours,
7208                weight,
7209                heuristic: unit_sentinel(ctx),
7210                goal,
7211            };
7212            match crate::graph::dijkstra_route(&mut oracle, start) {
7213                Ok(route) => states_as_optional_vec(
7214                    ctx,
7215                    state_descriptor(start),
7216                    route.as_ref().map(|r| r.states.as_slice()),
7217                ),
7218                Err(_) => unit_sentinel(ctx),
7219            }
7220        }
7221    })
7222}
7223
7224/// `a_star_distance(start, neighbours, weight, heuristic, is_goal)` — the cost
7225/// of the cheapest route to a goal, or `None` (§6.5).
7226///
7227/// # Safety
7228/// `ctx` must be live and wired; `start` must be a valid `GcRef` and each of
7229/// `neighbours`, `weight`, `heuristic` and `goal` a closure value of the type
7230/// the helper's signature declares.
7231#[unsafe(no_mangle)]
7232pub unsafe extern "C" fn praxis_a_star_distance(
7233    ctx: *mut RuntimeContext,
7234    start: GcRef,
7235    neighbours: GcRef,
7236    weight: GcRef,
7237    heuristic: GcRef,
7238    goal: GcRef,
7239) -> GcRef {
7240    abi_guard!("praxis_a_star_distance", ctx, {
7241        // SAFETY: the caller upholds ctx/operand validity.
7242        unsafe {
7243            let scope = NativeScope::new(ctx);
7244            let mut oracle = ClosureOracle {
7245                ctx,
7246                scope: &scope,
7247                neighbours,
7248                weight,
7249                heuristic,
7250                goal,
7251            };
7252            match crate::graph::a_star_route(&mut oracle, start) {
7253                Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7254                Err(_) => unit_sentinel(ctx),
7255            }
7256        }
7257    })
7258}
7259
7260/// `a_star_path(start, neighbours, weight, heuristic, is_goal)` — the cheapest
7261/// route to a goal, from the start to the goal inclusive, or `None` (§6.5).
7262///
7263/// # Safety
7264/// As [`praxis_a_star_distance`].
7265#[unsafe(no_mangle)]
7266pub unsafe extern "C" fn praxis_a_star_path(
7267    ctx: *mut RuntimeContext,
7268    start: GcRef,
7269    neighbours: GcRef,
7270    weight: GcRef,
7271    heuristic: GcRef,
7272    goal: GcRef,
7273) -> GcRef {
7274    abi_guard!("praxis_a_star_path", ctx, {
7275        // SAFETY: the caller upholds ctx/operand validity.
7276        unsafe {
7277            let scope = NativeScope::new(ctx);
7278            let mut oracle = ClosureOracle {
7279                ctx,
7280                scope: &scope,
7281                neighbours,
7282                weight,
7283                heuristic,
7284                goal,
7285            };
7286            match crate::graph::a_star_route(&mut oracle, start) {
7287                Ok(route) => states_as_optional_vec(
7288                    ctx,
7289                    state_descriptor(start),
7290                    route.as_ref().map(|r| r.states.as_slice()),
7291                ),
7292                Err(_) => unit_sentinel(ctx),
7293            }
7294        }
7295    })
7296}
7297
7298/// Allocate `Some(n)` or `None` for an `Option[Int]` result.
7299///
7300/// The tags are `Option`'s own declaration order — `Some` first, `None` second
7301/// (`TypeDb::new`) — which is the same order the codegen uses for a `Some(x)`
7302/// the program writes, so a runtime-built `Option` matches against the same
7303/// arms.
7304///
7305/// # Safety
7306/// `ctx` must be live and wired.
7307unsafe fn alloc_optional_int(ctx: *mut RuntimeContext, value: Option<i64>) -> GcRef {
7308    // SAFETY: the caller upholds ctx validity.
7309    unsafe {
7310        match value {
7311            Some(n) => {
7312                let boxed = int_ref(ctx, n);
7313                option_some(ctx, boxed)
7314            }
7315            None => option_none(ctx),
7316        }
7317    }
7318}
7319
7320/// Allocate `Some(states)` as an `Option[Vec[T]]`, or `None` when the search
7321/// found no route.
7322///
7323/// The `Vec` is rooted before the `Some` is built: `option_some` allocates an
7324/// enum, an allocation is a safepoint, and a bare `GcRef` in a local is in
7325/// nobody's root set. That ordering is the whole of what this adds to
7326/// [`states_as_vec`] and [`option_some`].
7327///
7328/// # Safety
7329/// `ctx` must be live and wired; every state must be a valid, rooted `GcRef`.
7330unsafe fn states_as_optional_vec(
7331    ctx: *mut RuntimeContext,
7332    element: *const TypeDescriptor,
7333    states: Option<&[GcRef]>,
7334) -> GcRef {
7335    // SAFETY: the caller upholds ctx/state validity.
7336    unsafe {
7337        match states {
7338            Some(states) => {
7339                let scope = NativeScope::new(ctx);
7340                let vec = scope.root(states_as_vec(ctx, element, states));
7341                option_some(ctx, vec.get())
7342            }
7343            None => option_none(ctx),
7344        }
7345    }
7346}
7347
7348#[cfg(test)]
7349mod tests {
7350    use super::*;
7351    use crate::context::{Fault, FaultKind, Runtime};
7352    use crate::parse_detail::ParseFail;
7353    use crate::shadow_stack::{SlotCount, push_frame};
7354
7355    /// A wired context backed by a real runtime.
7356    pub(super) fn wired_ctx(rt: &mut Runtime) -> *mut RuntimeContext {
7357        let ctx = Box::leak(Box::new(rt.context()));
7358        ctx as *mut RuntimeContext
7359    }
7360
7361    pub(super) unsafe fn drop_ctx(ctx: *mut RuntimeContext) {
7362        // Reclaim the leaked Box. The runtime outlives this call in tests.
7363        let _ = unsafe { Box::from_raw(ctx) };
7364    }
7365
7366    /// The first `Int` value the runtime does **not** intern.
7367    ///
7368    /// Every test below that detects a collection by watching the live registry
7369    /// *shrink* must allocate above this. An interned `Int` never enters the
7370    /// registry, so `praxis_alloc_int(ctx, 5)` in such a loop makes
7371    /// `after < before + 1` true on the first iteration and the test reports
7372    /// success without a collection ever having run — a false pass, which is
7373    /// strictly worse than the failure it replaces.
7374    const UNINTERNED: i64 = crate::small_int::SMALL_INT_MAX + 1;
7375
7376    /// Allocate through a safepointed ABI wrapper until its pre-allocation
7377    /// collection causes the live registry to shrink. Returns the live count
7378    /// immediately after that wrapper allocates its result.
7379    unsafe fn allocate_until_automatic_collection(rt: &Runtime, ctx: *mut RuntimeContext) -> usize {
7380        let mut before = rt.heap().stats().live_count;
7381        for i in 0..10_000_i64 {
7382            // Above the interned range: see `UNINTERNED`.
7383            let _ = unsafe { praxis_alloc_int(ctx, UNINTERNED + i) };
7384            let after = rt.heap().stats().live_count;
7385            if after < before.saturating_add(1) {
7386                return after;
7387            }
7388            before = after;
7389        }
7390        panic!("automatic collection did not run after 10,000 allocations");
7391    }
7392
7393    /// The version number this build declares.
7394    ///
7395    /// Named for the version rather than for any one change, because a version
7396    /// is a statement about a build and several changes share one bump. This
7397    /// pins the numeral so a build cannot ship a layout change without moving
7398    /// it.
7399    ///
7400    /// `gc::tests::the_folded_payload_offset_moved_at_v19_and_is_pinned_here`
7401    /// asserts the other direction, pinning the payload offset *to* a version
7402    /// number, so a layout change and the version that declares it cannot drift
7403    /// apart.
7404    #[test]
7405    fn version_is_twenty_for_the_batch_this_build_ships() {
7406        assert_eq!(RUNTIME_ABI_VERSION, 20);
7407    }
7408
7409    #[test]
7410    fn assert_passes_within_a_single_build() {
7411        assert_abi_version();
7412    }
7413
7414    /// [`int_payload`]'s width check must be a real branch, not a
7415    /// `debug_assert` — the read has to be bounded in the profile users
7416    /// actually run.
7417    ///
7418    /// `debug_assert_eq!` is compiled out of a release build, leaving
7419    /// `unsafe { *r.payload::<i64>() }` against a descriptor that may be zero
7420    /// bytes wide: an 8-byte out-of-bounds heap read, reachable from a program
7421    /// that passes `praxis check`.
7422    ///
7423    /// **This is a source gate on purpose, and it is the only kind that works
7424    /// here.** The defect is a difference *between profiles*, and `cargo test`
7425    /// builds exactly one of them — a behavioural test is green under
7426    /// `debug_assertions` whether the check is conditional or not. The companion
7427    /// below asserts the branch actually refuses; this asserts it is still
7428    /// *there* at `-O`.
7429    ///
7430    /// It reads the file rather than the function because there is nothing in a
7431    /// compiled artifact to ask. `every_no_mangle_wrapper_is_behind_the_panic_guard`
7432    /// is the same technique for the same reason.
7433    #[test]
7434    fn every_scalar_payload_read_goes_through_the_bounded_reader() {
7435        let source = include_str!("abi.rs");
7436
7437        // 1. The reader itself checks before it reads, and the check is an
7438        //    ordinary branch — not a `debug_assert`, which compiles out of a
7439        //    release build. That distinction is the point: with the check
7440        //    compiled out, a `praxis check`-clean program does an out-of-bounds
7441        //    read where a debug build aborts cleanly.
7442        const SIGNATURE: &str = "unsafe fn read_scalar<T: Copy>(r: GcRef, handle: crate::descriptor::Payload<T>) -> Option<T> {";
7443        let at = source
7444            .find(SIGNATURE)
7445            .expect("`read_scalar`'s definition moved; this gate names it by signature");
7446        let body_start = at + SIGNATURE.len();
7447        let body_len = source[body_start..]
7448            .find("\n}")
7449            .expect("`read_scalar` has no closing brace in the first column");
7450        let body = &source[body_start..body_start + body_len];
7451
7452        assert!(
7453            !body.contains("debug_assert"),
7454            "`read_scalar`'s type check is a `debug_assert`, which is compiled out of a \
7455             release build — and what is left is an unchecked read off a payload that may \
7456             be narrower (REP-56). Make it an ordinary branch.\nbody was:{body}"
7457        );
7458        assert!(
7459            body.contains("std::ptr::eq(r.descriptor(), handle.descriptor())"),
7460            "`read_scalar` no longer proves the value is the handle's type before reading \
7461             it (REP-37, REP-56).\nbody was:{body}"
7462        );
7463
7464        // 2. Nothing else in this file reads a scalar payload directly. This
7465        //    is the half that matters: a gate that names one function can only
7466        //    ever gate that function, and every scalar reader needs bounding.
7467        //
7468        //    Scanned over the crate's own code only: `include_str!` hands us this
7469        //    test too, whose list below would otherwise match itself, and
7470        //    comments naming the pattern are describing it rather than doing it.
7471        let code: String = source[..source
7472            .find("#[cfg(test)]")
7473            .expect("abi.rs has no test module marker")]
7474            .lines()
7475            .filter(|l| !l.trim_start().starts_with("//"))
7476            .collect::<Vec<_>>()
7477            .join("\n");
7478        //
7479        //    The patterns are the *bare* calls, not the dereferenced ones:
7480        //    binding `r.payload::<f64>()` to a local and dereferencing it on the
7481        //    next line breaks the spelling `*r.payload::<f64>()` without
7482        //    breaking the defect. Forbidding the call means no phrasing of it
7483        //    passes. `payload::<u8>()` stays legal: it is how
7484        //    `read_scalar` itself reaches the bytes, and how every *compound*
7485        //    payload (record, tuple, closure) is reached — those are cast to a
7486        //    struct the descriptor already vouched for, not read at a width.
7487        for forbidden in [
7488            "r.payload::<i64>()",
7489            "r.payload::<f64>()",
7490            "r.payload::<u32>()",
7491            "r.payload::<bool>()",
7492        ] {
7493            assert!(
7494                !code.contains(forbidden),
7495                "a scalar payload is read directly as `{forbidden}` instead of through \
7496                 `read_scalar`, so its type is unchecked in release (REP-56). Route it \
7497                 through `read_scalar(r, scalars::…_PAYLOAD)` instead."
7498            );
7499        }
7500
7501        // 3. And no Rust `bool` is ever materialized from a payload byte: a
7502        //    `bool` whose byte is not 0 or 1 is an *invalid value*, which is
7503        //    undefined behaviour independently of whether the read was in
7504        //    bounds. `BoolPayload` is a `u8` precisely so it never has to be.
7505        assert!(
7506            !code.contains("Payload<bool>") && !code.contains("read_scalar::<bool>"),
7507            "a `bool` is read straight out of a payload; read `scalars::BOOL_PAYLOAD` \
7508             (a `u8`) and compare it instead (REP-56)."
7509        );
7510    }
7511
7512    /// **ADR-111.** `praxis_alloc_text`'s UTF-8 backstop is unconditional in
7513    /// every profile, and it never becomes an unchecked read.
7514    ///
7515    /// The same source-gate technique as
7516    /// [`every_scalar_payload_read_goes_through_the_bounded_reader`], for the
7517    /// same reason and against a sharper temptation. Making the row `Allocates`
7518    /// says the caller promises UTF-8; the next tidy-up reads that as licence to
7519    /// delete the check — either into a `debug_assert` (which compiles out of a
7520    /// release build, so debug aborts and release builds a `Box<str>` of
7521    /// non-UTF-8 bytes that `text_str` later hands out as a `&str`) or into
7522    /// `from_utf8_unchecked` (the same hole, with the check deleted rather than
7523    /// compiled out). Both give two profiles two answers, and `just ci` never
7524    /// builds the one users get.
7525    ///
7526    /// A behavioural test cannot see this: under `cfg(debug_assertions)` a
7527    /// `debug_assert` version passes every test the branch version passes.
7528    #[test]
7529    fn the_text_precondition_backstop_is_unconditional_in_every_profile() {
7530        let source = include_str!("abi.rs");
7531        const SIGNATURE: &str = "pub unsafe extern \"C\" fn praxis_alloc_text(";
7532        let at = source
7533            .find(SIGNATURE)
7534            .expect("`praxis_alloc_text`'s definition moved; this gate names it by signature");
7535        let body_len = source[at..]
7536            .find("\n}")
7537            .expect("`praxis_alloc_text` has no closing brace in the first column");
7538        let body = &source[at..at + body_len];
7539
7540        assert!(
7541            body.contains("std::str::from_utf8(slice)"),
7542            "`praxis_alloc_text` no longer validates its buffer. The check is the \
7543             backstop on a raw read, not an optimization the `Allocates` row traded \
7544             away (ADR-111).\nbody was:{body}"
7545        );
7546        assert!(
7547            !body.contains("debug_assert"),
7548            "`praxis_alloc_text`'s UTF-8 check is a `debug_assert`, which is compiled \
7549             out of a release build — leaving a `Box<str>` built from bytes that are \
7550             not UTF-8 (REP-56's shape). Make it an ordinary branch.\nbody was:{body}"
7551        );
7552        assert!(
7553            !body.contains("from_utf8_unchecked"),
7554            "`praxis_alloc_text` skips the check outright. A precondition is not a \
7555             licence to read unvalidated bytes as a `str` — the refusal is \
7556             `text_bytes_are_not_utf8`, which costs a never-taken branch \
7557             (ADR-111).\nbody was:{body}"
7558        );
7559        // And the refusal is not a fault. If it were, the fault sweep would
7560        // classify the wrapper as faulting and correctly refuse the
7561        // `Allocates` row — this says so at the site rather than leaving the
7562        // failure to be diagnosed three tests away.
7563        assert!(
7564            !body.contains("set_fault"),
7565            "`praxis_alloc_text` sets a fault. Its row is `Effect::Allocates`, so no \
7566             `CheckFault` follows the call and nothing would ever observe it \
7567             (ADR-088, ADR-111).\nbody was:{body}"
7568        );
7569    }
7570
7571    /// The companion to the source gate above: the branch it insists on is real,
7572    /// and it refuses rather than reading.
7573    ///
7574    /// A `Unit` is zero bytes wide, which is the shape that must be refused.
7575    /// The refusal is a panic, which is ADR-080's defined path — inside a
7576    /// wrapper `abi_guard!` turns it into a `Panic` fault (or a message and an
7577    /// abort where the manifest makes that fault unobservable). What must not
7578    /// happen, in any profile, is the read.
7579    #[test]
7580    fn a_scalar_read_refuses_a_value_that_is_not_its_type() {
7581        let mut rt = Runtime::new();
7582        let ctx = wired_ctx(&mut rt);
7583        // SAFETY: ctx is wired to rt; the Unit immortal is a valid GcRef.
7584        let unit = unsafe { praxis_alloc_unit(ctx) };
7585        assert_eq!(unit.descriptor().size(), 0, "Unit is a zero-width payload");
7586
7587        // The panic is the refusal. `catch_unwind` here is the test standing in
7588        // for `abi_guard!`, which is what catches it in a real wrapper.
7589        let previous = std::panic::take_hook();
7590        std::panic::set_hook(Box::new(|_| {}));
7591        // `AssertUnwindSafe` for the same reason `abi_guard!` uses it: the
7592        // capture is a `Copy` C type and nothing observes a half-finished read.
7593        // SAFETY: `unit` is a valid GcRef into rt's live heap.
7594        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
7595            int_payload(unit)
7596        }));
7597        std::panic::set_hook(previous);
7598        unsafe { drop_ctx(ctx) };
7599
7600        let payload = outcome.expect_err("a zero-width payload must not be read as eight bytes");
7601        let message = payload
7602            .downcast_ref::<String>()
7603            .map(String::as_str)
7604            .or_else(|| payload.downcast_ref::<&str>().copied())
7605            .unwrap_or("");
7606        assert!(
7607            message.contains("int_payload wants a `Int` payload")
7608                && message.contains("this value is a `Unit`"),
7609            "unexpected panic message: {message:?}"
7610        );
7611    }
7612
7613    #[test]
7614    fn alloc_int_and_load_round_trip() {
7615        let mut rt = Runtime::new();
7616        let ctx = wired_ctx(&mut rt);
7617        // SAFETY: ctx is wired to rt.
7618        let r = unsafe { praxis_alloc_int(ctx, 9001) };
7619        // SAFETY: r is a valid Int allocated above.
7620        assert_eq!(unsafe { praxis_int_load(ctx, r) }, 9001);
7621        unsafe { drop_ctx(ctx) };
7622    }
7623
7624    /// The `Int` counterpart of
7625    /// [`bool_and_unit_abi_allocations_reuse_runtime_singletons`]: a small `Int`
7626    /// is one object per value, and an out-of-range one is still a fresh box.
7627    ///
7628    /// Both halves matter. The first is the optimization; the second is the
7629    /// branch a regression would silently delete, leaving every large `Int` in
7630    /// the language reading slot `value - SMALL_INT_MIN` of a table that ends
7631    /// long before it.
7632    #[test]
7633    fn small_ints_are_one_object_per_value_and_large_ones_are_not() {
7634        let mut rt = Runtime::new();
7635        let ctx = wired_ctx(&mut rt);
7636        // SAFETY: ctx is wired to rt throughout.
7637        unsafe {
7638            // In range: two calls, one object — and it is the runtime's own
7639            // table entry, not some other cache.
7640            let a = praxis_alloc_int(ctx, 7);
7641            let b = praxis_alloc_int(ctx, 7);
7642            assert_eq!(a.as_ptr(), b.as_ptr());
7643            assert_eq!(a.as_ptr(), rt.immortals().small_int(7).unwrap().as_ptr());
7644            assert_eq!(praxis_int_load(ctx, a), 7);
7645
7646            // The four boundary cases, through the ABI: the exact endpoints are
7647            // interned and one step outside either is not.
7648            for v in [
7649                crate::small_int::SMALL_INT_MIN,
7650                crate::small_int::SMALL_INT_MAX,
7651            ] {
7652                assert_eq!(
7653                    praxis_alloc_int(ctx, v).as_ptr(),
7654                    praxis_alloc_int(ctx, v).as_ptr(),
7655                    "{v} is the edge of the range and must be interned"
7656                );
7657            }
7658            for v in [
7659                crate::small_int::SMALL_INT_MIN - 1,
7660                crate::small_int::SMALL_INT_MAX + 1,
7661            ] {
7662                let x = praxis_alloc_int(ctx, v);
7663                let y = praxis_alloc_int(ctx, v);
7664                assert_ne!(
7665                    x.as_ptr(),
7666                    y.as_ptr(),
7667                    "{v} is outside the range and must still allocate"
7668                );
7669                assert_eq!(praxis_int_load(ctx, x), v, "and still hold its value");
7670                assert_eq!(praxis_int_load(ctx, y), v);
7671            }
7672
7673            // Distinct in-range values are distinct objects: interning shares
7674            // an object across *calls*, never across values.
7675            assert_ne!(
7676                praxis_alloc_int(ctx, 7).as_ptr(),
7677                praxis_alloc_int(ctx, 8).as_ptr()
7678            );
7679
7680            // The host helper and the ABI wrapper answer the same object, as
7681            // `Runtime::alloc_bool` and `praxis_alloc_bool` already do.
7682            assert_eq!(rt.alloc_int(7).as_ptr(), a.as_ptr());
7683        }
7684        unsafe { drop_ctx(ctx) };
7685    }
7686
7687    /// The `Char` counterpart of
7688    /// [`small_ints_are_one_object_per_value_and_large_ones_are_not`] (ADR-107).
7689    ///
7690    /// Both halves matter. The first is the optimization; the second is the
7691    /// branch a regression would silently delete, leaving every non-ASCII `Char`
7692    /// in the language reading slot `code` of a table that ends at 128.
7693    #[test]
7694    fn alloc_char_answers_one_object_per_ascii_code_point_and_a_large_one_still_allocates() {
7695        let mut rt = Runtime::new();
7696        let ctx = wired_ctx(&mut rt);
7697        // SAFETY: ctx is wired to rt throughout.
7698        unsafe {
7699            // In range: two calls, one object — and it is the runtime's own
7700            // table entry, not some other cache.
7701            let a = praxis_alloc_char(ctx, i64::from('a' as u32));
7702            let b = praxis_alloc_char(ctx, i64::from('a' as u32));
7703            assert_eq!(a.as_ptr(), b.as_ptr());
7704            assert_eq!(
7705                a.as_ptr(),
7706                rt.immortals().small_char('a' as u32).unwrap().as_ptr()
7707            );
7708            assert_eq!(a.as_char(), 'a');
7709
7710            // The ceiling is interned; one above it is not. There is no floor
7711            // case — the payload is unsigned and NUL is interned.
7712            let max = i64::from(crate::small_char::SMALL_CHAR_MAX);
7713            assert_eq!(
7714                praxis_alloc_char(ctx, max).as_ptr(),
7715                praxis_alloc_char(ctx, max).as_ptr(),
7716                "the last ASCII scalar is the edge of the range and must be interned"
7717            );
7718            assert_eq!(
7719                praxis_alloc_char(ctx, 0).as_ptr(),
7720                praxis_alloc_char(ctx, 0).as_ptr(),
7721                "NUL is the floor and must be interned"
7722            );
7723            for code in [max + 1, i64::from('é' as u32), 0x10_FFFF] {
7724                let x = praxis_alloc_char(ctx, code);
7725                let y = praxis_alloc_char(ctx, code);
7726                assert_ne!(
7727                    x.as_ptr(),
7728                    y.as_ptr(),
7729                    "{code:#x} is outside the range and must still allocate"
7730                );
7731                assert_eq!(
7732                    u32::from(x.as_char()),
7733                    code as u32,
7734                    "and still hold its code point"
7735                );
7736            }
7737
7738            // Distinct in-range code points are distinct objects: interning
7739            // shares an object across *calls*, never across values.
7740            assert_ne!(
7741                praxis_alloc_char(ctx, i64::from('a' as u32)).as_ptr(),
7742                praxis_alloc_char(ctx, i64::from('b' as u32)).as_ptr()
7743            );
7744
7745            // The validity rule is untouched by the table: an interned slot is
7746            // reached only after `checked_alloc_char` has approved the value, so
7747            // a code point that is not a scalar still faults.
7748            for bad in [-1_i64, 0xD800, 0x11_0000, 0x1_0000_0041] {
7749                let _ = praxis_alloc_char(ctx, bad);
7750                assert_eq!(
7751                    rt.take_fault(),
7752                    Some(FaultKind::InvalidChar),
7753                    "{bad:#x} is not a scalar value"
7754                );
7755            }
7756
7757            // The host helper and the ABI wrapper answer the same object, as
7758            // `Runtime::alloc_int`/`praxis_alloc_int` already do. This is what
7759            // makes "a `Char` is interned" one fact rather than two.
7760            assert_eq!(rt.alloc_char('a' as u32).as_ptr(), a.as_ptr());
7761            // …and the out-of-range halves still disagree as objects, which is
7762            // the same statement from the other side.
7763            assert_ne!(
7764                rt.alloc_char('é' as u32).as_ptr(),
7765                rt.alloc_char('é' as u32).as_ptr()
7766            );
7767        }
7768        unsafe { drop_ctx(ctx) };
7769    }
7770
7771    /// `praxis_text_get` is the interning's largest site: it is `t[i]` *and*
7772    /// every step of `for c in t`. It reaches [`char_ref`] directly rather than
7773    /// through [`checked_alloc_char`] — a Rust `char` needs no validity check —
7774    /// so it is its own door and must be pinned as one.
7775    ///
7776    /// `text_get_answers_a_char_object` covers the uninterned half (`é`) and the
7777    /// descriptor; this covers the identity.
7778    #[test]
7779    fn text_get_answers_the_interned_char() {
7780        let mut rt = Runtime::new();
7781        let ctx = wired_ctx(&mut rt);
7782        // SAFETY: ctx wired.
7783        unsafe {
7784            let s = "abca";
7785            let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
7786            let zero = praxis_alloc_int(ctx, 0);
7787            let three = praxis_alloc_int(ctx, 3);
7788            let first = praxis_text_get(ctx, text, zero);
7789            let last = praxis_text_get(ctx, text, three);
7790            assert!(!rt.has_pending_fault());
7791
7792            // Two reads of the same character are one object, and it is the same
7793            // object every other door answers.
7794            assert_eq!(first.as_ptr(), last.as_ptr());
7795            assert_eq!(
7796                first.as_ptr(),
7797                praxis_alloc_char(ctx, i64::from('a' as u32)).as_ptr()
7798            );
7799            assert_eq!(
7800                first.as_ptr(),
7801                rt.immortals().small_char('a' as u32).unwrap().as_ptr()
7802            );
7803            assert_eq!(first.as_char(), 'a');
7804
7805            // The non-ASCII half still allocates, and still answers the right
7806            // scalar — the branch a regression would delete.
7807            let u = "éé";
7808            let utext = praxis_alloc_text(ctx, u.as_ptr(), u.len());
7809            let one = praxis_alloc_int(ctx, 1);
7810            let x = praxis_text_get(ctx, utext, zero);
7811            let y = praxis_text_get(ctx, utext, one);
7812            assert_ne!(x.as_ptr(), y.as_ptr(), "`é` is outside the interned range");
7813            assert_eq!(x.as_char(), 'é');
7814            assert_eq!(y.as_char(), 'é');
7815        }
7816        unsafe { drop_ctx(ctx) };
7817    }
7818
7819    /// The two doors into [`checked_alloc_char`] answer the same object, which
7820    /// is that helper's whole reason for existing — a rule stated at both goes
7821    /// stale at one, and now the rule includes which object.
7822    #[test]
7823    fn int_to_char_answers_the_same_object_as_alloc_char() {
7824        let mut rt = Runtime::new();
7825        let ctx = wired_ctx(&mut rt);
7826        // SAFETY: ctx wired.
7827        unsafe {
7828            let code = praxis_alloc_int(ctx, i64::from('Z' as u32));
7829            let via_to_char = praxis_int_to_char(ctx, code);
7830            let via_alloc = praxis_alloc_char(ctx, i64::from('Z' as u32));
7831            assert!(!rt.has_pending_fault());
7832            assert_eq!(via_to_char.as_ptr(), via_alloc.as_ptr());
7833            assert_eq!(via_to_char.as_char(), 'Z');
7834
7835            // Outside the range both still allocate, and still not each other.
7836            let big = praxis_alloc_int(ctx, i64::from('é' as u32));
7837            assert_ne!(
7838                praxis_int_to_char(ctx, big).as_ptr(),
7839                praxis_alloc_char(ctx, i64::from('é' as u32)).as_ptr()
7840            );
7841        }
7842        unsafe { drop_ctx(ctx) };
7843    }
7844
7845    /// **ADR-143.** `Int.to_text()` answers exactly what `out` writes, and the
7846    /// assertion is against `out`'s own path rather than a literal.
7847    ///
7848    /// Comparing to `"1660"` would pass while the two renderers disagreed about
7849    /// everything else; comparing to `GcRef::format`'s output cannot, because
7850    /// that is the function `praxis_write_stdout` calls. `i64::MIN` is in the
7851    /// list because it is the one value whose negation does not fit, and
7852    /// therefore the first thing a hand-rolled renderer gets wrong.
7853    #[test]
7854    fn int_to_text_renders_exactly_what_out_renders() {
7855        let mut rt = Runtime::new();
7856        let ctx = wired_ctx(&mut rt);
7857        // SAFETY: ctx wired; every receiver is an Int.
7858        unsafe {
7859            for v in [0_i64, 1, -1, 1660, i64::MAX, i64::MIN, UNINTERNED] {
7860                let receiver = praxis_alloc_int(ctx, v);
7861                let answer = praxis_int_to_text(ctx, receiver);
7862                assert!(!rt.has_pending_fault(), "{v} faulted");
7863                let mut printed = String::new();
7864                receiver.format(&mut printed);
7865                assert_eq!(answer.as_text(), printed, "to_text and out disagree on {v}");
7866            }
7867        }
7868        unsafe { drop_ctx(ctx) };
7869    }
7870
7871    /// **ADR-143.** The same claim for `Char.to_text()`, at an interned ASCII
7872    /// character and an uninterned multi-byte one.
7873    ///
7874    /// The multi-byte case is the one that would catch reading the four-byte
7875    /// payload as an `i64`: `'é'` is `0xE9`, and eight bytes from a four-byte
7876    /// payload picks up whatever follows it.
7877    #[test]
7878    fn char_to_text_renders_exactly_what_out_renders() {
7879        let mut rt = Runtime::new();
7880        let ctx = wired_ctx(&mut rt);
7881        // SAFETY: ctx wired; every receiver is a Char.
7882        unsafe {
7883            for c in ['#', 'a', 'é', '☃', '\u{10FFFF}'] {
7884                let receiver = praxis_alloc_char(ctx, i64::from(u32::from(c)));
7885                let answer = praxis_char_to_text(ctx, receiver);
7886                assert!(!rt.has_pending_fault(), "{c} faulted");
7887                let mut printed = String::new();
7888                receiver.format(&mut printed);
7889                assert_eq!(answer.as_text(), printed, "to_text and out disagree on {c}");
7890                assert_eq!(answer.as_text(), c.to_string());
7891            }
7892        }
7893        unsafe { drop_ctx(ctx) };
7894    }
7895
7896    /// **ADR-147.** An interpolation hole renders exactly what `out` writes, for
7897    /// **every** type — including the ones with no `to_text()` row.
7898    ///
7899    /// This is the wrapper-level half of ADR-147 decision 2, and it is asserted
7900    /// against `GcRef::format` — `praxis_write_stdout`'s own call — rather than
7901    /// against a literal, for `int_to_text_renders_exactly_what_out_renders`'s
7902    /// reason: a literal comparison passes while the two agree by coincidence.
7903    ///
7904    /// The receivers deliberately span a scalar, a `Text` (whose rendering is
7905    /// its own characters and not a quoted form), a collection and a tuple, so a
7906    /// wrapper that reached for a scalar payload instead of the descriptor fails
7907    /// on the last two rather than on none of them.
7908    #[test]
7909    fn value_to_text_renders_exactly_what_out_renders() {
7910        let mut rt = Runtime::new();
7911        let ctx = wired_ctx(&mut rt);
7912        // SAFETY: ctx wired; every receiver below is freshly allocated here.
7913        unsafe {
7914            let empty = praxis_alloc_text(ctx, std::ptr::null(), 0);
7915            let hello = "hello";
7916            let text = praxis_alloc_text(ctx, hello.as_ptr(), hello.len());
7917            let vec = praxis_vec_new(ctx, &scalars::INT as *const _);
7918            for n in [1_i64, 2, 3] {
7919                let _ = praxis_vec_push(ctx, vec, praxis_alloc_int(ctx, n));
7920            }
7921            let receivers = [
7922                praxis_alloc_int(ctx, UNINTERNED),
7923                praxis_alloc_int(ctx, 0),
7924                praxis_alloc_bool(ctx, 1),
7925                praxis_alloc_char(ctx, i64::from(u32::from('☃'))),
7926                empty,
7927                text,
7928                vec,
7929            ];
7930            for receiver in receivers {
7931                let answer = praxis_value_to_text(ctx, receiver);
7932                assert!(!rt.has_pending_fault(), "value_to_text faulted");
7933                let mut printed = String::new();
7934                receiver.format(&mut printed);
7935                assert_eq!(
7936                    answer.as_text(),
7937                    printed,
7938                    "a hole and `out` must write the same characters"
7939                );
7940            }
7941            // The `Text` rows pin the shape a caller is most likely to assume
7942            // wrong: `out("hello")` writes `hello`, not `"hello"`, so `"{s}"`
7943            // must not add quotes either.
7944            assert_eq!(praxis_value_to_text(ctx, text).as_text(), "hello");
7945            assert_eq!(praxis_value_to_text(ctx, empty).as_text(), "");
7946        }
7947        unsafe { drop_ctx(ctx) };
7948    }
7949
7950    /// **ADR-144.** `join` puts the separator *between* elements and nowhere
7951    /// else, which is the whole of the specification and the whole of what an
7952    /// off-by-one gets wrong.
7953    #[test]
7954    fn vec_join_puts_the_separator_between_and_nowhere_else() {
7955        let mut rt = Runtime::new();
7956        let ctx = wired_ctx(&mut rt);
7957        // SAFETY: ctx wired; every element and separator is a Text.
7958        unsafe {
7959            let cases: [(&[&str], &str, &str); 5] = [
7960                (&[], ", ", ""),
7961                (&["only"], ", ", "only"),
7962                (&["a", "b", "c"], ", ", "a, b, c"),
7963                (&["a", "b", "c"], "", "abc"),
7964                (&["é", "☃"], " — ", "é — ☃"),
7965            ];
7966            for (items, sep, want) in cases {
7967                let members: Vec<GcRef> = items.iter().map(|s| rt.alloc_text(s)).collect();
7968                let vec = rt.alloc_vec(&crate::text::TEXT, members);
7969                let separator = rt.alloc_text(sep);
7970                let answer = praxis_vec_join(ctx, vec, separator);
7971                assert!(!rt.has_pending_fault(), "{items:?} faulted");
7972                assert_eq!(answer.as_text(), want);
7973            }
7974        }
7975        unsafe { drop_ctx(ctx) };
7976    }
7977
7978    /// **ADR-144.** A non-`Text` element is `TypeMismatch` and the Unit
7979    /// sentinel, not a `Text` payload read out of an `Int`.
7980    ///
7981    /// The catalog row's `Text` bound means only a compiler bug gets here, and
7982    /// this is what that bug looks like when it does: a fault the program can
7983    /// see, rather than a pointer-and-length pair read from eight bytes of
7984    /// integer.
7985    #[test]
7986    fn vec_join_refuses_a_non_text_element() {
7987        let mut rt = Runtime::new();
7988        let ctx = wired_ctx(&mut rt);
7989        // SAFETY: ctx wired.
7990        unsafe {
7991            let mixed = rt.alloc_vec(&scalars::INT, vec![rt.alloc_text("a"), rt.alloc_int(1)]);
7992            let sep = rt.alloc_text(",");
7993            let answer = praxis_vec_join(ctx, mixed, sep);
7994            assert!(rt.has_pending_fault());
7995            assert!(std::ptr::eq(answer.descriptor(), &scalars::UNIT));
7996        }
7997        unsafe { drop_ctx(ctx) };
7998    }
7999
8000    /// **ADR-144.** `Vec[Char].to_text()` is the characters with nothing between
8001    /// them, and it agrees with `out` on each of them for ADR-143's reason: it
8002    /// goes through `scalars::write_char` too.
8003    #[test]
8004    fn vec_to_text_renders_every_char() {
8005        let mut rt = Runtime::new();
8006        let ctx = wired_ctx(&mut rt);
8007        // SAFETY: ctx wired; every element is a Char.
8008        unsafe {
8009            for want in ["", ".", "..|", "héllo", "☃☃"] {
8010                let members: Vec<GcRef> = want
8011                    .chars()
8012                    .map(|c| praxis_alloc_char(ctx, i64::from(u32::from(c))))
8013                    .collect();
8014                let vec = rt.alloc_vec(&scalars::CHAR, members);
8015                let answer = praxis_vec_to_text(ctx, vec);
8016                assert!(!rt.has_pending_fault(), "{want:?} faulted");
8017                assert_eq!(answer.as_text(), want);
8018            }
8019        }
8020        unsafe { drop_ctx(ctx) };
8021    }
8022
8023    /// **ADR-144.** A non-`Char` element faults rather than being read as four
8024    /// bytes of something else.
8025    #[test]
8026    fn vec_to_text_refuses_a_non_char_element() {
8027        let mut rt = Runtime::new();
8028        let ctx = wired_ctx(&mut rt);
8029        // SAFETY: ctx wired.
8030        unsafe {
8031            let mixed = rt.alloc_vec(&scalars::CHAR, vec![rt.alloc_int(65)]);
8032            let answer = praxis_vec_to_text(ctx, mixed);
8033            assert!(rt.has_pending_fault());
8034            assert!(std::ptr::eq(answer.descriptor(), &scalars::UNIT));
8035        }
8036        unsafe { drop_ctx(ctx) };
8037    }
8038
8039    /// **ADR-145.** `reversed` answers a **new** `Vec` and leaves the receiver
8040    /// alone — the rule every barrier in this block states, and the one a
8041    /// wrapper that reversed in place would break invisibly for a caller still
8042    /// holding `v`.
8043    ///
8044    /// The empty case is here because `praxis_vec_sorted` needs a `len() > 1`
8045    /// guard for the analogous one and this needs none: there is no callback to
8046    /// avoid calling.
8047    #[test]
8048    fn vec_reversed_answers_a_new_vec_and_leaves_the_receiver_alone() {
8049        let mut rt = Runtime::new();
8050        let ctx = wired_ctx(&mut rt);
8051        // SAFETY: ctx wired.
8052        unsafe {
8053            let source = rt.alloc_vec(
8054                &scalars::INT,
8055                vec![rt.alloc_int(3), rt.alloc_int(1), rt.alloc_int(2)],
8056            );
8057            let answer = praxis_vec_reversed(ctx, source);
8058            assert!(!rt.has_pending_fault());
8059            let got: Vec<i64> = answer.as_vec().iter().map(|r| r.as_int()).collect();
8060            assert_eq!(got, vec![2, 1, 3]);
8061            let still: Vec<i64> = source.as_vec().iter().map(|r| r.as_int()).collect();
8062            assert_eq!(still, vec![3, 1, 2], "the receiver is not touched");
8063            assert_ne!(answer.as_ptr(), source.as_ptr());
8064
8065            let empty = rt.alloc_vec(&scalars::INT, vec![]);
8066            assert!(praxis_vec_reversed(ctx, empty).as_vec().is_empty());
8067            assert!(!rt.has_pending_fault());
8068        }
8069        unsafe { drop_ctx(ctx) };
8070    }
8071
8072    /// **ADR-145.** Reversal reads no descriptor callback, so a `Vec` of a type
8073    /// with no `compare` reverses where `sorted` faults.
8074    ///
8075    /// This is the runtime half of the catalog row carrying no capability bound.
8076    /// A `Unit` has no `compare` — `praxis_vec_sorted` raises `TypeMismatch` on
8077    /// one — and it reverses without a word.
8078    #[test]
8079    fn vec_reversed_needs_no_callback_where_sorted_needs_compare() {
8080        let mut rt = Runtime::new();
8081        let ctx = wired_ctx(&mut rt);
8082        // SAFETY: ctx wired.
8083        unsafe {
8084            let closures = rt.alloc_vec(
8085                &crate::closures::CLOSURE,
8086                vec![
8087                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8088                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8089                ],
8090            );
8091            assert_eq!(praxis_vec_reversed(ctx, closures).as_vec().len(), 2);
8092            assert!(!rt.has_pending_fault(), "reversal asks for no callback");
8093
8094            praxis_vec_sorted(ctx, closures);
8095            assert!(rt.has_pending_fault(), "ordering still asks for `compare`");
8096        }
8097        unsafe { drop_ctx(ctx) };
8098    }
8099
8100    /// The shape of both groupings, read back as nested `Int`s.
8101    ///
8102    /// # Safety
8103    /// `answer` must be a valid `Vec[Vec[Int]]` `GcRef`.
8104    unsafe fn groups_of_int(answer: GcRef) -> Vec<Vec<i64>> {
8105        answer
8106            .as_vec()
8107            .iter()
8108            .map(|inner| inner.as_vec().iter().map(|r| r.as_int()).collect())
8109            .collect()
8110    }
8111
8112    /// **ADR-149.** `chunks` partitions: every element appears once, in order,
8113    /// and a length the size does not divide leaves a *short last chunk* rather
8114    /// than dropping the tail or padding it.
8115    #[test]
8116    fn vec_chunks_partitions_and_keeps_a_short_tail() {
8117        let mut rt = Runtime::new();
8118        let ctx = wired_ctx(&mut rt);
8119        // SAFETY: ctx wired.
8120        unsafe {
8121            let ints: Vec<GcRef> = (1..=5).map(|n| rt.alloc_int(n)).collect();
8122            let source = rt.alloc_vec(&scalars::INT, ints);
8123
8124            let two = rt.alloc_int(2);
8125            let answer = praxis_vec_chunks(ctx, source, two);
8126            assert!(!rt.has_pending_fault());
8127            assert_eq!(groups_of_int(answer), vec![vec![1, 2], vec![3, 4], vec![5]]);
8128
8129            // A size that divides leaves no short chunk, which is the same rule
8130            // and is worth pinning beside the one that does.
8131            let five = rt.alloc_int(5);
8132            assert_eq!(
8133                groups_of_int(praxis_vec_chunks(ctx, source, five)),
8134                vec![vec![1, 2, 3, 4, 5]],
8135            );
8136
8137            // Wider than the receiver is not a fault: it is one short chunk.
8138            let nine = rt.alloc_int(9);
8139            assert_eq!(
8140                groups_of_int(praxis_vec_chunks(ctx, source, nine)),
8141                vec![vec![1, 2, 3, 4, 5]],
8142            );
8143
8144            let still: Vec<i64> = source.as_vec().iter().map(|r| r.as_int()).collect();
8145            assert_eq!(still, vec![1, 2, 3, 4, 5], "the receiver is not touched");
8146        }
8147        unsafe { drop_ctx(ctx) };
8148    }
8149
8150    /// **ADR-149.** `windows` slides by one and keeps only the runs that fit, so
8151    /// a receiver shorter than the size answers `[]` rather than one short run —
8152    /// the one place the two groupings differ.
8153    #[test]
8154    fn vec_windows_slide_by_one_and_drop_a_run_that_does_not_fit() {
8155        let mut rt = Runtime::new();
8156        let ctx = wired_ctx(&mut rt);
8157        // SAFETY: ctx wired.
8158        unsafe {
8159            let ints: Vec<GcRef> = (1..=4).map(|n| rt.alloc_int(n)).collect();
8160            let source = rt.alloc_vec(&scalars::INT, ints);
8161
8162            let two = rt.alloc_int(2);
8163            assert_eq!(
8164                groups_of_int(praxis_vec_windows(ctx, source, two)),
8165                vec![vec![1, 2], vec![2, 3], vec![3, 4]],
8166            );
8167            assert!(!rt.has_pending_fault());
8168
8169            // Exactly the length is one window; one past it is none. Off by one
8170            // here is the whole difference between `[]` and a wrong answer.
8171            let four = rt.alloc_int(4);
8172            assert_eq!(
8173                groups_of_int(praxis_vec_windows(ctx, source, four)),
8174                vec![vec![1, 2, 3, 4]],
8175            );
8176            let five = rt.alloc_int(5);
8177            let none = praxis_vec_windows(ctx, source, five);
8178            assert!(
8179                none.as_vec().is_empty(),
8180                "a run of five does not fit in four"
8181            );
8182            assert!(
8183                !rt.has_pending_fault(),
8184                "not fitting is an answer, not a fault"
8185            );
8186
8187            // Windows share their elements rather than copying them, which is
8188            // the language's reference semantics and not a rule of this wrapper.
8189            let answer = praxis_vec_windows(ctx, source, two);
8190            let first = answer.as_vec()[0].as_vec()[1].as_ptr();
8191            let second = answer.as_vec()[1].as_vec()[0].as_ptr();
8192            assert_eq!(first, second, "the overlapping element is one object");
8193        }
8194        unsafe { drop_ctx(ctx) };
8195    }
8196
8197    /// **ADR-149.** The only thing either grouping refuses: a run of `n <= 0` is
8198    /// not a short run, it is not a run, so there is no sequence of them to
8199    /// answer with.
8200    ///
8201    /// The empty receiver is here beside it because it is the case that looks
8202    /// like a fault and is not — `[].chunks(2)` is `[]`, the same way `[]`
8203    /// reverses to `[]`.
8204    #[test]
8205    fn a_group_size_of_zero_or_less_is_an_invalid_size_fault() {
8206        for size in [0i64, -1, i64::MIN] {
8207            for (name, wrapper) in [
8208                (
8209                    "chunks",
8210                    praxis_vec_chunks as unsafe extern "C" fn(_, _, _) -> _,
8211                ),
8212                ("windows", praxis_vec_windows),
8213            ] {
8214                let mut rt = Runtime::new();
8215                let ctx = wired_ctx(&mut rt);
8216                // SAFETY: ctx wired.
8217                unsafe {
8218                    let source = rt.alloc_vec(&scalars::INT, vec![rt.alloc_int(1)]);
8219                    let n = rt.alloc_int(size);
8220                    let answer = wrapper(ctx, source, n);
8221                    assert!(rt.has_pending_fault(), "{name}({size}) must fault");
8222                    assert_eq!(rt.fault(), crate::FaultKind::InvalidSize, "{name}({size})");
8223                    assert!(
8224                        std::ptr::eq(answer.descriptor(), &scalars::UNIT),
8225                        "{name}({size}) answers the Unit sentinel"
8226                    );
8227                }
8228                unsafe { drop_ctx(ctx) };
8229            }
8230        }
8231
8232        let mut rt = Runtime::new();
8233        let ctx = wired_ctx(&mut rt);
8234        // SAFETY: ctx wired.
8235        unsafe {
8236            let empty = rt.alloc_vec(&scalars::INT, vec![]);
8237            let two = rt.alloc_int(2);
8238            assert!(praxis_vec_chunks(ctx, empty, two).as_vec().is_empty());
8239            assert!(praxis_vec_windows(ctx, empty, two).as_vec().is_empty());
8240            assert!(
8241                !rt.has_pending_fault(),
8242                "an empty receiver is an empty answer"
8243            );
8244        }
8245        unsafe { drop_ctx(ctx) };
8246    }
8247
8248    /// **ADR-149 decision 1.** The outer `Vec` is labelled `VEC` at every length
8249    /// and the inner ones carry the receiver's element descriptor.
8250    ///
8251    /// The **empty** answer is the whole reason this test exists, and it is the
8252    /// only part that is a choice: `VEC` is what `outer.push(inner)` already
8253    /// produces, so a non-empty grouping could hardly answer anything else and
8254    /// asserting it proves little. With the label inferred from the first group
8255    /// there would be none to read, and `[1, 2].windows(5)` would carry a null
8256    /// where `[1, 2].windows(2)` carries `VEC` — one type with two labels, and
8257    /// the null is the one `vec_format` renders as `[]` and `push` treats as
8258    /// "adopt whatever arrives".
8259    #[test]
8260    fn a_grouping_labels_the_outer_vec_even_when_it_is_empty() {
8261        let mut rt = Runtime::new();
8262        let ctx = wired_ctx(&mut rt);
8263        // SAFETY: ctx wired.
8264        unsafe {
8265            let source = rt.alloc_vec(&scalars::INT, vec![rt.alloc_int(1), rt.alloc_int(2)]);
8266            let two = rt.alloc_int(2);
8267            let five = rt.alloc_int(5);
8268
8269            for answer in [
8270                praxis_vec_chunks(ctx, source, two),
8271                praxis_vec_windows(ctx, source, two),
8272                // The two that come out empty, and the reason this test exists.
8273                praxis_vec_windows(ctx, source, five),
8274                praxis_vec_chunks(ctx, rt.alloc_vec(&scalars::INT, vec![]), two),
8275            ] {
8276                let p = vec_payload(answer);
8277                assert!(
8278                    std::ptr::eq(p.element_descriptor, &crate::collections::VEC),
8279                    "the outer Vec holds Vecs whether or not it holds any"
8280                );
8281                for inner in p.items.iter() {
8282                    assert!(std::ptr::eq(
8283                        vec_payload(*inner).element_descriptor,
8284                        &scalars::INT
8285                    ));
8286                }
8287            }
8288        }
8289        unsafe { drop_ctx(ctx) };
8290    }
8291
8292    /// **ADR-149.** A grouping reads no descriptor callback, so a `Vec` of a
8293    /// type with no `compare` groups where `sorted` faults — `reversed`'s claim,
8294    /// and the runtime half of these two rows carrying no capability bound.
8295    #[test]
8296    fn a_grouping_needs_no_callback_where_sorted_needs_compare() {
8297        let mut rt = Runtime::new();
8298        let ctx = wired_ctx(&mut rt);
8299        // SAFETY: ctx wired.
8300        unsafe {
8301            let closures = rt.alloc_vec(
8302                &crate::closures::CLOSURE,
8303                vec![
8304                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8305                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8306                    praxis_alloc_closure(ctx, std::ptr::null(), 0),
8307                ],
8308            );
8309            let two = rt.alloc_int(2);
8310            assert_eq!(praxis_vec_chunks(ctx, closures, two).as_vec().len(), 2);
8311            assert_eq!(praxis_vec_windows(ctx, closures, two).as_vec().len(), 2);
8312            assert!(!rt.has_pending_fault(), "grouping asks for no callback");
8313
8314            praxis_vec_sorted(ctx, closures);
8315            assert!(rt.has_pending_fault(), "ordering still asks for `compare`");
8316        }
8317        unsafe { drop_ctx(ctx) };
8318    }
8319
8320    /// **ADR-107's pacing half, and ADR-100 §3's analogue.** [`char_ref`] must
8321    /// give the collector its turn on the path where it allocates *nothing*.
8322    ///
8323    /// `TextGet` is `AllocatesAndFaults` in the manifest, which is generated
8324    /// code's contract that the call site is a GC safepoint. A `for c in line`
8325    /// loop over ASCII touches nothing else that bumps the pacing counter, and
8326    /// the counter is the collector's only trigger — so an early return here
8327    /// would make such a loop run arbitrarily long with no collection at all.
8328    ///
8329    /// **The interleaved allocation is load-bearing and must stay unpaced.** The
8330    /// observable is the live registry *shrinking*, and an interned `Char` never
8331    /// enters it, so a loop of nothing but `praxis_text_get` could not shrink
8332    /// anything however well it paced — a guaranteed false pass (see
8333    /// `UNINTERNED`). `Runtime::alloc_int` is the one helper that grows the heap
8334    /// **without** pacing, so it supplies the pressure and the population while
8335    /// leaving `praxis_text_get` as the only safepoint in the loop. Swapping it
8336    /// for `praxis_alloc_int` would make the test pass with this function's
8337    /// safepoint deleted.
8338    #[test]
8339    fn char_ref_paces_the_collector_even_when_it_answers_from_the_table() {
8340        let mut rt = Runtime::new();
8341        let ctx = wired_ctx(&mut rt);
8342        // SAFETY: ctx wired throughout; `text` and `index` are rooted below.
8343        unsafe {
8344            let s = "abcdefgh";
8345            let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
8346            let index = praxis_alloc_int(ctx, 3);
8347            let mut frame = push_frame(ctx, SlotCount::new(2).unwrap());
8348            frame.set(0, text);
8349            frame.set(1, index);
8350
8351            let mut before = rt.heap().stats().live_count;
8352            let mut paced = false;
8353            for i in 0..100_000_i64 {
8354                // Registered, unrooted, and *unpaced*: pressure the collector can
8355                // see and reclaim, contributed by something that never offers a
8356                // turn itself.
8357                let _ = rt.alloc_int(UNINTERNED + i);
8358                // The wrapper under test. Every character of `s` is ASCII, so
8359                // this answers from the table and allocates nothing.
8360                let c = praxis_text_get(ctx, text, index);
8361                assert_eq!(c.as_char(), 'd');
8362                let after = rt.heap().stats().live_count;
8363                if after < before.saturating_add(1) {
8364                    paced = true;
8365                    break;
8366                }
8367                before = after;
8368            }
8369            drop(frame);
8370            assert!(
8371                paced,
8372                "praxis_text_get never gave the collector a turn on the interned path"
8373            );
8374        }
8375        unsafe { drop_ctx(ctx) };
8376    }
8377
8378    /// `default_cell`'s `Char` arm, the fourth boxing site. A `Grid[Char]`'s
8379    /// fill is NUL, which is inside the range.
8380    #[test]
8381    fn a_grid_of_char_fills_with_the_interned_nul() {
8382        let mut rt = Runtime::new();
8383        let ctx = wired_ctx(&mut rt);
8384        // SAFETY: ctx wired.
8385        unsafe {
8386            let grid = praxis_grid_new(ctx, &crate::scalars::CHAR, 3, 2);
8387            assert!(!rt.has_pending_fault());
8388            let nul = rt.immortals().small_char(0).expect("NUL is interned");
8389            for y in 0..2 {
8390                for x in 0..3 {
8391                    let xi = praxis_alloc_int(ctx, x);
8392                    let yi = praxis_alloc_int(ctx, y);
8393                    let cell = praxis_grid_get(ctx, grid, xi, yi);
8394                    assert_eq!(
8395                        cell.as_ptr(),
8396                        nul.as_ptr(),
8397                        "every cell of a fresh Grid[Char] is the one interned NUL"
8398                    );
8399                }
8400            }
8401        }
8402        unsafe { drop_ctx(ctx) };
8403    }
8404
8405    /// The executable form of "nothing in the language can observe `Char`
8406    /// identity", and the [`crate::dynamic_key::DynamicKey`] leg of ADR-107's
8407    /// argument.
8408    ///
8409    /// `DynamicKey::eq` opens with a pointer comparison, and that is the line
8410    /// interning could in principle have moved — but it is a fast path *for*
8411    /// structural equality and `char_equals` is a reflexive `u32 ==`, so sharing
8412    /// can only make it fire more often. This asserts the consequence rather than
8413    /// the argument: the same shape is run twice, once with interned keys and
8414    /// once with keys the runtime does not intern, and the two must agree.
8415    #[test]
8416    fn interning_a_char_does_not_change_keyed_collection_behaviour() {
8417        // (key, a different key, label): once inside the ASCII range and once
8418        // outside it. `é` and `ü` are two scalars the table does not hold.
8419        for (a_ch, b_ch, label) in [('a', 'b', "interned"), ('é', 'ü', "allocated")] {
8420            let mut rt = Runtime::new();
8421            let ctx = wired_ctx(&mut rt);
8422            // SAFETY: ctx wired; every ref below comes from the ABI.
8423            unsafe {
8424                let a = praxis_alloc_char(ctx, i64::from(a_ch as u32));
8425                // A *second* reference to the same value, built separately.
8426                // Interned it is `a`; uninterned it is a different object with
8427                // the same payload. Both must key the same slot.
8428                let a_again = praxis_alloc_char(ctx, i64::from(a_ch as u32));
8429                let b = praxis_alloc_char(ctx, i64::from(b_ch as u32));
8430                assert_eq!(
8431                    std::ptr::eq(a.as_ptr(), a_again.as_ptr()),
8432                    label == "interned",
8433                    "the fixture must actually be {label}"
8434                );
8435
8436                let set = praxis_set_new(ctx, &crate::scalars::CHAR);
8437                let _ = praxis_set_insert(ctx, set, a);
8438                assert_eq!(
8439                    praxis_bool_load(ctx, praxis_set_contains(ctx, set, a_again)),
8440                    1,
8441                    "{label}: an equal Char is the same set member"
8442                );
8443                assert_eq!(
8444                    praxis_bool_load(ctx, praxis_set_contains(ctx, set, b)),
8445                    0,
8446                    "{label}: a different Char is not"
8447                );
8448                // Inserting the equal-but-possibly-distinct object must not add
8449                // a second member — the property that would break if the pointer
8450                // fast path and `char_equals` ever disagreed.
8451                let _ = praxis_set_insert(ctx, set, a_again);
8452                assert_eq!(praxis_int_load(ctx, praxis_set_len(ctx, set)), 1, "{label}");
8453
8454                let counter = praxis_counter_new(ctx, &crate::scalars::CHAR);
8455                let _ = praxis_counter_inc(ctx, counter, a);
8456                let _ = praxis_counter_inc(ctx, counter, a_again);
8457                assert_eq!(
8458                    praxis_int_load(ctx, praxis_counter_get(ctx, counter, a)),
8459                    2,
8460                    "{label}: two bumps of an equal key are one key"
8461                );
8462                assert_eq!(
8463                    praxis_int_load(ctx, praxis_counter_len(ctx, counter)),
8464                    1,
8465                    "{label}"
8466                );
8467            }
8468            unsafe { drop_ctx(ctx) };
8469        }
8470    }
8471
8472    /// An interned `Char` is never registered, so a collection cannot reclaim it
8473    /// however unrooted it is — the `Char` half of
8474    /// [`an_interned_int_survives_collection_unrooted`].
8475    #[test]
8476    fn an_interned_char_survives_collection_unrooted() {
8477        let mut rt = Runtime::new();
8478        let ctx = wired_ctx(&mut rt);
8479        // SAFETY: ctx is wired to rt throughout.
8480        unsafe {
8481            let _ = praxis_alloc_char(ctx, i64::from('q' as u32));
8482        }
8483        assert_eq!(
8484            rt.heap().stats().live_count,
8485            0,
8486            "an interned Char must not enter the live registry"
8487        );
8488        // Nothing roots `'q'`: no shadow frame, no native scope, no Rust local
8489        // the collector can see. A registered object here would be swept.
8490        rt.collect_now();
8491        // SAFETY: ctx is still wired; the reference must still be readable.
8492        unsafe {
8493            let q = praxis_alloc_char(ctx, i64::from('q' as u32));
8494            assert!(!q.header().is_poisoned(), "an immortal is never swept");
8495            assert_eq!(q.as_char(), 'q');
8496        }
8497        unsafe { drop_ctx(ctx) };
8498    }
8499
8500    /// An interned `Int` is never registered, so a collection cannot reclaim it
8501    /// however unrooted it is. The `Int` analogue of
8502    /// `runtime_collect_keeps_immortals_alive_unrooted`.
8503    #[test]
8504    fn an_interned_int_survives_collection_unrooted() {
8505        let mut rt = Runtime::new();
8506        let ctx = wired_ctx(&mut rt);
8507        // SAFETY: ctx is wired to rt throughout.
8508        unsafe {
8509            let _ = praxis_alloc_int(ctx, 5);
8510        }
8511        assert_eq!(
8512            rt.heap().stats().live_count,
8513            0,
8514            "an interned Int must not enter the live registry"
8515        );
8516        // Nothing roots `5`: no shadow frame, no native scope, no Rust local the
8517        // collector can see. A registered object here would be swept.
8518        rt.collect_now();
8519        // SAFETY: ctx is still wired; the reference must still be readable.
8520        unsafe {
8521            let five = praxis_alloc_int(ctx, 5);
8522            assert!(!five.header().is_poisoned(), "an immortal is never swept");
8523            assert_eq!(praxis_int_load(ctx, five), 5);
8524        }
8525        unsafe { drop_ctx(ctx) };
8526    }
8527
8528    /// The executable form of "nothing in the language can observe `Int`
8529    /// identity": the three keyed collections must behave identically whether
8530    /// their keys are shared objects or distinct ones.
8531    ///
8532    /// [`crate::dynamic_key::DynamicKey`]'s `eq` opens with a pointer
8533    /// comparison, and that is the line interning could in principle have moved
8534    /// — but it is a fast path *for* structural equality and `int_equals` is
8535    /// reflexive, so sharing can only make it fire more often. This asserts the
8536    /// consequence rather than the argument: every operation below is run twice
8537    /// at the same shape, once with interned keys and once with uninterned ones,
8538    /// and the two must agree.
8539    #[test]
8540    fn interning_does_not_change_keyed_collection_behaviour() {
8541        // (key_a, key_b) pairs: two distinct keys, once inside the interned
8542        // range and once outside it.
8543        for (a_val, b_val, label) in [
8544            (5_i64, 6_i64, "interned"),
8545            (UNINTERNED, UNINTERNED + 1, "allocated"),
8546        ] {
8547            let mut rt = Runtime::new();
8548            let ctx = wired_ctx(&mut rt);
8549            // SAFETY: ctx wired; every ref below comes from the ABI.
8550            unsafe {
8551                let a = praxis_alloc_int(ctx, a_val);
8552                // A *second* reference to the same value, allocated separately.
8553                // Interned it is `a`; uninterned it is a different object with
8554                // the same payload. Both must key the same slot.
8555                let a_again = praxis_alloc_int(ctx, a_val);
8556                let b = praxis_alloc_int(ctx, b_val);
8557
8558                let map = praxis_map_new(ctx, &scalars::INT);
8559                let one = praxis_alloc_int(ctx, 1);
8560                let _ = praxis_map_insert(ctx, map, a, one);
8561                // `map_index` rather than `map_get`: the subscript answers the
8562                // value where `.get` answers an `Option` (ADR-076), and the
8563                // value is what has to match.
8564                assert_eq!(
8565                    praxis_int_load(ctx, praxis_map_index(ctx, map, a_again)),
8566                    1,
8567                    "{label}: an equal key must find the entry"
8568                );
8569                assert_eq!(
8570                    rt.fault(),
8571                    FaultKind::None,
8572                    "{label}: an equal key is a present key"
8573                );
8574                assert_eq!(
8575                    praxis_bool_load(ctx, praxis_map_contains(ctx, map, b)),
8576                    0,
8577                    "{label}: a different key must not"
8578                );
8579                assert_eq!(praxis_int_load(ctx, praxis_map_len(ctx, map)), 1);
8580
8581                let set = praxis_set_new(ctx, &scalars::INT);
8582                let _ = praxis_set_insert(ctx, set, a);
8583                let _ = praxis_set_insert(ctx, set, a_again);
8584                assert_eq!(
8585                    praxis_int_load(ctx, praxis_set_len(ctx, set)),
8586                    1,
8587                    "{label}: re-inserting an equal value must not grow the set"
8588                );
8589                assert_eq!(praxis_bool_load(ctx, praxis_set_contains(ctx, set, b)), 0);
8590
8591                let counter = praxis_counter_new(ctx, &scalars::INT);
8592                let _ = praxis_counter_inc(ctx, counter, a);
8593                let _ = praxis_counter_inc(ctx, counter, a_again);
8594                assert_eq!(
8595                    praxis_int_load(ctx, praxis_counter_get(ctx, counter, a)),
8596                    2,
8597                    "{label}: two bumps of an equal key are two bumps of one key"
8598                );
8599                assert_eq!(praxis_int_load(ctx, praxis_counter_len(ctx, counter)), 1);
8600            }
8601            unsafe { drop_ctx(ctx) };
8602        }
8603    }
8604
8605    #[test]
8606    fn bool_and_unit_abi_allocations_reuse_runtime_singletons() {
8607        let mut rt = Runtime::new();
8608        let ctx = wired_ctx(&mut rt);
8609        let (true_ref, false_ref, unit_ref) = unsafe {
8610            (
8611                praxis_alloc_bool(ctx, 1),
8612                praxis_alloc_bool(ctx, 0),
8613                praxis_alloc_unit(ctx),
8614            )
8615        };
8616        let expected = (
8617            rt.immortals().true_(),
8618            rt.immortals().false_(),
8619            rt.immortals().unit(),
8620        );
8621        unsafe { drop_ctx(ctx) };
8622
8623        assert_eq!(true_ref.as_ptr(), expected.0.as_ptr());
8624        assert_eq!(false_ref.as_ptr(), expected.1.as_ptr());
8625        assert_eq!(unit_ref.as_ptr(), expected.2.as_ptr());
8626    }
8627
8628    #[test]
8629    fn repeated_bool_allocation_mints_no_new_objects() {
8630        // A fresh *immortal* per call would be unregistered storage no
8631        // collection can reclaim, leaking one Bool per loop iteration. There
8632        // are two Bools; a hundred calls must name two objects.
8633        let mut rt = Runtime::new();
8634        let ctx = wired_ctx(&mut rt);
8635        let mut seen = std::collections::HashSet::new();
8636        // SAFETY: ctx wired.
8637        unsafe {
8638            for i in 0..100_i64 {
8639                seen.insert(praxis_alloc_bool(ctx, i % 2).as_ptr());
8640                seen.insert(praxis_alloc_unit(ctx).as_ptr());
8641            }
8642        }
8643        unsafe { drop_ctx(ctx) };
8644        assert_eq!(seen.len(), 3, "true, false and unit — and nothing else");
8645    }
8646
8647    /// Every wrapper that answers a *predicate* hands back one of the two `Bool`
8648    /// singletons — the comparisons and the `is_empty`/`contains` family, which
8649    /// are what a real program calls in a loop. It is also what makes their
8650    /// `Effect::Pure` rows honest: nothing here can collect, so the call site is
8651    /// not a safepoint.
8652    #[test]
8653    fn predicate_wrappers_return_bool_singletons_and_allocate_nothing() {
8654        let mut rt = Runtime::new();
8655        let ctx = wired_ctx(&mut rt);
8656        let (immortal_true, immortal_false) = (rt.immortals().true_(), rt.immortals().false_());
8657        // SAFETY: ctx wired; every argument below is allocated through the ABI.
8658        unsafe {
8659            let one = praxis_alloc_int(ctx, 1);
8660            let two = praxis_alloc_int(ctx, 2);
8661            let empty_vec = praxis_vec_new(ctx, &scalars::INT);
8662            let empty_text = praxis_alloc_text(ctx, std::ptr::null(), 0);
8663            let live_before = rt.heap().stats().live_count;
8664
8665            let answers = [
8666                (praxis_int_eq(ctx, one, two), false),
8667                (praxis_int_ne(ctx, one, two), true),
8668                (praxis_int_lt(ctx, one, two), true),
8669                (praxis_int_gt(ctx, one, two), false),
8670                (praxis_int_le(ctx, one, one), true),
8671                (praxis_int_ge(ctx, one, two), false),
8672                (praxis_vec_is_empty(ctx, empty_vec), true),
8673                (praxis_text_is_empty(ctx, empty_text), true),
8674            ];
8675
8676            assert_eq!(
8677                rt.heap().stats().live_count,
8678                live_before,
8679                "a predicate wrapper must not allocate"
8680            );
8681            for (answer, expected) in answers {
8682                let want = if expected {
8683                    immortal_true
8684                } else {
8685                    immortal_false
8686                };
8687                assert_eq!(
8688                    answer.as_ptr(),
8689                    want.as_ptr(),
8690                    "predicate answered with a fresh Bool instead of the singleton"
8691                );
8692            }
8693        }
8694        unsafe { drop_ctx(ctx) };
8695    }
8696
8697    /// Every wrapper that boxes a *derived* scalar — `Text` construction, the
8698    /// `.len()` family, `Grid` extents, checked arithmetic — paces the
8699    /// collector. Without that, a program whose pressure comes from those (a
8700    /// text-processing loop, say) could run arbitrarily long with the collector
8701    /// never offered a turn. Each is driven here until its own pacing collects.
8702    ///
8703    /// **Every receiver is sized past the interned range on purpose.** The first
8704    /// four wrappers answer a *length*, and a length inside
8705    /// [`crate::small_int`]'s range is an immortal that never enters the live
8706    /// registry — so with a five-byte `Text` or a 2×2 `Grid` the shrink test
8707    /// below is true on the first iteration and the test passes without a
8708    /// collection ever running (see `UNINTERNED`). Interning removes the
8709    /// allocation, not the pacing, and it is the pacing this test is about; the
8710    /// oversized receivers are what keep the observable in place.
8711    #[test]
8712    fn every_scalar_boxing_wrapper_paces_the_collector() {
8713        // (name, a closure that performs one allocating call)
8714        type Call = unsafe extern "C" fn(*mut RuntimeContext, GcRef) -> GcRef;
8715        let cases: [(&str, Call); 7] = [
8716            ("praxis_text_len", praxis_text_len),
8717            ("praxis_vec_len", praxis_vec_len),
8718            ("praxis_grid_width", praxis_grid_width),
8719            ("praxis_grid_height", praxis_grid_height),
8720            ("praxis_float_to_text", praxis_float_to_text),
8721            ("praxis_int_to_text", praxis_int_to_text),
8722            ("praxis_char_to_text", praxis_char_to_text),
8723        ];
8724        // One past the interned range, in whatever unit the receiver measures.
8725        let big = UNINTERNED as usize;
8726        for (name, call) in cases {
8727            let mut rt = Runtime::new();
8728            let ctx = wired_ctx(&mut rt);
8729            // SAFETY: ctx wired; each receiver matches its wrapper.
8730            unsafe {
8731                let text = "x".repeat(big);
8732                let receiver = match name {
8733                    "praxis_text_len" => praxis_alloc_text(ctx, text.as_ptr(), big),
8734                    "praxis_vec_len" => {
8735                        // The elements are all the same interned `0`, so the Vec
8736                        // costs one allocation regardless of its length — only
8737                        // its `len()` matters here.
8738                        rt.alloc_vec(&scalars::INT, vec![rt.alloc_int(0); big])
8739                    }
8740                    "praxis_float_to_text" => praxis_alloc_float(ctx, 1.5_f64.to_bits() as i64),
8741                    // The two `to_text` rows answer a fresh owned `Text` every
8742                    // call whatever the receiver is, so an interned receiver is
8743                    // the honest case: the allocation is the *answer*, not the
8744                    // argument.
8745                    "praxis_int_to_text" => praxis_alloc_int(ctx, big as i64),
8746                    "praxis_char_to_text" => praxis_alloc_char(ctx, i64::from(u32::from('e'))),
8747                    // `width` reads the first dimension and `height` the second,
8748                    // so each case makes *its own* answer uninterned and leaves
8749                    // the other dimension at one cell.
8750                    "praxis_grid_width" => praxis_grid_new(ctx, &scalars::INT, big as i64, 1),
8751                    _ => praxis_grid_new(ctx, &scalars::INT, 1, big as i64),
8752                };
8753                let mut frame = push_frame(ctx, SlotCount::new(1).unwrap());
8754                frame.set(0, receiver);
8755
8756                let mut before = rt.heap().stats().live_count;
8757                let mut paced = false;
8758                for _ in 0..10_000 {
8759                    let _ = call(ctx, receiver);
8760                    let after = rt.heap().stats().live_count;
8761                    if after < before.saturating_add(1) {
8762                        paced = true;
8763                        break;
8764                    }
8765                    before = after;
8766                }
8767                drop(frame);
8768                assert!(paced, "{name} never gave the collector a turn");
8769            }
8770            unsafe { drop_ctx(ctx) };
8771        }
8772    }
8773
8774    #[test]
8775    fn checked_add_returns_sum() {
8776        let mut rt = Runtime::new();
8777        let ctx = wired_ctx(&mut rt);
8778        // SAFETY: ctx wired; operands allocated as Ints.
8779        unsafe {
8780            let a = praxis_alloc_int(ctx, 40);
8781            let b = praxis_alloc_int(ctx, 2);
8782            let s = praxis_int_add(ctx, a, b);
8783            assert_eq!(praxis_int_load(ctx, s), 42);
8784            assert!(!rt.has_pending_fault());
8785        }
8786        unsafe { drop_ctx(ctx) };
8787    }
8788
8789    #[test]
8790    fn float_sign_of_zero_is_zero() {
8791        let mut rt = Runtime::new();
8792        let ctx = wired_ctx(&mut rt);
8793        let signed = unsafe {
8794            let zero = praxis_alloc_float(ctx, 0.0_f64.to_bits() as i64);
8795            let result = praxis_float_sign(ctx, zero);
8796            f64::from_bits(praxis_float_load(ctx, result) as u64)
8797        };
8798        unsafe { drop_ctx(ctx) };
8799
8800        assert_eq!(signed, 0.0);
8801    }
8802
8803    /// `-0.0` is still zero: `signum` reports the sign *bit* and would answer
8804    /// `-1.0` here.
8805    #[test]
8806    fn float_sign_of_negative_zero_is_zero() {
8807        let mut rt = Runtime::new();
8808        let ctx = wired_ctx(&mut rt);
8809        let signed = unsafe {
8810            let zero = praxis_alloc_float(ctx, (-0.0_f64).to_bits() as i64);
8811            let result = praxis_float_sign(ctx, zero);
8812            f64::from_bits(praxis_float_load(ctx, result) as u64)
8813        };
8814        unsafe { drop_ctx(ctx) };
8815
8816        assert_eq!(signed, 0.0);
8817    }
8818
8819    #[test]
8820    fn float_sign_of_nan_is_nan() {
8821        let mut rt = Runtime::new();
8822        let ctx = wired_ctx(&mut rt);
8823        let signed = unsafe {
8824            let nan = praxis_alloc_float(ctx, f64::NAN.to_bits() as i64);
8825            let result = praxis_float_sign(ctx, nan);
8826            f64::from_bits(praxis_float_load(ctx, result) as u64)
8827        };
8828        unsafe { drop_ctx(ctx) };
8829
8830        assert!(signed.is_nan());
8831    }
8832
8833    /// `min`/`max`/`clamp` hand back **the reference they were given**, not an
8834    /// equal copy (ADR-058). That is what makes them `Effect::Pure` — no
8835    /// allocation, so their call site is not a safepoint — and a version that
8836    /// allocated would pass every value test while quietly making three of the
8837    /// seven helpers collect.
8838    #[test]
8839    fn the_selecting_helpers_return_an_operand_and_allocate_nothing() {
8840        let mut rt = Runtime::new();
8841        let ctx = wired_ctx(&mut rt);
8842        // SAFETY: ctx wired; every operand is a valid Int.
8843        unsafe {
8844            let lo = praxis_alloc_int(ctx, 3);
8845            let hi = praxis_alloc_int(ctx, 7);
8846            assert_eq!(praxis_int_min(ctx, lo, hi).as_ptr(), lo.as_ptr());
8847            assert_eq!(praxis_int_min(ctx, hi, lo).as_ptr(), lo.as_ptr());
8848            assert_eq!(praxis_int_max(ctx, lo, hi).as_ptr(), hi.as_ptr());
8849            assert_eq!(praxis_int_max(ctx, hi, lo).as_ptr(), hi.as_ptr());
8850            // Equal operands pick the left one — arbitrary but fixed, so the
8851            // choice is a decision and not a coin flip.
8852            let three = praxis_alloc_int(ctx, 3);
8853            assert_eq!(praxis_int_min(ctx, lo, three).as_ptr(), lo.as_ptr());
8854            assert_eq!(praxis_int_max(ctx, lo, three).as_ptr(), lo.as_ptr());
8855            // `clamp` returns whichever of its three operands is the answer.
8856            let v = praxis_alloc_int(ctx, 5);
8857            assert_eq!(praxis_int_clamp(ctx, v, lo, hi).as_ptr(), v.as_ptr());
8858            let below = praxis_alloc_int(ctx, 1);
8859            assert_eq!(praxis_int_clamp(ctx, below, lo, hi).as_ptr(), lo.as_ptr());
8860            let above = praxis_alloc_int(ctx, 9);
8861            assert_eq!(praxis_int_clamp(ctx, above, lo, hi).as_ptr(), hi.as_ptr());
8862            assert!(!rt.has_pending_fault());
8863        }
8864        unsafe { drop_ctx(ctx) };
8865    }
8866
8867    /// An inverted `clamp` range is empty, so there is no operand to return and
8868    /// no answer that is not invented. It faults (ADR-058) and returns the Unit
8869    /// sentinel, like every other faulting wrapper.
8870    #[test]
8871    fn an_inverted_clamp_range_faults_rather_than_guessing() {
8872        let mut rt = Runtime::new();
8873        let ctx = wired_ctx(&mut rt);
8874        // SAFETY: ctx wired; every operand is a valid Int.
8875        unsafe {
8876            let v = praxis_alloc_int(ctx, 5);
8877            let lo = praxis_alloc_int(ctx, 10);
8878            let hi = praxis_alloc_int(ctx, 0);
8879            let r = praxis_int_clamp(ctx, v, lo, hi);
8880            assert!(rt.has_pending_fault());
8881            assert_eq!(rt.fault(), FaultKind::EmptyRange);
8882            assert_eq!(r.as_ptr(), rt.immortals().unit().as_ptr());
8883        }
8884        let _ = rt.take_fault();
8885        // A degenerate but *legal* range — one value wide — is not inverted.
8886        // SAFETY: ctx wired; every operand is a valid Int.
8887        unsafe {
8888            let v = praxis_alloc_int(ctx, 5);
8889            let same = praxis_alloc_int(ctx, 4);
8890            let r = praxis_int_clamp(ctx, v, same, same);
8891            assert!(!rt.has_pending_fault());
8892            assert_eq!(r.as_ptr(), same.as_ptr());
8893        }
8894        unsafe { drop_ctx(ctx) };
8895    }
8896
8897    /// A range whose member count has no `Int` faults with `IntOverflow`,
8898    /// answering the Unit sentinel like every other faulting wrapper.
8899    ///
8900    /// The kind is `IntOverflow` and not `EmptyRange` (ADR-059, ADR-075):
8901    /// `Int::MIN..Int::MAX` is the *widest* range expressible, so "empty range"
8902    /// would be a fault message that contradicts the input. `gcd`, `lcm` and
8903    /// A\*'s path cost answer `IntOverflow` for a result with no `Int` too.
8904    #[test]
8905    fn a_range_whose_count_has_no_int_faults_rather_than_wrapping_negative() {
8906        let mut rt = Runtime::new();
8907        let ctx = wired_ctx(&mut rt);
8908        // SAFETY: ctx wired; both bounds are valid Ints.
8909        unsafe {
8910            let lo = praxis_alloc_int(ctx, i64::MIN);
8911            let hi = praxis_alloc_int(ctx, i64::MAX);
8912            let r = praxis_range_new(ctx, lo, hi);
8913            let len = praxis_range_len(ctx, r);
8914            assert!(rt.has_pending_fault());
8915            assert_eq!(rt.fault(), FaultKind::IntOverflow);
8916            assert_eq!(len.as_ptr(), rt.immortals().unit().as_ptr());
8917        }
8918        let _ = rt.take_fault();
8919        // A range one narrower is countable, so the refusal is the edge and not
8920        // the rule.
8921        // SAFETY: ctx wired; both bounds are valid Ints.
8922        unsafe {
8923            let lo = praxis_alloc_int(ctx, 0);
8924            let hi = praxis_alloc_int(ctx, i64::MAX);
8925            let r = praxis_range_new(ctx, lo, hi);
8926            let len = praxis_range_len(ctx, r);
8927            assert!(!rt.has_pending_fault());
8928            assert_eq!(praxis_int_load(ctx, len), i64::MAX);
8929        }
8930        unsafe { drop_ctx(ctx) };
8931    }
8932
8933    /// `gcd` and `lcm` at the edges of what an `Int` can hold. Both are computed
8934    /// in `i128` and range-checked on the way out, so the only refusal is a
8935    /// result that genuinely has no `Int` — and `gcd`'s is exactly one input
8936    /// pair, which a naive `i64` implementation would have wrapped instead.
8937    #[test]
8938    fn gcd_and_lcm_are_non_negative_and_refuse_only_what_has_no_int() {
8939        let mut rt = Runtime::new();
8940        let ctx = wired_ctx(&mut rt);
8941        // SAFETY: ctx wired; every operand is a valid Int.
8942        unsafe {
8943            let load = |r: GcRef| praxis_int_load(ctx, r);
8944            // `Int::MIN`'s divisors: |Int::MIN| is out of range, but every gcd
8945            // *with* it that is not itself is in range.
8946            let min = praxis_alloc_int(ctx, i64::MIN);
8947            let two = praxis_alloc_int(ctx, 2);
8948            assert_eq!(load(praxis_int_gcd(ctx, min, two)), 2);
8949            assert!(!rt.has_pending_fault());
8950            // …and the one pair whose answer is 2^63 faults.
8951            let min2 = praxis_alloc_int(ctx, i64::MIN);
8952            let _ = praxis_int_gcd(ctx, min, min2);
8953            assert!(rt.has_pending_fault());
8954            assert_eq!(rt.fault(), FaultKind::IntOverflow);
8955        }
8956        let _ = rt.take_fault();
8957        // SAFETY: ctx wired; every operand is a valid Int.
8958        unsafe {
8959            let load = |r: GcRef| praxis_int_load(ctx, r);
8960            // Both signs, one answer: the lcm is non-negative.
8961            let neg = praxis_alloc_int(ctx, -4);
8962            let six = praxis_alloc_int(ctx, 6);
8963            assert_eq!(load(praxis_int_lcm(ctx, neg, six)), 12);
8964            let neg6 = praxis_alloc_int(ctx, -6);
8965            assert_eq!(load(praxis_int_lcm(ctx, neg, neg6)), 12);
8966            // `lcm(n, 0)` is 0, and the pair `(0, 0)` does not divide by zero.
8967            let zero = praxis_alloc_int(ctx, 0);
8968            assert_eq!(load(praxis_int_lcm(ctx, six, zero)), 0);
8969            assert_eq!(load(praxis_int_lcm(ctx, zero, zero)), 0);
8970            assert_eq!(load(praxis_int_gcd(ctx, zero, zero)), 0);
8971            assert!(!rt.has_pending_fault());
8972            // An lcm that does not fit: two coprime halves of the range.
8973            let big = praxis_alloc_int(ctx, i64::MAX);
8974            let three = praxis_alloc_int(ctx, 3);
8975            let _ = praxis_int_lcm(ctx, big, three);
8976            assert!(rt.has_pending_fault());
8977            assert_eq!(rt.fault(), FaultKind::IntOverflow);
8978        }
8979        let _ = rt.take_fault();
8980        unsafe { drop_ctx(ctx) };
8981    }
8982
8983    /// `abs` faults on the one input with no positive counterpart, and `sign` is
8984    /// total on the same input — the distinction the manifest records as
8985    /// `AllocatesAndFaults` versus `Allocates`.
8986    #[test]
8987    fn abs_faults_on_the_value_with_no_positive_and_sign_does_not() {
8988        let mut rt = Runtime::new();
8989        let ctx = wired_ctx(&mut rt);
8990        // SAFETY: ctx wired; every operand is a valid Int.
8991        unsafe {
8992            let min = praxis_alloc_int(ctx, i64::MIN);
8993            let r = praxis_int_abs(ctx, min);
8994            assert!(rt.has_pending_fault());
8995            assert_eq!(rt.fault(), FaultKind::IntOverflow);
8996            assert_eq!(r.as_ptr(), rt.immortals().unit().as_ptr());
8997        }
8998        let _ = rt.take_fault();
8999        // SAFETY: ctx wired; every operand is a valid Int.
9000        unsafe {
9001            let min = praxis_alloc_int(ctx, i64::MIN);
9002            assert_eq!(praxis_int_load(ctx, praxis_int_sign(ctx, min)), -1);
9003            let max = praxis_alloc_int(ctx, i64::MAX);
9004            assert_eq!(praxis_int_load(ctx, praxis_int_abs(ctx, max)), i64::MAX);
9005            assert_eq!(praxis_int_load(ctx, praxis_int_sign(ctx, max)), 1);
9006            assert!(!rt.has_pending_fault());
9007        }
9008        unsafe { drop_ctx(ctx) };
9009    }
9010
9011    #[test]
9012    fn overflow_sets_fault_and_returns_sentinel() {
9013        let mut rt = Runtime::new();
9014        let ctx = wired_ctx(&mut rt);
9015        // SAFETY: ctx wired; operands are valid Ints.
9016        unsafe {
9017            let a = praxis_alloc_int(ctx, i64::MAX);
9018            let b = praxis_alloc_int(ctx, 1);
9019            let s = praxis_int_add(ctx, a, b);
9020            // The fault is set; the return is the Unit sentinel.
9021            assert!(rt.has_pending_fault());
9022            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9023            assert_eq!(s.as_ptr(), rt.immortals().unit().as_ptr());
9024        }
9025        let _ = rt.take_fault();
9026        unsafe { drop_ctx(ctx) };
9027    }
9028
9029    #[test]
9030    fn division_by_zero_sets_fault() {
9031        let mut rt = Runtime::new();
9032        let ctx = wired_ctx(&mut rt);
9033        // SAFETY: ctx wired.
9034        unsafe {
9035            let a = praxis_alloc_int(ctx, 10);
9036            let b = praxis_alloc_int(ctx, 0);
9037            let _ = praxis_int_div(ctx, a, b);
9038            assert!(rt.has_pending_fault());
9039            assert_eq!(rt.fault(), FaultKind::DivByZero);
9040        }
9041        let _ = rt.take_fault();
9042        unsafe { drop_ctx(ctx) };
9043    }
9044
9045    #[test]
9046    fn remainder_by_zero_sets_fault() {
9047        let mut rt = Runtime::new();
9048        let ctx = wired_ctx(&mut rt);
9049        // SAFETY: ctx wired.
9050        unsafe {
9051            let a = praxis_alloc_int(ctx, 10);
9052            let b = praxis_alloc_int(ctx, 0);
9053            let _ = praxis_int_rem(ctx, a, b);
9054            assert!(rt.has_pending_fault());
9055            assert_eq!(rt.fault(), FaultKind::DivByZero);
9056        }
9057        let _ = rt.take_fault();
9058        unsafe { drop_ctx(ctx) };
9059    }
9060
9061    #[test]
9062    fn subtraction_overflow_sets_fault() {
9063        // The add/sub/mul overflow paths are symmetric. Sub: `Int::MIN - 1`
9064        // overflows.
9065        let mut rt = Runtime::new();
9066        let ctx = wired_ctx(&mut rt);
9067        // SAFETY: ctx wired; operands are valid Ints.
9068        unsafe {
9069            let a = praxis_alloc_int(ctx, i64::MIN);
9070            let b = praxis_alloc_int(ctx, 1);
9071            let _ = praxis_int_sub(ctx, a, b);
9072            assert!(rt.has_pending_fault());
9073            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9074        }
9075        let _ = rt.take_fault();
9076        unsafe { drop_ctx(ctx) };
9077    }
9078
9079    #[test]
9080    fn multiplication_overflow_sets_fault() {
9081        // `Int::MIN * -1` is the canonical mul overflow (same magnitude as
9082        // `Int::MAX + 1`).
9083        let mut rt = Runtime::new();
9084        let ctx = wired_ctx(&mut rt);
9085        // SAFETY: ctx wired; operands are valid Ints.
9086        unsafe {
9087            let a = praxis_alloc_int(ctx, i64::MIN);
9088            let b = praxis_alloc_int(ctx, -1);
9089            let _ = praxis_int_mul(ctx, a, b);
9090            assert!(rt.has_pending_fault());
9091            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9092        }
9093        let _ = rt.take_fault();
9094        unsafe { drop_ctx(ctx) };
9095    }
9096
9097    #[test]
9098    fn division_truncates_toward_zero() {
9099        // §4.12 / abi.rs comment: division truncates toward zero, so -7 / 2 == -3
9100        // (not -4 as floor division would give). Remainder takes the sign of the
9101        // dividend.
9102        let mut rt = Runtime::new();
9103        let ctx = wired_ctx(&mut rt);
9104        // SAFETY: ctx wired.
9105        unsafe {
9106            let a = praxis_alloc_int(ctx, -7);
9107            let b = praxis_alloc_int(ctx, 2);
9108            let q = praxis_int_div(ctx, a, b);
9109            assert!(!rt.has_pending_fault());
9110            assert_eq!(praxis_int_load(ctx, q), -3);
9111        }
9112        unsafe { drop_ctx(ctx) };
9113    }
9114
9115    #[test]
9116    fn remainder_truncates_toward_zero() {
9117        // -7 % 2 == -1 (remainder takes the dividend's sign under truncation).
9118        let mut rt = Runtime::new();
9119        let ctx = wired_ctx(&mut rt);
9120        // SAFETY: ctx wired.
9121        unsafe {
9122            let a = praxis_alloc_int(ctx, -7);
9123            let b = praxis_alloc_int(ctx, 2);
9124            let r = praxis_int_rem(ctx, a, b);
9125            assert!(!rt.has_pending_fault());
9126            assert_eq!(praxis_int_load(ctx, r), -1);
9127        }
9128        unsafe { drop_ctx(ctx) };
9129    }
9130
9131    #[test]
9132    fn division_min_div_minus_one_overflows() {
9133        // Regression for the §10.4 no-panic-across-ABI contract: `Int::MIN / -1`
9134        // is the sole signed-division case that overflows. The raw `/` panics in
9135        // debug builds; the wrapper must instead fault `IntOverflow`.
9136        let mut rt = Runtime::new();
9137        let ctx = wired_ctx(&mut rt);
9138        // SAFETY: ctx wired; operands are valid Ints.
9139        unsafe {
9140            let a = praxis_alloc_int(ctx, i64::MIN);
9141            let b = praxis_alloc_int(ctx, -1);
9142            let _ = praxis_int_div(ctx, a, b);
9143            assert!(rt.has_pending_fault());
9144            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9145        }
9146        let _ = rt.take_fault();
9147        unsafe { drop_ctx(ctx) };
9148    }
9149
9150    #[test]
9151    fn remainder_min_div_minus_one_overflows() {
9152        // Companion to the division regression: `Int::MIN % -1` traps in debug
9153        // builds even though the mathematical remainder is 0, because the
9154        // corresponding quotient overflows. The wrapper must fault instead.
9155        let mut rt = Runtime::new();
9156        let ctx = wired_ctx(&mut rt);
9157        // SAFETY: ctx wired; operands are valid Ints.
9158        unsafe {
9159            let a = praxis_alloc_int(ctx, i64::MIN);
9160            let b = praxis_alloc_int(ctx, -1);
9161            let _ = praxis_int_rem(ctx, a, b);
9162            assert!(rt.has_pending_fault());
9163            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9164        }
9165        let _ = rt.take_fault();
9166        unsafe { drop_ctx(ctx) };
9167    }
9168
9169    #[test]
9170    fn comparisons_yield_bools() {
9171        let mut rt = Runtime::new();
9172        let ctx = wired_ctx(&mut rt);
9173        // SAFETY: ctx wired.
9174        unsafe {
9175            let one = praxis_alloc_int(ctx, 1);
9176            let two = praxis_alloc_int(ctx, 2);
9177            assert_eq!(praxis_bool_load(ctx, praxis_int_lt(ctx, one, two)), 1);
9178            assert_eq!(praxis_bool_load(ctx, praxis_int_gt(ctx, one, two)), 0);
9179            assert_eq!(praxis_bool_load(ctx, praxis_int_eq(ctx, one, one)), 1);
9180        }
9181        unsafe { drop_ctx(ctx) };
9182    }
9183
9184    #[test]
9185    fn neg_of_min_overflows() {
9186        let mut rt = Runtime::new();
9187        let ctx = wired_ctx(&mut rt);
9188        // SAFETY: ctx wired.
9189        unsafe {
9190            let min = praxis_alloc_int(ctx, i64::MIN);
9191            let _ = praxis_int_neg(ctx, min);
9192            assert!(rt.has_pending_fault());
9193            assert_eq!(rt.fault(), FaultKind::IntOverflow);
9194        }
9195        let _ = rt.take_fault();
9196        unsafe { drop_ctx(ctx) };
9197    }
9198
9199    #[test]
9200    fn check_fault_reports_pending() {
9201        let mut rt = Runtime::new();
9202        let ctx = wired_ctx(&mut rt);
9203        // SAFETY: ctx wired.
9204        unsafe {
9205            assert_eq!(praxis_check_fault(ctx), 0);
9206            let a = praxis_alloc_int(ctx, 1);
9207            let b = praxis_alloc_int(ctx, 0);
9208            let _ = praxis_int_div(ctx, a, b);
9209            assert_eq!(praxis_check_fault(ctx), 1);
9210        }
9211        let _ = rt.take_fault();
9212        unsafe { drop_ctx(ctx) };
9213    }
9214
9215    #[test]
9216    fn alloc_text_round_trips() {
9217        let mut rt = Runtime::new();
9218        let ctx = wired_ctx(&mut rt);
9219        let s = "héllo";
9220        // SAFETY: ctx wired; `bytes` is a valid UTF-8 buffer for the call.
9221        unsafe {
9222            let r = praxis_alloc_text(ctx, s.as_ptr(), s.len());
9223            assert_eq!(r.as_text(), "héllo");
9224        }
9225        unsafe { drop_ctx(ctx) };
9226    }
9227
9228    #[test]
9229    fn alloc_bool_round_trips_value() {
9230        // This pins the *value*; `bool_and_unit_abi_allocations_reuse_runtime_singletons`
9231        // pins the identity. Bool equality is structural (§5.5).
9232        let mut rt = Runtime::new();
9233        let ctx = wired_ctx(&mut rt);
9234        // SAFETY: ctx wired.
9235        unsafe {
9236            let t = praxis_alloc_bool(ctx, 1);
9237            let f = praxis_alloc_bool(ctx, 0);
9238            assert_eq!(praxis_bool_load(ctx, t), 1);
9239            assert_eq!(praxis_bool_load(ctx, f), 0);
9240        }
9241        unsafe { drop_ctx(ctx) };
9242    }
9243
9244    #[test]
9245    fn fault_clear_default_is_none() {
9246        let f = Fault::clear();
9247        assert!(!f.is_pending());
9248        assert_eq!(f.kind(), FaultKind::None);
9249    }
9250
9251    // --- Vec[T] collection wrappers ----------------------------------------
9252
9253    #[test]
9254    fn vec_new_is_empty() {
9255        let mut rt = Runtime::new();
9256        let ctx = wired_ctx(&mut rt);
9257        // SAFETY: ctx wired; INT is a valid static descriptor.
9258        unsafe {
9259            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9260            assert_eq!(praxis_bool_load(ctx, praxis_vec_is_empty(ctx, v)), 1);
9261            assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 0);
9262        }
9263        unsafe { drop_ctx(ctx) };
9264    }
9265
9266    #[test]
9267    fn vec_push_grows_and_get_reads_back() {
9268        let mut rt = Runtime::new();
9269        let ctx = wired_ctx(&mut rt);
9270        // SAFETY: ctx wired; push mutates the vec in place (returns Unit), so we
9271        // keep using the same `v` GcRef throughout.
9272        unsafe {
9273            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9274            let a = praxis_alloc_int(ctx, 10);
9275            let b = praxis_alloc_int(ctx, 20);
9276            let c = praxis_alloc_int(ctx, 30);
9277            let _ = praxis_vec_push(ctx, v, a);
9278            let _ = praxis_vec_push(ctx, v, b);
9279            let _ = praxis_vec_push(ctx, v, c);
9280            assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 3);
9281            let i0 = praxis_alloc_int(ctx, 0);
9282            let i2 = praxis_alloc_int(ctx, 2);
9283            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, i0)), 10);
9284            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, i2)), 30);
9285        }
9286        unsafe { drop_ctx(ctx) };
9287    }
9288
9289    #[test]
9290    fn vec_get_out_of_bounds_faults() {
9291        let mut rt = Runtime::new();
9292        let ctx = wired_ctx(&mut rt);
9293        // SAFETY: ctx wired.
9294        unsafe {
9295            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9296            let one = praxis_alloc_int(ctx, 1);
9297            let _ = praxis_vec_get(ctx, v, one); // empty vec, index 0
9298            assert!(rt.has_pending_fault());
9299            assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9300        }
9301        let _ = rt.take_fault();
9302        unsafe { drop_ctx(ctx) };
9303    }
9304
9305    /// `praxis_vec_set` and `praxis_deque_set` **replace** — the property that
9306    /// separates them from the push beside them, and the one a store row pointed
9307    /// at the wrong wrapper would break silently.
9308    ///
9309    /// So each assertion is about what appending would get wrong: the length is
9310    /// unchanged, the neighbours are unchanged, an index one past the end faults
9311    /// instead of growing the collection, and a value of the wrong type is
9312    /// refused rather than retagging an explicitly typed collection.
9313    #[test]
9314    fn a_sequence_store_replaces_and_never_appends() {
9315        let mut rt = Runtime::new();
9316        let ctx = wired_ctx(&mut rt);
9317        // SAFETY: ctx wired; the stores mutate in place, so the same `GcRef`s
9318        // stay valid throughout.
9319        unsafe {
9320            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9321            for n in [10, 20, 30] {
9322                let _ = praxis_vec_push(ctx, v, praxis_alloc_int(ctx, n));
9323            }
9324            let d = praxis_deque_new(ctx, &crate::scalars::INT as *const _);
9325            for n in [10, 20] {
9326                let _ = praxis_deque_push_back(ctx, d, praxis_alloc_int(ctx, n));
9327            }
9328
9329            let one = praxis_alloc_int(ctx, 1);
9330            let ninety_nine = praxis_alloc_int(ctx, 99);
9331            let _ = praxis_vec_set(ctx, v, one, ninety_nine);
9332            let _ = praxis_deque_set(ctx, d, one, ninety_nine);
9333            assert!(!rt.has_pending_fault());
9334
9335            assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 3);
9336            assert_eq!(praxis_int_load(ctx, praxis_deque_len(ctx, d)), 2);
9337            let zero = praxis_alloc_int(ctx, 0);
9338            let two = praxis_alloc_int(ctx, 2);
9339            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, zero)), 10);
9340            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, one)), 99);
9341            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, two)), 30);
9342            assert_eq!(praxis_int_load(ctx, praxis_deque_get(ctx, d, zero)), 10);
9343            assert_eq!(praxis_int_load(ctx, praxis_deque_get(ctx, d, one)), 99);
9344
9345            // One past the end, and a negative index, are both out of range —
9346            // and neither grows the collection, which is what a store that fell
9347            // through to the appending wrapper would do.
9348            let three = praxis_alloc_int(ctx, 3);
9349            let neg = praxis_alloc_int(ctx, -1);
9350            type Store = unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef, GcRef) -> GcRef;
9351            type Len = unsafe extern "C" fn(*mut RuntimeContext, GcRef) -> GcRef;
9352            for (recv, idx, store, len_of) in [
9353                (v, three, praxis_vec_set as Store, praxis_vec_len as Len),
9354                (v, neg, praxis_vec_set as Store, praxis_vec_len as Len),
9355                (d, two, praxis_deque_set as Store, praxis_deque_len as Len),
9356                (d, neg, praxis_deque_set as Store, praxis_deque_len as Len),
9357            ] {
9358                let before = praxis_int_load(ctx, len_of(ctx, recv));
9359                let _ = store(ctx, recv, idx, ninety_nine);
9360                assert!(rt.has_pending_fault(), "an out-of-range store must fault");
9361                assert_eq!(rt.take_fault(), Some(FaultKind::IndexOutOfBounds));
9362                assert_eq!(
9363                    praxis_int_load(ctx, len_of(ctx, recv)),
9364                    before,
9365                    "a faulting store must not have grown the collection"
9366                );
9367            }
9368
9369            // A `Vec[Int]` refuses a `Float` rather than retagging itself.
9370            let float = praxis_alloc_float(ctx, 1.5_f64.to_bits() as i64);
9371            let _ = praxis_vec_set(ctx, v, zero, float);
9372            assert_eq!(rt.take_fault(), Some(FaultKind::TypeMismatch));
9373            assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, zero)), 10);
9374        }
9375        unsafe { drop_ctx(ctx) };
9376    }
9377
9378    #[test]
9379    fn vec_push_many_survive_collection() {
9380        // Stress: root the receiver/current element exactly as generated code
9381        // does, push enough elements to force multiple automatic collections,
9382        // and leave one unrooted allocation per iteration so collection is
9383        // observable as a live-registry shrink.
9384        //
9385        // Both the pushed element and the deliberately-unrooted allocation are
9386        // offset past the interned range: an interned `Int` is never registered,
9387        // so an in-range element would trip the shrink test on iteration zero
9388        // and neither the collection nor the rooting would be exercised (see
9389        // `UNINTERNED`). The offset is carried through the spot checks below so
9390        // the values read back are still the values pushed.
9391        let mut rt = Runtime::new();
9392        let ctx = wired_ctx(&mut rt);
9393        // SAFETY: ctx wired; push mutates in place so `v` stays valid throughout.
9394        unsafe {
9395            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9396            let mut frame = push_frame(ctx, SlotCount::new(2).unwrap());
9397            frame.set(0, v);
9398            let mut observed_reclamation = false;
9399            for i in 0..5000_i64 {
9400                let before_alloc = rt.heap().stats().live_count;
9401                let elem = praxis_alloc_int(ctx, UNINTERNED + i);
9402                if rt.heap().stats().live_count < before_alloc.saturating_add(1) {
9403                    observed_reclamation = true;
9404                }
9405                frame.set(1, elem);
9406                let before_push = rt.heap().stats().live_count;
9407                let _ = praxis_vec_push(ctx, v, elem);
9408                if rt.heap().stats().live_count < before_push {
9409                    observed_reclamation = true;
9410                }
9411                frame.clear(1);
9412                let _ = rt.alloc_int(-UNINTERNED - i - 1);
9413            }
9414            assert!(
9415                observed_reclamation,
9416                "the test must observe an automatic collection, not merely allocation pressure"
9417            );
9418            assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 5000);
9419            // Spot-check first/middle/last. The *indices* stay small (they are
9420            // interned, which is fine — nothing here watches them); the values
9421            // carry the offset the elements were pushed with.
9422            let zero = praxis_alloc_int(ctx, 0);
9423            assert_eq!(
9424                praxis_int_load(ctx, praxis_vec_get(ctx, v, zero)),
9425                UNINTERNED
9426            );
9427            let middle = praxis_alloc_int(ctx, 2500);
9428            assert_eq!(
9429                praxis_int_load(ctx, praxis_vec_get(ctx, v, middle)),
9430                UNINTERNED + 2500
9431            );
9432            let last = praxis_alloc_int(ctx, 4999);
9433            assert_eq!(
9434                praxis_int_load(ctx, praxis_vec_get(ctx, v, last)),
9435                UNINTERNED + 4999
9436            );
9437            drop(frame);
9438        }
9439        unsafe { drop_ctx(ctx) };
9440    }
9441
9442    #[test]
9443    fn vec_get_negative_index_faults() {
9444        // The `idx < 0` guard in `praxis_vec_get`: a negative index is out of
9445        // bounds, not a wrapped-around large one.
9446        let mut rt = Runtime::new();
9447        let ctx = wired_ctx(&mut rt);
9448        // SAFETY: ctx wired.
9449        unsafe {
9450            let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9451            let a = praxis_alloc_int(ctx, 1);
9452            let _ = praxis_vec_push(ctx, v, a); // non-empty vec, so only the sign can fail
9453            let neg = praxis_alloc_int(ctx, -1);
9454            let _ = praxis_vec_get(ctx, v, neg);
9455            assert!(rt.has_pending_fault());
9456            assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9457        }
9458        let _ = rt.take_fault();
9459        unsafe { drop_ctx(ctx) };
9460    }
9461
9462    #[test]
9463    fn text_get_negative_index_faults() {
9464        // Companion to `vec_get_negative_index_faults`, for `praxis_text_get`'s
9465        // own `idx < 0` guard.
9466        let mut rt = Runtime::new();
9467        let ctx = wired_ctx(&mut rt);
9468        // SAFETY: ctx wired.
9469        unsafe {
9470            let s = "ab";
9471            let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
9472            let neg = praxis_alloc_int(ctx, -1);
9473            let _ = praxis_text_get(ctx, text, neg);
9474            assert!(rt.has_pending_fault());
9475            assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9476        }
9477        let _ = rt.take_fault();
9478        unsafe { drop_ctx(ctx) };
9479    }
9480
9481    /// **ADR-086, the runtime half.** `praxis_text_get` allocates a `Char`.
9482    ///
9483    /// The catalog's twin (`the_two_text_reads_answer_a_char`) is pure data and
9484    /// cannot see this; this is pure runtime and cannot see that. Both halves
9485    /// are needed, and they must hold together: with only one, a `Char`-typed
9486    /// value routes into `praxis_char_load`, whose `read_scalar` answers `None`
9487    /// against the `INT` descriptor and panics.
9488    #[test]
9489    fn text_get_answers_a_char_object() {
9490        let mut rt = Runtime::new();
9491        let ctx = wired_ctx(&mut rt);
9492        // SAFETY: ctx wired.
9493        unsafe {
9494            // `"sddddd"[4]` must be `'d'` and not `100`: an object carrying the
9495            // `INT` descriptor would have the right value and the wrong type.
9496            let s = "sddddd";
9497            let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
9498            let four = praxis_alloc_int(ctx, 4);
9499            let got = praxis_text_get(ctx, text, four);
9500            assert!(!rt.has_pending_fault());
9501            assert!(
9502                std::ptr::eq(got.descriptor(), &crate::scalars::CHAR),
9503                "ADR-086: the read answers a Char, not the char's scalar value"
9504            );
9505            assert_eq!(got.as_char(), 'd');
9506
9507            // Scalar-not-byte indexing, pinned at the runtime level too: `é` is
9508            // one scalar and two UTF-8 bytes, so a byte index would answer 0xC3.
9509            let u = "héllo";
9510            let utext = praxis_alloc_text(ctx, u.as_ptr(), u.len());
9511            let one = praxis_alloc_int(ctx, 1);
9512            let got = praxis_text_get(ctx, utext, one);
9513            assert!(!rt.has_pending_fault());
9514            assert_eq!(got.as_char(), 'é');
9515        }
9516        unsafe { drop_ctx(ctx) };
9517    }
9518
9519    /// **ADR-115 at the ABI, on the shape that decides it.** `t.len()` and
9520    /// `t[i]` are defined on scalars (§4.3, ADR-086); indexing bytes is an
9521    /// optimization licensed by the count, and the licence must be refused on
9522    /// every text where it would be wrong.
9523    ///
9524    /// The cases are chosen to break a byte-indexing implementation that only
9525    /// looked at the text's own leading byte or only at its first scalar: a
9526    /// multi-byte scalar at the start, in the middle, at the end, a four-byte
9527    /// one, and a slice whose own bytes are all one-byte but whose owner's are
9528    /// not.
9529    #[test]
9530    fn a_text_reads_by_scalar_wherever_the_multi_byte_scalar_sits() {
9531        let mut rt = Runtime::new();
9532        let ctx = wired_ctx(&mut rt);
9533        // SAFETY: ctx wired for every call in this block.
9534        unsafe {
9535            for src in [
9536                "",
9537                "abc",
9538                "\u{0}\u{7f}",
9539                "éabc",
9540                "abéc",
9541                "abcé",
9542                "a\u{1F600}b",
9543                "\u{20AC}\u{20AC}",
9544                "héllo wörld",
9545            ] {
9546                let text = praxis_alloc_text(ctx, src.as_ptr(), src.len());
9547                let expected: Vec<char> = src.chars().collect();
9548
9549                let len = praxis_text_len(ctx, text);
9550                assert!(!rt.has_pending_fault(), "{src:?}");
9551                assert_eq!(len.as_int(), expected.len() as i64, "{src:?}");
9552
9553                let empty = praxis_text_is_empty(ctx, text);
9554                assert_eq!(empty.as_bool(), expected.is_empty(), "{src:?}");
9555
9556                for (i, want) in expected.iter().enumerate() {
9557                    let idx = praxis_alloc_int(ctx, i as i64);
9558                    let got = praxis_text_get(ctx, text, idx);
9559                    assert!(!rt.has_pending_fault(), "{src:?}[{i}]");
9560                    assert!(
9561                        std::ptr::eq(got.descriptor(), &crate::scalars::CHAR),
9562                        "{src:?}[{i}] answers a Char (ADR-086)"
9563                    );
9564                    assert_eq!(got.as_char(), *want, "{src:?}[{i}]");
9565                }
9566
9567                // One past the end faults, whichever path answered above.
9568                let past = praxis_alloc_int(ctx, expected.len() as i64);
9569                let _ = praxis_text_get(ctx, text, past);
9570                assert!(rt.has_pending_fault(), "{src:?}[{}]", expected.len());
9571                assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9572                let _ = rt.take_fault();
9573            }
9574
9575            // A view whose own bytes are all one-byte, inside an owner whose
9576            // are not. The answers are the same as the owner's corresponding
9577            // scalars; the byte-index path is refused because the licence is
9578            // the owner's to give.
9579            let owner_src = "héllo wörld";
9580            let owner = praxis_alloc_text(ctx, owner_src.as_ptr(), owner_src.len());
9581            // "llo " — bytes [3, 7) of the owner, all below 0x80.
9582            let view = rt
9583                .alloc_text_slice(owner, 3, 4)
9584                .expect("[3, 7) is on scalar boundaries");
9585            let len = praxis_text_len(ctx, view);
9586            assert_eq!(len.as_int(), 4);
9587            for (i, want) in "llo ".chars().enumerate() {
9588                let idx = praxis_alloc_int(ctx, i as i64);
9589                let got = praxis_text_get(ctx, view, idx);
9590                assert!(!rt.has_pending_fault());
9591                assert_eq!(got.as_char(), want);
9592            }
9593        }
9594        unsafe { drop_ctx(ctx) };
9595    }
9596
9597    /// **ADR-086's narrowing half.** `Int.to_char()` reaches the same
9598    /// range check `praxis_alloc_char` does, because they share one helper.
9599    ///
9600    /// A wrapper that forwarded `value as u32` without the check would answer
9601    /// `'A'` for `0x1_0000_0041` instead of faulting, which is the case that
9602    /// proves this door reaches the shared guard rather than restating it.
9603    #[test]
9604    fn int_to_char_rejects_what_is_not_a_scalar_value() {
9605        let mut rt = Runtime::new();
9606        let ctx = wired_ctx(&mut rt);
9607        // SAFETY: ctx wired.
9608        unsafe {
9609            for bad in [-1_i64, 0xD800, 0x11_0000, 0x1_0000_0041] {
9610                let n = praxis_alloc_int(ctx, bad);
9611                let got = praxis_int_to_char(ctx, n);
9612                assert!(rt.has_pending_fault(), "{bad} must not answer a Char");
9613                assert_eq!(rt.fault(), FaultKind::InvalidChar, "{bad}");
9614                assert!(std::ptr::eq(got.descriptor(), &crate::scalars::UNIT));
9615                let _ = rt.take_fault();
9616            }
9617
9618            // …and the round trip holds for one that is.
9619            let n = praxis_alloc_int(ctx, 233);
9620            let got = praxis_int_to_char(ctx, n);
9621            assert!(!rt.has_pending_fault());
9622            assert_eq!(got.as_char(), 'é');
9623            let back = praxis_char_to_int(ctx, got);
9624            assert!(!rt.has_pending_fault());
9625            assert_eq!(back.as_int(), 233);
9626        }
9627        unsafe { drop_ctx(ctx) };
9628    }
9629
9630    #[test]
9631    fn alloc_text_empty_string_round_trips() {
9632        // The `len == 0` branch in `praxis_alloc_text` treats an empty buffer as
9633        // the empty slice. An empty Text must format as "".
9634        let mut rt = Runtime::new();
9635        let ctx = wired_ctx(&mut rt);
9636        // SAFETY: ctx wired; null pointer + zero length is the documented empty path.
9637        unsafe {
9638            let r = praxis_alloc_text(ctx, std::ptr::null(), 0);
9639            assert_eq!(r.as_text(), "");
9640        }
9641        unsafe { drop_ctx(ctx) };
9642    }
9643
9644    #[test]
9645    fn vec_new_with_null_descriptor_defaults_to_int() {
9646        // A null element descriptor is kept null — "the caller has no static
9647        // element type" — and the vec must still be usable.
9648        let mut rt = Runtime::new();
9649        let ctx = wired_ctx(&mut rt);
9650        // SAFETY: ctx wired; null descriptor is the handled default case.
9651        unsafe {
9652            let v = praxis_vec_new(ctx, std::ptr::null());
9653            assert_eq!(praxis_bool_load(ctx, praxis_vec_is_empty(ctx, v)), 1);
9654        }
9655        unsafe { drop_ctx(ctx) };
9656    }
9657
9658    #[test]
9659    fn vec_push_rejects_a_value_with_the_wrong_descriptor() {
9660        let mut rt = Runtime::new();
9661        let ctx = wired_ctx(&mut rt);
9662        let length_after;
9663        unsafe {
9664            let ints = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9665            let float = praxis_alloc_float(ctx, 1.5_f64.to_bits() as i64);
9666            let _ = praxis_vec_push(ctx, ints, float);
9667            length_after = ints.as_vec().len();
9668        }
9669        unsafe { drop_ctx(ctx) };
9670
9671        assert_eq!(
9672            length_after, 0,
9673            "an ABI type mismatch must not silently retag and mutate an explicitly typed Vec[Int]"
9674        );
9675    }
9676
9677    #[test]
9678    fn alloc_char_rejects_values_that_only_become_valid_after_truncation() {
9679        let mut rt = Runtime::new();
9680        let ctx = wired_ctx(&mut rt);
9681        let result = unsafe { praxis_alloc_char(ctx, 0x1_0000_0041) };
9682        let unit = rt.immortals().unit();
9683        unsafe { drop_ctx(ctx) };
9684
9685        assert_eq!(
9686            result.as_ptr(),
9687            unit.as_ptr(),
9688            "the ABI must range-check the i64 code point before converting it to u32"
9689        );
9690        // And the fault it raises must name itself: a `FaultKind::None` here
9691        // would have the host report "no fault" while generated code took its
9692        // fault path.
9693        assert_eq!(rt.fault(), FaultKind::InvalidChar);
9694        assert!(rt.has_pending_fault());
9695    }
9696
9697    /// A negative code point is out of range for the same reason a too-large
9698    /// one is, and `as u32` wraps it into the valid range just as silently.
9699    #[test]
9700    fn alloc_char_rejects_a_negative_code_point() {
9701        let mut rt = Runtime::new();
9702        let ctx = wired_ctx(&mut rt);
9703        let result = unsafe { praxis_alloc_char(ctx, -1) };
9704        let unit = rt.immortals().unit();
9705        unsafe { drop_ctx(ctx) };
9706
9707        assert_eq!(result.as_ptr(), unit.as_ptr());
9708        assert_eq!(rt.fault(), FaultKind::InvalidChar);
9709    }
9710
9711    /// **ADR-111.** Input that is not UTF-8 faults at the `read`, because that
9712    /// is where the bytes stop being the compiler's and start being the host's.
9713    ///
9714    /// Asserted through `praxis_get_input` and never by feeding
9715    /// `praxis_alloc_text` bad bytes directly: that is a violated precondition,
9716    /// so it panics through `abi_guard!`, and `praxis_alloc_text`'s
9717    /// `Allocates` row makes the resulting `Panic` fault unobservable — the
9718    /// process aborts instead of failing. The property is that a program
9719    /// reading non-UTF-8 input gets `InvalidText` at its `read`.
9720    ///
9721    /// `praxis run` cannot reach this: `lazy_stdin::read` goes through
9722    /// `std::io::read_to_string` and exits 2 on non-UTF-8 stdin before the
9723    /// runtime sees a byte (`praxis-cli/src/run.rs`). The reachable caller is an
9724    /// embedder that installs its own `InputReader`, which is exactly what this
9725    /// test is.
9726    #[test]
9727    fn input_that_is_not_utf8_faults_at_the_read() {
9728        fn not_utf8() -> Vec<u8> {
9729            vec![0xF0, 0x28, 0x8C, 0x28]
9730        }
9731        let mut rt = Runtime::new();
9732        let ctx = wired_ctx(&mut rt);
9733        crate::input::install_input_reader(not_utf8);
9734        let result = unsafe { praxis_get_input(ctx) };
9735        let unit = rt.immortals().unit();
9736        crate::input::clear_input_reader();
9737        unsafe { drop_ctx(ctx) };
9738
9739        assert_eq!(rt.fault(), FaultKind::InvalidText);
9740        assert!(rt.has_pending_fault());
9741        assert_eq!(
9742            result.as_ptr(),
9743            unit.as_ptr(),
9744            "the fault path answers §10.4's defined dummy, not a half-built Text"
9745        );
9746    }
9747
9748    /// The mutation companion, and it is required: a `praxis_get_input` that
9749    /// faulted on *every* input would pass the gate above.
9750    ///
9751    /// Multi-byte on purpose — a validation that accepted only ASCII would also
9752    /// pass a test written with `"hi"`.
9753    #[test]
9754    fn input_that_is_utf8_still_becomes_the_buffer() {
9755        fn multibyte() -> Vec<u8> {
9756            "héllo wörld".as_bytes().to_vec()
9757        }
9758        let mut rt = Runtime::new();
9759        let ctx = wired_ctx(&mut rt);
9760        crate::input::install_input_reader(multibyte);
9761        let result = unsafe { praxis_get_input(ctx) };
9762        let contents = result.as_text().to_string();
9763        let descriptor = result.descriptor().name;
9764        crate::input::clear_input_reader();
9765        unsafe { drop_ctx(ctx) };
9766
9767        assert!(!rt.has_pending_fault(), "fault: {:?}", rt.fault());
9768        assert_eq!(descriptor, "Text");
9769        assert_eq!(contents, "héllo wörld");
9770    }
9771
9772    #[test]
9773    fn grid_cell_vectors_preserve_the_grid_element_descriptor() {
9774        let mut rt = Runtime::new();
9775        let ctx = wired_ctx(&mut rt);
9776        let cell = rt.alloc_text("x");
9777        let grid = rt.alloc_grid(&crate::text::TEXT, vec![cell], 1);
9778        let descriptors;
9779        unsafe {
9780            let zero = praxis_alloc_int(ctx, 0);
9781            let cells = praxis_grid_cells(ctx, grid);
9782            let row = praxis_grid_row(ctx, grid, zero);
9783            let column = praxis_grid_column(ctx, grid, zero);
9784            descriptors = [
9785                (*vec_payload(cells).element_descriptor).id(),
9786                (*vec_payload(row).element_descriptor).id(),
9787                (*vec_payload(column).element_descriptor).id(),
9788            ];
9789        }
9790        unsafe { drop_ctx(ctx) };
9791
9792        assert!(
9793            descriptors.iter().all(|id| *id == crate::text::TEXT.id()),
9794            "cells(), row(), and column() must return Vec values tagged with the Grid cell type"
9795        );
9796    }
9797
9798    #[test]
9799    fn constructed_grid_cells_satisfy_the_declared_element_descriptor() {
9800        let mut rt = Runtime::new();
9801        let ctx = wired_ctx(&mut rt);
9802        let cell_descriptor;
9803        unsafe {
9804            let grid = praxis_grid_new(ctx, &crate::scalars::INT as *const _, 1, 1);
9805            cell_descriptor = grid_payload(grid).items[0].descriptor().id();
9806        }
9807        unsafe { drop_ctx(ctx) };
9808
9809        assert_eq!(
9810            cell_descriptor,
9811            crate::scalars::INT.id(),
9812            "a live Grid[Int] must never contain a Unit placeholder observable through get/format/hash"
9813        );
9814    }
9815
9816    #[test]
9817    fn grid_position_vectors_use_the_point_tuple_descriptor() {
9818        let mut rt = Runtime::new();
9819        let ctx = wired_ctx(&mut rt);
9820        let cell = rt.alloc_int(1);
9821        let grid = rt.alloc_grid(&crate::scalars::INT, vec![cell], 1);
9822        let descriptors;
9823        unsafe {
9824            let point = alloc_point(ctx, 0, 0);
9825            let positions = praxis_grid_positions(ctx, grid);
9826            let neighbors4 = praxis_grid_neighbors4(ctx, grid, point);
9827            let neighbors8 = praxis_grid_neighbors8(ctx, grid, point);
9828            let matches = praxis_grid_find_all(ctx, grid, cell);
9829            descriptors = [
9830                (*vec_payload(positions).element_descriptor).id(),
9831                (*vec_payload(neighbors4).element_descriptor).id(),
9832                (*vec_payload(neighbors8).element_descriptor).id(),
9833                (*vec_payload(matches).element_descriptor).id(),
9834            ];
9835        }
9836        unsafe { drop_ctx(ctx) };
9837
9838        assert!(
9839            descriptors
9840                .iter()
9841                .all(|id| *id == crate::tuples::TUPLE.id()),
9842            "position-producing Grid methods must return Vec[Tuple[Int, Int]] at runtime"
9843        );
9844    }
9845
9846    /// An extent must be validated before it becomes a `usize`. Unchecked,
9847    /// `vec![unit; (w as usize) * (h as usize)]` turns `-1` into `usize::MAX`,
9848    /// and the products either overflow (a capacity panic across `extern "C"`)
9849    /// or ask the host for terabytes (an OOM abort). The wrapper must answer
9850    /// with a fault, and the heap must be untouched — a partly-built grid is as
9851    /// bad as a crash.
9852    #[test]
9853    fn a_negative_or_absurd_grid_extent_faults_instead_of_allocating() {
9854        let absurd = GridExtent::MAX_CELLS as i64 + 1;
9855        for (width, height) in [
9856            (-1_i64, 4_i64),
9857            (4, -1),
9858            (-1, -1),
9859            (i64::MIN, 1),
9860            // Overflows the `usize` multiplication outright.
9861            (i64::MAX, 2),
9862            (1 << 40, 1 << 40),
9863            // Multiplies cleanly and is still an allocation no host can serve.
9864            (absurd, 1),
9865            (1, absurd),
9866        ] {
9867            let mut rt = Runtime::new();
9868            let ctx = wired_ctx(&mut rt);
9869            let live_before = rt.heap().stats().live_count;
9870            let result =
9871                unsafe { praxis_grid_new(ctx, &crate::scalars::INT as *const _, width, height) };
9872            let live_after = rt.heap().stats().live_count;
9873            let unit = rt.immortals().unit();
9874            unsafe { drop_ctx(ctx) };
9875
9876            assert_eq!(
9877                rt.fault(),
9878                FaultKind::InvalidSize,
9879                "Grid[Int]({width}, {height}) must fault"
9880            );
9881            assert_eq!(
9882                result.as_ptr(),
9883                unit.as_ptr(),
9884                "a faulted Grid[Int]({width}, {height}) returns the Unit sentinel"
9885            );
9886            assert_eq!(
9887                live_after, live_before,
9888                "a rejected Grid[Int]({width}, {height}) allocates nothing"
9889            );
9890        }
9891    }
9892
9893    /// The other side of the same gate: an extent the runtime *can* serve still
9894    /// builds the grid it asked for, including the degenerate zero cases.
9895    #[test]
9896    fn an_in_range_grid_extent_still_builds_its_cells() {
9897        let mut rt = Runtime::new();
9898        let ctx = wired_ctx(&mut rt);
9899        let shapes: Vec<(i64, i64, usize, usize)> = vec![(0, 0, 0, 0), (0, 5, 0, 0), (3, 2, 6, 3)];
9900        let mut observed = Vec::new();
9901        for (width, height, _, _) in &shapes {
9902            let grid =
9903                unsafe { praxis_grid_new(ctx, &crate::scalars::INT as *const _, *width, *height) };
9904            let p = unsafe { grid_payload(grid) };
9905            observed.push((p.items.len(), p.width));
9906        }
9907        unsafe { drop_ctx(ctx) };
9908
9909        assert_eq!(rt.fault(), FaultKind::None, "no in-range extent faults");
9910        for ((w, h, cells, width), (got_cells, got_width)) in shapes.iter().zip(observed) {
9911            assert_eq!(
9912                (got_cells, got_width),
9913                (*cells, *width),
9914                "Grid[Int]({w}, {h}) shape"
9915            );
9916        }
9917    }
9918
9919    /// **ADR-146.** `Vec(n, fill)` builds `n` slots, all of them the fill, and
9920    /// the empty case is a `Vec` and not a fault.
9921    #[test]
9922    fn vec_filled_builds_n_copies_of_one_value() {
9923        let mut rt = Runtime::new();
9924        let ctx = wired_ctx(&mut rt);
9925        let observed: Vec<(usize, bool)> = [0_i64, 1, 7]
9926            .into_iter()
9927            .map(|n| unsafe {
9928                let count = praxis_alloc_int(ctx, n);
9929                let fill = praxis_alloc_int(ctx, 42);
9930                let v = praxis_vec_filled(ctx, &crate::scalars::INT as *const _, count, fill);
9931                let p = vec_payload(v);
9932                // Every slot is the *same* reference, which is the aliasing
9933                // ADR-146 decision 4 states rather than n copies of a value.
9934                let all_same = p.items.iter().all(|item| item.as_ptr() == fill.as_ptr());
9935                (p.items.len(), all_same)
9936            })
9937            .collect();
9938        unsafe { drop_ctx(ctx) };
9939
9940        assert_eq!(rt.fault(), FaultKind::None, "no in-range count faults");
9941        assert_eq!(observed, vec![(0, true), (1, true), (7, true)]);
9942    }
9943
9944    /// The other half of ADR-041 decision 1, for the newtype it added: a count
9945    /// the runtime cannot serve is a fault and not an allocation, and the heap
9946    /// is untouched — a half-built `Vec` is as bad as a crash.
9947    #[test]
9948    fn vec_filled_refuses_a_negative_or_absurd_count() {
9949        let absurd = crate::collections::VecExtent::MAX_ITEMS as i64 + 1;
9950        for n in [-1_i64, i64::MIN, absurd, i64::MAX] {
9951            let mut rt = Runtime::new();
9952            let ctx = wired_ctx(&mut rt);
9953            let (result, live_before, live_after, unit) = unsafe {
9954                let count = praxis_alloc_int(ctx, n);
9955                let fill = praxis_alloc_int(ctx, 0);
9956                let before = rt.heap().stats().live_count;
9957                let r = praxis_vec_filled(ctx, &crate::scalars::INT as *const _, count, fill);
9958                (
9959                    r,
9960                    before,
9961                    rt.heap().stats().live_count,
9962                    rt.immortals().unit(),
9963                )
9964            };
9965            unsafe { drop_ctx(ctx) };
9966
9967            assert_eq!(rt.fault(), FaultKind::InvalidSize, "Vec({n}, 0) must fault");
9968            assert_eq!(
9969                result.as_ptr(),
9970                unit.as_ptr(),
9971                "a faulted Vec({n}, 0) returns the Unit sentinel"
9972            );
9973            assert_eq!(
9974                live_after, live_before,
9975                "a rejected Vec({n}, 0) allocates nothing"
9976            );
9977        }
9978    }
9979
9980    /// A declared element type the fill is not is a `TypeMismatch`, through the
9981    /// same `adopt_or_reject` a `push` goes through — not a silent retag of the
9982    /// collection to the fill's type, which is the mislabelling defect one
9983    /// level down.
9984    /// A *null* static descriptor adopts instead, which is what "the caller has
9985    /// no static element type" already means for `praxis_vec_new`.
9986    #[test]
9987    fn vec_filled_reconciles_its_element_descriptor() {
9988        let mut rejecting = Runtime::new();
9989        let ctx = wired_ctx(&mut rejecting);
9990        unsafe {
9991            let count = praxis_alloc_int(ctx, 3);
9992            let text = praxis_alloc_text(ctx, b"x".as_ptr(), 1);
9993            praxis_vec_filled(ctx, &crate::scalars::INT as *const _, count, text);
9994            drop_ctx(ctx);
9995        }
9996        assert_eq!(
9997            rejecting.fault(),
9998            FaultKind::TypeMismatch,
9999            "a `Vec[Int]` filled with a `Text` is a mislabelled element descriptor"
10000        );
10001
10002        let mut adopting = Runtime::new();
10003        let ctx = wired_ctx(&mut adopting);
10004        let adopted = unsafe {
10005            let count = praxis_alloc_int(ctx, 3);
10006            let text = praxis_alloc_text(ctx, b"x".as_ptr(), 1);
10007            let v = praxis_vec_filled(ctx, std::ptr::null(), count, text);
10008            let matches = std::ptr::eq(vec_payload(v).element_descriptor, text.descriptor());
10009            drop_ctx(ctx);
10010            matches
10011        };
10012        assert_eq!(adopting.fault(), FaultKind::None);
10013        assert!(
10014            adopted,
10015            "a null static descriptor adopts the fill's, as `praxis_vec_new` already does"
10016        );
10017    }
10018
10019    /// **ADR-146 decision 6.** `Grid(w, h, fill)` accepts a fill
10020    /// `praxis_grid_new` cannot invent: `default_cell` has no zero value for a
10021    /// composite and answers `TypeMismatch`, and the explicit fill is exactly
10022    /// what removes the question. The contrast is the assertion — both calls
10023    /// are in one test so a later change that reintroduced `default_cell` here
10024    /// fails rather than passes quietly.
10025    #[test]
10026    fn grid_filled_accepts_a_composite_fill_where_grid_new_cannot() {
10027        let mut inventing = Runtime::new();
10028        let ctx = wired_ctx(&mut inventing);
10029        unsafe {
10030            praxis_grid_new(ctx, &crate::collections::VEC as *const _, 2, 2);
10031            drop_ctx(ctx);
10032        }
10033        assert_eq!(
10034            inventing.fault(),
10035            FaultKind::TypeMismatch,
10036            "`praxis_grid_new` still has no zero value for a `Vec` cell"
10037        );
10038
10039        let mut supplied = Runtime::new();
10040        let ctx = wired_ctx(&mut supplied);
10041        let (cells, all_same) = unsafe {
10042            let inner = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
10043            let (w, h) = (praxis_alloc_int(ctx, 2), praxis_alloc_int(ctx, 2));
10044            let g = praxis_grid_filled(ctx, &crate::collections::VEC as *const _, w, h, inner);
10045            let p = grid_payload(g);
10046            let same = p.items.iter().all(|c| c.as_ptr() == inner.as_ptr());
10047            let len = p.items.len();
10048            drop_ctx(ctx);
10049            (len, same)
10050        };
10051        assert_eq!(supplied.fault(), FaultKind::None);
10052        assert_eq!(cells, 4, "an explicit fill builds all four cells");
10053        assert!(
10054            all_same,
10055            "the four cells are one `Vec`, not four (ADR-146 decision 4)"
10056        );
10057    }
10058
10059    /// `Grid(w, h, fill)` takes the extents `praxis_grid_new` refuses, through
10060    /// the same `GridExtent::new` — a fill changes nothing about the
10061    /// arithmetic.
10062    #[test]
10063    fn grid_filled_refuses_the_extents_grid_new_refuses() {
10064        let absurd = GridExtent::MAX_CELLS as i64 + 1;
10065        for (width, height) in [(-1_i64, 4_i64), (4, -1), (i64::MAX, 2), (absurd, 1)] {
10066            let mut rt = Runtime::new();
10067            let ctx = wired_ctx(&mut rt);
10068            let (result, live_before, live_after, unit) = unsafe {
10069                let (w, h) = (praxis_alloc_int(ctx, width), praxis_alloc_int(ctx, height));
10070                let fill = praxis_alloc_int(ctx, 0);
10071                let before = rt.heap().stats().live_count;
10072                let r = praxis_grid_filled(ctx, &crate::scalars::INT as *const _, w, h, fill);
10073                (
10074                    r,
10075                    before,
10076                    rt.heap().stats().live_count,
10077                    rt.immortals().unit(),
10078                )
10079            };
10080            unsafe { drop_ctx(ctx) };
10081
10082            assert_eq!(
10083                rt.fault(),
10084                FaultKind::InvalidSize,
10085                "Grid({width}, {height}, 0) must fault"
10086            );
10087            assert_eq!(
10088                result.as_ptr(),
10089                unit.as_ptr(),
10090                "a faulted Grid({width}, {height}, 0) returns the Unit sentinel"
10091            );
10092            assert_eq!(
10093                live_after, live_before,
10094                "a rejected Grid({width}, {height}, 0) allocates nothing"
10095            );
10096        }
10097    }
10098
10099    /// A member the set cannot hold is a fault, and a negative one does not
10100    /// vanish silently. Unchecked, `bs.insert(10^18)` would ask `Vec::resize`
10101    /// for 10^16 words — an OOM abort from inside `extern "C"`.
10102    #[test]
10103    fn a_bitset_member_outside_the_representable_range_faults() {
10104        for member in [-1_i64, i64::MIN, i64::MAX, BitIndex::MAX + 1] {
10105            let mut rt = Runtime::new();
10106            let ctx = wired_ctx(&mut rt);
10107            let words;
10108            unsafe {
10109                let bs = praxis_bitset_new(ctx);
10110                let value = praxis_alloc_int(ctx, member);
10111                let _ = praxis_bitset_insert(ctx, bs, value);
10112                words = bitset_payload(bs).words.len();
10113            }
10114            unsafe { drop_ctx(ctx) };
10115
10116            assert_eq!(
10117                rt.fault(),
10118                FaultKind::InvalidSize,
10119                "BitSet.insert({member}) must fault"
10120            );
10121            assert_eq!(words, 0, "BitSet.insert({member}) must allocate no words");
10122        }
10123    }
10124
10125    /// The words of a **live, heap-allocated** `BitSet`, reached the way
10126    /// generated code reaches them: through
10127    /// [`INLINE_BITSET_SITE`](crate::bitset::INLINE_BITSET_SITE), from the
10128    /// object base, with no knowledge of the payload beyond what the site
10129    /// carries (ADR-118 part 2).
10130    ///
10131    /// `a_backend_can_read_the_length_and_the_elements_out_of_a_live_payload`
10132    /// in `collections.rs` is this test for `Vec`; this is the second payload's
10133    /// copy of the same agreement between the emitted load and the layout.
10134    ///
10135    /// Compiled out under `std-vec-payload`: that arm has no site to name, so
10136    /// naming one fails the *build* rather than miscompiling a load.
10137    #[cfg(not(feature = "std-vec-payload"))]
10138    #[test]
10139    fn the_inline_bitset_site_addresses_a_live_bitsets_words() {
10140        use crate::bitset::INLINE_BITSET_SITE;
10141
10142        let mut rt = Runtime::new();
10143        let ctx = wired_ctx(&mut rt);
10144        let (words, len) = unsafe {
10145            let bs = praxis_bitset_new(ctx);
10146            assert!(
10147                std::ptr::eq(INLINE_BITSET_SITE.type_id().descriptor(), bs.descriptor()),
10148                "the site names the descriptor the inline proof compares against"
10149            );
10150            for member in [0_i64, 63, 64, 200] {
10151                let value = praxis_alloc_int(ctx, member);
10152                let _ = praxis_bitset_insert(ctx, bs, value);
10153            }
10154            let base = bs.as_ptr().cast::<u8>().cast_const();
10155            (
10156                base.add(INLINE_BITSET_SITE.elements_offset())
10157                    .cast::<*const u64>()
10158                    .read(),
10159                base.add(INLINE_BITSET_SITE.len_offset())
10160                    .cast::<usize>()
10161                    .read(),
10162            )
10163        };
10164
10165        assert_eq!(len, 4, "bit 200 lives in the fourth word");
10166        assert_eq!(
10167            INLINE_BITSET_SITE.element_shift(),
10168            3,
10169            "a word is eight bytes"
10170        );
10171        for member in [0_u64, 63, 64, 200] {
10172            // SAFETY: `member >> 6 < len`, and `words` is the live buffer the
10173            // site's displacement just answered.
10174            let w = unsafe { *words.add((member >> 6) as usize) };
10175            assert!(
10176                (w >> (member & 63)) & 1 == 1,
10177                "bit {member} read back through the site's displacements"
10178            );
10179        }
10180        unsafe { drop_ctx(ctx) };
10181    }
10182
10183    /// Queries stay total: a value the set cannot hold is a value it does not
10184    /// contain, and removing one is a no-op. Neither may fault, and neither may
10185    /// grow the word vector.
10186    #[test]
10187    fn bitset_queries_outside_the_range_are_absent_rather_than_faults() {
10188        let mut rt = Runtime::new();
10189        let ctx = wired_ctx(&mut rt);
10190        let (present, words) = unsafe {
10191            let bs = praxis_bitset_new(ctx);
10192            let huge = praxis_alloc_int(ctx, i64::MAX);
10193            let _ = praxis_bitset_remove(ctx, bs, huge);
10194            // The answer is the scalar channel's `0`/`1` (ADR-118 decision 6),
10195            // so there is no box to load it back out of.
10196            let answer = praxis_bitset_contains(ctx, bs, huge);
10197            (answer != 0, bitset_payload(bs).words.len())
10198        };
10199        unsafe { drop_ctx(ctx) };
10200
10201        assert!(!present, "an unrepresentable member is absent");
10202        assert_eq!(words, 0, "a query allocates no words");
10203        assert_eq!(rt.fault(), FaultKind::None, "a query does not fault");
10204    }
10205
10206    /// `(i64::MAX, i64::MAX).neighbors4()` must not overflow the offset addition
10207    /// and panic across `extern "C"`. Every such neighbour is outside every
10208    /// grid, so the answer is an empty Vec.
10209    #[test]
10210    fn neighbors_of_an_extreme_point_are_empty_rather_than_a_panic() {
10211        let mut rt = Runtime::new();
10212        let ctx = wired_ctx(&mut rt);
10213        let cell = rt.alloc_int(1);
10214        let grid = rt.alloc_grid(&crate::scalars::INT, vec![cell], 1);
10215        let counts = unsafe {
10216            let mut counts = Vec::new();
10217            for (x, y) in [
10218                (i64::MAX, i64::MAX),
10219                (i64::MIN, i64::MIN),
10220                (i64::MAX, 0),
10221                (0, i64::MIN),
10222            ] {
10223                let point = alloc_point(ctx, x, y);
10224                counts.push((
10225                    vec_payload(praxis_grid_neighbors4(ctx, grid, point))
10226                        .items
10227                        .len(),
10228                    vec_payload(praxis_grid_neighbors8(ctx, grid, point))
10229                        .items
10230                        .len(),
10231                ));
10232            }
10233            counts
10234        };
10235        unsafe { drop_ctx(ctx) };
10236
10237        assert!(
10238            counts.iter().all(|(n4, n8)| *n4 == 0 && *n8 == 0),
10239            "an out-of-range point has no in-grid neighbours: {counts:?}"
10240        );
10241        assert_eq!(rt.fault(), FaultKind::None);
10242    }
10243
10244    /// A 3×3 grid of `Int`s holding `1..=9` in reading order, so a neighbour's
10245    /// cell names its own position.
10246    fn nine_grid(rt: &mut Runtime) -> GcRef {
10247        let cells: Vec<GcRef> = (1..=9).map(|n| rt.alloc_int(n)).collect();
10248        rt.alloc_grid(&crate::scalars::INT, cells, 3)
10249    }
10250
10251    /// Every field of a neighbourhood record as `(name, Some((x, y)) | None)`,
10252    /// **in slot order** — which is what a field read indexes, so a test that
10253    /// reads through this is a test of the order and not only of the values.
10254    ///
10255    /// # Safety
10256    /// `record` must be an `Around4`/`Around8` `GcRef`.
10257    unsafe fn around_fields(record: GcRef) -> Vec<(&'static str, Option<(i64, i64)>)> {
10258        // SAFETY: the caller guarantees a record built under one of the two
10259        // neighbourhood schemas, so every field is an `Option[(Int, Int)]`.
10260        unsafe {
10261            let rp = &*(record.payload::<u8>() as *const crate::records::RecordPayload);
10262            let schema = &*rp.schema;
10263            schema
10264                .fields
10265                .iter()
10266                .zip(&rp.items)
10267                .map(|(field, value)| {
10268                    let ep = &*(value.payload::<u8>() as *const crate::enums::EnumPayload);
10269                    let point = (i64::from(ep.tag) == crate::enums::OPTION_SOME_TAG)
10270                        .then(|| point_xy(ep.items[0]));
10271                    (field.name, point)
10272                })
10273                .collect()
10274        }
10275    }
10276
10277    /// `around4` names all four directions, and a direction that leaves the
10278    /// grid is `None` rather than absent.
10279    ///
10280    /// That is the whole difference from `neighbors4`, which answers a clipped
10281    /// `Vec` in which the corner case and the interior case are two lists of
10282    /// different lengths with no way to tell which entry was which direction.
10283    #[test]
10284    fn around4_answers_every_direction_and_the_missing_ones_are_none() {
10285        let mut rt = Runtime::new();
10286        let ctx = wired_ctx(&mut rt);
10287        let grid = nine_grid(&mut rt);
10288        let (middle, corner, far_corner) = unsafe {
10289            let m = around_fields(praxis_grid_around4(ctx, grid, alloc_point(ctx, 1, 1)));
10290            let c = around_fields(praxis_grid_around4(ctx, grid, alloc_point(ctx, 0, 0)));
10291            let f = around_fields(praxis_grid_around4(ctx, grid, alloc_point(ctx, 2, 2)));
10292            (m, c, f)
10293        };
10294        unsafe { drop_ctx(ctx) };
10295
10296        assert_eq!(
10297            middle,
10298            vec![
10299                ("up", Some((1, 0))),
10300                ("left", Some((0, 1))),
10301                ("right", Some((2, 1))),
10302                ("down", Some((1, 2))),
10303            ],
10304            "the plus in reading order, centre skipped"
10305        );
10306        assert_eq!(
10307            corner,
10308            vec![
10309                ("up", None),
10310                ("left", None),
10311                ("right", Some((1, 0))),
10312                ("down", Some((0, 1))),
10313            ]
10314        );
10315        assert_eq!(
10316            far_corner,
10317            vec![
10318                ("up", Some((2, 1))),
10319                ("left", Some((1, 2))),
10320                ("right", None),
10321                ("down", None),
10322            ]
10323        );
10324        assert_eq!(rt.fault(), FaultKind::None);
10325    }
10326
10327    /// `around8`'s slots are the 3×3 block in reading order, centre skipped —
10328    /// which is what makes a printed `Around8` look like the block it
10329    /// describes, and what a diagonal read depends on.
10330    #[test]
10331    fn around8_is_a_3x3_block_in_reading_order() {
10332        let mut rt = Runtime::new();
10333        let ctx = wired_ctx(&mut rt);
10334        let grid = nine_grid(&mut rt);
10335        let (middle, edge) = unsafe {
10336            let m = around_fields(praxis_grid_around8(ctx, grid, alloc_point(ctx, 1, 1)));
10337            let e = around_fields(praxis_grid_around8(ctx, grid, alloc_point(ctx, 1, 0)));
10338            (m, e)
10339        };
10340        unsafe { drop_ctx(ctx) };
10341
10342        assert_eq!(
10343            middle,
10344            vec![
10345                ("up_left", Some((0, 0))),
10346                ("up", Some((1, 0))),
10347                ("up_right", Some((2, 0))),
10348                ("left", Some((0, 1))),
10349                ("right", Some((2, 1))),
10350                ("down_left", Some((0, 2))),
10351                ("down", Some((1, 2))),
10352                ("down_right", Some((2, 2))),
10353            ]
10354        );
10355        // The top edge: the whole first row of the block is off the grid, and
10356        // says so three times rather than shortening the answer.
10357        assert_eq!(
10358            edge,
10359            vec![
10360                ("up_left", None),
10361                ("up", None),
10362                ("up_right", None),
10363                ("left", Some((0, 0))),
10364                ("right", Some((2, 0))),
10365                ("down_left", Some((0, 1))),
10366                ("down", Some((1, 1))),
10367                ("down_right", Some((2, 1))),
10368            ]
10369        );
10370        assert_eq!(rt.fault(), FaultKind::None);
10371    }
10372
10373    /// `neighbors_of_an_extreme_point_are_empty_rather_than_a_panic`'s case for
10374    /// the record and the counts: `grid_neighbor`'s `checked_add` is what keeps
10375    /// `(i64::MAX, i64::MAX)` from overflowing inside `extern "C"`, and every
10376    /// wrapper that steps a point has to be behind it.
10377    #[test]
10378    fn a_neighbourhood_of_an_extreme_point_is_all_absent_rather_than_a_panic() {
10379        let mut rt = Runtime::new();
10380        let ctx = wired_ctx(&mut rt);
10381        let grid = nine_grid(&mut rt);
10382        let one = rt.alloc_int(1);
10383        let observed = unsafe {
10384            let mut observed = Vec::new();
10385            for (x, y) in [
10386                (i64::MAX, i64::MAX),
10387                (i64::MIN, i64::MIN),
10388                (i64::MAX, 0),
10389                (0, i64::MIN),
10390            ] {
10391                let point = alloc_point(ctx, x, y);
10392                let four = around_fields(praxis_grid_around4(ctx, grid, point));
10393                let eight = around_fields(praxis_grid_around8(ctx, grid, point));
10394                observed.push((
10395                    four.iter().filter(|(_, p)| p.is_some()).count(),
10396                    eight.len(),
10397                    eight.iter().filter(|(_, p)| p.is_some()).count(),
10398                    int_payload(praxis_grid_count4(ctx, grid, point, one)),
10399                    int_payload(praxis_grid_count8(ctx, grid, point, one)),
10400                ));
10401            }
10402            observed
10403        };
10404        unsafe { drop_ctx(ctx) };
10405
10406        for row in &observed {
10407            assert_eq!(
10408                *row,
10409                (0, 8, 0, 0, 0),
10410                "an out-of-range point has no in-grid neighbours, and its \
10411                 record still has all eight fields: {observed:?}"
10412            );
10413        }
10414        assert_eq!(rt.fault(), FaultKind::None);
10415    }
10416
10417    /// `count4`/`count8` count cells, so a direction with no cell is not one of
10418    /// them — a corner counts over three neighbours, not eight.
10419    ///
10420    /// Equality is the descriptor's, which is `praxis_grid_find`'s path: the
10421    /// grid holds `1..=9`, so each count is the size of a known subset.
10422    #[test]
10423    fn a_neighbourhood_count_counts_only_the_cells_that_are_there() {
10424        let mut rt = Runtime::new();
10425        let ctx = wired_ctx(&mut rt);
10426        let grid = nine_grid(&mut rt);
10427        let counts = unsafe {
10428            let centre = alloc_point(ctx, 1, 1);
10429            let corner = alloc_point(ctx, 0, 0);
10430            let two = praxis_alloc_int(ctx, 2);
10431            let five = praxis_alloc_int(ctx, 5);
10432            let nine = praxis_alloc_int(ctx, 9);
10433            [
10434                // `2` is the cell above the centre: one orthogonal hit.
10435                int_payload(praxis_grid_count4(ctx, grid, centre, two)),
10436                // `9` is diagonal from the centre, so only the eight sees it.
10437                int_payload(praxis_grid_count4(ctx, grid, centre, nine)),
10438                int_payload(praxis_grid_count8(ctx, grid, centre, nine)),
10439                // The centre is never its own neighbour.
10440                int_payload(praxis_grid_count8(ctx, grid, centre, five)),
10441                // A corner's neighbourhood is two cells of four, three of eight.
10442                int_payload(praxis_grid_count4(ctx, grid, corner, five)),
10443                int_payload(praxis_grid_count8(ctx, grid, corner, five)),
10444            ]
10445        };
10446        unsafe { drop_ctx(ctx) };
10447
10448        assert_eq!(counts, [1, 0, 1, 0, 0, 1]);
10449        assert_eq!(rt.fault(), FaultKind::None);
10450    }
10451
10452    /// `map[key]` faults on an absent key where `.get` answers, and
10453    /// `praxis_counter_set` replaces a count where `praxis_counter_inc` only
10454    /// adds one.
10455    ///
10456    /// Here as well as in the JIT tests because §4.7's choice is the *runtime's*
10457    /// to make: the two map wrappers differ in one line, and a compiler that
10458    /// pointed both rows at `praxis_map_get` would still pass every type test.
10459    #[test]
10460    fn a_map_index_faults_where_get_answers_and_a_counter_set_replaces() {
10461        let mut rt = Runtime::new();
10462        let ctx = wired_ctx(&mut rt);
10463        let (present, absent_get) = unsafe {
10464            let map = praxis_map_new(ctx, &crate::scalars::INT as *const _);
10465            let key = praxis_alloc_int(ctx, 1);
10466            let val = praxis_alloc_int(ctx, 42);
10467            praxis_map_insert(ctx, map, key, val);
10468            let present = int_payload(praxis_map_index(ctx, map, key));
10469            assert_eq!(rt.fault(), FaultKind::None, "a present key does not fault");
10470            // `.get` on an absent key is `None` and no fault…
10471            let other = praxis_alloc_int(ctx, 2);
10472            let absent_get = praxis_map_get(ctx, map, other);
10473            assert_eq!(rt.fault(), FaultKind::None, "`.get` does not fault");
10474            // …and the subscript on the same key faults.
10475            praxis_map_index(ctx, map, other);
10476            (present, absent_get)
10477        };
10478        assert_eq!(present, 42);
10479        assert_eq!(
10480            absent_get.descriptor().id(),
10481            crate::enums::ENUM.id(),
10482            "`.get` answers with absence, and absence is an `Option` value"
10483        );
10484        assert_eq!(
10485            rt.fault(),
10486            FaultKind::IndexOutOfBounds,
10487            "§4.7: indexing a missing key faults"
10488        );
10489        unsafe { drop_ctx(ctx) };
10490
10491        // The counter half: `set` replaces, where `inc` adds one.
10492        let mut rt = Runtime::new();
10493        let ctx = wired_ctx(&mut rt);
10494        let (after_inc, after_set, len) = unsafe {
10495            let c = praxis_counter_new(ctx, &crate::scalars::INT as *const _);
10496            let key = praxis_alloc_int(ctx, 7);
10497            praxis_counter_inc(ctx, c, key);
10498            let after_inc = int_payload(praxis_counter_get(ctx, c, key));
10499            let five = praxis_alloc_int(ctx, 5);
10500            praxis_counter_set(ctx, c, key, five);
10501            let after_set = int_payload(praxis_counter_get(ctx, c, key));
10502            let len = int_payload(praxis_counter_len(ctx, c));
10503            (after_inc, after_set, len)
10504        };
10505        unsafe { drop_ctx(ctx) };
10506        assert_eq!(after_inc, 1);
10507        assert_eq!(after_set, 5, "a set replaces rather than adds");
10508        assert_eq!(len, 1, "and does not add a second entry for the same key");
10509        assert_eq!(rt.fault(), FaultKind::None);
10510    }
10511
10512    /// `Map.get` is statically value-typed, so an absent key cannot answer the
10513    /// Unit sentinel: a value whose static type is `V` and whose runtime
10514    /// descriptor is `Unit` is a type confusion the program cannot detect.
10515    ///
10516    /// The assertion names the variant, not merely `!= UNIT`: the answer is the
10517    /// `None` of the runtime's own `option_schema`, which is what makes it match
10518    /// a program's `None` arm.
10519    #[test]
10520    fn absent_map_get_does_not_return_an_untyped_unit_sentinel() {
10521        let mut rt = Runtime::new();
10522        let ctx = wired_ctx(&mut rt);
10523        let (missing, found);
10524        unsafe {
10525            let map = praxis_map_new(ctx, &crate::scalars::INT as *const _);
10526            let key = praxis_alloc_int(ctx, 1);
10527            missing = praxis_map_get(ctx, map, key);
10528            let value = praxis_alloc_int(ctx, 42);
10529            praxis_map_insert(ctx, map, key, value);
10530            found = praxis_map_get(ctx, map, key);
10531        }
10532
10533        assert_ne!(
10534            missing.descriptor().id(),
10535            crate::scalars::UNIT.id(),
10536            "Map.get is statically value-typed; absence needs Option or a checked fault, not Unit"
10537        );
10538        assert_eq!(missing.descriptor().id(), crate::enums::ENUM.id());
10539        assert_eq!(
10540            enum_tag_of(missing),
10541            crate::enums::OPTION_NONE_TAG as u32,
10542            "absence is `None`"
10543        );
10544        assert_eq!(enum_tag_of(found), crate::enums::OPTION_SOME_TAG as u32);
10545        // …and the `Some` carries the value, rather than merely not being Unit.
10546        let payload = unsafe { praxis_enum_payload(ctx, found, 0) };
10547        assert_eq!(unsafe { praxis_int_load(ctx, payload) }, 42);
10548        unsafe { drop_ctx(ctx) };
10549    }
10550
10551    /// A goal predicate that is `false` at every state, as a closure entry
10552    /// point. Rust-side `extern "C"` stands in for a JIT'd closure body: the
10553    /// oracle only cares that the pointer has the closure calling convention.
10554    ///
10555    /// # Safety
10556    /// Called only through [`ClosureOracle::call`], which upholds the ABI.
10557    unsafe extern "C" fn always_false(
10558        ctx: *mut RuntimeContext,
10559        _closure: GcRef,
10560        _state: GcRef,
10561    ) -> GcRef {
10562        // SAFETY: the oracle passes its own wired ctx.
10563        unsafe { bool_ref(ctx, false) }
10564    }
10565
10566    /// A neighbour function with no neighbours: the walk visits the start and
10567    /// stops, so the only thing that can decide the answer is the goal test.
10568    ///
10569    /// # Safety
10570    /// As [`always_false`].
10571    unsafe extern "C" fn no_neighbours(
10572        ctx: *mut RuntimeContext,
10573        _closure: GcRef,
10574        _state: GcRef,
10575    ) -> GcRef {
10576        // SAFETY: the oracle passes its own wired ctx.
10577        unsafe { praxis_vec_new(ctx, &crate::scalars::INT as *const _) }
10578    }
10579
10580    /// A `Bool` object laid out exactly the way [`Heap::alloc_raw`] lays one
10581    /// out — a `GcHeader` followed by its payload at
10582    /// `GcHeader::payload_offset_for(1)` — but in memory *this module* owns,
10583    /// and with the seven bytes after the one-byte payload set to `0xFF`.
10584    ///
10585    /// The heap cannot be asked for this shape. Under the page allocator
10586    /// (ADR-103) a `Bool` lands on the ladder's bottom rung, so its block is
10587    /// rounded up to an 8-byte boundary and the seven bytes after the one-byte
10588    /// payload are slack no object ever writes — and a fresh page is `mmap`ped
10589    /// zero, so an eight-byte read of a heap `Bool` answers *correctly* by
10590    /// accident. Owning the storage turns the padding from an accident into a
10591    /// fixture, which is the only way the read can be measured rather than
10592    /// sampled.
10593    ///
10594    /// The header carries a **freshly minted** `HeapId`, so the collector's
10595    /// provenance check (`Heap::mark`) skips this object instead of colouring
10596    /// it: the oracle roots every closure result, and a root the heap did not
10597    /// allocate is not the heap's to touch.
10598    #[repr(C)]
10599    struct DirtyPaddedBool {
10600        header: crate::gc::GcHeader,
10601        /// Byte 0 is the `BoolPayload`; bytes 1..8 are the neighbours a
10602        /// wrong-width or wrong-offset read would consume.
10603        payload: [u8; 8],
10604    }
10605
10606    /// A `false` whose seven following bytes are `0xFF`, leaked once per thread
10607    /// so a closure entry point can answer it. See [`DirtyPaddedBool`].
10608    fn dirty_padded_false() -> GcRef {
10609        thread_local! {
10610            static CELL: std::cell::Cell<*mut crate::gc::GcHeader> =
10611                const { std::cell::Cell::new(std::ptr::null_mut()) };
10612        }
10613        CELL.with(|cell| {
10614            if cell.get().is_null() {
10615                let object = Box::leak(Box::new(DirtyPaddedBool {
10616                    header: crate::gc::GcHeader::new(
10617                        &scalars::BOOL,
10618                        crate::gc::GcHeader::payload_offset_for(scalars::BOOL.align()) as u16,
10619                        crate::gc::HeapId::mint(),
10620                    ),
10621                    payload: [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
10622                }));
10623                cell.set(&mut object.header as *mut crate::gc::GcHeader);
10624            }
10625            // SAFETY: the pointer heads a leaked, correctly-laid-out `Bool`
10626            // object that lives for the rest of the process.
10627            unsafe { GcRef::from_raw(cell.get()) }
10628        })
10629    }
10630
10631    /// As [`always_false`], but answering the [`dirty_padded_false`] fixture.
10632    ///
10633    /// # Safety
10634    /// As [`always_false`].
10635    unsafe extern "C" fn always_dirty_false(
10636        _ctx: *mut RuntimeContext,
10637        _closure: GcRef,
10638        _state: GcRef,
10639    ) -> GcRef {
10640        dirty_padded_false()
10641    }
10642
10643    /// `ClosureOracle::is_goal` must read the closure's `Bool` answer at a
10644    /// `Bool`'s width: the payload is **one** byte (`BoolPayload = u8`, and
10645    /// `BOOL` is built from it), so an eight-byte read would take seven further
10646    /// bytes from past the object.
10647    ///
10648    /// Every goal predicate of every goal-directed helper goes
10649    /// through that read, so this walk is the whole class: the goal answers
10650    /// `false` at the only reachable state, and the answer must be `None`.
10651    ///
10652    /// The `false` it answers is [`dirty_padded_false`], whose payload byte is
10653    /// `0x00` and whose next seven bytes are `0xFF`. That is what makes this a
10654    /// gate rather than a walk: an eight-byte read answers
10655    /// `0xFFFF_FFFF_FFFF_FF00`, which is "goal reached at the start state" and
10656    /// therefore `Some(0)`; a read at *any* offset past byte zero answers
10657    /// `0xFF`, likewise `Some(0)`. Only one byte at offset zero answers `None`,
10658    /// so the test fails if either the width or the offset is wrong. Asking the
10659    /// heap for this shape does not work — see [`DirtyPaddedBool`].
10660    ///
10661    /// [`read_scalar`] is what makes both mistakes unspellable at the call
10662    /// site, and `int_payload`'s width check — an ordinary branch, so it holds
10663    /// in every profile — is what stops the next site from making them;
10664    /// `read_scalar_answers_none_for_a_foreign_descriptor` pins the reader
10665    /// itself.
10666    #[test]
10667    fn a_graph_goal_predicate_reads_a_bool_at_a_bool_s_width() {
10668        // The fixture is only a gate while its padding is dirty: state that
10669        // here, so a later edit that zeroes it fails loudly rather than
10670        // silently turning this back into the walk it replaced.
10671        let fixture = dirty_padded_false();
10672        assert!(std::ptr::eq(fixture.descriptor(), &scalars::BOOL));
10673        assert_eq!(
10674            unsafe { read_scalar(fixture, scalars::BOOL_PAYLOAD) },
10675            Some(0u8),
10676            "the fixture is `false` at a Bool's width"
10677        );
10678        assert_ne!(
10679            unsafe { *fixture.payload::<i64>() },
10680            0,
10681            "…and non-zero at an Int's, which is what the wrong read consumed"
10682        );
10683
10684        let mut rt = Runtime::new();
10685        let ctx = wired_ctx(&mut rt);
10686        let answer = unsafe {
10687            let goal = praxis_alloc_closure(ctx, always_dirty_false as *const u8, 0);
10688            let neighbours = praxis_alloc_closure(ctx, no_neighbours as *const u8, 0);
10689            let start = praxis_alloc_int(ctx, 0);
10690            praxis_bfs_distance(ctx, start, neighbours, goal)
10691        };
10692        assert!(!rt.has_pending_fault(), "fault: {:?}", rt.fault());
10693        assert_eq!(answer.descriptor().id(), crate::enums::ENUM.id());
10694        assert_eq!(
10695            enum_tag_of(answer),
10696            crate::enums::OPTION_NONE_TAG as u32,
10697            "the goal answered `false` at every state, so no distance was found"
10698        );
10699        unsafe { drop_ctx(ctx) };
10700    }
10701
10702    /// The same walk against the immortal `false` every real program's closure
10703    /// answers — a companion, not a gate: a wrong-width read passes it, because
10704    /// the allocator leaves a `Bool`'s slack bytes zero. It rules out a reader
10705    /// that only handles the fixture correctly.
10706    #[test]
10707    fn a_graph_goal_predicate_that_is_false_everywhere_finds_nothing() {
10708        let mut rt = Runtime::new();
10709        let ctx = wired_ctx(&mut rt);
10710        let answer = unsafe {
10711            let goal = praxis_alloc_closure(ctx, always_false as *const u8, 0);
10712            let neighbours = praxis_alloc_closure(ctx, no_neighbours as *const u8, 0);
10713            let start = praxis_alloc_int(ctx, 0);
10714            praxis_bfs_distance(ctx, start, neighbours, goal)
10715        };
10716        assert!(!rt.has_pending_fault(), "fault: {:?}", rt.fault());
10717        assert_eq!(answer.descriptor().id(), crate::enums::ENUM.id());
10718        assert_eq!(enum_tag_of(answer), crate::enums::OPTION_NONE_TAG as u32);
10719        unsafe { drop_ctx(ctx) };
10720    }
10721
10722    /// The reader itself, both directions. `read_scalar` is what makes a
10723    /// wrong-type or wrong-width read unspellable at a call site, so its own
10724    /// contract is pinned here: the right type reads at the right width, and a
10725    /// foreign type answers `None` instead of reinterpreting the bytes.
10726    #[test]
10727    fn read_scalar_answers_none_for_a_foreign_descriptor() {
10728        let mut rt = Runtime::new();
10729        let ctx = wired_ctx(&mut rt);
10730        unsafe {
10731            let t = bool_ref(ctx, true);
10732            let f = bool_ref(ctx, false);
10733            assert_eq!(read_scalar(t, crate::scalars::BOOL_PAYLOAD), Some(1u8));
10734            assert_eq!(read_scalar(f, crate::scalars::BOOL_PAYLOAD), Some(0u8));
10735            // An `Int` is not a `Bool`, and the answer is absence rather than
10736            // the first byte of the `i64`.
10737            let n = praxis_alloc_int(ctx, 1);
10738            assert_eq!(read_scalar(n, crate::scalars::BOOL_PAYLOAD), None);
10739            assert_eq!(read_scalar(n, crate::scalars::INT_PAYLOAD), Some(1i64));
10740            drop_ctx(ctx);
10741        }
10742    }
10743
10744    /// The variant tag of an enum value, read the way the runtime's own
10745    /// `enum_format` reads it.
10746    fn enum_tag_of(value: GcRef) -> u32 {
10747        // SAFETY: the caller passes an ENUM-descriptor object.
10748        unsafe { (*(value.payload::<u8>() as *const crate::enums::EnumPayload)).tag }
10749    }
10750
10751    /// The same rule under a *tuple* static type:
10752    /// `Grid.find` answers `(Int, Int)`, so "nothing matched" cannot be the Unit
10753    /// sentinel wearing that type.
10754    #[test]
10755    fn absent_grid_find_does_not_return_an_untyped_unit_sentinel() {
10756        let mut rt = Runtime::new();
10757        let ctx = wired_ctx(&mut rt);
10758        let (missing, found);
10759        unsafe {
10760            let cell = praxis_alloc_int(ctx, 1);
10761            let sought = praxis_alloc_int(ctx, 2);
10762            let grid = rt.alloc_grid(&crate::scalars::INT, vec![cell], 1);
10763            missing = praxis_grid_find(ctx, grid, sought);
10764            let present = praxis_alloc_int(ctx, 1);
10765            found = praxis_grid_find(ctx, grid, present);
10766        }
10767
10768        assert_ne!(
10769            missing.descriptor().id(),
10770            crate::scalars::UNIT.id(),
10771            "Grid.find is statically point-typed; absence needs Option or a checked fault, not Unit"
10772        );
10773        assert_eq!(missing.descriptor().id(), crate::enums::ENUM.id());
10774        assert_eq!(enum_tag_of(missing), crate::enums::OPTION_NONE_TAG as u32);
10775        // …and a hit is `Some((x, y))`, still a real point inside the option.
10776        assert_eq!(enum_tag_of(found), crate::enums::OPTION_SOME_TAG as u32);
10777        let point = unsafe { praxis_enum_payload(ctx, found, 0) };
10778        assert_eq!(point.descriptor().id(), crate::tuples::TUPLE.id());
10779        unsafe { drop_ctx(ctx) };
10780    }
10781
10782    // --- GC pacing (§12.4, ADR-019) ----------------------------------------
10783    //
10784    // `maybe_collect` is the load-bearing mechanism for the shadow-stack
10785    // spill: the alloc wrappers call it so collection happens automatically
10786    // inside JIT'd code.
10787
10788    #[test]
10789    fn maybe_collect_skips_below_threshold() {
10790        // A fresh heap with a single small allocation is well under the 64 KiB
10791        // threshold, so `maybe_collect` must report no collection ran.
10792        let mut rt = Runtime::new();
10793        let ctx = wired_ctx(&mut rt);
10794        // SAFETY: ctx wired.
10795        unsafe {
10796            let _ = praxis_alloc_int(ctx, 1);
10797            // Nothing live matters here; we only ask whether collection *ran*.
10798            let roots = crate::roots::RuntimeRoots::from_context(ctx);
10799            let ran = rt.heap().maybe_collect(&roots);
10800            assert!(
10801                !ran,
10802                "a single small Int must not trip the 64 KiB threshold"
10803            );
10804        }
10805        unsafe { drop_ctx(ctx) };
10806    }
10807
10808    #[test]
10809    fn maybe_collect_runs_under_pressure() {
10810        // Allocating past the 64 KiB threshold collects on its own, with no
10811        // generated frame on the stack and no hand-written `maybe_collect`
10812        // call. The helper asserts it happens within 10,000 allocations.
10813        let mut rt = Runtime::new();
10814        let ctx = wired_ctx(&mut rt);
10815        // SAFETY: ctx wired.
10816        unsafe {
10817            let _ = allocate_until_automatic_collection(&rt, ctx);
10818            // After a collection the pacing counter resets, so an immediate
10819            // call (no new allocations) does not collect again.
10820            let roots = crate::roots::RuntimeRoots::from_context(ctx);
10821            assert!(
10822                !rt.heap().maybe_collect(&roots),
10823                "counter must reset after a collection"
10824            );
10825        }
10826        unsafe { drop_ctx(ctx) };
10827    }
10828
10829    #[test]
10830    fn checked_int_add_is_an_automatic_gc_safepoint() {
10831        let mut rt = Runtime::new();
10832        let ctx = wired_ctx(&mut rt);
10833        let collected;
10834        unsafe {
10835            // The *sum* has to be uninterned, not just the operands: what is
10836            // watched below is whether `praxis_int_add`'s result enters the live
10837            // registry, and an interned sum never does (see `UNINTERNED`).
10838            let lhs = praxis_alloc_int(ctx, UNINTERNED);
10839            let rhs = praxis_alloc_int(ctx, 22);
10840            let mut frame = push_frame(ctx, SlotCount::new(2).unwrap());
10841            frame.set(0, lhs);
10842            frame.set(1, rhs);
10843
10844            let mut before = rt.heap().stats().live_count;
10845            let mut observed = false;
10846            for _ in 0..10_000 {
10847                let _ = praxis_int_add(ctx, lhs, rhs);
10848                let after = rt.heap().stats().live_count;
10849                if after < before.saturating_add(1) {
10850                    observed = true;
10851                    break;
10852                }
10853                before = after;
10854            }
10855            collected = observed;
10856            drop(frame);
10857        }
10858        unsafe { drop_ctx(ctx) };
10859
10860        assert!(
10861            collected,
10862            "every allocating ABI wrapper must participate in automatic GC pacing"
10863        );
10864    }
10865
10866    #[test]
10867    fn automatic_gc_roots_the_ambient_input_buffer() {
10868        let mut rt = Runtime::new();
10869        let ctx = wired_ctx(&mut rt);
10870        let live_after_collection;
10871        unsafe {
10872            (*ctx).input_source = rt.alloc_text("input that main has not read yet");
10873            let frame = push_frame(ctx, SlotCount::new(0).unwrap());
10874            live_after_collection = allocate_until_automatic_collection(&rt, ctx);
10875            drop(frame);
10876        }
10877        unsafe { drop_ctx(ctx) };
10878
10879        assert!(
10880            live_after_collection >= 2,
10881            "the ambient input Text and the allocation returned after collection must both remain live"
10882        );
10883    }
10884
10885    #[test]
10886    fn automatic_gc_roots_parse_failure_partial_values() {
10887        let mut rt = Runtime::new();
10888        let ctx = wired_ctx(&mut rt);
10889        // Uninterned: what this test watches is a *registered* object surviving
10890        // a collection that roots only through `ParseDetail.partial`, and an
10891        // interned `Int` is never registered, so it would survive whether the
10892        // root set included the slot or not (see `UNINTERNED`).
10893        let partial = rt.alloc_int(UNINTERNED);
10894        rt.parse_detail_mut()
10895            .consider(ParseFail::here(0, "test").with_partial(Some(partial)), b"");
10896        let live_after_collection;
10897        unsafe {
10898            let frame = push_frame(ctx, SlotCount::new(0).unwrap());
10899            live_after_collection = allocate_until_automatic_collection(&rt, ctx);
10900            drop(frame);
10901        }
10902        unsafe { drop_ctx(ctx) };
10903
10904        assert!(
10905            live_after_collection >= 2,
10906            "ParseDetail.partial is runtime-owned and must be included in every automatic root set"
10907        );
10908    }
10909
10910    #[test]
10911    fn automatic_gc_roots_runtime_owned_crash_snapshots() {
10912        let mut rt = Runtime::new();
10913        let ctx = wired_ctx(&mut rt);
10914        // Uninterned, for `automatic_gc_roots_parse_failure_partial_values`'s
10915        // reason: the observable is a registered object surviving because the
10916        // snapshot rooted it.
10917        let captured = rt.alloc_int(UNINTERNED);
10918        let local_name = b"value";
10919        let meta = crate::debug::DebugLocalMeta {
10920            callee_name: std::ptr::null(),
10921            callee_name_len: 0,
10922            source_name: local_name.as_ptr(),
10923            name_len: local_name.len() as u32,
10924            symbol_id: 1,
10925            descriptor: &crate::scalars::INT as *const _,
10926            type_id: 0,
10927            kind: crate::debug::LOCAL_KIND_USER,
10928            span_start: 0,
10929            span_end: 0,
10930            slot_kind: crate::debug::DebugSlotKind::Reference,
10931        };
10932        let metas = [meta];
10933        let func_name = b"main";
10934        let func_meta = crate::debug::FunctionDebugMeta {
10935            func_name: func_name.as_ptr(),
10936            func_name_len: func_name.len() as u32,
10937            local_count: 1,
10938            locals: metas.as_ptr(),
10939            span_start: 0,
10940            span_end: 0,
10941        };
10942        let live_after_collection;
10943        // SAFETY: `ctx` is wired to `rt`; `func_meta`/`metas` outlive the guard,
10944        // and the snapshot is taken while the frame is still claimed — the
10945        // ordering a generated fault epilogue has (ADR-033 decision 1).
10946        unsafe {
10947            let mut debug_frame = crate::debug::push_frame(ctx, &func_meta);
10948            debug_frame.set(0, captured);
10949            crate::crash_snapshot::praxis_snapshot_debug_chain(ctx);
10950            drop(debug_frame);
10951            assert!(rt.crash_snapshot().is_some());
10952
10953            let shadow_frame = push_frame(ctx, SlotCount::new(0).unwrap());
10954            live_after_collection = allocate_until_automatic_collection(&rt, ctx);
10955            drop(shadow_frame);
10956        }
10957        unsafe { drop_ctx(ctx) };
10958
10959        assert!(
10960            live_after_collection >= 2,
10961            "a runtime-owned CrashSnapshot must root its copied local values during automatic GC"
10962        );
10963    }
10964
10965    #[test]
10966    fn nested_allocating_helpers_root_intermediate_results() {
10967        // `Grid.positions` builds its result Vec in a Rust local and fills it by
10968        // calling `alloc_point`, which allocates three times per point. Every
10969        // one of those is a safepoint, and the shadow stack only sees what
10970        // generated code spilled — this is all native code, so the result Vec,
10971        // the points already in it and the tuple `alloc_point` is midway
10972        // through filling are rooted by the helper's own `NativeScope`.
10973        //
10974        // This calls the real helper rather than inlining a sketch of it, and
10975        // reading the points back afterwards is what proves nothing was
10976        // reclaimed.
10977        // The grid is wide enough that the helper's own point allocations cross
10978        // the pacing threshold partway through the loop — the collection has to
10979        // happen *inside* the helper for this to test anything.
10980        const W: usize = 40;
10981        let mut rt = Runtime::new();
10982        let ctx = wired_ctx(&mut rt);
10983        let mut coords: Vec<(i64, i64)> = Vec::new();
10984        let collections_inside_the_helper;
10985        unsafe {
10986            let cells: Vec<GcRef> = (0..(W * W) as i64).map(|i| rt.alloc_int(i)).collect();
10987            let grid = rt.alloc_grid(&scalars::INT, cells, W);
10988            let mut frame = push_frame(ctx, SlotCount::new(1).unwrap());
10989            frame.set(0, grid);
10990
10991            let before = rt.heap().stats().live_count;
10992            let positions = praxis_grid_positions(ctx, grid);
10993            // Every point survived, so the live count only grew; a collection
10994            // that reclaimed the half-built result would show up as a drop.
10995            collections_inside_the_helper = rt.heap().stats().live_count > before;
10996
10997            let items = &(*positions.payload::<VecPayload>()).items;
10998            assert_eq!(items.len(), W * W, "one position per cell");
10999            for point in items {
11000                let tuple = &*point.payload::<crate::tuples::TuplePayload>();
11001                coords.push((int_payload(tuple.items[0]), int_payload(tuple.items[1])));
11002            }
11003
11004            drop(frame);
11005        }
11006        unsafe { drop_ctx(ctx) };
11007
11008        assert!(collections_inside_the_helper);
11009        let expected: Vec<(i64, i64)> = (0..W * W)
11010            .map(|i| ((i % W) as i64, (i / W) as i64))
11011            .collect();
11012        assert_eq!(
11013            coords, expected,
11014            "every point and coordinate the helper allocated must survive the \
11015             collections the helper itself triggers"
11016        );
11017    }
11018
11019    // --- null-context safety (defensive guards, §10.4 spirit) --------------
11020
11021    #[test]
11022    fn check_fault_on_null_context_is_zero() {
11023        // A null/unwired context must report no fault rather than dereferencing
11024        // the null pointer (the guard at `praxis_check_fault`).
11025        // SAFETY: passing a null context is the exact case the guard handles.
11026        assert_eq!(unsafe { praxis_check_fault(std::ptr::null_mut()) }, 0);
11027    }
11028
11029    // The inline prologue deliberately does not null-check the context
11030    // (ADR-101): the check would cost every call in the language, and
11031    // `Runtime::context` is the only producer of a context generated code is
11032    // handed. `RuntimeContext::placeholder` carries the obligation in its doc.
11033
11034    // --- a null element type stays unknown ---------------------------------
11035
11036    /// A collection built with no static element type must not claim to hold
11037    /// `Int`s, and what it holds must render as what it is.
11038    ///
11039    /// The codegen passes a **null** descriptor for `var c = Counter()` — its
11040    /// contract above `collection_element_descriptor_for` says so, and says
11041    /// every `praxis_*_new` wrapper reads it that way. Replacing the null with
11042    /// `&INT` is not a default but a false claim, and the label is dispatched
11043    /// through: a `Text` key would hash and print as an `i64`, a `Float`
11044    /// element as the integer its bits spell.
11045    ///
11046    /// Both halves are asserted because either alone passes a wrong fix: the
11047    /// absent label is the representation, the rendering is the answer.
11048    #[test]
11049    fn a_collection_with_no_static_element_type_does_not_claim_int() {
11050        let mut rt = Runtime::new();
11051        let ctx = wired_ctx(&mut rt);
11052        // SAFETY: `ctx` is wired; every argument below is a wrapper's own.
11053        unsafe {
11054            // Exactly what the codegen passes when the element type is still an
11055            // inference variable.
11056            let null = std::ptr::null::<TypeDescriptor>();
11057            let counter = praxis_counter_new(ctx, null);
11058            let set = praxis_set_new(ctx, null);
11059            let map = praxis_map_new(ctx, null);
11060            let min_heap = praxis_min_heap_new(ctx, null);
11061            let max_heap = praxis_max_heap_new(ctx, null);
11062
11063            assert!(
11064                counter_payload(counter).key().is_none(),
11065                "a Counter with no static key type must not claim one"
11066            );
11067            assert!(set_payload(set).element().is_none());
11068            assert!(map_payload(map).key().is_none());
11069            assert!(min_heap_payload(min_heap).element().is_none());
11070            assert!(max_heap_payload(max_heap).element().is_none());
11071
11072            // …and the values come back out as themselves. A `Text` key through
11073            // a `Counter`, a `Float` through a `MinHeap`: neither is an `Int`,
11074            // and a guessed `Int` label would print both as one.
11075            let key = praxis_alloc_text(ctx, "ab".as_ptr(), 2);
11076            praxis_counter_inc(ctx, counter, key);
11077            let keys = praxis_counter_keys(ctx, counter);
11078            let mut rendered = String::new();
11079            keys.format(&mut rendered);
11080            assert_eq!(rendered, "[ab]", "a Counter's keys are its keys");
11081
11082            let half = praxis_alloc_float(ctx, 1.5f64.to_bits() as i64);
11083            praxis_min_heap_push(ctx, min_heap, half);
11084            let mut rendered = String::new();
11085            min_heap.format(&mut rendered);
11086            assert_eq!(rendered, "[1.5]", "a MinHeap prints the elements it holds");
11087
11088            let member = praxis_alloc_text(ctx, "zz".as_ptr(), 2);
11089            praxis_set_insert(ctx, set, member);
11090            let items = praxis_set_items(ctx, set);
11091            let mut rendered = String::new();
11092            items.format(&mut rendered);
11093            assert_eq!(rendered, "[zz]");
11094        }
11095        // SAFETY: the context was leaked by `wired_ctx` and is unused after this.
11096        unsafe { drop_ctx(ctx) };
11097    }
11098
11099    /// A `Map` does not claim its values are `Int`s.
11100    ///
11101    /// `praxis_map_new` takes one descriptor — the key's — because the `MapNew`
11102    /// row carries one type argument, so the value slot starts **null**.
11103    /// Writing `INT` there would be a claim rather than a default, and it would
11104    /// be the same word as "unknown", so the adoption that follows could not
11105    /// tell a `Map` that really holds `Int`s from one that had never been told
11106    /// anything — and a `Map[Text, Text]`'s value would be read as an `i64`.
11107    #[test]
11108    fn a_map_does_not_claim_its_values_are_ints() {
11109        let mut rt = Runtime::new();
11110        let ctx = wired_ctx(&mut rt);
11111        // SAFETY: `ctx` is wired; every argument below is a wrapper's own.
11112        unsafe {
11113            let empty = praxis_map_new(ctx, &crate::text::TEXT);
11114            assert!(
11115                map_payload(empty).value().is_none(),
11116                "an empty Map has been told nothing about its values"
11117            );
11118            // …so the `Vec` its `values()` answers is not labelled `Int`
11119            // either. A guessed `Int` there would make an empty
11120            // `Map[Text, Text]`'s values unequal to an empty `Vec[Text]`,
11121            // because `vec_equals` compares element labels.
11122            let none_yet = praxis_map_values(ctx, empty);
11123            assert!(vec_payload(none_yet).element().is_none());
11124
11125            // The first insert is what the map learns from.
11126            let k = praxis_alloc_text(ctx, "k".as_ptr(), 1);
11127            let v = praxis_alloc_text(ctx, "vv".as_ptr(), 2);
11128            praxis_map_insert(ctx, empty, k, v);
11129            assert!(
11130                std::ptr::eq(map_payload(empty).value().unwrap(), &crate::text::TEXT),
11131                "a Map learns its value type from the first value inserted"
11132            );
11133            let mut rendered = String::new();
11134            praxis_map_values(ctx, empty).format(&mut rendered);
11135            assert_eq!(rendered, "[vv]");
11136
11137            // A `Map` that really does hold `Int`s says so — an assertion only
11138            // a null "unknown" makes possible, since a hardcoded `INT` would be
11139            // the same word as "never been told".
11140            let ints = praxis_map_new(ctx, &crate::text::TEXT);
11141            let ik = praxis_alloc_text(ctx, "n".as_ptr(), 1);
11142            praxis_map_insert(ctx, ints, ik, praxis_alloc_int(ctx, 7));
11143            assert!(std::ptr::eq(
11144                map_payload(ints).value().unwrap(),
11145                &scalars::INT
11146            ));
11147        }
11148        // SAFETY: the context was leaked by `wired_ctx` and is unused after this.
11149        unsafe { drop_ctx(ctx) };
11150    }
11151
11152    /// An unlearned label is not a label, so it cannot make two empty
11153    /// collections unequal.
11154    ///
11155    /// `same_element` is not pointer identity, because a never-inserted `Map`'s
11156    /// `values()` carries no label and an equally-typed empty `Vec[Int]`
11157    /// carries `Int`. ADR-066 decision 5 is the rule: a null slot means the
11158    /// *value's own* descriptor answers, and a collection with no label has no
11159    /// values, so nothing is left to disagree. Reinstating a guessed descriptor
11160    /// is the fix this rejects.
11161    ///
11162    /// The last case is the limit: two collections that have each been told
11163    /// their element type must still disagree when the types differ.
11164    #[test]
11165    fn an_unlearned_element_label_does_not_make_two_empty_collections_unequal() {
11166        use crate::collections::same_element;
11167        let int: *const crate::descriptor::TypeDescriptor = &scalars::INT;
11168        let text: *const crate::descriptor::TypeDescriptor = &crate::text::TEXT;
11169        let unlearned: *const crate::descriptor::TypeDescriptor = std::ptr::null();
11170
11171        assert!(same_element(unlearned, int), "no label agrees with `Int`");
11172        assert!(same_element(int, unlearned), "and in the other order");
11173        assert!(same_element(unlearned, unlearned));
11174        assert!(same_element(int, int));
11175        // The rule this must not weaken: two *learned* labels that differ are
11176        // two different collections, empty or not.
11177        assert!(!same_element(int, text));
11178
11179        // End to end: a `Map` never inserted into has no value label, and its
11180        // `values()` is an empty `Vec` that is equal to an empty `Vec` however
11181        // that one was labelled.
11182        let mut rt = Runtime::new();
11183        let ctx = wired_ctx(&mut rt);
11184        // SAFETY: `ctx` is wired; every argument below is a wrapper's own.
11185        unsafe {
11186            let never_inserted = praxis_map_new(ctx, &crate::text::TEXT);
11187            let unlabelled = praxis_map_values(ctx, never_inserted);
11188            assert!(vec_payload(unlabelled).element().is_none());
11189
11190            let labelled_ints = praxis_vec_new(ctx, &scalars::INT as *const _);
11191            assert!(
11192                praxis_struct_eq(ctx, unlabelled, labelled_ints) != 0,
11193                "an empty Map's values are an empty Vec[Int]"
11194            );
11195            assert!(
11196                praxis_struct_eq(ctx, labelled_ints, unlabelled) != 0,
11197                "and equality is symmetric"
11198            );
11199
11200            // A non-empty collection is still not an empty one: the length
11201            // check behind `same_element` is what answers, and it must.
11202            praxis_vec_push(ctx, labelled_ints, praxis_alloc_int(ctx, 1));
11203            assert!(praxis_struct_eq(ctx, unlabelled, labelled_ints) == 0);
11204        }
11205        // SAFETY: the context was leaked by `wired_ctx` and is unused after this.
11206        unsafe { drop_ctx(ctx) };
11207    }
11208
11209    // --- the manifest's fault column is checked against the code -----------
11210
11211    /// Every function defined in this file, as `(name, body)`.
11212    ///
11213    /// Line-based on purpose: a definition is a line whose first tokens are one
11214    /// of Rust's `fn` spellings, and its body runs to the line where the brace
11215    /// depth opened by that definition returns to zero. Anything cleverer would
11216    /// be a Rust parser, and anything looser — matching `fn` anywhere — reads
11217    /// the word out of doc comments and glues unrelated bodies together.
11218    fn functions_in_this_file() -> Vec<(String, String)> {
11219        functions_in(include_str!("abi.rs"))
11220    }
11221
11222    /// The code of one line, with any `//` comment removed.
11223    ///
11224    /// The sweep reads what the **compiler** sees, not what a reader wrote
11225    /// beside it. The fixed point matches `set_fault(` as a plain substring, so
11226    /// a comment inside a wrapper naming the helper would otherwise classify
11227    /// that wrapper as faulting. A sweep a comment can fool is a sweep that gets
11228    /// edited around rather than satisfied, which is the failure mode the whole
11229    /// invariant exists to prevent.
11230    ///
11231    /// A `//` inside a string or `char` literal is **not** a comment — `"//"`
11232    /// and `'/'` both occur in this file — so the scan tracks which it is in.
11233    /// It is not a Rust lexer: a raw string's hashes and a block comment are
11234    /// not modelled, because neither appears in a function body here and a
11235    /// half-lexer that claimed to be one would be worse than a stated
11236    /// limitation. It errs toward keeping code, never toward dropping it.
11237    fn code_only(line: &str) -> &str {
11238        let bytes = line.as_bytes();
11239        let (mut in_str, mut in_char, mut escaped) = (false, false, false);
11240        let mut i = 0;
11241        while i < bytes.len() {
11242            let c = bytes[i];
11243            if escaped {
11244                escaped = false;
11245            } else if c == b'\\' && (in_str || in_char) {
11246                escaped = true;
11247            } else if in_str {
11248                in_str = c != b'"';
11249            } else if in_char {
11250                in_char = c != b'\'';
11251            } else if c == b'"' {
11252                in_str = true;
11253            } else if c == b'\'' {
11254                // A lifetime (`'a`) is not a `char` literal; a `char` literal's
11255                // closing quote is at most three bytes away (`'\\n'`, `'\\''`).
11256                in_char = bytes[i + 1..].iter().take(4).any(|b| *b == b'\'');
11257            } else if c == b'/' && bytes.get(i + 1) == Some(&b'/') {
11258                return &line[..i];
11259            }
11260            i += 1;
11261        }
11262        line
11263    }
11264
11265    /// Every function defined in `src`, as `(name, code)` — the code only, with
11266    /// comments stripped by [`code_only`]. Braces are counted on the code too,
11267    /// so a comment holding an unbalanced brace cannot end a body early or run
11268    /// two bodies together.
11269    fn functions_in(src: &str) -> Vec<(String, String)> {
11270        const PREFIXES: [&str; 6] = [
11271            "fn ",
11272            "pub fn ",
11273            "pub(crate) fn ",
11274            "unsafe fn ",
11275            "pub unsafe fn ",
11276            "pub unsafe extern \"C\" fn ",
11277        ];
11278        let mut out: Vec<(String, String)> = Vec::new();
11279        // (name, body so far, brace depth, whether the body has opened at all —
11280        // a multi-line signature spends several lines at depth zero before its
11281        // `{`, and closing there would give every such wrapper a one-line body).
11282        let mut open: Option<(String, String, i32, bool)> = None;
11283        for raw in src.lines() {
11284            let line = code_only(raw);
11285            let depth_change = |l: &str| -> i32 {
11286                l.chars().filter(|c| *c == '{').count() as i32
11287                    - l.chars().filter(|c| *c == '}').count() as i32
11288            };
11289            if let Some((name, body, depth, opened)) = open.as_mut() {
11290                body.push_str(line);
11291                body.push('\n');
11292                *depth += depth_change(line);
11293                *opened |= line.contains('{');
11294                if *opened && *depth <= 0 {
11295                    out.push((std::mem::take(name), std::mem::take(body)));
11296                    open = None;
11297                }
11298                continue;
11299            }
11300            let trimmed = line.trim_start();
11301            let Some(rest) = PREFIXES
11302                .iter()
11303                .find_map(|p| trimmed.strip_prefix(p).filter(|_| trimmed.starts_with(p)))
11304            else {
11305                continue;
11306            };
11307            let name: String = rest
11308                .chars()
11309                .take_while(|c| c.is_alphanumeric() || *c == '_')
11310                .collect();
11311            if name.is_empty() {
11312                continue;
11313            }
11314            let depth = depth_change(line);
11315            let opened = line.contains('{');
11316            // A one-line definition has already opened and closed its body.
11317            if opened && depth <= 0 {
11318                out.push((name, line.to_string()));
11319            } else {
11320                open = Some((name, format!("{line}\n"), depth, opened));
11321            }
11322        }
11323        out
11324    }
11325
11326    /// A wrapper that can raise a fault says so in the manifest.
11327    ///
11328    /// A row declared `Allocates` makes `RuntimeSymbol::faults()` answer
11329    /// `false`, so no `CheckFault` follows the call. If such a wrapper can reach
11330    /// `set_fault` the consequence is not cosmetic: the operation is silently
11331    /// abandoned, the wrapper answers the Unit sentinel, and the fault is
11332    /// observed by some later unrelated check — at the wrong source location,
11333    /// after the program has computed and possibly printed an answer.
11334    ///
11335    /// A hand-corrected row drifts again, so this is the invariant instead: the
11336    /// file is read at compile time, each `praxis_*` wrapper's body is walked,
11337    /// and any body that can reach `set_fault` — directly or through a helper
11338    /// defined here, transitively — must belong to a symbol whose row says
11339    /// `faults()`.
11340    ///
11341    /// One direction only. A row may declare a fault the reader cannot see: the
11342    /// arithmetic wrappers are generated by `checked_int_binop!` and have no
11343    /// textual definition at all, and a future wrapper may fault through a
11344    /// helper in another module. Those are false negatives — this test is weaker
11345    /// than the truth, never stricter — and the direction it does check is the
11346    /// one that produces wrong answers.
11347    /// The fixed point of "can reach `set_fault`" over `defs`: a function
11348    /// faults if it calls `set_fault`, or calls something that does.
11349    fn faulting_functions(defs: &[(String, String)]) -> std::collections::HashSet<String> {
11350        let mut faulting: std::collections::HashSet<String> =
11351            ["set_fault".to_string()].into_iter().collect();
11352        loop {
11353            let mut grew = false;
11354            for (name, body) in defs {
11355                if faulting.contains(name) {
11356                    continue;
11357                }
11358                if faulting.iter().any(|f| body.contains(&format!("{f}("))) {
11359                    faulting.insert(name.clone());
11360                    grew = true;
11361                }
11362            }
11363            if !grew {
11364                break;
11365            }
11366        }
11367        faulting
11368    }
11369
11370    #[test]
11371    fn a_wrapper_that_can_raise_a_fault_declares_that_it_faults() {
11372        let defs = functions_in_this_file();
11373        let faulting = faulting_functions(&defs);
11374
11375        let mut checked = 0usize;
11376        for (name, _) in &defs {
11377            let Some(sym) = praxis_stdlib::abi::RuntimeSymbol::from_name(name) else {
11378                continue;
11379            };
11380            if !faulting.contains(name) {
11381                continue;
11382            }
11383            assert!(
11384                sym.faults(),
11385                "{name} can reach `set_fault`, but its manifest row says it \
11386                 cannot fault — so no `CheckFault` follows the call and the \
11387                 fault is observed somewhere else entirely"
11388            );
11389            checked += 1;
11390        }
11391        // If the scan stopped finding wrappers, the assertion above would be
11392        // vacuous and this test would pass while saying nothing.
11393        assert!(
11394            checked >= 20,
11395            "expected the fault-raising wrappers to be found; saw {checked}"
11396        );
11397        // These three reach `set_fault` only through a helper, so they are the
11398        // shape the transitive scan has to be able to see.
11399        for name in [
11400            "praxis_vec_push",
11401            "praxis_deque_push_front",
11402            "praxis_deque_push_back",
11403        ] {
11404            assert!(
11405                faulting.contains(name),
11406                "{name} reaches `set_fault` through `adopt_or_reject`; a scan \
11407                 that cannot see that cannot hold the invariant"
11408            );
11409        }
11410        // **`InvalidText` lives at exactly one site (ADR-111).**
11411        // `praxis_alloc_text` trusts its caller; the one caller holding raw host
11412        // bytes raises the fault instead. Asserting the destination is visible
11413        // to the scan is what distinguishes a relocated fault from a deleted
11414        // one — without it, deleting the validation outright would leave this
11415        // test just as green.
11416        assert!(
11417            faulting.contains("praxis_get_input"),
11418            "`praxis_get_input` validates the host's input and raises \
11419             `InvalidText` itself (ADR-111); a scan that cannot see that cannot \
11420             tell a relocated fault from a deleted one"
11421        );
11422        assert!(
11423            !faulting.contains("praxis_alloc_text"),
11424            "`praxis_alloc_text` reaches `set_fault` again. Its row is \
11425             `Effect::Allocates`, so nothing observes the fault — a violated \
11426             UTF-8 precondition aborts through `abi_guard!` instead (ADR-111)"
11427        );
11428    }
11429    /// **The sweep above reads code, not prose.** Its classification is a
11430    /// substring match, so without [`code_only`] a *comment* inside a wrapper
11431    /// naming the helper would classify that wrapper as faulting and fail the
11432    /// invariant for a wrapper that cannot fault. A sweep a comment can fool is
11433    /// a sweep that gets edited around rather than satisfied.
11434    ///
11435    /// Synthetic source, because the real file must not contain the shape: the
11436    /// point is that it may, safely.
11437    #[test]
11438    fn the_manifest_sweep_reads_code_and_not_comments() {
11439        let src = r#"
11440pub unsafe extern "C" fn praxis_pretend_pure(ctx: *mut RuntimeContext) -> GcRef {
11441    // It used to call set_fault(ctx, RaisedFault::TYPE_MISMATCH) here, and a
11442    // later edit removed the only path that could. Prose, not code. }
11443    let sep = "//";
11444    let slash = '/';
11445    let _ = (sep, slash);
11446    unit_sentinel(ctx)
11447}
11448
11449pub unsafe extern "C" fn praxis_pretend_faulting(ctx: *mut RuntimeContext) -> GcRef {
11450    set_fault(ctx, RaisedFault::TYPE_MISMATCH);
11451    unit_sentinel(ctx)
11452}
11453"#;
11454        let defs = functions_in(src);
11455        let names: Vec<&str> = defs.iter().map(|(n, _)| n.as_str()).collect();
11456        assert_eq!(names, ["praxis_pretend_pure", "praxis_pretend_faulting"]);
11457        // The unbalanced `}` in that comment must not close the body early:
11458        // braces are counted on the code too, so the whole function is read.
11459        assert!(
11460            defs[0].1.contains("unit_sentinel(ctx)"),
11461            "a brace inside a comment ended the body early: {:?}",
11462            defs[0].1
11463        );
11464
11465        let faulting = faulting_functions(&defs);
11466        assert!(
11467            !faulting.contains("praxis_pretend_pure"),
11468            "a comment naming `set_fault` is not a call to it"
11469        );
11470        assert!(
11471            faulting.contains("praxis_pretend_faulting"),
11472            "and a real call still is — stripping comments must not blind the sweep"
11473        );
11474    }
11475
11476    /// [`code_only`]'s own contract, both directions.
11477    #[test]
11478    fn code_only_keeps_a_slash_inside_a_literal() {
11479        assert_eq!(code_only("let x = 1; // two"), "let x = 1; ");
11480        assert_eq!(code_only(r#"let s = "a//b";"#), r#"let s = "a//b";"#);
11481        assert_eq!(code_only(r"let c = '/'; // gone"), r"let c = '/'; ");
11482        assert_eq!(
11483            code_only(r#"let e = "\"//"; // gone"#),
11484            r#"let e = "\"//"; "#
11485        );
11486        assert_eq!(code_only("    /// a doc comment"), "    ");
11487        assert_eq!(code_only("no comment here"), "no comment here");
11488        // A lifetime is not a `char` literal, so the comment after it is still
11489        // a comment.
11490        assert_eq!(
11491            code_only("fn f<'a>(x: &'a str) {} // gone"),
11492            "fn f<'a>(x: &'a str) {} "
11493        );
11494    }
11495
11496    // --- the panic backstop ------------------------------------------------
11497
11498    /// Every `#[unsafe(no_mangle)] extern "C"` function in this crate has its body
11499    /// inside `abi_guard!`.
11500    ///
11501    /// Per-wrapper totality is the contract: a wrapper validates its arguments
11502    /// and reports a bad one as a fault, so the guard never fires. This is the
11503    /// proof that the contract cannot be violated *silently* — a panic
11504    /// unwinding out of `extern "C"` into Cranelift frames is undefined
11505    /// behaviour, and the failure mode of forgetting is a corrupted process at
11506    /// some unrelated later point rather than a message.
11507    ///
11508    /// Read as source text on purpose. The property is "every entry point is
11509    /// wrapped", which is a property of the *set* of entry points; a test that
11510    /// called them one by one would be a test of the ones somebody remembered.
11511    ///
11512    /// **The file set is discovered, not declared.** A hand-written list of
11513    /// files would make the guarantee "every entry point in a file somebody
11514    /// remembered to list", and the `wrappers > 100` floor would still pass on
11515    /// the files that were listed. So the walk covers **every crate's `src/`**,
11516    /// not only this one: nothing says a future `#[unsafe(no_mangle)]` has to live
11517    /// here.
11518    #[test]
11519    fn every_no_mangle_wrapper_is_behind_the_panic_guard() {
11520        /// Every `.rs` file under `dir`, recursively, in a stable order.
11521        fn rust_sources(dir: &std::path::Path, out: &mut Vec<(String, String)>) {
11522            let entries =
11523                std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {}: {e}", dir.display()));
11524            let mut entries: Vec<_> = entries.map(|e| e.expect("dir entry").path()).collect();
11525            entries.sort();
11526            for path in entries {
11527                let name = path.file_name().unwrap_or_default().to_string_lossy();
11528                if name == "target" || name.starts_with('.') {
11529                    continue;
11530                }
11531                if path.is_dir() {
11532                    rust_sources(&path, out);
11533                } else if path.extension().is_some_and(|e| e == "rs") {
11534                    let text = std::fs::read_to_string(&path)
11535                        .unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
11536                    out.push((path.display().to_string(), text));
11537                }
11538            }
11539        }
11540
11541        // `crates/`, from this crate's manifest directory.
11542        let mut crates_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
11543        crates_dir.pop();
11544        let mut sources: Vec<(String, String)> = Vec::new();
11545        rust_sources(&crates_dir, &mut sources);
11546        // A guard against the walk silently covering nothing — a wrong root
11547        // would otherwise pass by finding no wrapper attribute at all.
11548        assert!(
11549            sources.len() > 50,
11550            "the walk of {} found only {} Rust files, so it is not reading the workspace",
11551            crates_dir.display(),
11552            sources.len()
11553        );
11554
11555        let mut wrappers = 0usize;
11556        let mut unguarded: Vec<String> = Vec::new();
11557        for (file, source) in &sources {
11558            let lines: Vec<&str> = source.lines().collect();
11559            for (n, line) in lines.iter().enumerate() {
11560                // Both spellings: `#[unsafe(no_mangle)]` is the edition-2024
11561                // form and the only one this crate uses, but matching the bare
11562                // attribute too keeps the scan from going blind if a wrapper is
11563                // ever pasted in from older code.
11564                if !matches!(line.trim(), "#[unsafe(no_mangle)]" | "#[no_mangle]") {
11565                    continue;
11566                }
11567                wrappers += 1;
11568                // Walk to the line that opens the body, then require the very
11569                // next non-blank line to be the guard.
11570                let mut k = n + 1;
11571                while k < lines.len() && !lines[k].trim_end().ends_with('{') {
11572                    k += 1;
11573                }
11574                // Inclusive of `k`: a one-line signature puts `fn name(` on the
11575                // very line that opens the body, so an exclusive range would
11576                // report `<unnamed>` in the message that tells someone which
11577                // wrapper they forgot.
11578                let name = lines[n..=k.min(lines.len() - 1)]
11579                    .iter()
11580                    .find_map(|l| l.split("fn ").nth(1))
11581                    .and_then(|l| l.split('(').next())
11582                    .unwrap_or("<unnamed>")
11583                    .trim()
11584                    .to_string();
11585                let opens_guard = lines
11586                    .get(k + 1)
11587                    .map(|l| l.trim_start().starts_with("abi_guard!("))
11588                    .unwrap_or(false);
11589                if !opens_guard {
11590                    unguarded.push(format!("{file}:{} {name}", n + 1));
11591                }
11592            }
11593        }
11594
11595        assert!(
11596            wrappers > 100,
11597            "the scan found only {wrappers} wrappers, so it is not reading the ABI surface"
11598        );
11599        assert!(
11600            unguarded.is_empty(),
11601            "these `extern \"C\"` entry points can let a panic unwind into generated frames: {unguarded:#?}"
11602        );
11603    }
11604
11605    /// The guard's own behaviour: a panic inside a wrapper becomes a fault with
11606    /// a message naming the wrapper, and the wrapper returns its defined dummy.
11607    ///
11608    /// `praxis_dbg` is the one wrapper that can be made to panic on demand
11609    /// without an invalid argument — it formats its value, and a `Text` whose
11610    /// payload is a live `Unit` is a descriptor/payload pairing no validation
11611    /// catches. Every *reachable* panic is a bug to fix in the wrapper; this
11612    /// test is about what happens when one is missed.
11613    #[test]
11614    fn a_panic_inside_a_wrapper_becomes_a_fault_and_a_defined_dummy() {
11615        let value = {
11616            abi_guard!(
11617                "praxis_test_panics",
11618                std::ptr::null_mut::<RuntimeContext>(),
11619                {
11620                    #[allow(unreachable_code)]
11621                    {
11622                        if std::hint::black_box(false) {
11623                            panic!("this is the guard under test");
11624                        }
11625                        7i64
11626                    }
11627                }
11628            )
11629        };
11630        assert_eq!(value, 7, "the guard is transparent when nothing panics");
11631
11632        // A **faulting** wrapper: its call sites can carry a `CheckFault`, so
11633        // generated code observes the fault before it looks at the value, and
11634        // the defined dummy is the right answer. The name has to be a real
11635        // manifest symbol — see `panic_fault_is_observable`, which is what
11636        // decides whether the dummy is returned at all.
11637        let mut runtime = crate::Runtime::new();
11638        let mut ctx = runtime.context();
11639        let previous = std::panic::take_hook();
11640        std::panic::set_hook(Box::new(|_| {}));
11641        let dummy: GcRef = abi_guard!("praxis_run_parser", &mut ctx as *mut RuntimeContext, {
11642            panic!("a wrapper that forgot to be total");
11643        });
11644        std::panic::set_hook(previous);
11645
11646        assert_eq!(
11647            runtime.fault(),
11648            crate::FaultKind::Panic,
11649            "an escaped panic is a fault, not an unwind into generated code"
11650        );
11651        assert!(
11652            runtime
11653                .fault_message()
11654                .is_some_and(|m| m.contains("praxis_run_parser")),
11655            "the fault names the wrapper it escaped, which a bare kind could not"
11656        );
11657        assert_eq!(
11658            dummy.descriptor().id(),
11659            crate::scalars::UNIT.id(),
11660            "the dummy is the Unit sentinel §10.4 already specifies"
11661        );
11662    }
11663
11664    /// **The dummy is only returned where the fault will be seen.**
11665    ///
11666    /// Generated code tests the fault slot only where MIR emitted a
11667    /// `CheckFault`, and **`praxis_mir::verify` is what makes that true of a
11668    /// non-faulting wrapper** — its `RedundantFaultCheck` rule rejects a check
11669    /// after an instruction that cannot fault (ADR-088). So for the wrappers the
11670    /// manifest declares non-faulting there is no check, and returning
11671    /// `unit_sentinel` there would hand a `Unit` into a slot generated code
11672    /// believes holds a Record, a Tuple or a closure — a descriptor/payload
11673    /// confusion introduced by the backstop meant to prevent worse. Those abort
11674    /// instead.
11675    ///
11676    /// This test states the classification. The abort itself cannot be asserted
11677    /// in-process, which is exactly why the rule has to be a total function of
11678    /// the manifest rather than a case-by-case judgement.
11679    #[test]
11680    fn a_panic_dummy_is_only_returned_where_a_fault_check_can_follow() {
11681        use praxis_stdlib::abi::RuntimeSymbol;
11682
11683        let mut pure = 0usize;
11684        let mut faulting = 0usize;
11685        for symbol in RuntimeSymbol::ALL.iter().copied() {
11686            let observable = panic_fault_is_observable(symbol.name());
11687            assert_eq!(
11688                observable,
11689                symbol.faults(),
11690                "`{}` is declared {:?}; the panic dummy must be returned iff a \
11691                 fault check can follow it",
11692                symbol.name(),
11693                symbol.sig().effect
11694            );
11695            if symbol.faults() {
11696                faulting += 1;
11697            } else {
11698                pure += 1;
11699            }
11700        }
11701        assert!(
11702            pure > 0 && faulting > 0,
11703            "the manifest must contain both classes for this rule to mean anything \
11704             ({pure} non-faulting, {faulting} faulting)"
11705        );
11706
11707        // Every `#[unsafe(no_mangle)]` wrapper in this crate is manifested, so the
11708        // unobservable case left is a name that is not a wrapper at all.
11709        assert!(
11710            !panic_fault_is_observable("praxis_not_a_wrapper_at_all"),
11711            "an unknown name is never treated as observable"
11712        );
11713    }
11714
11715    // ---- Process input (§7.10) ----
11716
11717    /// A reader that answers nothing. A `fn` and not a closure because
11718    /// [`crate::input::InputReader`] is a plain `fn` pointer.
11719    fn no_bytes() -> Vec<u8> {
11720        Vec::new()
11721    }
11722
11723    /// Read the bytes behind a `Text` `GcRef`.
11724    ///
11725    /// # Safety
11726    /// `r` must be a live `Text`.
11727    unsafe fn text_bytes_of(r: GcRef) -> &'static [u8] {
11728        // SAFETY: the caller guarantees `r` is a live Text, so its payload is a
11729        // validly-linked `TextPayload`.
11730        unsafe { crate::text::text_bytes(r.payload::<crate::text::TextPayload>() as *const _) }
11731    }
11732
11733    /// A reader that answers zero bytes has given *empty input*, not no input,
11734    /// so its answer is installed as `input_source` whatever its length.
11735    ///
11736    /// Allocating the buffer only `if !bytes.is_empty()` would leave empty
11737    /// standard input at the immortal Unit, so `praxis_run_parser`'s §6.3
11738    /// descriptor guard would fault *before* the parser ran — a `ParseFailed`
11739    /// with no input span, no `expected` and no `actual`, which is none of the
11740    /// six fields §7.11 says a mismatch carries. A fault raised before any
11741    /// buffer exists cannot carry them; the buffer is what makes the diagnostic
11742    /// possible at all (ADR-087).
11743    #[test]
11744    fn a_reader_that_answers_zero_bytes_installs_an_empty_text() {
11745        let mut rt = Runtime::new();
11746        let ctx = wired_ctx(&mut rt);
11747        crate::input::install_input_reader(no_bytes);
11748        // SAFETY: ctx is wired to rt and live for this call.
11749        let source = unsafe { praxis_get_input(ctx) };
11750        assert_eq!(
11751            source.descriptor().id(),
11752            crate::text::TEXT.id(),
11753            "a zero-byte answer is still an input buffer"
11754        );
11755        // SAFETY: the assertion above proves `source` is a Text.
11756        assert!(
11757            unsafe { text_bytes_of(source) }.is_empty(),
11758            "and the buffer holds exactly what the reader answered"
11759        );
11760        // SAFETY: ctx is wired to rt and live for this call.
11761        assert_eq!(
11762            unsafe { (*ctx).input_source }.as_ptr(),
11763            source.as_ptr(),
11764            "the buffer is installed, not merely returned — §7.10's later \
11765             `read`s reuse it"
11766        );
11767        // SAFETY: ctx came from `wired_ctx` and is not used again.
11768        unsafe { drop_ctx(ctx) };
11769    }
11770
11771    /// **A mutation companion, not a gate.**
11772    ///
11773    /// The cheapest wrong repair is to allocate a `Text` unconditionally in
11774    /// `praxis_get_input`, which passes the gate above and quietly deletes the
11775    /// one state the §6.3 descriptor guard exists for. A host that installs
11776    /// **neither** a buffer nor a reader — every JIT test, every embedder — must
11777    /// still reach `praxis_run_parser` with the Unit source, because
11778    /// `adv_read_against_non_text_input_faults_cleanly` in the codegen crate's
11779    /// `jit.rs` is the probe that a `read` there faults instead of
11780    /// reinterpreting Unit's payload as a `TextPayload` and segfaulting.
11781    ///
11782    /// That is the boundary ADR-087 draws: a reader answering zero bytes is a
11783    /// program state (empty input); no reader at all is a host state (no input),
11784    /// and no `praxis run` reaches it.
11785    #[test]
11786    fn a_host_that_installs_no_reader_keeps_the_unit_source() {
11787        let mut rt = Runtime::new();
11788        let ctx = wired_ctx(&mut rt);
11789        crate::input::clear_input_reader();
11790        // SAFETY: ctx is wired to rt and live for these calls.
11791        let before = unsafe { (*ctx).input_source };
11792        // SAFETY: as above.
11793        let source = unsafe { praxis_get_input(ctx) };
11794        assert_eq!(
11795            source.as_ptr(),
11796            before.as_ptr(),
11797            "with no reader installed there is nothing to call and nothing to \
11798             install; `input_source` is answered untouched"
11799        );
11800        assert_ne!(
11801            source.descriptor().id(),
11802            crate::text::TEXT.id(),
11803            "and it is still the Unit the §6.3 guard is the net under"
11804        );
11805        // SAFETY: ctx came from `wired_ctx` and is not used again.
11806        unsafe { drop_ctx(ctx) };
11807    }
11808
11809    /// **The guard must not report a parse that never ran.**
11810    ///
11811    /// `praxis_run_parser` returns early for a non-Text `input` (§6.3) —
11812    /// `run_plan` would otherwise reinterpret the payload as a `TextPayload` —
11813    /// and that early return must still perform the `clear_parse_detail` every
11814    /// other entry into the parser performs. Without it, a host reaching the
11815    /// guard after an earlier mismatch reports *that* mismatch's offset and
11816    /// expectation for a parse that never started.
11817    ///
11818    /// Not reachable end to end: a fault is terminal within one `praxis run`,
11819    /// so the shape is an embedder calling `main` twice (or the crash debugger's
11820    /// `restart`). This test pins it at the level where the hazard exists.
11821    ///
11822    /// Fabricating a `ParseFail` here instead would be worse than clearing: with
11823    /// no buffer there is no input span, and an invented `expected` would make
11824    /// an embedder's host bug read as a parse failure at an offset that does not
11825    /// exist.
11826    #[test]
11827    fn the_non_text_guard_does_not_report_a_previous_parses_failure() {
11828        let mut rt = Runtime::new();
11829        let ctx = wired_ctx(&mut rt);
11830        rt.parse_detail_mut()
11831            .consider(ParseFail::here(7, "int"), b"0123456789");
11832        assert!(rt.parse_detail().is_set(), "the seed is in place");
11833        // SAFETY: ctx is wired to rt; the plan index is never read, because the
11834        // descriptor guard returns before it.
11835        unsafe {
11836            let plan = praxis_alloc_int(ctx, 1);
11837            let unit = (*ctx).unit_ref;
11838            let result = praxis_run_parser(ctx, plan, unit);
11839            assert_eq!(
11840                result.descriptor().id(),
11841                crate::scalars::UNIT.id(),
11842                "the guard answers the sentinel"
11843            );
11844        }
11845        assert!(rt.has_pending_fault());
11846        assert_eq!(rt.fault(), FaultKind::ParseFailed);
11847        assert!(
11848            !rt.parse_detail().is_set(),
11849            "the §6.3 guard runs no parse, so it has no detail to report — and \
11850             it must not report the previous parse's"
11851        );
11852        // SAFETY: ctx came from `wired_ctx` and is not used again.
11853        unsafe { drop_ctx(ctx) };
11854    }
11855}
11856
11857#[cfg(test)]
11858mod growth_charging_tests {
11859    //! **Every wrapper that can grow a collection's buffer charges the pacer**
11860    //! (ADR-121). See [`super::charge_growth`] for why.
11861    //!
11862    //! The values pushed are all inside `small_int`'s interned range, and that
11863    //! is the whole design of these tests rather than a convenience: an interned
11864    //! `Int` is an immortal the allocator never charges for, so the *only* thing
11865    //! that can move `bytes_since_collect` here is the spine. Push
11866    //! `UNINTERNED + i` instead and every one of these passes whether or not the
11867    //! growth is charged, because the elements would be paying for it.
11868
11869    use super::tests::{drop_ctx, wired_ctx};
11870    use super::*;
11871    use crate::Runtime;
11872
11873    /// Reset the counter, run `body`, and answer what it charged.
11874    fn charged_by(rt: &Runtime, body: impl FnOnce()) -> usize {
11875        // A collection zeroes the counter, so take a reading either side and
11876        // require the run not to have collected; the pushes below are far too
11877        // few to reach any threshold.
11878        let before = rt.heap().bytes_since_collect();
11879        body();
11880        rt.heap().bytes_since_collect().saturating_sub(before)
11881    }
11882
11883    /// Enough pushes that amortized doubling must have reallocated at least
11884    /// once, whatever the initial capacity is.
11885    const PUSHES: i64 = 256;
11886
11887    macro_rules! charges_its_spine {
11888        ($name:ident, $make:expr_2021, $push:expr_2021) => {
11889            #[test]
11890            fn $name() {
11891                let mut rt = Runtime::new();
11892                let ctx = wired_ctx(&mut rt);
11893                // SAFETY: `ctx` is wired to `rt` for the whole test.
11894                unsafe {
11895                    let subject = $make(ctx);
11896                    let charged = charged_by(&rt, || {
11897                        for i in 0..PUSHES {
11898                            $push(ctx, subject, i);
11899                        }
11900                    });
11901                    assert!(
11902                        charged > 0,
11903                        "growing this collection charged the pacer nothing, so a \
11904                         program whose memory is this buffer would never collect \
11905                         (ADR-121); every value pushed is an interned immortal, so \
11906                         the spine is the only thing that could have charged"
11907                    );
11908                    drop_ctx(ctx);
11909                }
11910            }
11911        };
11912    }
11913
11914    charges_its_spine!(
11915        vec_push_charges_its_spine,
11916        |c| praxis_vec_new(c, &crate::scalars::INT),
11917        |c, s, i| { praxis_vec_push(c, s, praxis_alloc_int(c, i)) }
11918    );
11919    charges_its_spine!(
11920        deque_push_back_charges_its_spine,
11921        |c| praxis_deque_new(c, &crate::scalars::INT),
11922        |c, s, i| praxis_deque_push_back(c, s, praxis_alloc_int(c, i))
11923    );
11924    charges_its_spine!(
11925        deque_push_front_charges_its_spine,
11926        |c| praxis_deque_new(c, &crate::scalars::INT),
11927        |c, s, i| praxis_deque_push_front(c, s, praxis_alloc_int(c, i))
11928    );
11929    charges_its_spine!(
11930        map_insert_charges_its_spine,
11931        |c| praxis_map_new(c, &crate::scalars::INT),
11932        |c, s, i| praxis_map_insert(c, s, praxis_alloc_int(c, i), praxis_alloc_int(c, i))
11933    );
11934    charges_its_spine!(
11935        set_insert_charges_its_spine,
11936        |c| praxis_set_new(c, &crate::scalars::INT),
11937        |c, s, i| praxis_set_insert(c, s, praxis_alloc_int(c, i))
11938    );
11939    charges_its_spine!(
11940        counter_set_charges_its_spine,
11941        |c| praxis_counter_new(c, &crate::scalars::INT),
11942        |c, s, i| praxis_counter_set(c, s, praxis_alloc_int(c, i), praxis_alloc_int(c, i))
11943    );
11944    charges_its_spine!(
11945        bitset_insert_charges_its_spine,
11946        |c| praxis_bitset_new(c),
11947        |c, s, i| praxis_bitset_insert(c, s, praxis_alloc_int(c, i))
11948    );
11949    charges_its_spine!(
11950        max_heap_push_charges_its_spine,
11951        |c| praxis_max_heap_new(c, &crate::scalars::INT),
11952        |c, s, i| praxis_max_heap_push(c, s, praxis_alloc_int(c, i))
11953    );
11954    charges_its_spine!(
11955        min_heap_push_charges_its_spine,
11956        |c| praxis_min_heap_new(c, &crate::scalars::INT),
11957        |c, s, i| praxis_min_heap_push(c, s, praxis_alloc_int(c, i))
11958    );
11959}