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}