praxis_runtime/abi.rs
1//! Runtime ABI versioning (§11.6) and the `praxis_*` extern wrappers (§10.2,
2//! §10.4, §11.1) that JIT-generated code calls.
3//!
4//! The runtime ABI is private to one Praxis executable build: there is no
5//! cross-version compatibility promise, and no externally linkable surface for
6//! user programs. Even so, the compiler and runtime are built from the same
7//! workspace, and a single constant — checked at startup — catches accidental
8//! internal drift between the code that *generates* calls and the code that
9//! *implements* them.
10//!
11//! The `praxis_*` wrappers are the **only** functions generated code may call.
12//! Every argument and return value that represents a language value is a
13//! [`GcRef`]; scalars (`i64` payloads) cross the ABI only as transient values
14//! tied to a single non-safepointed computation (§10.3). Per §10.4, **no wrapper
15//! ever lets a Rust panic unwind across the ABI**: on overflow or division by
16//! zero the wrapper writes the fault into the context's fault slot and returns a
17//! defined sentinel.
18
19use crate::context::{RaisedFault, RuntimeContext};
20use crate::dynamic_key::DynamicKey;
21use crate::gc::GcRef;
22use crate::graph::GraphOracle;
23use crate::heap::{Heap, Safepoint};
24use crate::roots::{NativeScope, Rooted};
25use crate::scalars;
26use crate::{
27 collections::VecPayload,
28 descriptor::{Payload, TypeDescriptor},
29 repr_c_vec::ReprCVec,
30};
31pub use praxis_stdlib::abi::{AbiKind, AbiRet, AbiSig, Effect, RuntimeSymbol};
32
33/// The runtime ABI version for this build. Bump it whenever a program compiled
34/// against one version could be misled by a runtime of another.
35///
36/// Three classes of change owe a bump:
37///
38/// * **Layout, calling convention, or signature.** Any move of a field
39/// generated code reads; any change to a `praxis_*` wrapper's parameters or
40/// return type; and any change to the *size* of
41/// [`RuntimeContext`](crate::RuntimeContext), because a host that built a
42/// context of the previous size would have this runtime read past its end.
43/// * **Meaning, with the layout unchanged.** A field or wrapper whose bits stay
44/// where they were but stand for something else: a slot whose "absent" value
45/// moves from a sentinel to all-zero `None`; a counter that counted calls up
46/// to a limit now counting a native-stack budget down; a wrapper whose
47/// manifest [`Effect`] row changes, and with it whether the caller must emit a
48/// `CheckFault` after it.
49/// * **A new dependency of generated code.** Nothing moves, but the compiler
50/// starts reading a field it never read, so repacking that field becomes a
51/// generated-code change from then on.
52///
53/// All three are about what generated code or a host can observe, so a field
54/// with no reader outside `praxis-runtime` whose displacement and width do not
55/// move owes nothing, however much the thing it points at changes.
56/// `RuntimeContext.native_roots` is the one such field: its writers are
57/// `Runtime::context` and `NativeScope`, its only reader is
58/// `RuntimeRoots::from_context`, and no `offset_of!` in
59/// `crates/praxis-codegen-cranelift/src/lower.rs` names it (ADR-114).
60///
61/// What generated code reads today — and therefore what the third class now
62/// covers — is: [`RuntimeContext`](crate::RuntimeContext)'s own `shadow`,
63/// `stack_left`, `pending_fault`, `unit_ref`, `true_ref`, `false_ref`,
64/// `small_ints`, `small_chars`, `descriptors`, `debug_frames`, `debug_values`
65/// and `heap`; [`Fault::KIND_OFFSET`](crate::Fault::KIND_OFFSET) and
66/// [`GcHeader::DESCRIPTOR_OFFSET`](crate::GcHeader::DESCRIPTOR_OFFSET);
67/// `EnumPayload::tag`; the two pacing words at
68/// [`Heap::BYTES_SINCE_COLLECT_OFFSET`](crate::Heap::BYTES_SINCE_COLLECT_OFFSET)
69/// and [`Heap::COLLECT_THRESHOLD_OFFSET`](crate::Heap::COLLECT_THRESHOLD_OFFSET),
70/// plus `Heap::live_count`; `PageHeader`'s and `GcHeader`'s fields, which the
71/// inline claim path both reads and writes; and `VecPayload`'s and
72/// `BitSetPayload`'s leading words, reached through
73/// [`ReprCVec`](crate::ReprCVec). A debug value slot's word is an
74/// `Option<GcRef>` only when the `DebugLocalMeta` beside it says so: a temp
75/// whose box was elided stores its payload raw (ADR-120).
76///
77/// One numeral per build, not one per change — a version is a statement about a
78/// build, so several packages landing in the same round share a single bump,
79/// owned by one of them, rather than taking four bumps in four worktrees that a
80/// merge would silently reduce to the last.
81pub const RUNTIME_ABI_VERSION: u32 = 20;
82
83/// Assert that the compiler's expected ABI version matches this build's.
84///
85/// Called once at CLI / LSP startup. Today the compiler and runtime are the
86/// same binary, so the assertion is trivially satisfied; the point is to have
87/// the check in place before the runtime is split across build artifacts.
88///
89/// # Panics
90/// Panics if the versions disagree. A disagreement is always a build bug, never
91/// a user-facing condition.
92pub fn assert_abi_version() {
93 assert_eq!(
94 COMPILER_EXPECTED_ABI_VERSION, RUNTIME_ABI_VERSION,
95 "compiler/runtime ABI version mismatch: compiler expected \
96 {COMPILER_EXPECTED_ABI_VERSION}, runtime reports {RUNTIME_ABI_VERSION}. \
97 This is a build inconsistency; rebuild the workspace."
98 );
99}
100
101/// The ABI version the compiler front end assumes when generating code. Kept in
102/// lockstep with [`RUNTIME_ABI_VERSION`] within a single build.
103const COMPILER_EXPECTED_ABI_VERSION: u32 = 20;
104
105// ---------------------------------------------------------------------------
106// The runtime symbol table.
107// ---------------------------------------------------------------------------
108
109/// The address of a runtime wrapper, for the JIT to resolve an import to.
110///
111/// This match is the **only** symbol→address table in the workspace, and it is
112/// exhaustive over [`RuntimeSymbol`]: adding a row to the manifest without
113/// giving it an address here is a compile error. There is no fallback — the JIT
114/// never reaches `dlsym`, so a symbol the compiler failed to register cannot
115/// accidentally "work" because it happens to be linked in.
116#[must_use]
117pub fn address(symbol: RuntimeSymbol) -> *const u8 {
118 let ptr: *const () = match symbol {
119 RuntimeSymbol::AllocBool => praxis_alloc_bool as *const (),
120 RuntimeSymbol::AllocChar => praxis_alloc_char as *const (),
121 RuntimeSymbol::AllocClosure => praxis_alloc_closure as *const (),
122 RuntimeSymbol::AllocEnum => praxis_alloc_enum as *const (),
123 RuntimeSymbol::AllocFloat => praxis_alloc_float as *const (),
124 RuntimeSymbol::AllocInt => praxis_alloc_int as *const (),
125 RuntimeSymbol::AllocRecord => praxis_alloc_record as *const (),
126 RuntimeSymbol::AllocText => praxis_alloc_text as *const (),
127 RuntimeSymbol::AllocTuple => praxis_alloc_tuple as *const (),
128 RuntimeSymbol::AllocUnit => praxis_alloc_unit as *const (),
129 RuntimeSymbol::AllocVarCell => praxis_alloc_var_cell as *const (),
130 RuntimeSymbol::Assert => praxis_assert as *const (),
131 RuntimeSymbol::AStarDistance => praxis_a_star_distance as *const (),
132 RuntimeSymbol::AStarPath => praxis_a_star_path as *const (),
133 RuntimeSymbol::Bfs => praxis_bfs as *const (),
134 RuntimeSymbol::BfsDistance => praxis_bfs_distance as *const (),
135 RuntimeSymbol::BfsPath => praxis_bfs_path as *const (),
136 RuntimeSymbol::BitsetContains => praxis_bitset_contains as *const (),
137 RuntimeSymbol::BitsetInsert => praxis_bitset_insert as *const (),
138 RuntimeSymbol::BitsetIsEmpty => praxis_bitset_is_empty as *const (),
139 RuntimeSymbol::BitsetItems => praxis_bitset_items as *const (),
140 RuntimeSymbol::BitsetLen => praxis_bitset_len as *const (),
141 RuntimeSymbol::BitsetNew => praxis_bitset_new as *const (),
142 RuntimeSymbol::Breakpoint => praxis_breakpoint as *const (),
143 RuntimeSymbol::BitsetRemove => praxis_bitset_remove as *const (),
144 RuntimeSymbol::BoolLoad => praxis_bool_load as *const (),
145 RuntimeSymbol::CharLoad => praxis_char_load as *const (),
146 RuntimeSymbol::CharToInt => praxis_char_to_int as *const (),
147 RuntimeSymbol::CharToText => praxis_char_to_text as *const (),
148 RuntimeSymbol::CheckFault => praxis_check_fault as *const (),
149 RuntimeSymbol::ClosureCapture => praxis_closure_capture as *const (),
150 RuntimeSymbol::ClosureFnPtr => praxis_closure_fn_ptr as *const (),
151 RuntimeSymbol::ClosureSetCapture => praxis_closure_set_capture as *const (),
152 RuntimeSymbol::CounterGet => praxis_counter_get as *const (),
153 RuntimeSymbol::CounterInc => praxis_counter_inc as *const (),
154 RuntimeSymbol::CounterIsEmpty => praxis_counter_is_empty as *const (),
155 RuntimeSymbol::CounterLen => praxis_counter_len as *const (),
156 RuntimeSymbol::CounterKeys => praxis_counter_keys as *const (),
157 RuntimeSymbol::CounterNew => praxis_counter_new as *const (),
158 RuntimeSymbol::CounterSet => praxis_counter_set as *const (),
159 RuntimeSymbol::CounterValues => praxis_counter_values as *const (),
160 RuntimeSymbol::DequeGet => praxis_deque_get as *const (),
161 RuntimeSymbol::DequeSet => praxis_deque_set as *const (),
162 RuntimeSymbol::DequeUpdateMax => praxis_deque_update_max as *const (),
163 RuntimeSymbol::DequeUpdateMin => praxis_deque_update_min as *const (),
164 RuntimeSymbol::DequeIsEmpty => praxis_deque_is_empty as *const (),
165 RuntimeSymbol::DequeLen => praxis_deque_len as *const (),
166 RuntimeSymbol::DequeNew => praxis_deque_new as *const (),
167 RuntimeSymbol::DequePopBack => praxis_deque_pop_back as *const (),
168 RuntimeSymbol::DequePopFront => praxis_deque_pop_front as *const (),
169 RuntimeSymbol::DequePushBack => praxis_deque_push_back as *const (),
170 RuntimeSymbol::DequePushFront => praxis_deque_push_front as *const (),
171 RuntimeSymbol::Dbg => praxis_dbg as *const (),
172 RuntimeSymbol::Dfs => praxis_dfs as *const (),
173 RuntimeSymbol::DfsDistance => praxis_dfs_distance as *const (),
174 RuntimeSymbol::DfsPath => praxis_dfs_path as *const (),
175 RuntimeSymbol::Dijkstra => praxis_dijkstra as *const (),
176 RuntimeSymbol::DijkstraDistance => praxis_dijkstra_distance as *const (),
177 RuntimeSymbol::DijkstraPath => praxis_dijkstra_path as *const (),
178 RuntimeSymbol::EnumPayload => praxis_enum_payload as *const (),
179 RuntimeSymbol::EnumSetPayload => praxis_enum_set_payload as *const (),
180 RuntimeSymbol::EnumTag => praxis_enum_tag as *const (),
181 RuntimeSymbol::FloatAbs => praxis_float_abs as *const (),
182 RuntimeSymbol::FloatCeil => praxis_float_ceil as *const (),
183 RuntimeSymbol::FloatE => praxis_float_e as *const (),
184 RuntimeSymbol::FloatFloor => praxis_float_floor as *const (),
185 RuntimeSymbol::FloatIsInfinite => praxis_float_is_infinite as *const (),
186 RuntimeSymbol::FloatIsNan => praxis_float_is_nan as *const (),
187 RuntimeSymbol::FloatLoad => praxis_float_load as *const (),
188 RuntimeSymbol::FloatMax => praxis_float_max as *const (),
189 RuntimeSymbol::FloatMin => praxis_float_min as *const (),
190 RuntimeSymbol::FloatPi => praxis_float_pi as *const (),
191 RuntimeSymbol::FloatRound => praxis_float_round as *const (),
192 RuntimeSymbol::FloatSign => praxis_float_sign as *const (),
193 RuntimeSymbol::FloatSqrt => praxis_float_sqrt as *const (),
194 RuntimeSymbol::FloatToInt => praxis_float_to_int as *const (),
195 RuntimeSymbol::FloatToText => praxis_float_to_text as *const (),
196 RuntimeSymbol::FloodFill => praxis_flood_fill as *const (),
197 RuntimeSymbol::GetInput => praxis_get_input as *const (),
198 RuntimeSymbol::GridAround4 => praxis_grid_around4 as *const (),
199 RuntimeSymbol::GridAround8 => praxis_grid_around8 as *const (),
200 RuntimeSymbol::GridCells => praxis_grid_cells as *const (),
201 RuntimeSymbol::GridColumn => praxis_grid_column as *const (),
202 RuntimeSymbol::GridContains => praxis_grid_contains as *const (),
203 RuntimeSymbol::GridCount4 => praxis_grid_count4 as *const (),
204 RuntimeSymbol::GridCount4Where => praxis_grid_count4_where as *const (),
205 RuntimeSymbol::GridCount8 => praxis_grid_count8 as *const (),
206 RuntimeSymbol::GridCount8Where => praxis_grid_count8_where as *const (),
207 RuntimeSymbol::GridFind => praxis_grid_find as *const (),
208 RuntimeSymbol::GridFindAll => praxis_grid_find_all as *const (),
209 RuntimeSymbol::GridGet => praxis_grid_get as *const (),
210 RuntimeSymbol::GridHeight => praxis_grid_height as *const (),
211 RuntimeSymbol::GridNeighbors4 => praxis_grid_neighbors4 as *const (),
212 RuntimeSymbol::GridNeighbors8 => praxis_grid_neighbors8 as *const (),
213 RuntimeSymbol::GridFilled => praxis_grid_filled as *const (),
214 RuntimeSymbol::GridNew => praxis_grid_new as *const (),
215 RuntimeSymbol::GridPositions => praxis_grid_positions as *const (),
216 RuntimeSymbol::GridRotateLeft => praxis_grid_rotate_left as *const (),
217 RuntimeSymbol::GridRotateRight => praxis_grid_rotate_right as *const (),
218 RuntimeSymbol::GridRow => praxis_grid_row as *const (),
219 RuntimeSymbol::GridSet => praxis_grid_set as *const (),
220 RuntimeSymbol::GridTranspose => praxis_grid_transpose as *const (),
221 RuntimeSymbol::GridUpdateMax => praxis_grid_update_max as *const (),
222 RuntimeSymbol::GridUpdateMin => praxis_grid_update_min as *const (),
223 RuntimeSymbol::GridWidth => praxis_grid_width as *const (),
224 RuntimeSymbol::IntAbs => praxis_int_abs as *const (),
225 RuntimeSymbol::IntAdd => praxis_int_add as *const (),
226 RuntimeSymbol::IntCheckedAdd => praxis_int_checked_add as *const (),
227 RuntimeSymbol::IntCheckedMul => praxis_int_checked_mul as *const (),
228 RuntimeSymbol::IntCheckedSub => praxis_int_checked_sub as *const (),
229 RuntimeSymbol::IntClamp => praxis_int_clamp as *const (),
230 RuntimeSymbol::IntDiv => praxis_int_div as *const (),
231 RuntimeSymbol::IntEq => praxis_int_eq as *const (),
232 RuntimeSymbol::IntGcd => praxis_int_gcd as *const (),
233 RuntimeSymbol::IntGe => praxis_int_ge as *const (),
234 RuntimeSymbol::IntGt => praxis_int_gt as *const (),
235 RuntimeSymbol::IntLcm => praxis_int_lcm as *const (),
236 RuntimeSymbol::IntLe => praxis_int_le as *const (),
237 RuntimeSymbol::IntLoad => praxis_int_load as *const (),
238 RuntimeSymbol::IntLt => praxis_int_lt as *const (),
239 RuntimeSymbol::IntMax => praxis_int_max as *const (),
240 RuntimeSymbol::IntMin => praxis_int_min as *const (),
241 RuntimeSymbol::IntMul => praxis_int_mul as *const (),
242 RuntimeSymbol::IntNe => praxis_int_ne as *const (),
243 RuntimeSymbol::IntNeg => praxis_int_neg as *const (),
244 RuntimeSymbol::IntRem => praxis_int_rem as *const (),
245 RuntimeSymbol::IntSaturatingAdd => praxis_int_saturating_add as *const (),
246 RuntimeSymbol::IntSaturatingMul => praxis_int_saturating_mul as *const (),
247 RuntimeSymbol::IntSaturatingSub => praxis_int_saturating_sub as *const (),
248 RuntimeSymbol::IntSign => praxis_int_sign as *const (),
249 RuntimeSymbol::IntSub => praxis_int_sub as *const (),
250 RuntimeSymbol::IntToChar => praxis_int_to_char as *const (),
251 RuntimeSymbol::IntToFloat => praxis_int_to_float as *const (),
252 RuntimeSymbol::IntToText => praxis_int_to_text as *const (),
253 RuntimeSymbol::IntWrappingAdd => praxis_int_wrapping_add as *const (),
254 RuntimeSymbol::IntWrappingMul => praxis_int_wrapping_mul as *const (),
255 RuntimeSymbol::IntWrappingSub => praxis_int_wrapping_sub as *const (),
256 RuntimeSymbol::MapContains => praxis_map_contains as *const (),
257 RuntimeSymbol::RangeGet => praxis_range_get as *const (),
258 RuntimeSymbol::RangeLen => praxis_range_len as *const (),
259 RuntimeSymbol::RangeNew => praxis_range_new as *const (),
260 RuntimeSymbol::RangeNewInclusive => praxis_range_new_inclusive as *const (),
261 RuntimeSymbol::MapGet => praxis_map_get as *const (),
262 RuntimeSymbol::MapIndex => praxis_map_index as *const (),
263 RuntimeSymbol::MapInsert => praxis_map_insert as *const (),
264 RuntimeSymbol::MapIsEmpty => praxis_map_is_empty as *const (),
265 RuntimeSymbol::MapKeys => praxis_map_keys as *const (),
266 RuntimeSymbol::MapLen => praxis_map_len as *const (),
267 RuntimeSymbol::MapNew => praxis_map_new as *const (),
268 RuntimeSymbol::MapRemove => praxis_map_remove as *const (),
269 RuntimeSymbol::MapUpdateMax => praxis_map_update_max as *const (),
270 RuntimeSymbol::MapUpdateMin => praxis_map_update_min as *const (),
271 RuntimeSymbol::MapValues => praxis_map_values as *const (),
272 RuntimeSymbol::MaxHeapIsEmpty => praxis_max_heap_is_empty as *const (),
273 RuntimeSymbol::MaxHeapItems => praxis_max_heap_items as *const (),
274 RuntimeSymbol::MaxHeapLen => praxis_max_heap_len as *const (),
275 RuntimeSymbol::MaxHeapNew => praxis_max_heap_new as *const (),
276 RuntimeSymbol::MaxHeapPeek => praxis_max_heap_peek as *const (),
277 RuntimeSymbol::MaxHeapPop => praxis_max_heap_pop as *const (),
278 RuntimeSymbol::MaxHeapPush => praxis_max_heap_push as *const (),
279 RuntimeSymbol::MinHeapIsEmpty => praxis_min_heap_is_empty as *const (),
280 RuntimeSymbol::MinHeapItems => praxis_min_heap_items as *const (),
281 RuntimeSymbol::MinHeapLen => praxis_min_heap_len as *const (),
282 RuntimeSymbol::MinHeapNew => praxis_min_heap_new as *const (),
283 RuntimeSymbol::MinHeapPeek => praxis_min_heap_peek as *const (),
284 RuntimeSymbol::MinHeapPop => praxis_min_heap_pop as *const (),
285 RuntimeSymbol::MinHeapPush => praxis_min_heap_push as *const (),
286 RuntimeSymbol::Panic => praxis_panic as *const (),
287 RuntimeSymbol::RaiseDivByZeroIf => praxis_raise_div_by_zero_if as *const (),
288 RuntimeSymbol::RaiseEmptyCollection => praxis_raise_empty_collection as *const (),
289 RuntimeSymbol::RaiseIntOverflowIf => praxis_raise_int_overflow_if as *const (),
290 RuntimeSymbol::RaiseStackOverflow => praxis_raise_stack_overflow as *const (),
291 RuntimeSymbol::RecordField => praxis_record_field as *const (),
292 RuntimeSymbol::RecordSetField => praxis_record_set_field as *const (),
293 RuntimeSymbol::RunParser => praxis_run_parser as *const (),
294 RuntimeSymbol::SetContains => praxis_set_contains as *const (),
295 RuntimeSymbol::SetInsert => praxis_set_insert as *const (),
296 RuntimeSymbol::SetIsEmpty => praxis_set_is_empty as *const (),
297 RuntimeSymbol::SetItems => praxis_set_items as *const (),
298 RuntimeSymbol::SetLen => praxis_set_len as *const (),
299 RuntimeSymbol::SetNew => praxis_set_new as *const (),
300 RuntimeSymbol::SetRemove => praxis_set_remove as *const (),
301 RuntimeSymbol::SnapshotDebugChain => {
302 crate::crash_snapshot::praxis_snapshot_debug_chain as *const ()
303 }
304 RuntimeSymbol::StructEq => praxis_struct_eq as *const (),
305 RuntimeSymbol::TextConcat => praxis_text_concat as *const (),
306 RuntimeSymbol::TextGet => praxis_text_get as *const (),
307 RuntimeSymbol::TextFloat => praxis_text_float as *const (),
308 RuntimeSymbol::TextInt => praxis_text_int as *const (),
309 RuntimeSymbol::TextIsEmpty => praxis_text_is_empty as *const (),
310 RuntimeSymbol::TextLen => praxis_text_len as *const (),
311 RuntimeSymbol::TupleGet => praxis_tuple_get as *const (),
312 RuntimeSymbol::TupleSet => praxis_tuple_set as *const (),
313 RuntimeSymbol::ValueCmp => praxis_value_cmp as *const (),
314 RuntimeSymbol::ValueKeepMax => praxis_value_keep_max as *const (),
315 RuntimeSymbol::ValueKeepMin => praxis_value_keep_min as *const (),
316 RuntimeSymbol::ValueToText => praxis_value_to_text as *const (),
317 RuntimeSymbol::VarCellGet => praxis_var_cell_get as *const (),
318 RuntimeSymbol::VarCellSet => praxis_var_cell_set as *const (),
319 RuntimeSymbol::VecFrequencies => praxis_vec_frequencies as *const (),
320 RuntimeSymbol::VecGet => praxis_vec_get as *const (),
321 RuntimeSymbol::VecSet => praxis_vec_set as *const (),
322 RuntimeSymbol::VecIsEmpty => praxis_vec_is_empty as *const (),
323 RuntimeSymbol::VecJoin => praxis_vec_join as *const (),
324 RuntimeSymbol::VecLen => praxis_vec_len as *const (),
325 RuntimeSymbol::VecChunks => praxis_vec_chunks as *const (),
326 RuntimeSymbol::VecFilled => praxis_vec_filled as *const (),
327 RuntimeSymbol::VecNew => praxis_vec_new as *const (),
328 RuntimeSymbol::VecPush => praxis_vec_push as *const (),
329 RuntimeSymbol::VecReversed => praxis_vec_reversed as *const (),
330 RuntimeSymbol::VecSorted => praxis_vec_sorted as *const (),
331 RuntimeSymbol::VecSortedByKey => praxis_vec_sorted_by_key as *const (),
332 RuntimeSymbol::VecToText => praxis_vec_to_text as *const (),
333 RuntimeSymbol::VecUnique => praxis_vec_unique as *const (),
334 RuntimeSymbol::VecUpdateMax => praxis_vec_update_max as *const (),
335 RuntimeSymbol::VecUpdateMin => praxis_vec_update_min as *const (),
336 RuntimeSymbol::VecWindows => praxis_vec_windows as *const (),
337 RuntimeSymbol::WriteStdout => praxis_write_stdout as *const (),
338 };
339 ptr as *const u8
340}
341
342// ---------------------------------------------------------------------------
343// The panic backstop (§9.2, §10.4)
344// ---------------------------------------------------------------------------
345
346/// The defined dummy a wrapper returns when it has raised a fault and has no
347/// real answer (§10.4).
348///
349/// A wrapper's return type is part of the ABI, so "return nothing" is not
350/// available; every type generated code can receive needs a value that is safe
351/// to hold and never read. `GcRef` is `NonNull`, so its dummy is the context's
352/// `Unit` — the same sentinel the fault epilogue returns — and integer zero
353/// would be an invalid reference, not a dummy.
354pub(crate) trait AbiSentinel {
355 /// # Safety
356 /// `ctx` must be null or point at a live, wired `RuntimeContext`.
357 unsafe fn sentinel(ctx: *mut RuntimeContext) -> Self;
358}
359
360impl AbiSentinel for () {
361 unsafe fn sentinel(_ctx: *mut RuntimeContext) {}
362}
363
364impl AbiSentinel for i64 {
365 unsafe fn sentinel(_ctx: *mut RuntimeContext) -> i64 {
366 0
367 }
368}
369
370impl AbiSentinel for GcRef {
371 unsafe fn sentinel(ctx: *mut RuntimeContext) -> GcRef {
372 // SAFETY: the caller guarantees a live, wired context. A null one
373 // cannot produce a `GcRef` at all, and `abi_panic_escaped` refuses to
374 // reach here with one.
375 unsafe { unit_sentinel(ctx) }
376 }
377}
378
379impl<T> AbiSentinel for *mut T {
380 unsafe fn sentinel(_ctx: *mut RuntimeContext) -> *mut T {
381 std::ptr::null_mut()
382 }
383}
384
385impl<T> AbiSentinel for *const T {
386 unsafe fn sentinel(_ctx: *mut RuntimeContext) -> *const T {
387 std::ptr::null()
388 }
389}
390
391/// Translate a panic that reached an `extern "C"` boundary into a fault, and
392/// return the boundary's defined dummy.
393///
394/// **This must never fire.** Totality is the contract — a wrapper validates its
395/// arguments and reports a bad one as a fault — and this exists because a
396/// contract that cannot be checked is a hope. A Rust panic unwinding out of
397/// `extern "C"` into Cranelift frames is undefined behaviour; the guard turns
398/// the one outcome nobody can reason about into the one §10.4 already
399/// specifies, and does it uniformly so no future wrapper has to remember.
400///
401/// The kind is [`FaultKind::Panic`](crate::FaultKind::Panic), with a message
402/// naming the wrapper. It is deliberately not a new `FaultKind::Internal`: a
403/// new kind is a `#[repr(C)]` layout change that costs an ABI bump (ADR-075),
404/// and `Panic` plus a message that names the function carries strictly more
405/// information for the crash report (§9.4) than a bare kind would.
406///
407/// # Safety
408/// `ctx` must be null or point at a live, wired `RuntimeContext`.
409#[cold]
410#[inline(never)]
411pub(crate) unsafe fn abi_panic_escaped<T: AbiSentinel>(
412 ctx: *mut RuntimeContext,
413 wrapper: &'static str,
414) -> T {
415 if ctx.is_null() {
416 // There is no fault slot to write and no `Unit` to return. Aborting is
417 // the only defined answer left, and it is still better than unwinding
418 // into generated frames.
419 std::process::abort();
420 }
421 // SAFETY: the caller guarantees a live, wired context.
422 unsafe { set_fault(ctx, RaisedFault::PANIC) };
423 let message = format!("internal error: a panic escaped the runtime wrapper `{wrapper}`");
424 // SAFETY: as above.
425 unsafe { set_fault_message(ctx, message.clone()) };
426 if !panic_fault_is_observable(wrapper) {
427 // **The dummy has to be unreachable where nobody will look at the
428 // fault.** Generated code tests the fault slot only where MIR emitted
429 // a `CheckFault`, and MIR emits one only after a call it classifies as
430 // faultable — so for a wrapper the manifest declares non-faulting there
431 // is *no* check by construction, and returning `unit_sentinel` would
432 // hand a `Unit` into a slot generated code believes holds a Record, a
433 // Tuple or a closure. Aborting with the message is the only answer that
434 // does not introduce a descriptor/payload confusion.
435 eprintln!("{message}");
436 std::process::abort();
437 }
438 // SAFETY: as above.
439 unsafe { T::sentinel(ctx) }
440}
441
442/// Whether generated code can be expected to observe a `Panic` fault raised by
443/// `wrapper` — i.e. whether the wrapper's defined dummy is ever consumed under
444/// a fault check rather than as a value.
445///
446/// The manifest is the authority: a symbol declared [`Effect::Pure`] or
447/// [`Effect::Allocates`] cannot be followed by a `CheckFault`. **That is
448/// `praxis_mir::verify`'s rule, not a claim restated here** — its
449/// `RedundantFaultCheck` variant rejects a check after an instruction that
450/// cannot fault, so this function's premise is enforced rather than assumed
451/// (ADR-088). A wrapper the manifest does not name at all is in the same
452/// position and is treated the same way.
453///
454/// The converse is *not* claimed here: a declared-faulting wrapper's call sites
455/// are MIR's business. What this rules out is the class where the check is
456/// impossible.
457fn panic_fault_is_observable(wrapper: &str) -> bool {
458 praxis_stdlib::abi::RuntimeSymbol::from_name(wrapper).is_some_and(|s| s.faults())
459}
460
461/// Wrap an `extern "C"` wrapper's body so a panic becomes a fault.
462///
463/// Every `#[unsafe(no_mangle)] extern "C" fn` in this crate has its body inside
464/// these, and `every_no_mangle_wrapper_is_behind_the_panic_guard` is the test
465/// that keeps it that way — a new wrapper that forgets is a failing test rather
466/// than a latent abort.
467macro_rules! abi_guard {
468 ($wrapper:expr_2021, $ctx:expr_2021, $body:block) => {{
469 // `AssertUnwindSafe`: the body's captures are the wrapper's own
470 // arguments, which are `Copy` C types, and the fault protocol is how
471 // the runtime already communicates a half-finished operation.
472 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || $body)) {
473 Ok(value) => value,
474 // SAFETY: `ctx` is the wrapper's own context argument, whose
475 // validity every wrapper's `# Safety` section already requires.
476 Err(_) => unsafe { crate::abi::abi_panic_escaped($ctx, $wrapper) },
477 }
478 }};
479}
480
481pub(crate) use abi_guard;
482
483// ---------------------------------------------------------------------------
484// Internals the wrappers share.
485// ---------------------------------------------------------------------------
486
487/// Raise `fault` on `ctx`'s fault slot (§10.4). Does nothing if the context's
488/// fault pointer is null (a misuse, but never panics across the ABI).
489///
490/// Takes a [`RaisedFault`], not a `FaultKind`: every raise names a kind that
491/// describes it, and "no fault" is not spellable here.
492unsafe fn set_fault(ctx: *mut RuntimeContext, fault: RaisedFault) {
493 if let Some(slot) = unsafe { (*ctx).pending_fault.as_mut() } {
494 slot.set(fault);
495 }
496}
497
498/// Record `text` as the message the fault about to be raised carries (§9.1).
499/// Does nothing if the context's message slot is null (a host that wired no
500/// runtime), so a `panic` still faults even where nothing can render its words.
501unsafe fn set_fault_message(ctx: *mut RuntimeContext, text: String) {
502 if let Some(slot) = unsafe { (*ctx).fault_message.as_mut() } {
503 slot.set(text);
504 }
505}
506
507/// The heap pointer out of a context, or null.
508#[inline]
509unsafe fn heap<'a>(ctx: *mut RuntimeContext) -> &'a Heap {
510 // SAFETY: the caller guarantees `ctx` points at a live, wired context whose
511 // `heap` field references a valid `Heap` for the duration of the call.
512 unsafe { &*(*ctx).heap }
513}
514
515#[inline]
516/// Charge the pacer for a collection's buffer growing (ADR-121).
517///
518/// `before` and `after` are the same payload's `owned_bytes()`, read either
519/// side of a mutation that may reallocate. Nothing is charged when the buffer
520/// did not grow, which is the overwhelmingly common case: amortized doubling
521/// means a `push` reallocates once every *n* pushes, so this is a compare and a
522/// not-taken branch on the hot path.
523///
524/// # Why every growing wrapper has to call this
525///
526/// `Heap::alloc_raw` charges `stride + owned_bytes_of(payload)` once, at
527/// construction. Leaving later growth uncharged relies on the elements
528/// themselves being paced allocations, so that the residual under-count is only
529/// the spine — and scalar promotion deletes exactly those element allocations,
530/// so an allocation-light program that grows a large buffer paces nothing.
531/// Uncharged, `bfs` runs 6 collections instead of 41 and reaches a peak
532/// resident set of 224 MiB against 61 (ADR-121).
533///
534/// So the rule is: **a wrapper that can grow a buffer charges the growth.** The
535/// `growth_charging_tests` module below has one case per such wrapper, because
536/// the failure mode is silent — a program that simply stops collecting, which
537/// reads as a leak nobody connects to the wrapper that was added.
538fn charge_growth(ctx: *mut RuntimeContext, before: usize, after: usize) {
539 let Some(grown) = after.checked_sub(before).filter(|g| *g != 0) else {
540 return;
541 };
542 if ctx.is_null() {
543 return;
544 }
545 // SAFETY: every caller is inside `abi_guard!`, which established that `ctx`
546 // is live and wired; the null check above covers the guard's own edge.
547 unsafe { heap(ctx).charge_owned_growth(grown) };
548}
549
550/// Trigger a collection on allocation pressure, rooting from the context
551/// (§12.4, ADR-019, ADR-101). Called by every allocating `praxis_*` wrapper.
552/// Safe to call with a null/unwired context (no-op).
553///
554/// The roots are every **strong** arm of
555/// [`RuntimeRoots`](crate::roots::RuntimeRoots) — the shadow stack, the ambient
556/// input buffer, a parse failure's partial value, a runtime-owned crash
557/// snapshot, and the native root store. All five, not the shadow stack alone:
558/// host-driven allocation and the parser interpreter push no shadow frame, and
559/// the other four owners are reachable regardless. The debug arm is
560/// deliberately *weak* (ADR-106): it names storage without keeping it alive, so
561/// it is cleared after the sweep rather than traced here.
562unsafe fn maybe_collect(ctx: *mut RuntimeContext) {
563 if ctx.is_null() {
564 return;
565 }
566 // SAFETY: ctx is live and wired.
567 let roots = unsafe { crate::roots::RuntimeRoots::from_context(ctx) };
568 unsafe { heap(ctx).maybe_collect(&roots) };
569}
570
571/// Pace the collector and mint the token one allocation needs.
572///
573/// Every `praxis_*` wrapper reaches the heap through [`gc_alloc`] or
574/// [`gc_alloc_owned`], which call this — and even a wrapper that reached
575/// `Heap::alloc` directly would have to come through here, because the token
576/// has no other producer. That is the whole point: an allocation that skipped
577/// the pacer would let a program whose pressure comes from `Text`, `.len()` or
578/// checked arithmetic run arbitrarily long without the collector ever being
579/// offered a turn.
580///
581/// # Safety
582/// `ctx` must point at a live, wired `RuntimeContext`. Every allocating
583/// wrapper already requires this to reach the heap at all.
584#[inline]
585unsafe fn safepoint<'a>(ctx: *mut RuntimeContext) -> (&'a Heap, Safepoint<'a>) {
586 // SAFETY: caller upholds ctx validity.
587 let h = unsafe { heap(ctx) };
588 // SAFETY: as above; the roots are read out of the same live context.
589 let roots = unsafe { crate::roots::RuntimeRoots::from_context(ctx) };
590 let sp = h.pace(&roots);
591 (h, sp)
592}
593
594/// Pace the collector, then allocate a `Copy` payload (§12.4).
595///
596/// The descriptor arrives as a [`Payload<T>`] — `scalars::INT_PAYLOAD`, not
597/// `&scalars::INT` — so `value`'s Rust type is checked against it here, at the
598/// call: a value of the wrong type is an `E0308`, and an *untyped* literal
599/// infers as the payload type instead of defaulting to `i32`. A bare descriptor
600/// reference with `T` free would let a width mismatch reach the heap and abort
601/// the process from inside `extern "C"`, which §10.4 forbids.
602///
603/// # Safety
604/// `ctx` must be live and wired.
605#[inline]
606unsafe fn gc_alloc<T: Copy>(ctx: *mut RuntimeContext, payload: Payload<T>, value: T) -> GcRef {
607 // SAFETY: caller upholds ctx validity.
608 let (h, sp) = unsafe { safepoint(ctx) };
609 h.alloc(sp, payload, value)
610}
611
612/// Pace the collector, then allocate a payload that owns Rust resources.
613///
614/// [`gc_alloc`]'s counterpart for the payloads no [`Payload<T>`] can describe.
615/// The type is named **once**, as `P`: [`Heap::alloc_payload`] derives the size,
616/// the alignment and the write from it, so no wrapper restates all three and
617/// keeps them in agreement by hand.
618///
619/// The payload arrives as a producer rather than a value, and that is
620/// load-bearing: `init` runs *after* [`safepoint`] has given the collector its
621/// turn, so a payload built out of bare `GcRef`s — `vec![fill; n]` in
622/// [`praxis_vec_filled`] and [`praxis_grid_filled`] — is never a `Vec<GcRef>`
623/// live across a collection with no root set able to see it.
624///
625/// # Safety
626/// `ctx` must be live and wired, and `descriptor` must be `P`'s own descriptor
627/// ([`Heap::alloc_payload`]'s contract).
628#[inline]
629unsafe fn gc_alloc_owned<P>(
630 ctx: *mut RuntimeContext,
631 descriptor: &'static TypeDescriptor,
632 init: impl FnOnce() -> P,
633) -> GcRef {
634 // SAFETY: caller upholds ctx validity.
635 let (h, sp) = unsafe { safepoint(ctx) };
636 // `init()` is evaluated here, downstream of the safepoint above — see the
637 // ordering note in this function's doc.
638 // SAFETY: forwarded from this function's contract.
639 unsafe { h.alloc_payload(sp, descriptor, init()) }
640}
641
642/// The immortal `Bool` for `value`, off the context's cached singletons.
643///
644/// Never an allocation: there are exactly two `Bool` values and the runtime
645/// minted both at startup, so every comparison, `contains` and `is_empty` a
646/// program evaluates answers with a singleton rather than consuming arena
647/// storage permanently. It is also what makes those manifest rows honestly
648/// `Effect::Pure`: nothing here can collect, so the call site is not a
649/// safepoint.
650///
651/// # Safety
652/// `ctx` must point at a live, wired `RuntimeContext`.
653#[inline]
654unsafe fn bool_ref(ctx: *mut RuntimeContext, value: bool) -> GcRef {
655 // SAFETY: caller upholds ctx validity.
656 let c = unsafe { &*ctx };
657 if value { c.true_ref } else { c.false_ref }
658}
659
660/// The `Int` for `value`: the interned immortal when it is small
661/// ([`crate::small_int`]), a fresh allocation otherwise.
662///
663/// [`bool_ref`]'s shape, one step less absolute: `Bool` has two values so it is
664/// always the singleton, while `Int` has a *range* that is interned and an
665/// unbounded remainder that is not. Every wrapper that answers an `Int` reaches
666/// the heap through here, so the interning covers not just literals but
667/// `Vec.len()`, a `Counter` bump, an enum tag, a comparison's index and the
668/// result of arithmetic — which is where most of a real program's small `Int`s
669/// come from.
670///
671/// # It paces even when it does not allocate, and that is deliberate
672///
673/// The manifest declares `VecLen`, `MapLen`, `EnumTag`, `TextLen`, `CounterGet`
674/// and two dozen more `Effect::Allocates`, which is generated code's contract
675/// that the call site is a GC safepoint. If this returned before [`safepoint`],
676/// a loop whose only allocations were small `Int`s would never offer the
677/// collector a turn — the collector's *only* trigger is the pacing counter, and
678/// nothing else in such a loop touches it. So the token is minted and then
679/// dropped: [`Safepoint`] is `#[must_use]`, so `drop(sp)` is the honest spelling
680/// of "the collector got its turn and we allocated nothing", and it is a
681/// compile error to forget which of the two happened.
682///
683/// The interned path therefore costs a threshold compare and a range test rather
684/// than an allocation. `Inst::ConstGc` is what removes even that, but only for a
685/// *literal*, where the compiler knows the value and no manifest row applies.
686///
687/// # Safety
688/// `ctx` must point at a live, wired `RuntimeContext`.
689#[inline]
690unsafe fn int_ref(ctx: *mut RuntimeContext, value: i64) -> GcRef {
691 // SAFETY: caller upholds ctx validity.
692 let (h, sp) = unsafe { safepoint(ctx) };
693 match crate::small_int::index_of(value) {
694 Some(i) => {
695 drop(sp);
696 // SAFETY: `index_of` bounds `i` by `SMALL_INT_COUNT`, and
697 // `Runtime::context` points `small_ints` at a table of exactly that
698 // length whose slot `i` holds `SMALL_INT_MIN + i`.
699 unsafe { *(*ctx).small_ints.add(i) }
700 }
701 None => h.alloc(sp, scalars::INT_PAYLOAD, value),
702 }
703}
704
705/// The `Char` for `code`: the interned immortal when it is ASCII
706/// ([`crate::small_char`]), a fresh allocation otherwise.
707///
708/// [`int_ref`]'s shape and its argument (ADR-107). Every wrapper that answers a
709/// `Char` reaches the heap through here — [`checked_alloc_char`], which is both
710/// `praxis_alloc_char` and `praxis_int_to_char`; [`praxis_text_get`], which is
711/// both `t[i]` and every step of `for c in t`; and [`default_cell`], which is a
712/// `Grid[Char]`'s fill. `praxis_text_get` is the one that matters for real code:
713/// an AoC-shaped program that walks a line of text would otherwise box a fresh
714/// object per character read, and every character of such a line is ASCII.
715///
716/// # It paces even when it does not allocate
717///
718/// The manifest declares `TextGet`, `AllocChar` and `IntToChar`
719/// `Effect::AllocatesAndFaults`, which is generated code's contract that the call
720/// site is a GC safepoint. If this returned before [`safepoint`], `for c in text`
721/// over an ASCII line would never offer the collector a turn — the collector's
722/// *only* trigger is the pacing counter, and a loop that reads characters and
723/// compares them touches nothing else that would bump it. So the token is minted
724/// and then dropped: [`Safepoint`] is `#[must_use]`, so `drop(sp)` is the honest
725/// spelling of "the collector got its turn and we allocated nothing", and it is a
726/// compile error to forget which of the two happened. Pinned by
727/// `char_ref_paces_the_collector_even_when_it_answers_from_the_table`.
728///
729/// Unlike `int_ref` there is no `Inst::ConstGc` that removes even the pacing
730/// check: that instruction exists for a *literal*, and the language has no
731/// character literal (ADR-107 Decision 2).
732///
733/// # Safety
734/// `ctx` must point at a live, wired `RuntimeContext`, and `code` must be a valid
735/// Unicode scalar value — every caller has already established this, either by
736/// [`checked_alloc_char`]'s range check or by starting from a Rust `char`.
737#[inline]
738unsafe fn char_ref(ctx: *mut RuntimeContext, code: u32) -> GcRef {
739 debug_assert!(
740 crate::scalars::is_valid_char(code),
741 "char_ref's callers validate first"
742 );
743 // SAFETY: caller upholds ctx validity.
744 let (h, sp) = unsafe { safepoint(ctx) };
745 match crate::small_char::index_of(code) {
746 Some(i) => {
747 drop(sp);
748 // SAFETY: `index_of` bounds `i` by `SMALL_CHAR_COUNT`, and
749 // `Runtime::context` points `small_chars` at a table of exactly that
750 // length whose slot `i` holds code point `i`.
751 unsafe { *(*ctx).small_chars.add(i) }
752 }
753 None => h.alloc(sp, scalars::CHAR_PAYLOAD, code),
754 }
755}
756
757/// A fresh owned `Text` holding `s`.
758///
759/// [`bool_ref`]/[`int_ref`]/[`char_ref`]'s place in the file but not their
760/// shape: there is nothing interned to answer from — every `Text` is a distinct
761/// object — so this always allocates, and always paces.
762///
763/// `impl Into<Box<str>>` is what lets every text-producing wrapper share this
764/// one allocation without a defensive `.clone()`: a `String` from a renderer, a
765/// `Box<str>` the caller already built ([`praxis_alloc_text`]), a `&str`.
766///
767/// # Safety
768/// `ctx` must point at a live, wired `RuntimeContext`.
769#[inline]
770unsafe fn text_ref(ctx: *mut RuntimeContext, s: impl Into<Box<str>>) -> GcRef {
771 // SAFETY: caller upholds ctx validity; `TextPayload` is `TEXT`'s payload type.
772 unsafe {
773 gc_alloc_owned(ctx, &crate::text::TEXT, || {
774 crate::text::TextPayload::owned(s)
775 })
776 }
777}
778
779/// Read `r`'s payload through a [`Payload`] handle, first checking that `r`
780/// really is that handle's type.
781///
782/// This is the reader to reach for whenever a wrapper receives a `GcRef` it did
783/// not itself allocate — a value handed back by a program's closure, most of
784/// all. Two mistakes are impossible through it: reading a value of the wrong
785/// *type* (the identity check answers `None`, and the caller decides whether
786/// that is a `TypeMismatch` fault), and reading the right type at the wrong
787/// *width* (the width is `size_of::<T>()`, which [`Payload::new`] proved is the
788/// descriptor's width when the handle was declared). Reading a one-byte `Bool`
789/// payload with an eight-byte `int_payload` is the class it rules out.
790///
791/// # Safety
792/// `r` must be a valid `GcRef` into a live heap.
793#[inline]
794unsafe fn read_scalar<T: Copy>(r: GcRef, handle: crate::descriptor::Payload<T>) -> Option<T> {
795 if !std::ptr::eq(r.descriptor(), handle.descriptor()) {
796 return None;
797 }
798 // SAFETY: the identity check proves `r`'s payload is this handle's type, and
799 // the handle's own construction proved `T` is that type's layout.
800 Some(unsafe { handle.read(r.payload::<u8>()) })
801}
802
803/// Read the `i64` payload of an `Int` `GcRef`. Used by every arithmetic wrapper.
804///
805/// Prefer [`read_scalar`] for any value whose type is not already established:
806/// this reads eight bytes, and the descriptor check is all that stands between
807/// it and a narrower payload.
808///
809/// # The width check is a branch, not a `debug_assert`
810///
811/// A `debug_assert` is not a bound — it compiles out of a release build, leaving
812/// an eight-byte read against a descriptor that may be narrower or zero bytes
813/// wide, so the two profiles would answer differently and the wrong one is the
814/// one users get. As an ordinary branch it holds in every profile: the read
815/// cannot happen. What happens instead is ADR-080's defined panic path —
816/// `abi_guard` catches it, raises `RaisedFault::PANIC` with a message naming
817/// the wrapper, and either faults into the crash debugger (a wrapper the
818/// manifest declares faultable) or prints that message and aborts (one it does
819/// not, which is `praxis_int_load`'s case). The guard is a memory-safety check
820/// on a raw read, not a stand-in for a type system.
821///
822/// This stays `-> i64` rather than becoming fallible: sixty-odd wrappers read
823/// through it, and a `ctx`-threading signature change is a larger edit than the
824/// one memory safety needs.
825#[inline]
826unsafe fn int_payload(r: GcRef) -> i64 {
827 // SAFETY: `read_scalar` proves `r`'s descriptor *is* `INT` before reading,
828 // so the eight bytes are in bounds and are an `i64`. The compiler only emits
829 // these calls with Int-typed operands, and a fault that would feed a
830 // non-`Int` (the Unit sentinel, say) into an arithmetic wrapper is diverted
831 // by `Inst::CheckFault` before it gets here (§10.4).
832 unsafe { read_scalar(r, scalars::INT_PAYLOAD) }
833 .unwrap_or_else(|| scalar_type_mismatch("int_payload", "Int", r.descriptor().name))
834}
835
836/// The refusal every scalar reader shares, out of line so the check costs a
837/// never-taken branch on the hot path.
838///
839/// `#[cold]` and `#[inline(never)]` are what let the check be unconditional, and
840/// it must be unconditional: a `debug_assert` compiles out, so a release build
841/// would do an out-of-bounds heap read where a debug build aborted.
842///
843/// A panic here is ADR-080's defined path: `abi_guard!` catches it, raises
844/// `RaisedFault::PANIC` naming the wrapper, and either faults into the crash
845/// debugger or prints the message and aborts. That is the backstop, not the
846/// primary defence — a raw scalar read must prove its own width whatever the
847/// type system believes.
848#[cold]
849#[inline(never)]
850fn scalar_type_mismatch(what: &'static str, want: &'static str, found: &'static str) -> ! {
851 panic!("{what} wants a `{want}` payload; this value is a `{found}` (REP-56)");
852}
853
854/// [`praxis_alloc_text`]'s refusal when its buffer is not UTF-8 — a violated
855/// precondition, not a runtime condition (ADR-111).
856///
857/// **Why this is a panic and not a fault.** The precondition is the one that is
858/// actually true: the compiler's bytes are a Rust `&str` unbroken from
859/// `Lit::Text(String)` through `AllocKind::Text { value: String }` to
860/// `Generation::alloc_str`, and the one caller in this crate that holds raw
861/// *host* bytes — [`praxis_get_input`] — validates them itself and raises
862/// `InvalidText` there, where the `read` can observe it. Spelling it as a fault
863/// instead would cost a `CheckFault` after every text literal for a fault no
864/// generated call site can raise.
865///
866/// **Why the `from_utf8` call above stays, in every profile.** This is
867/// [`scalar_type_mismatch`]'s argument verbatim and it is why that function is
868/// the neighbour: a `debug_assert` is not a bound, because it compiles out of a
869/// release build. What would be left in release is a `Box<str>` built from bytes
870/// that are not UTF-8, which [`crate::text::text_str`] later hands out as a
871/// `&str` — so the two profiles would answer differently and the wrong one is
872/// the one users get. `from_utf8_unchecked` is the same hole with the check
873/// deleted rather than compiled out. The unconditional branch costs a
874/// never-taken jump to this cold callee, which is the price ADR-102 §1 already
875/// established for the inline scalar loads.
876///
877/// **It must not reach `set_fault`, and that is enforced.**
878/// `a_wrapper_that_can_raise_a_fault_declares_that_it_faults` computes a textual
879/// fixed point over this file: a body that can reach `set_fault`, directly or
880/// through a helper defined here, must belong to a symbol whose manifest row
881/// says it faults. `AllocText`'s row is `Effect::Allocates`, so a refusal
882/// spelled as a fault would fail that test.
883///
884/// The end-to-end path on a violation is ADR-080's: panic → `abi_guard!`
885/// catches → `panic_fault_is_observable("praxis_alloc_text")` reads the
886/// `Allocates` row and answers `false` → the message is printed and the process
887/// aborts. That is the same outcome `praxis_int_load` gives a wrong descriptor,
888/// and it falls out of the row change with no code of its own.
889#[cold]
890#[inline(never)]
891fn text_bytes_are_not_utf8(len: usize) -> ! {
892 panic!(
893 "praxis_alloc_text was handed {len} bytes that are not valid UTF-8; its \
894 `# Safety` contract requires them to be (ADR-111). A host with untrusted \
895 bytes must validate them first, as `praxis_get_input` does."
896 );
897}
898
899/// The Unit `GcRef` returned on fault paths as the "defined dummy" (§10.4).
900/// Reads the cached immortal `unit_ref` from the context, which is stable for
901/// the program's lifetime.
902#[inline]
903unsafe fn unit_sentinel(ctx: *mut RuntimeContext) -> GcRef {
904 unsafe { (*ctx).unit_ref }
905}
906
907/// The slot a source index `i` names in a container of `len` elements, or
908/// `None` when it is outside `0..len` (§9.2, §11.1).
909///
910/// **The one place a source index becomes a `usize`.** The `< 0` test has to
911/// run before the cast: a negative `i64` casts to a value near `usize::MAX` and
912/// would sail straight past a bare length comparison. Every accessor shares this
913/// one copy, so no single site can drop the guard invisibly. The construction
914/// side guards the same hazard with a named
915/// [`GridExtent`](crate::collections::GridExtent).
916///
917/// `Vec` and `Deque` indexing is one language rule applied to two containers,
918/// so they share this rather than each spelling it out.
919fn linear_index(i: i64, len: usize) -> Option<usize> {
920 if i < 0 {
921 return None;
922 }
923 // Non-negative above, so the cast is exact.
924 let i = i as usize;
925 (i < len).then_some(i)
926}
927
928/// The row-major slot `(x, y)` names in a `width × height` grid, or `None` when
929/// either axis falls outside it.
930///
931/// Both axes go through [`linear_index`], so the 2-D rule is the 1-D rule twice
932/// and the signed-to-`usize` cast still exists in exactly one place. The
933/// product cannot overflow: `y < height` and `x < width` with
934/// `height = items.len() / width`, so the result is below `items.len()`.
935fn cell_index(x: i64, y: i64, width: usize, height: usize) -> Option<usize> {
936 let x = linear_index(x, width)?;
937 let y = linear_index(y, height)?;
938 Some(y * width + x)
939}
940
941/// [`linear_index`], raising `IndexOutOfBounds` when the index is out of range.
942/// `None` means the fault is already set and the caller owes only its sentinel
943/// return.
944///
945/// The raising wrapper is deliberately separate from the pure predicate,
946/// because not every bounds question may fault. [`praxis_grid_contains`] is
947/// declared `Pure` in the ABI manifest while `GridGet` is `Faults`, and MIR's
948/// `RedundantFaultCheck` emits no `CheckFault` after a `Pure` call — so a fault
949/// raised on every legitimate `false` would sit pending until some later check
950/// mistook it for its own. Bounds-testing sites take [`cell_index`] /
951/// [`linear_index`]; only sites the manifest says can fault take these.
952///
953/// # Safety
954/// `ctx` must be live and wired.
955unsafe fn checked_index(ctx: *mut RuntimeContext, i: i64, len: usize) -> Option<usize> {
956 let idx = linear_index(i, len);
957 if idx.is_none() {
958 unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
959 }
960 idx
961}
962
963/// [`cell_index`], raising `IndexOutOfBounds` when `(x, y)` is off the grid.
964/// See [`checked_index`] for why the raising and the predicate are two
965/// functions.
966///
967/// # Safety
968/// `ctx` must be live and wired.
969unsafe fn checked_cell(
970 ctx: *mut RuntimeContext,
971 x: i64,
972 y: i64,
973 width: usize,
974 height: usize,
975) -> Option<usize> {
976 let idx = cell_index(x, y, width, height);
977 if idx.is_none() {
978 unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
979 }
980 idx
981}
982
983// ---------------------------------------------------------------------------
984// Allocation wrappers.
985// ---------------------------------------------------------------------------
986
987/// The `Int` for `value` (§4.3, §11.1) — the interned immortal when it is small
988/// ([`crate::small_int`]), a fresh box otherwise.
989///
990/// The row stays `Effect::Allocates`, not `Pure` as `AllocBool`'s is: this
991/// wrapper still allocates for an out-of-range value, so the call site is still
992/// a GC safepoint and generated code must still spill its roots across it. The
993/// interning is invisible to the caller by design.
994///
995/// # Safety
996/// `ctx` must point at a live, wired `RuntimeContext` whose `heap` is valid.
997#[unsafe(no_mangle)]
998pub unsafe extern "C" fn praxis_alloc_int(ctx: *mut RuntimeContext, value: i64) -> GcRef {
999 abi_guard!("praxis_alloc_int", ctx, {
1000 // `int_ref` paces first, rooted at the whole `RuntimeRoots`. The new object
1001 // is not yet a root, but it is returned by value to the caller, which spills
1002 // it — so it is safe across this collection (the *previous* allocation's
1003 // result was already spilled by the backend before this wrapper was called).
1004 // SAFETY: caller upholds the ctx/heap validity.
1005 unsafe { int_ref(ctx, value) }
1006 })
1007}
1008
1009/// Allocate a boxed `Bool` from a 0/1 value (§4.3). Returns the immortal
1010/// singleton, never a fresh allocation.
1011///
1012/// # Safety
1013/// `ctx` must point at a live, wired `RuntimeContext`.
1014#[unsafe(no_mangle)]
1015pub unsafe extern "C" fn praxis_alloc_bool(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1016 abi_guard!("praxis_alloc_bool", ctx, {
1017 // There are two `Bool` values, and the runtime allocated both at startup.
1018 // `value != 0` is true; `0` is false.
1019 // SAFETY: caller upholds ctx validity.
1020 let c = unsafe { &*ctx };
1021 if value != 0 { c.true_ref } else { c.false_ref }
1022 })
1023}
1024
1025/// Allocate the `Unit` singleton (§4.3).
1026///
1027/// # Safety
1028/// `ctx` must point at a live, wired `RuntimeContext`.
1029#[unsafe(no_mangle)]
1030pub unsafe extern "C" fn praxis_alloc_unit(ctx: *mut RuntimeContext) -> GcRef {
1031 abi_guard!("praxis_alloc_unit", ctx, {
1032 // The one `Unit` value, cached on the context for the fault path.
1033 // SAFETY: caller upholds ctx validity.
1034 unsafe { (*ctx).unit_ref }
1035 })
1036}
1037
1038/// Allocate a boxed `Char` from a Unicode scalar value (§4.3). The `value`
1039/// is the `u32` code point carried as `i64` (the uniform scalar ABI width). If
1040/// the code point is not a valid scalar, the fault is set and the Unit sentinel
1041/// is returned (no panic crosses the ABI).
1042///
1043/// # Safety
1044/// `ctx` must point at a live, wired `RuntimeContext`.
1045#[unsafe(no_mangle)]
1046pub unsafe extern "C" fn praxis_alloc_char(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1047 abi_guard!("praxis_alloc_char", ctx, {
1048 // SAFETY: caller upholds ctx/heap validity.
1049 unsafe { checked_alloc_char(ctx, value) }
1050 })
1051}
1052
1053/// Box an `i64` as a `Char`, or raise `InvalidChar` and answer the Unit sentinel.
1054///
1055/// The one place the `i64`-to-code-point rule is enforced, because there are two
1056/// doors into it — `praxis_alloc_char` (the parser and codegen's `AllocKind::Char`)
1057/// and `praxis_int_to_char` (`Int.to_char()`, ADR-086) — and a rule stated at both
1058/// goes stale at one.
1059///
1060/// **Range-check before narrowing.** `value as u32` truncates, so
1061/// `0x1_0000_0041` would silently become `'A'`. The scalar ABI is 64 bits wide;
1062/// a code point is not, and the conversion has to say so rather than wrap. The
1063/// surrogate range is rejected for the same reason: `char::from_u32` is what
1064/// decides, not a width.
1065///
1066/// # Safety
1067/// `ctx` must point at a live, wired `RuntimeContext`.
1068unsafe fn checked_alloc_char(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1069 let Ok(code) = u32::try_from(value) else {
1070 unsafe { set_fault(ctx, RaisedFault::INVALID_CHAR) };
1071 return unsafe { unit_sentinel(ctx) };
1072 };
1073 if !crate::scalars::is_valid_char(code) {
1074 unsafe { set_fault(ctx, RaisedFault::INVALID_CHAR) };
1075 return unsafe { unit_sentinel(ctx) };
1076 }
1077 // SAFETY: caller upholds ctx/heap validity; code is a validated scalar.
1078 unsafe { char_ref(ctx, code) }
1079}
1080
1081/// Allocate an owned `Text` from a UTF-8 byte buffer (§4.3, ADR-013).
1082///
1083/// **UTF-8 is the caller's precondition, and this wrapper cannot fault**
1084/// (ADR-111). Its row is `Effect::Allocates`, so `Inst::Alloc { AllocKind::Text }`
1085/// is followed by no `CheckFault` — `praxis_mir::verify` rejects one — and a
1086/// `Text` literal in a loop is hoisted into the preheader like a `Float` one
1087/// (ADR-108 §3). Handing this bytes that are not UTF-8 is a violated contract,
1088/// not a runtime condition, and it aborts through `text_bytes_are_not_utf8`
1089/// (whose doc carries the argument) rather than raising `InvalidText`.
1090///
1091/// A host that holds *untrusted* bytes validates them before calling. There is
1092/// exactly one such caller in this crate — [`praxis_get_input`], whose row is
1093/// `AllocatesAndFaults` — and it raises `InvalidText` itself, so the fault a
1094/// `read` can observe still lands at the `read` (§4.3, §7.10).
1095///
1096/// # Safety
1097/// `ctx` must point at a live, wired `RuntimeContext`; `bytes` must point at
1098/// `len` valid UTF-8 bytes that remain valid for the duration of the call.
1099#[unsafe(no_mangle)]
1100pub unsafe extern "C" fn praxis_alloc_text(
1101 ctx: *mut RuntimeContext,
1102 bytes: *const u8,
1103 len: usize,
1104) -> GcRef {
1105 abi_guard!("praxis_alloc_text", ctx, {
1106 let slice = if bytes.is_null() || len == 0 {
1107 &[]
1108 } else {
1109 // SAFETY: caller guarantees `bytes..bytes+len` is a valid, UTF-8 buffer.
1110 unsafe { std::slice::from_raw_parts(bytes, len) }
1111 };
1112 // The check is unconditional in every profile: it is the backstop on a
1113 // raw read, the same standing `read_scalar` has, and its argument is
1114 // written out at `text_bytes_are_not_utf8`. A violation refuses rather
1115 // than recovering lossily behind a fault nobody at a generated call site
1116 // could observe (ADR-111).
1117 let owned: Box<str> = match std::str::from_utf8(slice) {
1118 Ok(s) => s.into(),
1119 Err(_) => text_bytes_are_not_utf8(len),
1120 };
1121 // SAFETY: ctx/heap valid.
1122 unsafe { text_ref(ctx, owned) }
1123 })
1124}
1125
1126// ---------------------------------------------------------------------------
1127// Scalar extraction / materialization.
1128// ---------------------------------------------------------------------------
1129
1130/// Read the `i64` payload of an `Int` `GcRef` (§10.3 transient scalar).
1131///
1132/// # Safety
1133/// `r` must be a valid `Int` `GcRef`.
1134#[unsafe(no_mangle)]
1135pub unsafe extern "C" fn praxis_int_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1136 abi_guard!("praxis_int_load", _ctx, {
1137 // SAFETY: caller guarantees `r` is an Int.
1138 unsafe { int_payload(r) }
1139 })
1140}
1141
1142/// Read a `Bool` payload as 0/1 (§10.3 transient scalar).
1143///
1144/// # Safety
1145/// `r` must be a valid `Bool` `GcRef`.
1146#[unsafe(no_mangle)]
1147pub unsafe extern "C" fn praxis_bool_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1148 abi_guard!("praxis_bool_load", _ctx, {
1149 // Read the byte, then decide — never `*r.payload::<bool>()`. A Rust
1150 // `bool` whose byte is not 0 or 1 is an *invalid value*, and
1151 // materializing one is undefined behaviour whatever the read's bounds
1152 // are; `BoolPayload` is a `u8` precisely so the runtime never has to.
1153 // SAFETY: `read_scalar` bounds the read against `r`'s own descriptor.
1154 let byte = unsafe { read_scalar(r, scalars::BOOL_PAYLOAD) }.unwrap_or_else(|| {
1155 scalar_type_mismatch("praxis_bool_load", "Bool", r.descriptor().name)
1156 });
1157 i64::from(byte != 0)
1158 })
1159}
1160
1161/// Read a `Char` payload as its `u32` code point widened to `i64` (§4.3).
1162///
1163/// # Safety
1164/// `r` must be a valid `Char` `GcRef`.
1165#[unsafe(no_mangle)]
1166pub unsafe extern "C" fn praxis_char_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1167 abi_guard!("praxis_char_load", _ctx, {
1168 // SAFETY: `read_scalar` bounds the read against `r`'s own descriptor.
1169 let code = unsafe { read_scalar(r, scalars::CHAR_PAYLOAD) }.unwrap_or_else(|| {
1170 scalar_type_mismatch("praxis_char_load", "Char", r.descriptor().name)
1171 });
1172 i64::from(code)
1173 })
1174}
1175
1176/// Allocate a boxed `Float` from an `i64` carrying the IEEE-754 binary64 bit
1177/// pattern (§4.3, §4.12). The uniform scalar ABI carries every payload as
1178/// `i64`; a float is transported as `f64::to_bits()` and reassembled here.
1179///
1180/// # Safety
1181/// `ctx` must point at a live, wired `RuntimeContext`.
1182#[unsafe(no_mangle)]
1183pub unsafe extern "C" fn praxis_alloc_float(ctx: *mut RuntimeContext, value: i64) -> GcRef {
1184 abi_guard!("praxis_alloc_float", ctx, {
1185 let f = f64::from_bits(value as u64);
1186 // SAFETY: caller upholds ctx/heap validity; all f64 values are valid Floats.
1187 unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, f) }
1188 })
1189}
1190
1191/// Read a `Float` payload as its IEEE-754 bit pattern widened to `i64`
1192/// (§10.3 transient scalar). Generated code keeps floats in the uniform `i64`
1193/// scalar channel; the bit pattern is reassembled into an `f64` only at the
1194/// point of an arithmetic/comparison instruction.
1195///
1196/// # Safety
1197/// `r` must be a valid `Float` `GcRef`.
1198#[unsafe(no_mangle)]
1199pub unsafe extern "C" fn praxis_float_load(_ctx: *mut RuntimeContext, r: GcRef) -> i64 {
1200 abi_guard!("praxis_float_load", _ctx, {
1201 // Through `float_payload`, which goes through `read_scalar`: the read
1202 // proves its own width rather than taking the caller's word for it.
1203 //
1204 // It matters more since ADR-102: generated code reads a `Float`
1205 // payload inline behind a descriptor check, and this wrapper is the
1206 // cold path that check branches to. If it read unchecked, the two
1207 // would disagree about what a wrong descriptor means — the inline
1208 // path would refuse and the fallback would read anyway.
1209 //
1210 // SAFETY: caller guarantees `r` is a valid `GcRef`; `float_payload`
1211 // proves it is a `Float` before reading.
1212 unsafe { float_payload(r) }.to_bits() as i64
1213 })
1214}
1215
1216// ---------------------------------------------------------------------------
1217// Float conversion & methods (§4.12). Float arithmetic never faults (IEEE-754
1218// produces inf/nan); only the narrowing `to_int` conversion does.
1219// ---------------------------------------------------------------------------
1220
1221/// Read a `Float` payload as an `f64` (private helper).
1222///
1223/// # Safety
1224/// `r` must be a valid `Float` `GcRef`.
1225unsafe fn float_payload(r: GcRef) -> f64 {
1226 // SAFETY: `read_scalar` proves `r`'s descriptor is `FLOAT` before reading.
1227 unsafe { read_scalar(r, scalars::FLOAT_PAYLOAD) }
1228 .unwrap_or_else(|| scalar_type_mismatch("float_payload", "Float", r.descriptor().name))
1229}
1230
1231/// Widen an `Int` to a `Float` (§4.12). Never faults — every `i64` is exactly
1232/// representable as an `f64`? No: integers above 2^53 lose precision, but the
1233/// conversion is still total and well-defined (rounds to nearest). This is the
1234/// explicit widening method `Int.to_float()`.
1235///
1236/// # Safety
1237/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1238#[unsafe(no_mangle)]
1239pub unsafe extern "C" fn praxis_int_to_float(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1240 abi_guard!("praxis_int_to_float", ctx, {
1241 let i = unsafe { int_payload(r) };
1242 // SAFETY: ctx/heap valid; every widened int is a valid Float payload.
1243 unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, i as f64) }
1244 })
1245}
1246
1247/// `Char.to_int()` — the Unicode scalar value, as an `Int` (ADR-086). Never
1248/// faults: every valid scalar fits an `i64`.
1249///
1250/// This reads through [`read_scalar`] with the `Char` handle rather than
1251/// `int_payload`, because a `Char` payload is **four** bytes and an `i64` read
1252/// would take eight of them.
1253///
1254/// # Safety
1255/// `ctx` must be live and wired; `r` must be a valid `Char` `GcRef`.
1256#[unsafe(no_mangle)]
1257pub unsafe extern "C" fn praxis_char_to_int(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1258 abi_guard!("praxis_char_to_int", ctx, {
1259 // SAFETY: caller guarantees `r` is a valid `GcRef`; `read_scalar` proves
1260 // the descriptor is `CHAR` before reading its four bytes.
1261 let code = unsafe { read_scalar(r, scalars::CHAR_PAYLOAD) }.unwrap_or_else(|| {
1262 scalar_type_mismatch("praxis_char_to_int", "Char", r.descriptor().name)
1263 });
1264 // SAFETY: ctx/heap valid; every scalar value is a valid Int payload.
1265 unsafe { int_ref(ctx, i64::from(code)) }
1266 })
1267}
1268
1269/// `Int.to_char()` — the `Char` with this Unicode scalar value (ADR-086).
1270/// Faults (`InvalidChar`) on a negative value, one above `0x10FFFF`, or one in
1271/// the surrogate range: those are not scalar values and have no `Char`.
1272///
1273/// It is `Char.to_int()`'s partial half exactly as `Float.to_int()` is
1274/// `Int.to_float()`'s — the narrowing direction is the one that can fail. The
1275/// check lives in [`checked_alloc_char`] and is not restated here.
1276///
1277/// # Safety
1278/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1279#[unsafe(no_mangle)]
1280pub unsafe extern "C" fn praxis_int_to_char(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1281 abi_guard!("praxis_int_to_char", ctx, {
1282 let value = unsafe { int_payload(r) };
1283 // SAFETY: caller upholds ctx/heap validity.
1284 unsafe { checked_alloc_char(ctx, value) }
1285 })
1286}
1287
1288/// Narrow a `Float` to an `Int` by truncating toward zero (§4.12). Faults
1289/// (`FloatToInt`) on NaN, ±infinity, or a finite value outside the signed
1290/// 64-bit range — these have no exact `Int` representation. On fault, sets
1291/// `pending_fault` and returns the Unit sentinel (no panic crosses the ABI).
1292///
1293/// # Safety
1294/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1295#[unsafe(no_mangle)]
1296pub unsafe extern "C" fn praxis_float_to_int(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1297 abi_guard!("praxis_float_to_int", ctx, {
1298 let f = unsafe { float_payload(r) };
1299 // NaN, infinities, and out-of-range finite values are not exactly
1300 // representable as i64. Rust's `as i64` saturates (inf→i64::MAX,
1301 // -inf→i64::MIN, nan→0), which would silently produce a plausible-but-wrong
1302 // value; per §4.12 these cases fault instead.
1303 if f.is_nan() || f.is_infinite() || f < i64::MIN as f64 || f >= i64::MAX as f64 {
1304 unsafe { set_fault(ctx, RaisedFault::FLOAT_TO_INT) };
1305 return unsafe { unit_sentinel(ctx) };
1306 }
1307 // The range check above bounds f to (-2^63, 2^63); truncation toward zero is
1308 // then exact for every representable integer and inexact-but-safe for the
1309 // fractional part (which is discarded).
1310 // SAFETY: ctx/heap valid; the value is in i64 range.
1311 unsafe { int_ref(ctx, f as i64) }
1312 })
1313}
1314
1315/// Re-box a `Float` after a pure transform (no fault possible). Used by
1316/// `abs`/`sqrt`/`floor`/`ceil`/`round`/`sign`.
1317unsafe fn rebox_float(ctx: *mut RuntimeContext, out: f64) -> GcRef {
1318 // SAFETY: ctx/heap valid; every f64 is a valid Float payload.
1319 unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, out) }
1320}
1321
1322/// `Float.abs()` — absolute value (§4.12).
1323///
1324/// # Safety
1325/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1326#[unsafe(no_mangle)]
1327pub unsafe extern "C" fn praxis_float_abs(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1328 abi_guard!("praxis_float_abs", ctx, {
1329 let f = unsafe { float_payload(r) };
1330 unsafe { rebox_float(ctx, f.abs()) }
1331 })
1332}
1333
1334/// `Float.sqrt()` — square root (§4.12). Negative inputs yield NaN (IEEE-754);
1335/// this never faults.
1336///
1337/// # Safety
1338/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1339#[unsafe(no_mangle)]
1340pub unsafe extern "C" fn praxis_float_sqrt(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1341 abi_guard!("praxis_float_sqrt", ctx, {
1342 let f = unsafe { float_payload(r) };
1343 unsafe { rebox_float(ctx, f.sqrt()) }
1344 })
1345}
1346
1347/// `Float.floor()` — round toward negative infinity (§4.12).
1348///
1349/// # Safety
1350/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1351#[unsafe(no_mangle)]
1352pub unsafe extern "C" fn praxis_float_floor(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1353 abi_guard!("praxis_float_floor", ctx, {
1354 let f = unsafe { float_payload(r) };
1355 unsafe { rebox_float(ctx, f.floor()) }
1356 })
1357}
1358
1359/// `Float.ceil()` — round toward positive infinity (§4.12).
1360///
1361/// # Safety
1362/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1363#[unsafe(no_mangle)]
1364pub unsafe extern "C" fn praxis_float_ceil(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1365 abi_guard!("praxis_float_ceil", ctx, {
1366 let f = unsafe { float_payload(r) };
1367 unsafe { rebox_float(ctx, f.ceil()) }
1368 })
1369}
1370
1371/// `Float.round()` — round half away from zero (§4.12, matches Rust's `f64::round`).
1372///
1373/// # Safety
1374/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1375#[unsafe(no_mangle)]
1376pub unsafe extern "C" fn praxis_float_round(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1377 abi_guard!("praxis_float_round", ctx, {
1378 let f = unsafe { float_payload(r) };
1379 unsafe { rebox_float(ctx, f.round()) }
1380 })
1381}
1382
1383/// `Float.sign()` — sign as -1.0 / 0.0 / 1.0 (§4.12). NaN yields NaN.
1384///
1385/// Not `f64::signum`: that returns `1.0` for `+0.0` and `-1.0` for `-0.0`,
1386/// because it reports the IEEE *sign bit*, not the sign of the value. Zero has
1387/// no sign in the sense `sign()` documents, so both zeros yield `0.0`.
1388///
1389/// # Safety
1390/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1391#[unsafe(no_mangle)]
1392pub unsafe extern "C" fn praxis_float_sign(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1393 abi_guard!("praxis_float_sign", ctx, {
1394 let f = unsafe { float_payload(r) };
1395 let sign = if f.is_nan() || f == 0.0 {
1396 // `f == 0.0` is true for both `+0.0` and `-0.0`; NaN falls through as
1397 // itself, which is what §4.12 specifies.
1398 f
1399 } else if f > 0.0 {
1400 1.0
1401 } else {
1402 -1.0
1403 };
1404 unsafe { rebox_float(ctx, sign) }
1405 })
1406}
1407
1408/// `Float.is_nan()` — true iff NaN (§4.12).
1409///
1410/// # Safety
1411/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1412#[unsafe(no_mangle)]
1413pub unsafe extern "C" fn praxis_float_is_nan(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1414 abi_guard!("praxis_float_is_nan", ctx, {
1415 let result = unsafe { float_payload(r) }.is_nan();
1416 // SAFETY: ctx valid; Bool immortal path.
1417 unsafe { bool_ref(ctx, result) }
1418 })
1419}
1420
1421/// `Float.is_infinite()` — true iff ±infinity (§4.12).
1422///
1423/// # Safety
1424/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1425#[unsafe(no_mangle)]
1426pub unsafe extern "C" fn praxis_float_is_infinite(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1427 abi_guard!("praxis_float_is_infinite", ctx, {
1428 let result = unsafe { float_payload(r) }.is_infinite();
1429 // SAFETY: ctx valid; Bool immortal path.
1430 unsafe { bool_ref(ctx, result) }
1431 })
1432}
1433
1434/// `Float.min(other)` — the smaller of two floats (§4.12). Per IEEE-754 /
1435/// Rust's `f64::min`: if either operand is NaN, returns the other (NaN only
1436/// propagates when both are NaN). `-0.0` is less than `+0.0`.
1437///
1438/// # Safety
1439/// `ctx` must be live and wired; both operands must be valid `Float` `GcRef`s.
1440#[unsafe(no_mangle)]
1441pub unsafe extern "C" fn praxis_float_min(
1442 ctx: *mut RuntimeContext,
1443 lhs: GcRef,
1444 rhs: GcRef,
1445) -> GcRef {
1446 abi_guard!("praxis_float_min", ctx, {
1447 let a = unsafe { float_payload(lhs) };
1448 let b = unsafe { float_payload(rhs) };
1449 unsafe { rebox_float(ctx, a.min(b)) }
1450 })
1451}
1452
1453/// `Float.max(other)` — the larger of two floats (§4.12). See `praxis_float_min`
1454/// for NaN handling.
1455///
1456/// # Safety
1457/// `ctx` must be live and wired; both operands must be valid `Float` `GcRef`s.
1458#[unsafe(no_mangle)]
1459pub unsafe extern "C" fn praxis_float_max(
1460 ctx: *mut RuntimeContext,
1461 lhs: GcRef,
1462 rhs: GcRef,
1463) -> GcRef {
1464 abi_guard!("praxis_float_max", ctx, {
1465 let a = unsafe { float_payload(lhs) };
1466 let b = unsafe { float_payload(rhs) };
1467 unsafe { rebox_float(ctx, a.max(b)) }
1468 })
1469}
1470
1471/// `Float.to_text()` — the same text `out()` writes, which is the shortest form
1472/// that reads back as the same Praxis `Float` (§4.12, ADR-083).
1473///
1474/// It goes through `scalars::write_float` rather than restating the rule,
1475/// because `to_text()` and `out()` disagreeing is a defect in itself: a program
1476/// that prints a value and a program that builds a string from it must produce
1477/// the same characters.
1478///
1479/// # Safety
1480/// `ctx` must be live and wired; `r` must be a valid `Float` `GcRef`.
1481#[unsafe(no_mangle)]
1482pub unsafe extern "C" fn praxis_float_to_text(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1483 abi_guard!("praxis_float_to_text", ctx, {
1484 let f = unsafe { float_payload(r) };
1485 let mut s = String::new();
1486 scalars::write_float(&mut s, f);
1487 // SAFETY: `s` is valid UTF-8 for the duration of the call; ctx/heap valid.
1488 unsafe { text_ref(ctx, s) }
1489 })
1490}
1491
1492/// `Int.to_text()` — the same digits `out()` writes (ADR-143).
1493///
1494/// It goes through `scalars::write_int` rather than restating the rendering,
1495/// because `to_text()` and `out()` disagreeing is a defect in itself: a program
1496/// that prints a value and a program that builds a string from it must produce
1497/// the same characters. That is the guarantee, and the shared writer is what
1498/// makes it structural rather than a thing a test happens to check.
1499///
1500/// Never faults: every `i64` renders, `i64::MIN` included.
1501///
1502/// # Safety
1503/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1504#[unsafe(no_mangle)]
1505pub unsafe extern "C" fn praxis_int_to_text(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1506 abi_guard!("praxis_int_to_text", ctx, {
1507 let v = unsafe { int_payload(r) };
1508 let mut s = String::new();
1509 scalars::write_int(&mut s, v);
1510 // SAFETY: `s` is valid UTF-8; ctx/heap valid.
1511 unsafe { text_ref(ctx, s) }
1512 })
1513}
1514
1515/// `Char.to_text()` — the one-character `Text` holding this scalar, which is the
1516/// same character `out()` writes (ADR-143).
1517///
1518/// Shares `scalars::write_char` with the descriptor's `format` callback for
1519/// [`praxis_int_to_text`]'s reason. Never faults: a `CharPayload` is a validated
1520/// Unicode scalar value by construction (ADR-086).
1521///
1522/// Reads through [`read_scalar`] with the `Char` handle rather than
1523/// `int_payload`, because a `Char` payload is **four** bytes and an `i64` read
1524/// would take eight of them — the same care [`praxis_char_to_int`] takes.
1525///
1526/// # Safety
1527/// `ctx` must be live and wired; `r` must be a valid `Char` `GcRef`.
1528#[unsafe(no_mangle)]
1529pub unsafe extern "C" fn praxis_char_to_text(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1530 abi_guard!("praxis_char_to_text", ctx, {
1531 // SAFETY: caller guarantees `r` is a valid `GcRef`; `read_scalar` proves
1532 // the descriptor is `CHAR` before reading its four bytes.
1533 let code = unsafe { read_scalar(r, scalars::CHAR_PAYLOAD) }.unwrap_or_else(|| {
1534 scalar_type_mismatch("praxis_char_to_text", "Char", r.descriptor().name)
1535 });
1536 let mut s = String::new();
1537 scalars::write_char(&mut s, code);
1538 // SAFETY: `s` is valid UTF-8; ctx/heap valid.
1539 unsafe { text_ref(ctx, s) }
1540 })
1541}
1542
1543/// `pi()` — the constant π as a `Float` (§4.12 prelude free function).
1544///
1545/// # Safety
1546/// `ctx` must be live and wired.
1547#[unsafe(no_mangle)]
1548pub unsafe extern "C" fn praxis_float_pi(ctx: *mut RuntimeContext) -> GcRef {
1549 abi_guard!("praxis_float_pi", ctx, {
1550 // SAFETY: ctx/heap valid.
1551 unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, core::f64::consts::PI) }
1552 })
1553}
1554
1555/// `e()` — Euler's number as a `Float` (§4.12 prelude free function).
1556///
1557/// # Safety
1558/// `ctx` must be live and wired.
1559#[unsafe(no_mangle)]
1560pub unsafe extern "C" fn praxis_float_e(ctx: *mut RuntimeContext) -> GcRef {
1561 abi_guard!("praxis_float_e", ctx, {
1562 // SAFETY: ctx/heap valid.
1563 unsafe { gc_alloc(ctx, scalars::FLOAT_PAYLOAD, core::f64::consts::E) }
1564 })
1565}
1566
1567// ---------------------------------------------------------------------------
1568// Checked arithmetic (§4.12). All fault rather than panic (§10.4).
1569// ---------------------------------------------------------------------------
1570
1571macro_rules! checked_int_binop {
1572 ($name:ident, $op:tt, $fault:expr_2021) => {
1573 #[doc = concat!("Checked `Int ", stringify!($op), "` (§4.12). On fault sets `pending_fault` and returns Unit.")]
1574 ///
1575 /// # Safety
1576 /// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1577 #[unsafe(no_mangle)]
1578 pub unsafe extern "C" fn $name(
1579 ctx: *mut RuntimeContext,
1580 lhs: GcRef,
1581 rhs: GcRef,
1582 ) -> GcRef {
1583 abi_guard!(stringify!($name), ctx, {
1584 let a = unsafe { int_payload(lhs) };
1585 let b = unsafe { int_payload(rhs) };
1586 match a.$op(b) {
1587 Some(result) => unsafe { int_ref(ctx, result) },
1588 None => {
1589 unsafe { set_fault(ctx, $fault) };
1590 unsafe { unit_sentinel(ctx) }
1591 }
1592 }
1593 })
1594 }
1595 };
1596}
1597
1598checked_int_binop!(praxis_int_add, checked_add, RaisedFault::INT_OVERFLOW);
1599checked_int_binop!(praxis_int_sub, checked_sub, RaisedFault::INT_OVERFLOW);
1600checked_int_binop!(praxis_int_mul, checked_mul, RaisedFault::INT_OVERFLOW);
1601
1602/// Checked `Int` division (§4.12). Faults on division by zero, and on overflow
1603/// (`Int::MIN / -1`, the one signed-division case that overflows §4.12).
1604///
1605/// # Safety
1606/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1607#[unsafe(no_mangle)]
1608pub unsafe extern "C" fn praxis_int_div(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
1609 abi_guard!("praxis_int_div", ctx, {
1610 let a = unsafe { int_payload(lhs) };
1611 let b = unsafe { int_payload(rhs) };
1612 if b == 0 {
1613 unsafe { set_fault(ctx, RaisedFault::DIV_BY_ZERO) };
1614 return unsafe { unit_sentinel(ctx) };
1615 }
1616 // `i64::MIN / -1` is the sole overflowing signed division: the mathematical
1617 // result (+2^63) is not representable, and the raw `/` panics on overflow in
1618 // debug builds (violating the no-panic-across-the-ABI rule, §10.4). Treat it
1619 // as checked-arithmetic overflow per §4.12.
1620 if a == i64::MIN && b == -1 {
1621 unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1622 return unsafe { unit_sentinel(ctx) };
1623 }
1624 // Division truncates toward zero (Rust's `i64::div_euclid` rounds differently;
1625 // Praxis follows C/Rust integer division semantics toward zero).
1626 unsafe { int_ref(ctx, a / b) }
1627 })
1628}
1629
1630/// Checked `Int` remainder (§4.12). Faults on division by zero, and on overflow
1631/// (`Int::MIN % -1`, whose result is not representable under the §4.12 rule).
1632///
1633/// # Safety
1634/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1635#[unsafe(no_mangle)]
1636pub unsafe extern "C" fn praxis_int_rem(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
1637 abi_guard!("praxis_int_rem", ctx, {
1638 let a = unsafe { int_payload(lhs) };
1639 let b = unsafe { int_payload(rhs) };
1640 if b == 0 {
1641 unsafe { set_fault(ctx, RaisedFault::DIV_BY_ZERO) };
1642 return unsafe { unit_sentinel(ctx) };
1643 }
1644 // `i64::MIN % -1`: the remainder is 0 mathematically, but the raw `%` traps
1645 // on this exact case in debug builds because the corresponding quotient
1646 // overflows. Guard it for the same no-panic reason as `praxis_int_div`.
1647 if a == i64::MIN && b == -1 {
1648 unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1649 return unsafe { unit_sentinel(ctx) };
1650 }
1651 unsafe { int_ref(ctx, a % b) }
1652 })
1653}
1654
1655/// Negate an `Int` (§4.12). Faults on overflow (`Int::MIN`).
1656///
1657/// # Safety
1658/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1659#[unsafe(no_mangle)]
1660pub unsafe extern "C" fn praxis_int_neg(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1661 abi_guard!("praxis_int_neg", ctx, {
1662 let a = unsafe { int_payload(r) };
1663 match a.checked_neg() {
1664 Some(result) => unsafe { int_ref(ctx, result) },
1665 None => {
1666 unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1667 unsafe { unit_sentinel(ctx) }
1668 }
1669 }
1670 })
1671}
1672
1673// ---------------------------------------------------------------------------
1674// §4.12's explicit overflow alternatives: three modes —
1675// `wrapping_`, `saturating_`, `checked_` — over `add`, `sub` and `mul`.
1676//
1677// §4.12 states the family and its two closures (no `_div`/`_rem`, no
1678// `_neg`/`_abs`) and is the only place that rule is written; the catalog test
1679// `the_overflow_alternative_family_is_three_modes_over_three_operators` is what
1680// enforces it. Do not restate it here.
1681//
1682// **None of the nine can fault, and that is the whole point of them** — their
1683// manifest rows are `Allocates`, so ADR-088's verifier rule means no
1684// `CheckFault` follows the call. They allocate, like every other wrapper that
1685// answers a fresh number.
1686// ---------------------------------------------------------------------------
1687
1688/// `a.wrapping_add(b)` (§4.12): two's-complement wraparound instead of a fault.
1689///
1690/// # Safety
1691/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1692#[unsafe(no_mangle)]
1693pub unsafe extern "C" fn praxis_int_wrapping_add(
1694 ctx: *mut RuntimeContext,
1695 a: GcRef,
1696 b: GcRef,
1697) -> GcRef {
1698 abi_guard!("praxis_int_wrapping_add", ctx, {
1699 let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1700 unsafe { int_ref(ctx, x.wrapping_add(y)) }
1701 })
1702}
1703
1704/// `a.saturating_add(b)` (§4.12): clamp to `Int`'s ends instead of faulting.
1705///
1706/// # Safety
1707/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1708#[unsafe(no_mangle)]
1709pub unsafe extern "C" fn praxis_int_saturating_add(
1710 ctx: *mut RuntimeContext,
1711 a: GcRef,
1712 b: GcRef,
1713) -> GcRef {
1714 abi_guard!("praxis_int_saturating_add", ctx, {
1715 let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1716 unsafe { int_ref(ctx, x.saturating_add(y)) }
1717 })
1718}
1719
1720/// `a.checked_add(b)` (§4.12): `Option[Int]` — `None` where the checked `+`
1721/// would fault.
1722///
1723/// It answers a real `Option` (ADR-076): the absence is the *answer* here, not
1724/// an error channel, which is exactly §4.7's distinction.
1725///
1726/// # Safety
1727/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1728#[unsafe(no_mangle)]
1729pub unsafe extern "C" fn praxis_int_checked_add(
1730 ctx: *mut RuntimeContext,
1731 a: GcRef,
1732 b: GcRef,
1733) -> GcRef {
1734 abi_guard!("praxis_int_checked_add", ctx, {
1735 let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1736 match x.checked_add(y) {
1737 Some(sum) => unsafe {
1738 let scope = NativeScope::new(ctx);
1739 let boxed = int_ref(ctx, sum);
1740 let rooted = scope.root(boxed);
1741 option_some(ctx, rooted.get())
1742 },
1743 None => unsafe { option_none(ctx) },
1744 }
1745 })
1746}
1747
1748/// `a.wrapping_sub(b)` (§4.12): two's-complement wraparound instead of a fault.
1749///
1750/// # Safety
1751/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1752#[unsafe(no_mangle)]
1753pub unsafe extern "C" fn praxis_int_wrapping_sub(
1754 ctx: *mut RuntimeContext,
1755 a: GcRef,
1756 b: GcRef,
1757) -> GcRef {
1758 abi_guard!("praxis_int_wrapping_sub", ctx, {
1759 let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1760 unsafe { int_ref(ctx, x.wrapping_sub(y)) }
1761 })
1762}
1763
1764/// `a.saturating_sub(b)` (§4.12): clamp to `Int`'s ends instead of faulting.
1765///
1766/// # Safety
1767/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1768#[unsafe(no_mangle)]
1769pub unsafe extern "C" fn praxis_int_saturating_sub(
1770 ctx: *mut RuntimeContext,
1771 a: GcRef,
1772 b: GcRef,
1773) -> GcRef {
1774 abi_guard!("praxis_int_saturating_sub", ctx, {
1775 let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1776 unsafe { int_ref(ctx, x.saturating_sub(y)) }
1777 })
1778}
1779
1780/// `a.checked_sub(b)` (§4.12): `Option[Int]` — `None` where the checked `-`
1781/// would fault.
1782///
1783/// # Safety
1784/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1785#[unsafe(no_mangle)]
1786pub unsafe extern "C" fn praxis_int_checked_sub(
1787 ctx: *mut RuntimeContext,
1788 a: GcRef,
1789 b: GcRef,
1790) -> GcRef {
1791 abi_guard!("praxis_int_checked_sub", ctx, {
1792 let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1793 match x.checked_sub(y) {
1794 Some(difference) => unsafe {
1795 let scope = NativeScope::new(ctx);
1796 let boxed = int_ref(ctx, difference);
1797 let rooted = scope.root(boxed);
1798 option_some(ctx, rooted.get())
1799 },
1800 None => unsafe { option_none(ctx) },
1801 }
1802 })
1803}
1804
1805/// `a.wrapping_mul(b)` (§4.12): two's-complement wraparound instead of a fault.
1806///
1807/// This is the one of the nine a program could not write for itself: with every
1808/// arithmetic operator checked and no bitwise operators in the language, there
1809/// is no in-language spelling of modular multiplication (§4.12).
1810///
1811/// # Safety
1812/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1813#[unsafe(no_mangle)]
1814pub unsafe extern "C" fn praxis_int_wrapping_mul(
1815 ctx: *mut RuntimeContext,
1816 a: GcRef,
1817 b: GcRef,
1818) -> GcRef {
1819 abi_guard!("praxis_int_wrapping_mul", ctx, {
1820 let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1821 unsafe { int_ref(ctx, x.wrapping_mul(y)) }
1822 })
1823}
1824
1825/// `a.saturating_mul(b)` (§4.12): clamp to `Int`'s ends instead of faulting.
1826///
1827/// # Safety
1828/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1829#[unsafe(no_mangle)]
1830pub unsafe extern "C" fn praxis_int_saturating_mul(
1831 ctx: *mut RuntimeContext,
1832 a: GcRef,
1833 b: GcRef,
1834) -> GcRef {
1835 abi_guard!("praxis_int_saturating_mul", ctx, {
1836 let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1837 unsafe { int_ref(ctx, x.saturating_mul(y)) }
1838 })
1839}
1840
1841/// `a.checked_mul(b)` (§4.12): `Option[Int]` — `None` where the checked `*`
1842/// would fault.
1843///
1844/// # Safety
1845/// `ctx` must be live and wired; `a` and `b` must be valid `Int` `GcRef`s.
1846#[unsafe(no_mangle)]
1847pub unsafe extern "C" fn praxis_int_checked_mul(
1848 ctx: *mut RuntimeContext,
1849 a: GcRef,
1850 b: GcRef,
1851) -> GcRef {
1852 abi_guard!("praxis_int_checked_mul", ctx, {
1853 let (x, y) = unsafe { (int_payload(a), int_payload(b)) };
1854 match x.checked_mul(y) {
1855 Some(product) => unsafe {
1856 let scope = NativeScope::new(ctx);
1857 let boxed = int_ref(ctx, product);
1858 let rooted = scope.root(boxed);
1859 option_some(ctx, rooted.get())
1860 },
1861 None => unsafe { option_none(ctx) },
1862 }
1863 })
1864}
1865
1866// ---------------------------------------------------------------------------
1867// The §16.1 numeric prelude helpers: `abs`, `sign`, `min`, `max`, `clamp`,
1868// `gcd`, `lcm`.
1869//
1870// All seven are monomorphic on `Int` (ADR-058), so every payload read here is
1871// an `Int` payload and no descriptor check is needed. `min`/`max`/`clamp` hand
1872// back one of the references they were given rather than allocating a copy:
1873// an `Int` object is immutable, so sharing it is what "the smaller of the two"
1874// means. The four that compute a *new* number allocate one, and the three that
1875// can leave the `Int` range fault rather than wrapping — `abs(Int::MIN)` has no
1876// positive counterpart, and `gcd`/`lcm` reach the same edge through it.
1877// ---------------------------------------------------------------------------
1878
1879/// `abs(n)` (§16.1). Faults on overflow: `Int::MIN` has no positive
1880/// counterpart, exactly as `praxis_int_neg` faults on it.
1881///
1882/// # Safety
1883/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1884#[unsafe(no_mangle)]
1885pub unsafe extern "C" fn praxis_int_abs(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1886 abi_guard!("praxis_int_abs", ctx, {
1887 let a = unsafe { int_payload(r) };
1888 match a.checked_abs() {
1889 Some(result) => unsafe { int_ref(ctx, result) },
1890 None => {
1891 unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
1892 unsafe { unit_sentinel(ctx) }
1893 }
1894 }
1895 })
1896}
1897
1898/// `sign(n)` (§16.1): `-1`, `0` or `1`. Total — every `Int`, `Int::MIN`
1899/// included, has a sign in range.
1900///
1901/// # Safety
1902/// `ctx` must be live and wired; `r` must be a valid `Int` `GcRef`.
1903#[unsafe(no_mangle)]
1904pub unsafe extern "C" fn praxis_int_sign(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
1905 abi_guard!("praxis_int_sign", ctx, {
1906 let a = unsafe { int_payload(r) };
1907 unsafe { int_ref(ctx, a.signum()) }
1908 })
1909}
1910
1911/// `min(a, b)` (§16.1): the smaller operand, returned as **the reference that
1912/// was passed in**. Allocates nothing.
1913///
1914/// # Safety
1915/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1916#[unsafe(no_mangle)]
1917pub unsafe extern "C" fn praxis_int_min(
1918 _ctx: *mut RuntimeContext,
1919 lhs: GcRef,
1920 rhs: GcRef,
1921) -> GcRef {
1922 abi_guard!("praxis_int_min", _ctx, {
1923 let a = unsafe { int_payload(lhs) };
1924 let b = unsafe { int_payload(rhs) };
1925 if b < a { rhs } else { lhs }
1926 })
1927}
1928
1929/// `max(a, b)` (§16.1): the larger operand, returned as **the reference that
1930/// was passed in**. Allocates nothing.
1931///
1932/// # Safety
1933/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
1934#[unsafe(no_mangle)]
1935pub unsafe extern "C" fn praxis_int_max(
1936 _ctx: *mut RuntimeContext,
1937 lhs: GcRef,
1938 rhs: GcRef,
1939) -> GcRef {
1940 abi_guard!("praxis_int_max", _ctx, {
1941 let a = unsafe { int_payload(lhs) };
1942 let b = unsafe { int_payload(rhs) };
1943 if b > a { rhs } else { lhs }
1944 })
1945}
1946
1947/// `clamp(value, low, high)` (§16.1): `value` confined to the inclusive range
1948/// `low..=high`, returned as one of the three references passed in.
1949///
1950/// **Faults when `low > high`.** The range is empty, so there is no value to
1951/// return and no answer that is not a guess — clamping to an empty range is a
1952/// mistake in the program, not in the data, and a mistake is reported rather
1953/// than answered with an invented number. (Rust's `Ord::clamp` panics on the
1954/// same input; a panic across `extern "C"` is what §10.4 forbids, so it is a
1955/// fault.) The kind is `EmptyRange` (ADR-058).
1956///
1957/// # Safety
1958/// `ctx` must be live and wired; all three operands must be valid `Int`
1959/// `GcRef`s.
1960#[unsafe(no_mangle)]
1961pub unsafe extern "C" fn praxis_int_clamp(
1962 ctx: *mut RuntimeContext,
1963 value: GcRef,
1964 low: GcRef,
1965 high: GcRef,
1966) -> GcRef {
1967 abi_guard!("praxis_int_clamp", ctx, {
1968 let v = unsafe { int_payload(value) };
1969 let lo = unsafe { int_payload(low) };
1970 let hi = unsafe { int_payload(high) };
1971 if lo > hi {
1972 unsafe { set_fault(ctx, RaisedFault::EMPTY_RANGE) };
1973 return unsafe { unit_sentinel(ctx) };
1974 }
1975 if v < lo {
1976 low
1977 } else if v > hi {
1978 high
1979 } else {
1980 value
1981 }
1982 })
1983}
1984
1985/// The non-negative greatest common divisor of two `i64`s, computed by
1986/// Euclid's algorithm **in `i128`** so that `Int::MIN`'s absolute value needs no
1987/// special case. Returns `None` only when the mathematical result is outside the
1988/// `Int` range, which happens for exactly one input pair:
1989/// `gcd(Int::MIN, Int::MIN)` is `2^63`.
1990///
1991/// `gcd(0, 0)` is `0` — the conventional answer, and the identity `gcd(n, 0) ==
1992/// abs(n)` extended to `n == 0`.
1993fn checked_gcd(a: i64, b: i64) -> Option<i64> {
1994 let mut x = (a as i128).abs();
1995 let mut y = (b as i128).abs();
1996 while y != 0 {
1997 let t = x % y;
1998 x = y;
1999 y = t;
2000 }
2001 i64::try_from(x).ok()
2002}
2003
2004/// `gcd(a, b)` (§16.1): the non-negative greatest common divisor. Faults on the
2005/// one pair whose result is out of range (`gcd(Int::MIN, Int::MIN)`).
2006///
2007/// # Safety
2008/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
2009#[unsafe(no_mangle)]
2010pub unsafe extern "C" fn praxis_int_gcd(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
2011 abi_guard!("praxis_int_gcd", ctx, {
2012 let a = unsafe { int_payload(lhs) };
2013 let b = unsafe { int_payload(rhs) };
2014 match checked_gcd(a, b) {
2015 Some(result) => unsafe { int_ref(ctx, result) },
2016 None => {
2017 unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
2018 unsafe { unit_sentinel(ctx) }
2019 }
2020 }
2021 })
2022}
2023
2024/// `lcm(a, b)` (§16.1): the non-negative least common multiple, `0` when either
2025/// operand is `0`. Faults when the result does not fit an `Int` — which it
2026/// often does not, since the product of two large operands overflows long
2027/// before their multiple does.
2028///
2029/// # Safety
2030/// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
2031#[unsafe(no_mangle)]
2032pub unsafe extern "C" fn praxis_int_lcm(ctx: *mut RuntimeContext, lhs: GcRef, rhs: GcRef) -> GcRef {
2033 abi_guard!("praxis_int_lcm", ctx, {
2034 let a = unsafe { int_payload(lhs) };
2035 let b = unsafe { int_payload(rhs) };
2036 // `lcm(n, 0)` is 0 for every n: 0 is a multiple of everything, and dividing
2037 // by the gcd below would divide by zero when both are 0.
2038 if a == 0 || b == 0 {
2039 return unsafe { int_ref(ctx, 0i64) };
2040 }
2041 // |a / gcd * b| in i128, which cannot overflow: both operands fit i64, so
2042 // the product fits i128 with room to spare. The range check is the only
2043 // thing that can refuse.
2044 let result = checked_gcd(a, b)
2045 .map(|g| ((a as i128) / (g as i128) * (b as i128)).abs())
2046 .and_then(|m| i64::try_from(m).ok());
2047 match result {
2048 Some(result) => unsafe { int_ref(ctx, result) },
2049 None => {
2050 unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
2051 unsafe { unit_sentinel(ctx) }
2052 }
2053 }
2054 })
2055}
2056
2057// ---------------------------------------------------------------------------
2058// Comparisons (yield a Bool GcRef).
2059// ---------------------------------------------------------------------------
2060
2061macro_rules! int_cmp {
2062 ($name:ident, $op:tt) => {
2063 #[doc = concat!(" `Int ", stringify!($op), "` comparison; returns a Bool GcRef (§4.12).")]
2064 ///
2065 /// # Safety
2066 /// `ctx` must be live and wired; both operands must be valid `Int` `GcRef`s.
2067 #[unsafe(no_mangle)]
2068 pub unsafe extern "C" fn $name(
2069 ctx: *mut RuntimeContext,
2070 lhs: GcRef,
2071 rhs: GcRef,
2072 ) -> GcRef {
2073 abi_guard!(stringify!($name), ctx, {
2074 let a = unsafe { int_payload(lhs) };
2075 let b = unsafe { int_payload(rhs) };
2076 let result = a $op b;
2077 // SAFETY: ctx/heap valid; Bool immortal path.
2078 unsafe { bool_ref(ctx, result) }
2079 })
2080 }
2081 };
2082}
2083
2084int_cmp!(praxis_int_eq, ==);
2085int_cmp!(praxis_int_ne, !=);
2086int_cmp!(praxis_int_lt, <);
2087int_cmp!(praxis_int_gt, >);
2088int_cmp!(praxis_int_le, <=);
2089int_cmp!(praxis_int_ge, >=);
2090
2091// ---------------------------------------------------------------------------
2092// Fault check.
2093// ---------------------------------------------------------------------------
2094
2095/// Return 1 if a fault is pending on `ctx`, else 0 (§10.4).
2096///
2097/// **Generated code does not call this.** An `Inst::CheckFault` is a load of
2098/// `ctx.pending_fault`, a load of the kind at
2099/// [`Fault::KIND_OFFSET`](crate::Fault::KIND_OFFSET) and a `brif` (ADR-102) —
2100/// the same question, without the call, the `catch_unwind` region and the
2101/// `Result` discriminant, on a path that runs once per faultable instruction.
2102///
2103/// The wrapper stays: it is the named ABI entry point for a host asking the
2104/// question from Rust (the JIT test harness does), it keeps its manifest row and
2105/// its address-table arm so `RuntimeSymbol` stays a bijection onto real
2106/// addresses, and deleting it would churn ADR-080's source-reading test for no
2107/// gain. Its two null tests are the difference between it and the inline form,
2108/// and they are why *this* is what a host with a possibly-unwired context calls.
2109///
2110/// # Safety
2111/// `ctx` must point at a live `RuntimeContext` (a null/unwired context reports
2112/// no fault rather than panicking).
2113#[unsafe(no_mangle)]
2114pub unsafe extern "C" fn praxis_check_fault(ctx: *mut RuntimeContext) -> i64 {
2115 abi_guard!("praxis_check_fault", ctx, {
2116 if ctx.is_null() {
2117 return 0;
2118 }
2119 if let Some(fault) = unsafe { (*ctx).pending_fault.as_ref() } {
2120 return fault.is_pending().into();
2121 }
2122 0
2123 })
2124}
2125
2126/// Stop the program at a `:bp` marker and show the host its frame chain (§9.8).
2127///
2128/// `span_start`/`span_end` are the marker's own source span, passed as
2129/// immediates: this is a call with no operands from the program, and boxing a
2130/// span so it could ride a `GcRef` argument would put an allocation at the one
2131/// site whose cost has to stay a single call.
2132///
2133/// Everything that makes a stop *not* a fault lives in
2134/// [`crate::breakpoint::stop`]: the host handler is given a snapshot and no
2135/// context, so it cannot allocate, cannot collect and cannot raise. That is what
2136/// lets this be declared [`Effect::Pure`](praxis_stdlib::abi::Effect::Pure), and
2137/// therefore what lets generated code emit no root spill before it and no fault
2138/// check after.
2139///
2140/// A program with no handler installed — every JIT test, every embedder that
2141/// wants none — finds nothing to call and returns.
2142///
2143/// # Safety
2144/// `ctx` must point at a live, wired `RuntimeContext` whose claimed debug frames
2145/// satisfy `copy_stack`'s contract, which every generated prologue establishes.
2146#[unsafe(no_mangle)]
2147pub unsafe extern "C" fn praxis_breakpoint(
2148 ctx: *mut RuntimeContext,
2149 span_start: u32,
2150 span_end: u32,
2151) {
2152 abi_guard!("praxis_breakpoint", ctx, {
2153 if ctx.is_null() {
2154 return;
2155 }
2156 // SAFETY: `ctx` is non-null and the caller guarantees it is live and
2157 // wired; the debug frames are the ones its prologue chain claimed.
2158 unsafe { crate::breakpoint::stop(ctx, (span_start, span_end)) };
2159 })
2160}
2161
2162/// Raise a [`FaultKind::StackOverflow`] fault on `ctx` (§9.2, §17.4). Called by
2163/// the generated prologue guard when `ctx.stack_left` is less than this frame's
2164/// [`frame_cost`](crate::frame_cost), so the host survives deep recursion
2165/// instead of overflowing the native stack. The prologue then unwinds to its
2166/// fault epilogue (pop frame + return Unit) — same path as any other fault.
2167///
2168/// # Safety
2169/// `ctx` must point at a live, wired `RuntimeContext`.
2170#[unsafe(no_mangle)]
2171pub unsafe extern "C" fn praxis_raise_stack_overflow(ctx: *mut RuntimeContext) {
2172 abi_guard!("praxis_raise_stack_overflow", ctx, {
2173 unsafe { set_fault(ctx, RaisedFault::STACK_OVERFLOW) };
2174 })
2175}
2176
2177/// Raise a [`FaultKind::EmptyCollection`] fault on `ctx` (§9.2).
2178///
2179/// `reduce`, `min_by` and `max_by` have no answer for an empty sequence: they
2180/// seed their accumulator from the first element, and there is no first
2181/// element. Handing back an unwritten accumulator slot would materialize
2182/// whatever the register held as a `GcRef` that is `NonNull` by type and
2183/// arbitrary in fact; this is the defined failure instead, and a fault is what
2184/// the other empty-collection accessors (`Deque.pop_front`, heap `pop`/`peek`)
2185/// already raise for the same reason.
2186///
2187/// Unconditional, unlike the two arithmetic raise wrappers: the emptiness test
2188/// is a branch generated code has to make anyway (the seen-flag gates the whole
2189/// sink), so there is no predicate worth passing. It returns the Unit sentinel
2190/// rather than nothing, so the MIR `Call` that emits it has an ordinary `Gc`
2191/// destination — a `Void` row would put the context pointer in a rootable slot.
2192///
2193/// # Safety
2194/// `ctx` must point at a live, wired `RuntimeContext`.
2195#[unsafe(no_mangle)]
2196pub unsafe extern "C" fn praxis_raise_empty_collection(ctx: *mut RuntimeContext) -> GcRef {
2197 abi_guard!("praxis_raise_empty_collection", ctx, {
2198 unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
2199 unsafe { unit_sentinel(ctx) }
2200 })
2201}
2202
2203/// Raise a [`FaultKind::IntOverflow`] fault on `ctx` iff `condition` is
2204/// non-zero (§4.12).
2205///
2206/// Generated code lowers `Int` arithmetic natively — `iadd`/`isub`/`imul` on
2207/// the raw scalar channel — and computes the overflow predicate inline. This is
2208/// how it reports one. It allocates nothing, so an arithmetic site is not a GC
2209/// safepoint and spills no roots.
2210///
2211/// **The call site branches; this is the cold path.** Calling unconditionally
2212/// and letting `condition` decide would keep arithmetic to a single basic
2213/// block, but a branch does not clobber registers and a call does, so it would
2214/// force a spill and reload of every live value around an arithmetic op that
2215/// never faults. The site is a `brif` to a cold block (ADR-102);
2216/// `raise_on_cold_path` in the backend carries the full argument.
2217///
2218/// The cold block passes a constant `1` — honest, since it is reached only when
2219/// the predicate held, and it keeps the test below a true statement rather than
2220/// dead code.
2221///
2222/// # Safety
2223/// `ctx` must point at a live, wired `RuntimeContext`.
2224#[unsafe(no_mangle)]
2225pub unsafe extern "C" fn praxis_raise_int_overflow_if(ctx: *mut RuntimeContext, condition: i64) {
2226 abi_guard!("praxis_raise_int_overflow_if", ctx, {
2227 if condition != 0 {
2228 unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
2229 }
2230 })
2231}
2232
2233/// Raise a [`FaultKind::DivByZero`] fault on `ctx` iff `condition` is non-zero
2234/// (§4.12). The division counterpart of [`praxis_raise_int_overflow_if`].
2235///
2236/// # Safety
2237/// `ctx` must point at a live, wired `RuntimeContext`.
2238#[unsafe(no_mangle)]
2239pub unsafe extern "C" fn praxis_raise_div_by_zero_if(ctx: *mut RuntimeContext, condition: i64) {
2240 abi_guard!("praxis_raise_div_by_zero_if", ctx, {
2241 if condition != 0 {
2242 unsafe { set_fault(ctx, RaisedFault::DIV_BY_ZERO) };
2243 }
2244 })
2245}
2246
2247// ---------------------------------------------------------------------------
2248// Collection payload accessors (§11.1, §11.4).
2249//
2250// Every collection below reaches its payload through a named accessor —
2251// `vec_payload`, `map_payload_mut`, … — and the name is the descriptor
2252// assertion the wrapper is making, which is why the nine kinds keep nine
2253// (shared, mut) pairs rather than calling `payload::<P>()` inline. The two
2254// casts underneath live here once, and each named accessor is the one line
2255// that spells its payload type.
2256// ---------------------------------------------------------------------------
2257
2258/// Read a `P` payload out of a `GcRef` as a shared ref.
2259///
2260/// # Safety
2261/// `r` must be a valid `GcRef` whose descriptor's payload type is `P`.
2262unsafe fn payload_ref<P>(r: GcRef) -> &'static P {
2263 // SAFETY: caller guarantees `r`'s payload is a `P`; the non-moving GC
2264 // (ADR-011) keeps the payload address stable for the object's lifetime. The
2265 // `'static` is unbounded because the raw FFI boundary has no lifetime to
2266 // carry; the caller (a wrapper that holds `ctx`) ensures the object outlives
2267 // the use.
2268 unsafe { &*r.payload::<P>() }
2269}
2270
2271/// Read a `P` payload out of a rooted `GcRef` as a mutable ref — the accessor
2272/// the wrappers that mutate in place go through (§11.1).
2273///
2274/// # Safety
2275/// `r` must be a valid `GcRef` whose descriptor's payload type is `P`, rooted
2276/// for `'s`.
2277unsafe fn payload_mut<'s, P>(r: Rooted<'s>) -> &'s mut P {
2278 // SAFETY: caller guarantees `r`'s payload is a `P`; the non-moving GC
2279 // (ADR-011) keeps the payload address stable for the object's lifetime, and
2280 // `Rooted` proves the object is in the collector's root set for `'s`, so a
2281 // collection triggered while this reference is held cannot reclaim what it
2282 // points at.
2283 unsafe { &mut *r.get().payload::<P>() }
2284}
2285
2286// ---------------------------------------------------------------------------
2287// Vec[T] collection methods (§11.1, §11.2, §11.5).
2288//
2289// `VecPayload` stores a growable [`ReprCVec<GcRef>`](crate::ReprCVec), so
2290// `push` mutates the existing payload in place and the receiver's `GcRef` stays
2291// valid across it. Per §11.5 reallocation safety, no interior pointer into that
2292// buffer is retained across a capacity-mutating op.
2293// ---------------------------------------------------------------------------
2294
2295/// Read the `VecPayload` out of a `GcRef` as a shared ref, asserting it is a Vec.
2296///
2297/// # Safety
2298/// `r` must be a valid `Vec` `GcRef`.
2299unsafe fn vec_payload(r: GcRef) -> &'static VecPayload {
2300 // SAFETY: caller guarantees `r` is a Vec; see `payload_ref`.
2301 unsafe { payload_ref::<VecPayload>(r) }
2302}
2303
2304/// Read the `VecPayload` out of a `GcRef` as a mutable ref, asserting it is a
2305/// Vec. Used by `push` to mutate the vector in place (§11.1).
2306///
2307/// # Safety
2308/// `r` must be a valid `Vec` `GcRef`, rooted for `'s`.
2309unsafe fn vec_payload_mut<'s>(r: Rooted<'s>) -> &'s mut VecPayload {
2310 // SAFETY: caller guarantees `r` is a Vec; see `payload_mut`.
2311 unsafe { payload_mut::<VecPayload>(r) }
2312}
2313
2314/// Build a `Vec[T]` holding `items`, with `element_descriptor` as its element
2315/// type — the shape every wrapper that answers with a collection needs.
2316///
2317/// The `Vec` is rooted across the pushes, which is the part worth having in one
2318/// place: `praxis_vec_new` allocates, and so may the caller's own iteration, so a
2319/// collection between the allocation and the last push would reclaim it.
2320///
2321/// `element_descriptor` may be **null**: the source collection's label is what
2322/// its own construction site knew, and that may have been nothing. A
2323/// `Vec`'s null means "empty" — `vec_format` reads it that way — so a null label
2324/// with members present would answer `[]`. The first member's own descriptor is
2325/// what the `Vec` adopts instead, which is exactly what `praxis_vec_push` does.
2326///
2327/// # Safety
2328/// `ctx` must be live and wired; `element_descriptor` must be a valid
2329/// `'static TypeDescriptor` or null; every item must be a valid `GcRef` whose
2330/// payload matches its own header.
2331unsafe fn vec_of(
2332 ctx: *mut RuntimeContext,
2333 element_descriptor: *const TypeDescriptor,
2334 items: impl Iterator<Item = GcRef>,
2335) -> GcRef {
2336 let items: Vec<GcRef> = items.collect();
2337 let element_descriptor = if element_descriptor.is_null() {
2338 items
2339 .first()
2340 .map_or(std::ptr::null(), |first| first.descriptor() as *const _)
2341 } else {
2342 element_descriptor
2343 };
2344 let result = unsafe { praxis_vec_new(ctx, element_descriptor) };
2345 let scope = unsafe { NativeScope::new(ctx) };
2346 let rp = unsafe { vec_payload_mut(scope.root(result)) };
2347 rp.items.extend(items);
2348 result
2349}
2350
2351/// Allocate a new empty `Vec[T]` with the given element descriptor (§11.2).
2352/// Returns a `GcRef` to a zero-length vector.
2353///
2354/// # Safety
2355/// `ctx` must be live and wired. `element_descriptor` must be a valid pointer to
2356/// a `'static TypeDescriptor`.
2357#[unsafe(no_mangle)]
2358pub unsafe extern "C" fn praxis_vec_new(
2359 ctx: *mut RuntimeContext,
2360 element_descriptor: *const TypeDescriptor,
2361) -> GcRef {
2362 abi_guard!("praxis_vec_new", ctx, {
2363 // A null descriptor is kept null: it means "the caller has no static
2364 // element type", which is a thing this payload can hold. Spelling it
2365 // `INT` instead would make an empty `Vec[Float]` claim to hold `Int`s.
2366 // SAFETY: VecPayload is VEC's payload type.
2367 unsafe {
2368 gc_alloc_owned(ctx, &crate::collections::VEC, || VecPayload {
2369 element_descriptor,
2370 items: ReprCVec::new(),
2371 })
2372 }
2373 })
2374}
2375
2376/// Allocate a `Vec[T]` of `count` slots, every one holding `fill` (ADR-146's
2377/// `Vec(n, fill)`).
2378///
2379/// Faults `InvalidSize` if `count` is negative or exceeds
2380/// [`VecExtent::MAX_ITEMS`](crate::collections::VecExtent::MAX_ITEMS): the count
2381/// arrives from source and would otherwise become a `usize` cast, where a
2382/// negative value lands near `usize::MAX` (ADR-041 decision 1).
2383///
2384/// Faults `TypeMismatch` if the caller declared an element type that the fill is
2385/// not, through the same [`adopt_or_reject`] `push` uses — a `Vec[Int]` filled
2386/// with a `Float` is a mislabelled element descriptor, and every later
2387/// `equals`/`hash`/`format` would read the payloads as the wrong type. A null
2388/// static descriptor adopts the fill's, which is what "the caller has no static
2389/// element type" already means here.
2390///
2391/// **`fill` is stored `count` times, not copied `count` times.** Every slot is
2392/// the same `GcRef`, so `Vec(3, Vec())` is three names for one inner `Vec`.
2393/// That is the language's existing reference semantics — `outer.push(a)` twice
2394/// aliases too — stated at a new site rather than a new rule (ADR-146 decision
2395/// 4).
2396///
2397/// `count` arrives boxed rather than as a `RawI64` like [`praxis_grid_new`]'s
2398/// extents: MIR lowers an argument expression to a `Gc` local, and unboxing it
2399/// there would cost an `ExtractScalar` and a second shape in the codegen's
2400/// allocation arm. `praxis_grid_new`'s two are `iconst` immediates with no local
2401/// to unbox, which is why the two wrappers differ.
2402///
2403/// # Safety
2404/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
2405/// a `'static TypeDescriptor` (or null); `count` must be a valid `Int` `GcRef`;
2406/// `fill` must be a valid `GcRef`.
2407#[unsafe(no_mangle)]
2408pub unsafe extern "C" fn praxis_vec_filled(
2409 ctx: *mut RuntimeContext,
2410 element_descriptor: *const TypeDescriptor,
2411 count: GcRef,
2412 fill: GcRef,
2413) -> GcRef {
2414 abi_guard!("praxis_vec_filled", ctx, {
2415 // SAFETY: caller guarantees `count` is a valid Int.
2416 let n = unsafe { int_payload(count) };
2417 let Some(extent) = crate::collections::VecExtent::new(n) else {
2418 unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
2419 return unsafe { unit_sentinel(ctx) };
2420 };
2421 let mut descriptor = element_descriptor;
2422 if !unsafe { adopt_or_reject(ctx, &mut descriptor, fill) } {
2423 return unsafe { unit_sentinel(ctx) };
2424 }
2425 // `fill` is a bare `GcRef` argument, and `gc_alloc_owned` may collect.
2426 // Rooting it in a native scope is what keeps it addressable across the
2427 // allocation — the caller's shadow frame roots it up to the call, and
2428 // this roots it through it.
2429 let scope = unsafe { NativeScope::new(ctx) };
2430 let fill = scope.root(fill).get();
2431 // The items are built inside the initializer, which `gc_alloc_owned`
2432 // runs *after* the safepoint: no untraced `Vec<GcRef>` is ever live
2433 // across a collection.
2434 // SAFETY: VecPayload is VEC's payload type.
2435 unsafe {
2436 gc_alloc_owned(ctx, &crate::collections::VEC, || VecPayload {
2437 element_descriptor: descriptor,
2438 items: ReprCVec::from_vec(vec![fill; extent.len()]),
2439 })
2440 }
2441 })
2442}
2443
2444/// Allocate a nominal record (§4.5) with all fields initialized to Unit.
2445/// The `schema_ptr` points at a `'static RecordSchema` (built and leaked by the
2446/// codegen from the record def). Fields are filled in declaration order via
2447/// [`praxis_record_set_field`] after allocation. Returns the record `GcRef`.
2448///
2449/// # Safety
2450/// `ctx` must be live and wired; `schema_ptr` must be a valid `'static` pointer.
2451#[unsafe(no_mangle)]
2452pub unsafe extern "C" fn praxis_alloc_record(
2453 ctx: *mut RuntimeContext,
2454 schema_ptr: *const crate::records::RecordSchema,
2455) -> GcRef {
2456 abi_guard!("praxis_alloc_record", ctx, {
2457 if schema_ptr.is_null() {
2458 // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2459 // caller already guarantees.
2460 return unsafe { unit_sentinel(ctx) };
2461 }
2462 // SAFETY: caller guarantees schema_ptr is a valid 'static pointer.
2463 let schema = unsafe { &*schema_ptr };
2464 let arity = schema.fields.len();
2465 // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2466 // caller already guarantees.
2467 let unit = unsafe { unit_sentinel(ctx) };
2468 // SAFETY: RecordPayload is RECORD's payload type.
2469 // Every field slot starts as Unit (a valid GcRef), keeping the GC sound
2470 // before the caller fills them in via praxis_record_set_field.
2471 unsafe {
2472 gc_alloc_owned(ctx, &crate::records::RECORD, || {
2473 crate::records::RecordPayload {
2474 schema: schema_ptr,
2475 items: vec![unit; arity],
2476 }
2477 })
2478 }
2479 })
2480}
2481
2482/// Set field `idx` of `record` to `value` (§4.5). Used by the codegen to
2483/// fill in fields after [`praxis_alloc_record`]. Returns the record (the
2484/// receiver is mutated in place).
2485///
2486/// # Safety
2487/// `ctx` must be live; `record` must be a valid record `GcRef`; `idx` must be
2488/// in bounds.
2489#[unsafe(no_mangle)]
2490pub unsafe extern "C" fn praxis_record_set_field(
2491 ctx: *mut RuntimeContext,
2492 record: GcRef,
2493 idx: u32,
2494 value: GcRef,
2495) -> GcRef {
2496 abi_guard!("praxis_record_set_field", ctx, {
2497 let _ = ctx;
2498 // SAFETY: caller guarantees record is a valid record GcRef.
2499 let payload = record.payload::<u8>() as *mut crate::records::RecordPayload;
2500 // SAFETY: the payload is a RecordPayload for any RECORD-descriptor object.
2501 let rp = unsafe { &mut *payload };
2502 if let Some(slot) = rp.items.get_mut(idx as usize) {
2503 *slot = value;
2504 }
2505 record
2506 })
2507}
2508
2509/// Read field `idx` out of a record `GcRef` (§4.5). Returns the field's
2510/// `GcRef` value. Returns Unit if the record is malformed or the index is out
2511/// of bounds (defensive; the type checker prevents this in well-typed code).
2512///
2513/// # Safety
2514/// `ctx` must be live; `record` must be a valid record `GcRef`.
2515#[unsafe(no_mangle)]
2516pub unsafe extern "C" fn praxis_record_field(
2517 ctx: *mut RuntimeContext,
2518 record: GcRef,
2519 idx: u32,
2520) -> GcRef {
2521 abi_guard!("praxis_record_field", ctx, {
2522 // SAFETY: caller guarantees record is a valid record GcRef; the payload is
2523 // a RecordPayload for any RECORD-descriptor object.
2524 let payload = record.payload::<u8>() as *const crate::records::RecordPayload;
2525 let rp = unsafe { &*payload };
2526 rp.items
2527 .get(idx as usize)
2528 .copied()
2529 .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2530 })
2531}
2532
2533/// Allocate an enum value (§4.6) of the type `schema_ptr` describes, with
2534/// variant `tag` and every payload slot initialized to Unit. Payload values are
2535/// filled via [`praxis_enum_set_payload`] after allocation. Returns the enum
2536/// `GcRef`.
2537///
2538/// The arity is **read from the schema** rather than passed alongside it, as
2539/// [`praxis_alloc_tuple`] already does: a schema and an arity that disagree is
2540/// a state no caller can now reach. A null schema, or a tag the schema has no
2541/// variant for, allocates nothing and answers the Unit sentinel — the same
2542/// answer `praxis_alloc_tuple` gives a null schema.
2543///
2544/// # Safety
2545/// `ctx` must be live and wired; `schema_ptr` must be null or a valid
2546/// `'static` pointer.
2547#[unsafe(no_mangle)]
2548pub unsafe extern "C" fn praxis_alloc_enum(
2549 ctx: *mut RuntimeContext,
2550 schema_ptr: *const crate::enums::EnumSchema,
2551 tag: i64,
2552) -> GcRef {
2553 abi_guard!("praxis_alloc_enum", ctx, {
2554 if schema_ptr.is_null() || tag < 0 {
2555 return unsafe { unit_sentinel(ctx) };
2556 }
2557 // SAFETY: caller guarantees schema_ptr is a valid 'static pointer.
2558 let schema = unsafe { &*schema_ptr };
2559 if schema.variant_at(tag as usize).is_none() {
2560 return unsafe { unit_sentinel(ctx) };
2561 }
2562 let arity = schema.arity_of(tag as usize);
2563 let unit = unsafe { unit_sentinel(ctx) };
2564 let items = vec![unit; arity];
2565 // SAFETY: EnumPayload is ENUM's payload type.
2566 unsafe {
2567 gc_alloc_owned(ctx, &crate::enums::ENUM, || crate::enums::EnumPayload {
2568 schema: schema_ptr,
2569 tag: tag as u32,
2570 items,
2571 })
2572 }
2573 })
2574}
2575
2576/// Allocate `Some(value)` under the runtime's own [`option_schema`].
2577///
2578/// `value` is rooted across the enum allocation: the allocation is a safepoint,
2579/// and a bare `GcRef` argument is not in anyone's root set.
2580///
2581/// [`option_schema`]: crate::enums::option_schema
2582///
2583/// # Safety
2584/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
2585pub(crate) unsafe fn option_some(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
2586 // SAFETY: the caller upholds ctx/value validity.
2587 unsafe {
2588 let scope = NativeScope::new(ctx);
2589 let rooted = scope.root(value);
2590 let some = praxis_alloc_enum(
2591 ctx,
2592 crate::enums::option_schema(),
2593 crate::enums::OPTION_SOME_TAG,
2594 );
2595 praxis_enum_set_payload(ctx, some, 0, rooted.get());
2596 some
2597 }
2598}
2599
2600/// Allocate `None` under the runtime's own `option_schema`.
2601///
2602/// # Safety
2603/// `ctx` must be live and wired.
2604pub(crate) unsafe fn option_none(ctx: *mut RuntimeContext) -> GcRef {
2605 // SAFETY: the caller upholds ctx validity.
2606 unsafe {
2607 praxis_alloc_enum(
2608 ctx,
2609 crate::enums::option_schema(),
2610 crate::enums::OPTION_NONE_TAG,
2611 )
2612 }
2613}
2614
2615/// Set payload slot `idx` of `enum_value` to `value` (§4.6). Returns the
2616/// enum value (mutated in place).
2617///
2618/// # Safety
2619/// `ctx` must be live; `enum_value` must be a valid enum `GcRef`; `idx` in bounds.
2620#[unsafe(no_mangle)]
2621pub unsafe extern "C" fn praxis_enum_set_payload(
2622 ctx: *mut RuntimeContext,
2623 enum_value: GcRef,
2624 idx: i64,
2625 value: GcRef,
2626) -> GcRef {
2627 abi_guard!("praxis_enum_set_payload", ctx, {
2628 let _ = ctx;
2629 // SAFETY: caller guarantees enum_value is a valid enum GcRef.
2630 let payload = enum_value.payload::<u8>() as *mut crate::enums::EnumPayload;
2631 let ep = unsafe { &mut *payload };
2632 if let Some(slot) = ep.items.get_mut(idx as usize) {
2633 *slot = value;
2634 }
2635 enum_value
2636 })
2637}
2638
2639/// Read the variant tag (discriminant) of an enum value (§4.6). Returns the
2640/// tag as a boxed `Int` `GcRef` (the uniform ABI convention), so the `match`
2641/// lowering can extract the scalar and compare. Used by `match` to branch.
2642///
2643/// # Safety
2644/// `ctx` must be live; `enum_value` must be a valid enum `GcRef`.
2645#[unsafe(no_mangle)]
2646pub unsafe extern "C" fn praxis_enum_tag(ctx: *mut RuntimeContext, enum_value: GcRef) -> GcRef {
2647 abi_guard!("praxis_enum_tag", ctx, {
2648 // SAFETY: caller guarantees enum_value is a valid enum GcRef.
2649 // Read the tag BEFORE allocating — the alloc below may trigger GC, and
2650 // enum_value is not explicitly rooted (it's only in a Cranelift local).
2651 let payload = enum_value.payload::<u8>() as *const crate::enums::EnumPayload;
2652 let tag = unsafe { (*payload).tag as i64 };
2653 // SAFETY: alloc boxes the i64 into a fresh Int object. The tag value is
2654 // already in a register, so GC collecting enum_value here is safe.
2655 unsafe { int_ref(ctx, tag) }
2656 })
2657}
2658
2659/// Read payload slot `idx` of an enum value (§4.6). Returns the slot's
2660/// `GcRef`. Used by `match` to bind variant payload variables.
2661///
2662/// # Safety
2663/// `ctx` must be live; `enum_value` must be a valid enum `GcRef`; `idx` in bounds.
2664#[unsafe(no_mangle)]
2665pub unsafe extern "C" fn praxis_enum_payload(
2666 ctx: *mut RuntimeContext,
2667 enum_value: GcRef,
2668 idx: i64,
2669) -> GcRef {
2670 abi_guard!("praxis_enum_payload", ctx, {
2671 // SAFETY: caller guarantees enum_value is a valid enum GcRef.
2672 let payload = enum_value.payload::<u8>() as *const crate::enums::EnumPayload;
2673 let ep = unsafe { &*payload };
2674 ep.items
2675 .get(idx as usize)
2676 .copied()
2677 .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2678 })
2679}
2680
2681/// Allocate a tuple (§4.5 structural tuples) with all element slots
2682/// initialized to Unit. The `schema_ptr` points at a `'static TupleSchema`
2683/// (built and leaked by the codegen from the tuple's element-type sequence).
2684/// Elements are filled in positional order via [`praxis_tuple_set`] after
2685/// allocation. Returns the tuple `GcRef`.
2686///
2687/// # Safety
2688/// `ctx` must be live and wired; `schema_ptr` must be a valid `'static` pointer.
2689#[unsafe(no_mangle)]
2690pub unsafe extern "C" fn praxis_alloc_tuple(
2691 ctx: *mut RuntimeContext,
2692 schema_ptr: *const crate::tuples::TupleSchema,
2693) -> GcRef {
2694 abi_guard!("praxis_alloc_tuple", ctx, {
2695 if schema_ptr.is_null() {
2696 // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2697 // caller already guarantees.
2698 return unsafe { unit_sentinel(ctx) };
2699 }
2700 // SAFETY: caller guarantees schema_ptr is a valid 'static pointer.
2701 let schema = unsafe { &*schema_ptr };
2702 let arity = schema.descriptors.len();
2703 // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2704 // caller already guarantees.
2705 let unit = unsafe { unit_sentinel(ctx) };
2706 // SAFETY: TuplePayload is TUPLE's payload type.
2707 // Every element slot starts as Unit (a valid GcRef), keeping the GC sound
2708 // before the caller fills them in via praxis_tuple_set.
2709 unsafe {
2710 gc_alloc_owned(ctx, &crate::tuples::TUPLE, || crate::tuples::TuplePayload {
2711 schema: schema_ptr,
2712 items: vec![unit; arity],
2713 })
2714 }
2715 })
2716}
2717
2718/// Set element `idx` of `tuple` to `value` (§4.5). Used by the codegen to
2719/// fill in elements after [`praxis_alloc_tuple`]. Returns the tuple (the
2720/// receiver is mutated in place).
2721///
2722/// # Safety
2723/// `ctx` must be live; `tuple` must be a valid tuple `GcRef`; `idx` in bounds.
2724#[unsafe(no_mangle)]
2725pub unsafe extern "C" fn praxis_tuple_set(
2726 ctx: *mut RuntimeContext,
2727 tuple: GcRef,
2728 idx: i64,
2729 value: GcRef,
2730) -> GcRef {
2731 abi_guard!("praxis_tuple_set", ctx, {
2732 let _ = ctx;
2733 // SAFETY: caller guarantees tuple is a valid tuple GcRef.
2734 let payload = tuple.payload::<u8>() as *mut crate::tuples::TuplePayload;
2735 // SAFETY: the payload is a TuplePayload for any TUPLE-descriptor object.
2736 let tp = unsafe { &mut *payload };
2737 if let Some(slot) = tp.items.get_mut(idx as usize) {
2738 *slot = value;
2739 }
2740 tuple
2741 })
2742}
2743
2744/// Read element `idx` out of a tuple `GcRef` (§4.5). Returns the element's
2745/// `GcRef` value. Returns Unit if the tuple is malformed or the index is out of
2746/// bounds (defensive; the type checker prevents this in well-typed code).
2747///
2748/// # Safety
2749/// `ctx` must be live; `tuple` must be a valid tuple `GcRef`.
2750#[unsafe(no_mangle)]
2751pub unsafe extern "C" fn praxis_tuple_get(
2752 ctx: *mut RuntimeContext,
2753 tuple: GcRef,
2754 idx: i64,
2755) -> GcRef {
2756 abi_guard!("praxis_tuple_get", ctx, {
2757 // SAFETY: caller guarantees tuple is a valid tuple GcRef; the payload is a
2758 // TuplePayload for any TUPLE-descriptor object.
2759 let payload = tuple.payload::<u8>() as *const crate::tuples::TuplePayload;
2760 let tp = unsafe { &*payload };
2761 tp.items
2762 .get(idx as usize)
2763 .copied()
2764 .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
2765 })
2766}
2767
2768/// Structural equality between two GC values (§5.5). Reads the descriptor
2769/// from `a` and dispatches to its `equals` callback, which recurses element/field
2770/// wise for composite types (records, tuples, enums, collections). Returns 1 for
2771/// equal, 0 for not equal. Returns 0 if `a`'s type is not equatable (functions
2772/// are never equatable, §5.5) — the type checker rejects this in well-typed code,
2773/// so this is defensive.
2774///
2775/// # Safety
2776/// `ctx` must be live and wired; `a` and `b` must be valid `GcRef`s of the same
2777/// type (the caller has already unified their types at compile time).
2778#[unsafe(no_mangle)]
2779pub unsafe extern "C" fn praxis_struct_eq(ctx: *mut RuntimeContext, a: GcRef, b: GcRef) -> i64 {
2780 abi_guard!("praxis_struct_eq", ctx, {
2781 let _ = ctx;
2782 // SAFETY: caller guarantees a is a valid GcRef; the descriptor header is
2783 // always present and its `equals` (if Some) is safe to call with a/b.
2784 let desc = a.descriptor();
2785 // Both operands must be the same runtime type before any callback runs
2786 // (ADR-045 decision 3). Well-typed code has unified them, so this is the
2787 // miscompile case — and a callback dispatched on a foreign layout is how a
2788 // type confusion becomes a wild read rather than a wrong answer.
2789 if !std::ptr::eq(desc, b.descriptor()) {
2790 return 0;
2791 }
2792 match desc.equals {
2793 // SAFETY: both a and b are values of desc's type (caller has type-checked
2794 // them equal); the equals callback is safe under that invariant.
2795 Some(eq) => {
2796 let pa = a.payload::<u8>() as *const u8;
2797 let pb = b.payload::<u8>() as *const u8;
2798 if unsafe { eq(pa, pb) } { 1 } else { 0 }
2799 }
2800 // Not equatable: treat as not-equal. The type checker rejects this in
2801 // well-typed code; the defensive default keeps runtime sound.
2802 None => 0,
2803 }
2804 })
2805}
2806
2807/// Order two GC values through their descriptor's `compare` callback (ADR-045).
2808/// Returns `-1`, `0` or `1` — the caller turns that into the `<`/`<=`/`>`/`>=`
2809/// it wanted by comparing against zero.
2810///
2811/// This is the ordering counterpart of [`praxis_struct_eq`], and it exists for
2812/// the same reason: a `Text` is a pointer-and-length structure, so ordering one
2813/// by loading its first eight payload bytes would compare *addresses*.
2814///
2815/// Raises `FaultKind::TypeMismatch` and answers `0` when the two operands are
2816/// not the same runtime type, or when the type has no `compare`. The type
2817/// checker rejects both in well-typed code (`Y006`), so reaching either is a
2818/// compiler bug — reported as a fault rather than a callback dispatched on a
2819/// foreign layout.
2820///
2821/// The second guard is a weak backstop, and deliberately named as one: ADR-138
2822/// populated `compare` on every type a `Map` key can be, including tuples and
2823/// records, so a *miscompile* that lowered `(1, 2) < (1, 3)` to this wrapper
2824/// would be answered rather than faulted. `capability::supports_ord` refuses it
2825/// at `praxis check`, so no well-typed program reaches here either way.
2826///
2827/// # Safety
2828/// `ctx` must be live and wired; `a` and `b` must be valid `GcRef`s.
2829#[unsafe(no_mangle)]
2830pub unsafe extern "C" fn praxis_value_cmp(ctx: *mut RuntimeContext, a: GcRef, b: GcRef) -> i64 {
2831 abi_guard!("praxis_value_cmp", ctx, {
2832 // SAFETY: caller guarantees both are valid GcRefs; every object carries a
2833 // descriptor in its header.
2834 let desc = a.descriptor();
2835 if !std::ptr::eq(desc, b.descriptor()) {
2836 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
2837 return 0;
2838 }
2839 let Some(compare) = desc.compare else {
2840 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
2841 return 0;
2842 };
2843 // SAFETY: both values carry `desc` (checked above), so both payloads are
2844 // values of its type.
2845 let ordering = unsafe {
2846 compare(
2847 a.payload::<u8>() as *const u8,
2848 b.payload::<u8>() as *const u8,
2849 )
2850 };
2851 match ordering {
2852 std::cmp::Ordering::Less => -1,
2853 std::cmp::Ordering::Equal => 0,
2854 std::cmp::Ordering::Greater => 1,
2855 }
2856 })
2857}
2858
2859/// `x min= candidate` on a name or a record field (ADR-161): the one of the two
2860/// to keep, answered rather than computed.
2861///
2862/// # Why this is a wrapper and the collections' updates are not
2863///
2864/// A `Map`, a `Vec`, a `Deque` and a `Grid` each own the storage their update
2865/// writes to, so the wrapper does the whole read-compare-write and answers Unit.
2866/// A `var` and a record field are storage the *compiler* owns — a MIR slot and a
2867/// field index — so there is nothing for a runtime wrapper to write. What is
2868/// left is the part the runtime is needed for at all: the ordering, which is the
2869/// value's own (ADR-045). MIR takes it from here and does the store it was
2870/// already doing for `+=`.
2871///
2872/// A tie answers `current`, which is every other updating store's rule and is
2873/// what makes `x min= v` idempotent.
2874///
2875/// # Safety
2876/// `ctx` must be live and wired; `current` and `candidate` must be valid
2877/// `GcRef`s.
2878#[unsafe(no_mangle)]
2879pub unsafe extern "C" fn praxis_value_keep_min(
2880 ctx: *mut RuntimeContext,
2881 current: GcRef,
2882 candidate: GcRef,
2883) -> GcRef {
2884 abi_guard!("praxis_value_keep_min", ctx, {
2885 // SAFETY: both are valid `GcRef`s, so each payload matches the
2886 // descriptor in its own header.
2887 if unsafe { update_cmp(candidate, current) } == std::cmp::Ordering::Less {
2888 candidate
2889 } else {
2890 current
2891 }
2892 })
2893}
2894
2895/// `x max= candidate` — the dual of [`praxis_value_keep_min`], including its tie
2896/// rule.
2897///
2898/// # Safety
2899/// As [`praxis_value_keep_min`].
2900#[unsafe(no_mangle)]
2901pub unsafe extern "C" fn praxis_value_keep_max(
2902 ctx: *mut RuntimeContext,
2903 current: GcRef,
2904 candidate: GcRef,
2905) -> GcRef {
2906 abi_guard!("praxis_value_keep_max", ctx, {
2907 // SAFETY: as `praxis_value_keep_min`.
2908 if unsafe { update_cmp(candidate, current) } == std::cmp::Ordering::Greater {
2909 candidate
2910 } else {
2911 current
2912 }
2913 })
2914}
2915
2916/// Allocate a closure value (§4.10) with `fn_ptr` as its entry point and
2917/// `n_captures` environment slots initialized to Unit. Captures are filled via
2918/// [`praxis_closure_set_capture`] after allocation. Returns the closure `GcRef`.
2919///
2920/// # Safety
2921/// `ctx` must be live and wired; `fn_ptr` must be a valid JIT'd function pointer
2922/// whose calling convention matches `fn(ctx, params..., env...) -> i64`.
2923#[unsafe(no_mangle)]
2924pub unsafe extern "C" fn praxis_alloc_closure(
2925 ctx: *mut RuntimeContext,
2926 fn_ptr: *const u8,
2927 n_captures: i64,
2928) -> GcRef {
2929 abi_guard!("praxis_alloc_closure", ctx, {
2930 // SAFETY: `ctx` is the wrapper's own context argument, whose validity the
2931 // caller already guarantees.
2932 let unit = unsafe { unit_sentinel(ctx) };
2933 let env = vec![unit; n_captures as usize];
2934 // SAFETY: ClosurePayload is CLOSURE's payload type.
2935 unsafe {
2936 gc_alloc_owned(ctx, &crate::closures::CLOSURE, || {
2937 crate::closures::ClosurePayload { fn_ptr, env }
2938 })
2939 }
2940 })
2941}
2942
2943/// Set capture slot `idx` of `closure` to `value` (§4.10). Returns the
2944/// closure (mutated in place).
2945///
2946/// # Safety
2947/// `ctx` must be live; `closure` must be a valid closure `GcRef`; `idx` in bounds.
2948#[unsafe(no_mangle)]
2949pub unsafe extern "C" fn praxis_closure_set_capture(
2950 ctx: *mut RuntimeContext,
2951 closure: GcRef,
2952 idx: i64,
2953 value: GcRef,
2954) -> GcRef {
2955 abi_guard!("praxis_closure_set_capture", ctx, {
2956 let _ = ctx;
2957 // SAFETY: caller guarantees closure is a valid closure GcRef.
2958 let payload = closure.payload::<u8>() as *mut crate::closures::ClosurePayload;
2959 let cp = unsafe { &mut *payload };
2960 if let Some(slot) = cp.env.get_mut(idx as usize) {
2961 *slot = value;
2962 }
2963 closure
2964 })
2965}
2966
2967/// Read the function pointer out of a closure `GcRef` (§4.10). Used by the
2968/// indirect-call lowering to obtain the entry point before a native call.
2969///
2970/// `ctx` is accepted (and unused) for ABI uniformity with every other `praxis_*`
2971/// wrapper — generated code calls all wrappers as `fn(ctx, args...)`, so this
2972/// keeps the calling convention consistent. The returned `*const u8` is carried
2973/// as an `i64` (pointer-sized) back into the JIT'd code.
2974///
2975/// # Safety
2976/// `ctx` must be live; `closure` must be a valid closure `GcRef`.
2977#[unsafe(no_mangle)]
2978pub unsafe extern "C" fn praxis_closure_fn_ptr(
2979 ctx: *mut RuntimeContext,
2980 closure: GcRef,
2981) -> *const u8 {
2982 abi_guard!("praxis_closure_fn_ptr", ctx, {
2983 let _ = ctx;
2984 // SAFETY: caller guarantees closure is a valid closure GcRef.
2985 let payload = closure.payload::<u8>() as *const crate::closures::ClosurePayload;
2986 unsafe { (*payload).fn_ptr }
2987 })
2988}
2989
2990/// Read capture slot `idx` out of a closure `GcRef` (§4.10). Used by the
2991/// closure's synthetic function to load its captured values from the env.
2992///
2993/// # Safety
2994/// `ctx` must be live; `closure` must be a valid closure `GcRef`; `idx` in bounds.
2995#[unsafe(no_mangle)]
2996pub unsafe extern "C" fn praxis_closure_capture(
2997 ctx: *mut RuntimeContext,
2998 closure: GcRef,
2999 idx: i64,
3000) -> GcRef {
3001 abi_guard!("praxis_closure_capture", ctx, {
3002 // SAFETY: caller guarantees closure is a valid closure GcRef.
3003 let payload = closure.payload::<u8>() as *const crate::closures::ClosurePayload;
3004 let cp = unsafe { &*payload };
3005 cp.env
3006 .get(idx as usize)
3007 .copied()
3008 .unwrap_or_else(|| unsafe { unit_sentinel(ctx) })
3009 })
3010}
3011
3012/// Allocate a `VarCell` holding `value` (§4.10). The cell is the shared
3013/// mutable storage for a captured `var` binding: the binding site and every
3014/// closure that captures the `var` refer to the same cell. Returns the cell
3015/// `GcRef`.
3016///
3017/// # Safety
3018/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
3019#[unsafe(no_mangle)]
3020pub unsafe extern "C" fn praxis_alloc_var_cell(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
3021 abi_guard!("praxis_alloc_var_cell", ctx, {
3022 // SAFETY: VarCellPayload is VAR_CELL's payload type.
3023 unsafe {
3024 gc_alloc_owned(ctx, &crate::var_cell::VAR_CELL, || {
3025 crate::var_cell::VarCellPayload { value }
3026 })
3027 }
3028 })
3029}
3030
3031/// Read the current value out of a `VarCell` (§4.10). Used by `Path`
3032/// reads of a captured `var` (the local holds the cell; this derefs it).
3033///
3034/// # Safety
3035/// `ctx` must be live; `cell` must be a valid `VarCell` `GcRef`.
3036#[unsafe(no_mangle)]
3037pub unsafe extern "C" fn praxis_var_cell_get(ctx: *mut RuntimeContext, cell: GcRef) -> GcRef {
3038 abi_guard!("praxis_var_cell_get", ctx, {
3039 let _ = ctx;
3040 // SAFETY: caller guarantees cell is a valid VarCell GcRef.
3041 let payload = cell.payload::<u8>() as *const crate::var_cell::VarCellPayload;
3042 unsafe { (*payload).value }
3043 })
3044}
3045
3046/// Store `value` into a `VarCell` (§4.10). Used by `Assign` to a
3047/// captured `var`. Returns the cell (mutated in place).
3048///
3049/// # Safety
3050/// `ctx` must be live; `cell` must be a valid `VarCell` `GcRef`; `value` valid.
3051#[unsafe(no_mangle)]
3052pub unsafe extern "C" fn praxis_var_cell_set(
3053 ctx: *mut RuntimeContext,
3054 cell: GcRef,
3055 value: GcRef,
3056) -> GcRef {
3057 abi_guard!("praxis_var_cell_set", ctx, {
3058 let _ = ctx;
3059 // SAFETY: caller guarantees cell is a valid VarCell GcRef.
3060 let payload = cell.payload::<u8>() as *mut crate::var_cell::VarCellPayload;
3061 unsafe {
3062 (*payload).value = value;
3063 }
3064 cell
3065 })
3066}
3067
3068/// Append `value` to `vec` in place (§11.1). Returns the Unit sentinel — the
3069/// receiver is mutated directly, so the caller's `GcRef` remains valid (the
3070/// `VecPayload` object does not move; only its internal buffer may grow).
3071///
3072/// # Safety
3073/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; `value`
3074/// must be a valid `GcRef` whose type matches the vector's element descriptor.
3075#[unsafe(no_mangle)]
3076pub unsafe extern "C" fn praxis_vec_push(
3077 ctx: *mut RuntimeContext,
3078 vec: GcRef,
3079 value: GcRef,
3080) -> GcRef {
3081 abi_guard!("praxis_vec_push", ctx, {
3082 // `push` may grow the Vec's backing buffer, which allocates Rust heap memory
3083 // (not GC memory). A GC collection during this would be safe (the vec
3084 // object is rooted by the caller's spilled `vec` local), but we trigger it
3085 // *before* the mutation to keep the rooting story simple: `value` is passed
3086 // by value and is not yet in the vec, so it must survive across this
3087 // collection via the caller's shadow frame.
3088 unsafe { maybe_collect(ctx) };
3089 // SAFETY: caller guarantees `vec` is a valid Vec.
3090 let scope = unsafe { NativeScope::new(ctx) };
3091 let p = unsafe { vec_payload_mut(scope.root(vec)) };
3092 // A vector that was never told its element type adopts the first pushed
3093 // value's — the `forall T. () -> Vec[T]` builtin leaves `T` generalized
3094 // until first use, so construction genuinely has nothing to record. A
3095 // vector that *was* told rejects a mismatch instead of retagging itself:
3096 // retagging would turn an explicitly typed `Vec[Int]` into a `Vec[Float]`
3097 // on one bad push, and every later `equals`/`hash`/`format` would then
3098 // read the remaining `Int` payloads as `f64`.
3099 if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3100 return unsafe { unit_sentinel(ctx) };
3101 }
3102 // Charge the spine when it grows (ADR-121; see
3103 // `Heap::charge_owned_growth`). Measured either side of the mutation
3104 // through the payload's own `owned_bytes`, so the growth policy stays
3105 // `RawVec`'s and the size formula stays the descriptor's.
3106 let before = p.owned_bytes();
3107 p.items.push(value);
3108 charge_growth(ctx, before, p.owned_bytes());
3109 unsafe { unit_sentinel(ctx) }
3110 })
3111}
3112
3113/// Reconcile a collection's element descriptor with a value about to be stored
3114/// in it: adopt the value's descriptor if the collection has none, accept if
3115/// they agree, and raise `TypeMismatch` if they do not.
3116///
3117/// Returns whether the store may proceed. Descriptors are `static`, so pointer
3118/// identity is the authoritative test (ADR-038).
3119///
3120/// # Safety
3121/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
3122unsafe fn adopt_or_reject(
3123 ctx: *mut RuntimeContext,
3124 element_descriptor: &mut *const TypeDescriptor,
3125 value: GcRef,
3126) -> bool {
3127 let pushed = value.descriptor();
3128 if element_descriptor.is_null() {
3129 *element_descriptor = pushed;
3130 return true;
3131 }
3132 if std::ptr::eq(*element_descriptor, pushed) {
3133 return true;
3134 }
3135 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3136 false
3137}
3138
3139/// The number of elements in `vec`, as a boxed `Int` (§11.1).
3140///
3141/// # Safety
3142/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3143#[unsafe(no_mangle)]
3144pub unsafe extern "C" fn praxis_vec_len(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3145 abi_guard!("praxis_vec_len", ctx, {
3146 // SAFETY: caller guarantees `vec` is a valid Vec.
3147 let p = unsafe { vec_payload(vec) };
3148 let len = p.items.len() as i64;
3149 // len allocates the returned Int, but the input vec is still live via `vec`.
3150 unsafe { int_ref(ctx, len) }
3151 })
3152}
3153
3154/// The element at `index`, or an `IndexOutOfBounds` fault if out of range
3155/// (§9.2, §11.1). Returns the Unit sentinel on fault.
3156///
3157/// # Safety
3158/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; `index`
3159/// must be a valid `Int` `GcRef`.
3160#[unsafe(no_mangle)]
3161pub unsafe extern "C" fn praxis_vec_get(
3162 ctx: *mut RuntimeContext,
3163 vec: GcRef,
3164 index: GcRef,
3165) -> GcRef {
3166 abi_guard!("praxis_vec_get", ctx, {
3167 // SAFETY: caller guarantees `vec` is a valid Vec.
3168 let p = unsafe { vec_payload(vec) };
3169 // SAFETY: caller guarantees `index` is a valid Int.
3170 let idx = unsafe { int_payload(index) };
3171 // SAFETY: `abi_guard!` established that `ctx` is live and wired.
3172 let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3173 return unsafe { unit_sentinel(ctx) };
3174 };
3175 // Return the element by value (a copy of the GcRef). No allocation, so no
3176 // collection is needed; the vec stays live via `vec`.
3177 p.items[idx]
3178 })
3179}
3180
3181/// Replace the element at `index`; faults `IndexOutOfBounds` if out of range
3182/// (§9.2, §11.1). Returns the Unit sentinel.
3183///
3184/// **Replaces, and never appends.** `v[v.len()] = x` is out of range rather than
3185/// a push, which is `praxis_vec_push`'s job: a store whose index decides between
3186/// the two operations makes an off-by-one grow the vector instead of reporting.
3187///
3188/// The element descriptor goes through the same [`adopt_or_reject`] every push
3189/// does, so a store into a vector that was never told its element type adopts
3190/// the first value's, and one into a `Vec[Int]` raises `TypeMismatch` rather
3191/// than retagging the collection — `push`'s rule, at the second door.
3192///
3193/// # Safety
3194/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; `index`
3195/// must be a valid `Int` `GcRef`; `value` must be a valid `GcRef`.
3196#[unsafe(no_mangle)]
3197pub unsafe extern "C" fn praxis_vec_set(
3198 ctx: *mut RuntimeContext,
3199 vec: GcRef,
3200 index: GcRef,
3201 value: GcRef,
3202) -> GcRef {
3203 abi_guard!("praxis_vec_set", ctx, {
3204 // SAFETY: caller guarantees `vec` is a valid Vec.
3205 let scope = unsafe { NativeScope::new(ctx) };
3206 let p = unsafe { vec_payload_mut(scope.root(vec)) };
3207 // SAFETY: caller guarantees `index` is a valid Int.
3208 let idx = unsafe { int_payload(index) };
3209 // SAFETY: `abi_guard!` established that `ctx` is live and wired.
3210 let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3211 return unsafe { unit_sentinel(ctx) };
3212 };
3213 if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3214 return unsafe { unit_sentinel(ctx) };
3215 }
3216 // No allocation: the slot takes a `GcRef` the caller already holds.
3217 p.items[idx] = value;
3218 unsafe { unit_sentinel(ctx) }
3219 })
3220}
3221
3222/// `v[i] min= candidate` and `v[i] max= candidate` (ADR-161): keep whichever of
3223/// the two the operator asks for, at an index that must already exist.
3224///
3225/// # An indexed receiver has no absent entry, so it has no first value to accept
3226///
3227/// §6.2's "an absent entry accepts the first value" is the `Map` clause and it
3228/// has no analogue here: a `Vec` index is in range or it is out of range, and
3229/// out of range is the `IndexOutOfBounds` [`praxis_vec_set`] already raises. So
3230/// the row exists for the *comparison* rather than for the insert — `v[i] min= x`
3231/// is `v[i] = min(v[i], x)` with the place evaluated once and the ordering taken
3232/// from the element's own descriptor.
3233///
3234/// `keep` is the ordering the candidate must have against the incumbent for the
3235/// store to happen: `Less` is `min=` and `Greater` is `max=`. A tie keeps the
3236/// incumbent, as [`praxis_map_update_min`]'s does and for its reason.
3237///
3238/// # Safety
3239/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`, `index` a
3240/// valid `Int`, and `value` of the vector's element type.
3241unsafe fn vec_update(
3242 ctx: *mut RuntimeContext,
3243 vec: GcRef,
3244 index: GcRef,
3245 value: GcRef,
3246 keep: std::cmp::Ordering,
3247) -> GcRef {
3248 // SAFETY: the caller guarantees `vec` is a valid Vec.
3249 let scope = unsafe { NativeScope::new(ctx) };
3250 let p = unsafe { vec_payload_mut(scope.root(vec)) };
3251 // SAFETY: the caller guarantees `index` is a valid Int.
3252 let idx = unsafe { int_payload(index) };
3253 // SAFETY: `abi_guard!` established that `ctx` is live and wired.
3254 let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
3255 return unsafe { unit_sentinel(ctx) };
3256 };
3257 // Before the comparison, exactly as the plain store does it: a candidate of
3258 // the wrong type is rejected whether or not it would have won.
3259 if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3260 return unsafe { unit_sentinel(ctx) };
3261 }
3262 // SAFETY: both are elements of this vector, so each payload matches the
3263 // descriptor in its own header.
3264 if unsafe { update_cmp(value, p.items[idx]) } == keep {
3265 p.items[idx] = value;
3266 }
3267 unsafe { unit_sentinel(ctx) }
3268}
3269
3270/// `v[i] min= candidate` — keep the smaller of the two. See [`vec_update`].
3271///
3272/// # Safety
3273/// As [`vec_update`].
3274#[unsafe(no_mangle)]
3275pub unsafe extern "C" fn praxis_vec_update_min(
3276 ctx: *mut RuntimeContext,
3277 vec: GcRef,
3278 index: GcRef,
3279 value: GcRef,
3280) -> GcRef {
3281 abi_guard!("praxis_vec_update_min", ctx, {
3282 unsafe { vec_update(ctx, vec, index, value, std::cmp::Ordering::Less) }
3283 })
3284}
3285
3286/// `v[i] max= candidate` — keep the larger of the two. See [`vec_update`].
3287///
3288/// # Safety
3289/// As [`vec_update`].
3290#[unsafe(no_mangle)]
3291pub unsafe extern "C" fn praxis_vec_update_max(
3292 ctx: *mut RuntimeContext,
3293 vec: GcRef,
3294 index: GcRef,
3295 value: GcRef,
3296) -> GcRef {
3297 abi_guard!("praxis_vec_update_max", ctx, {
3298 unsafe { vec_update(ctx, vec, index, value, std::cmp::Ordering::Greater) }
3299 })
3300}
3301
3302/// True iff `vec` has no elements, as a boxed `Bool` (§11.1).
3303///
3304/// # Safety
3305/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3306#[unsafe(no_mangle)]
3307pub unsafe extern "C" fn praxis_vec_is_empty(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3308 abi_guard!("praxis_vec_is_empty", ctx, {
3309 // SAFETY: caller guarantees `vec` is a valid Vec.
3310 let p = unsafe { vec_payload(vec) };
3311 let empty = p.items.is_empty();
3312 // SAFETY: ctx/heap valid; Bool immortal path.
3313 unsafe { bool_ref(ctx, empty) }
3314 })
3315}
3316
3317// --- the §6.3 barrier combinators ------------------------------------------
3318//
3319// A *barrier* is a pipeline stage that cannot be fused into the loop feeding it
3320// because it needs the whole sequence before it can answer its first element:
3321// `sorted` has to see the largest element before it knows the smallest is first.
3322// So each is a real runtime call over a materialized `Vec` rather than an
3323// intrinsic the MIR fuser expands, and the fuser's own recognizer already knows
3324// to end a chain at one and start a fresh chain from its result.
3325//
3326// All three rebuild through [`vec_of`] rather than mutating the receiver, which
3327// is the shape `praxis_set_items` and `praxis_counter_keys` already use.
3328// `v.sorted()` is an expression, not a statement: §6.3 lists it beside `map` and
3329// `filter`, and a caller that also holds `v` must still see `v`'s own order.
3330
3331/// `v.sorted()` — the elements of `vec` in ascending order, as a **new** `Vec`
3332/// (§6.3). The receiver is not touched.
3333///
3334/// Ordering goes through the element descriptor's `compare` callback — the same
3335/// callback [`praxis_value_cmp`] uses, and for the same reason: a `Text` is a
3336/// pointer-and-length structure, so ordering one by its first eight payload
3337/// bytes compares *addresses*. That sorts `Vec[Int]` correctly and `Vec[Text]`
3338/// into allocation order, which is the failure that looks like it works.
3339///
3340/// The sort is **stable**, so equal elements keep their input order and the
3341/// answer is a function of the input alone.
3342///
3343/// Raises `FaultKind::TypeMismatch` and answers Unit when the elements are not
3344/// all one type, or when that type has no `compare`. The catalog row's `Ord`
3345/// bound (ADR-093, `Bound::Kind`) rejects both at `praxis check`, so reaching
3346/// either is a compiler bug — reported as a fault rather than as a callback
3347/// dispatched on a foreign layout. As [`praxis_value_cmp`], the second guard
3348/// covers fewer types since ADR-138 populated `compare` on the composites.
3349///
3350/// It is the callback a `Set` and a `Map` order their keys through too
3351/// (ADR-138), which is what makes `out(s)` and `out(s.sorted())` print one
3352/// sequence rather than one numeric and one lexicographic.
3353///
3354/// # Safety
3355/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3356#[unsafe(no_mangle)]
3357pub unsafe extern "C" fn praxis_vec_sorted(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3358 abi_guard!("praxis_vec_sorted", ctx, {
3359 // SAFETY: caller guarantees `vec` is a valid Vec.
3360 let p = unsafe { vec_payload(vec) };
3361 let mut items: Vec<GcRef> = p.items.to_vec();
3362 // Nothing to order, and nothing to check: a zero- or one-element Vec has
3363 // no pair to compare, so an empty `Vec[fn(Int) -> Int]` sorts rather than
3364 // faulting on a `compare` it would never have called.
3365 if items.len() > 1 {
3366 // The elements' *own* descriptors decide, not the Vec's label: the
3367 // label may be null (the construction site knew no element type)
3368 // while every member is a perfectly good `Text`.
3369 let desc = items[0].descriptor();
3370 if !items.iter().all(|i| std::ptr::eq(i.descriptor(), desc)) {
3371 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3372 return unsafe { unit_sentinel(ctx) };
3373 }
3374 let Some(compare) = desc.compare else {
3375 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3376 return unsafe { unit_sentinel(ctx) };
3377 };
3378 items.sort_by(|a, b| {
3379 // SAFETY: every element carries `desc` (checked above), so both
3380 // payloads are values of its type; the non-moving GC keeps them
3381 // stable, and `sort_by` allocates nothing that could collect.
3382 unsafe {
3383 compare(
3384 a.payload::<u8>() as *const u8,
3385 b.payload::<u8>() as *const u8,
3386 )
3387 }
3388 });
3389 }
3390 unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
3391 })
3392}
3393
3394/// `v.sorted_by_key(f)` — the elements of `vec` ordered by the key `f` extracts,
3395/// as a **new** `Vec` (§6.3, ADR-127 decision 5). The receiver is not touched.
3396///
3397/// # Why this row exists, and why it is not `sorted_by`
3398///
3399/// ADR-045 decided that no composite is orderable, so the moment a pipeline's
3400/// item is a pair — which is the moment its source is a `Map` or a `Counter` —
3401/// `sorted` is unavailable and "the five most common values" has no spelling.
3402/// The closure extracts an orderable key from an item that is not.
3403///
3404/// Not a `(T, T) -> Bool` comparator: `min_by`/`max_by` already own the
3405/// less-than-predicate shape, and a comparator is O(n log n) calls back into
3406/// JIT'd code where a key extractor is n.
3407///
3408/// **Decorate–sort–undecorate.** Every key is extracted once, up front, and the
3409/// sort orders the (key, element) pairs — which is what makes it n calls. The
3410/// keys are held in a `Vec<GcRef>` the collector cannot see, so they are rooted
3411/// in a native scope: extraction allocates, and a collection triggered by the
3412/// *next* call would otherwise free the key the previous one produced.
3413///
3414/// Ordering goes through the same `compare` callback [`praxis_value_cmp`] uses,
3415/// so the `Ord` bound the catalog row puts on the *key* is the rule this
3416/// enforces. The sort is **stable**, so items with equal keys keep their input
3417/// order and the answer is a function of the input alone.
3418///
3419/// Raises `FaultKind::TypeMismatch` and answers Unit when the keys are not all
3420/// one type, or when that type has no ordering; a fault the closure itself
3421/// raised stops the sort and is left for the call site's own check.
3422///
3423/// # Safety
3424/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `key`
3425/// a valid closure `GcRef`.
3426#[unsafe(no_mangle)]
3427pub unsafe extern "C" fn praxis_vec_sorted_by_key(
3428 ctx: *mut RuntimeContext,
3429 vec: GcRef,
3430 key: GcRef,
3431) -> GcRef {
3432 abi_guard!("praxis_vec_sorted_by_key", ctx, {
3433 let scope = unsafe { NativeScope::new(ctx) };
3434 // The receiver is rooted explicitly: its items are read into a Rust
3435 // `Vec` and held across one closure call *per element*, which is a far
3436 // longer window than a single-allocation wrapper's.
3437 let _receiver = scope.root(vec);
3438 // SAFETY: caller guarantees `vec` is a valid Vec.
3439 let p = unsafe { vec_payload(vec) };
3440 let element_descriptor = p.element_descriptor;
3441 let items: Vec<GcRef> = p.items.to_vec();
3442 for item in &items {
3443 scope.root(*item);
3444 }
3445 // Decorate: one call per element, keys rooted as they arrive.
3446 let mut decorated: Vec<(GcRef, GcRef)> = Vec::with_capacity(items.len());
3447 for item in items {
3448 let Some(k) = (unsafe { call_unary_closure(ctx, key, item) }) else {
3449 // The closure faulted (or is not a closure, which the type
3450 // checker already refused). Its answer is the Unit sentinel, so
3451 // sorting on it would order garbage; stop and leave the fault
3452 // for the call site.
3453 return unsafe { unit_sentinel(ctx) };
3454 };
3455 decorated.push((scope.root(k).get(), item));
3456 }
3457 if decorated.len() > 1 {
3458 // The keys' *own* descriptors decide, as `praxis_vec_sorted`'s
3459 // elements' do: the source Vec's label says nothing about them.
3460 let desc = decorated[0].0.descriptor();
3461 if !decorated
3462 .iter()
3463 .all(|(k, _)| std::ptr::eq(k.descriptor(), desc))
3464 {
3465 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3466 return unsafe { unit_sentinel(ctx) };
3467 }
3468 let Some(compare) = desc.compare else {
3469 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3470 return unsafe { unit_sentinel(ctx) };
3471 };
3472 decorated.sort_by(|(a, _), (b, _)| {
3473 // SAFETY: every key carries `desc` (checked above), so both
3474 // payloads are values of its type; the non-moving GC keeps them
3475 // stable, and `sort_by` allocates nothing that could collect.
3476 unsafe {
3477 compare(
3478 a.payload::<u8>() as *const u8,
3479 b.payload::<u8>() as *const u8,
3480 )
3481 }
3482 });
3483 }
3484 // Undecorate.
3485 unsafe {
3486 vec_of(
3487 ctx,
3488 element_descriptor,
3489 decorated.into_iter().map(|(_, item)| item),
3490 )
3491 }
3492 })
3493}
3494
3495/// Call a `(T) -> U` Praxis closure with one argument, or `None` if it faulted —
3496/// or if it is not a closure at all.
3497///
3498/// The descriptor is checked rather than assumed: the type checker says the
3499/// operand is a function and the only runtime representation of one is a closure
3500/// object, but the alternative to a `TypeMismatch` fault is transmuting whatever
3501/// the payload's first word happens to be into a function pointer and jumping to
3502/// it.
3503///
3504/// # Safety
3505/// `ctx` must be live and wired; `closure` and `arg` must be valid `GcRef`s.
3506unsafe fn call_unary_closure(
3507 ctx: *mut RuntimeContext,
3508 closure: GcRef,
3509 arg: GcRef,
3510) -> Option<GcRef> {
3511 if !std::ptr::eq(closure.descriptor(), &crate::closures::CLOSURE) {
3512 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3513 return None;
3514 }
3515 // SAFETY: the descriptor check proves the payload is a `ClosurePayload`, so
3516 // `fn_ptr` is the entry point the codegen wrote there.
3517 let fn_ptr = unsafe { (*closure.payload::<crate::closures::ClosurePayload>()).fn_ptr };
3518 // A closure's entry point is `fn(ctx, closure_self, params...) -> GcRef`
3519 // (§4.10, Approach B): the closure value itself is a hidden first argument,
3520 // and the prologue loads its captures from it.
3521 //
3522 // SAFETY: `fn_ptr` is a finalized JIT entry whose parameter count is the one
3523 // the type checker enforced for this operand; every value crossing is a
3524 // `GcRef`, which is the ABI's only value kind.
3525 let result = unsafe {
3526 let f: unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef) -> GcRef =
3527 std::mem::transmute(fn_ptr);
3528 f(ctx, closure, arg)
3529 };
3530 // The closure ran arbitrary Praxis code and may have faulted; its result on
3531 // that path is the Unit sentinel.
3532 if unsafe { praxis_check_fault(ctx) } != 0 {
3533 return None;
3534 }
3535 Some(result)
3536}
3537
3538/// `v.unique()` — the elements of `vec` with later duplicates dropped, as a
3539/// **new** `Vec`, in first-occurrence order (§6.3). The receiver is not touched.
3540///
3541/// First-occurrence order rather than sorted-and-deduped: `unique` is listed
3542/// separately from `sorted` in §6.3, so composing them has to be the user's
3543/// choice, and an order that depends on a hash map's iteration would make the
3544/// same program answer differently on two runs — and here the order is the
3545/// program's *answer*, not only its printing.
3546///
3547/// Sameness is [`DynamicKey`]'s — the descriptor's `hash` and `equals`
3548/// callbacks, which is what "the same value" means everywhere else in this
3549/// runtime (§5.5, §11.3). The catalog row's `HashStable` bound is what keeps a
3550/// mutable element out; a key that can change after it is stored cannot be found
3551/// again.
3552///
3553/// # Safety
3554/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3555#[unsafe(no_mangle)]
3556pub unsafe extern "C" fn praxis_vec_unique(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3557 abi_guard!("praxis_vec_unique", ctx, {
3558 // SAFETY: caller guarantees `vec` is a valid Vec.
3559 let p = unsafe { vec_payload(vec) };
3560 let mut seen: std::collections::HashSet<DynamicKey> = std::collections::HashSet::new();
3561 let mut kept: Vec<GcRef> = Vec::new();
3562 for item in &p.items {
3563 if seen.insert(DynamicKey::new(*item)) {
3564 kept.push(*item);
3565 }
3566 }
3567 unsafe { vec_of(ctx, p.element_descriptor, kept.into_iter()) }
3568 })
3569}
3570
3571/// `v.reversed()` — the elements of `vec` in the opposite order, as a **new**
3572/// `Vec` (ADR-145). The receiver is not touched.
3573///
3574/// A barrier for `praxis_vec_sorted`'s reason and not a fused stage: reversal
3575/// cannot answer its first element until it has seen the last one.
3576///
3577/// It reads **no descriptor callback** — not `compare`, not `equals`, not
3578/// `hash` — so unlike `sorted` and `unique` there is no element it can be handed
3579/// that it cannot reverse, and its catalog row carries no capability bound. That
3580/// is why the manifest row is `Allocates` and there is no `TypeMismatch` path
3581/// here to read.
3582///
3583/// The element label is copied through unchanged, the null a construction site
3584/// that knew no element type leaves included.
3585///
3586/// # Safety
3587/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3588#[unsafe(no_mangle)]
3589pub unsafe extern "C" fn praxis_vec_reversed(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3590 abi_guard!("praxis_vec_reversed", ctx, {
3591 // SAFETY: caller guarantees `vec` is a valid Vec.
3592 let p = unsafe { vec_payload(vec) };
3593 let items: Vec<GcRef> = p.items.iter().rev().copied().collect();
3594 unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
3595 })
3596}
3597
3598/// The group size `chunks(n)` and `windows(n)` share, or `None` when `n` names no
3599/// group at all (ADR-149).
3600///
3601/// **The only thing either wrapper refuses.** A run of zero elements is not a
3602/// short run — chunking a non-empty sequence into them has no finite answer, and
3603/// sliding one along it has no useful one — and a negative run names nothing.
3604/// Every other `n` has an answer, including one larger than the receiver: a
3605/// `chunks` wider than the sequence is one short chunk, a `windows` wider than it
3606/// is no windows. So this returns an `Option` of a size rather than clamping to
3607/// one, and the two callers spell those two answers themselves.
3608///
3609/// There is no upper bound here and none is missing. Both results are *shorter*
3610/// than the receiver — one group per start position at most — so neither can
3611/// ask for an extent [`VecExtent`](crate::collections::VecExtent) would refuse,
3612/// which is the bound `praxis_vec_filled` needs and these do not.
3613fn group_size(n: i64) -> Option<usize> {
3614 if n <= 0 {
3615 return None;
3616 }
3617 usize::try_from(n).ok()
3618}
3619
3620/// The `Vec[Vec[T]]` both groupings answer, built from the half-open source
3621/// ranges `groups` names (ADR-149).
3622///
3623/// **The outer label is `collections::VEC` at every length, and it is *passed*
3624/// rather than inferred** (ADR-149 decision 1). Which label belongs there is not
3625/// this wrapper's choice — `outer.push(inner)` builds a `Vec[Vec[T]]` today and
3626/// `adopt_or_reject` labels it `VEC`, so anything else would disagree with
3627/// `push`. What is chosen here is only that it is written down: letting
3628/// [`vec_of`] infer it from the first group would answer `VEC` for
3629/// `[1].chunks(2)` and *null* for `[].chunks(2)` — one type with two labels, and
3630/// the null is the one `vec_format` renders as `[]`.
3631///
3632/// That is [`praxis_grid_positions`]'s argument, not a new one: it passes
3633/// `&tuples::TUPLE` for the same reason, and `Grid(0, 0, 1).positions()` is the
3634/// same empty case. Naming the label is what a wrapper does whenever its result's
3635/// element kind is not its receiver's.
3636///
3637/// The inner labels *are* the receiver's own, passed through unchanged the way
3638/// `praxis_vec_reversed` passes its one through, null included.
3639///
3640/// # Safety
3641/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`; every
3642/// range `groups` yields must lie within its length.
3643unsafe fn vec_of_groups(
3644 ctx: *mut RuntimeContext,
3645 vec: GcRef,
3646 groups: impl Iterator<Item = (usize, usize)>,
3647) -> GcRef {
3648 // SAFETY: caller guarantees `vec` is a valid Vec.
3649 let element_descriptor = unsafe { vec_payload(vec) }.element_descriptor;
3650 let outer = unsafe { praxis_vec_new(ctx, &crate::collections::VEC as *const _) };
3651 let scope = unsafe { NativeScope::new(ctx) };
3652 let op = unsafe { vec_payload_mut(scope.root(outer)) };
3653 for (start, end) in groups {
3654 // The elements are read out of the receiver, which the caller's shadow
3655 // frame roots across this call, so the untraced `Vec<GcRef>` below holds
3656 // nothing a collection inside `vec_of` could reclaim — the receiver
3657 // holds every one of them too. That is `praxis_vec_unique`'s argument at
3658 // a second site, and it is why the *groups* are what need rooting and
3659 // the items are not: an inner `Vec` is reachable from nothing until it
3660 // is pushed, which is why it is pushed before the next one is built.
3661 //
3662 // SAFETY: caller guarantees `vec` is a valid Vec and that `start..end`
3663 // lies within its length.
3664 let items: Vec<GcRef> = unsafe { vec_payload(vec) }.items[start..end].to_vec();
3665 let inner = unsafe { vec_of(ctx, element_descriptor, items.into_iter()) };
3666 op.items.push(inner);
3667 }
3668 outer
3669}
3670
3671/// `seq.chunks(n)` — these elements in consecutive non-overlapping runs of `n`,
3672/// the last short if the length does not divide (ADR-149). The receiver is not
3673/// touched.
3674///
3675/// `[1, 2, 3, 4, 5].chunks(2)` is `[[1, 2], [3, 4], [5]]`. An empty receiver
3676/// answers `[]` at any size, and an `n` at or above the length answers one chunk
3677/// holding everything.
3678///
3679/// Raises `FaultKind::InvalidSize` and answers Unit when `n` is not positive —
3680/// [`group_size`] has the reason, and it is the wrapper's whole faulting
3681/// surface. It reads **no descriptor callback**, so unlike `sorted` there is no
3682/// element it can be handed that it cannot group.
3683///
3684/// # Safety
3685/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `n` a
3686/// valid `Int` `GcRef`.
3687#[unsafe(no_mangle)]
3688pub unsafe extern "C" fn praxis_vec_chunks(
3689 ctx: *mut RuntimeContext,
3690 vec: GcRef,
3691 n: GcRef,
3692) -> GcRef {
3693 abi_guard!("praxis_vec_chunks", ctx, {
3694 // SAFETY: caller guarantees `n` is a valid Int.
3695 let Some(size) = group_size(unsafe { int_payload(n) }) else {
3696 unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
3697 return unsafe { unit_sentinel(ctx) };
3698 };
3699 // SAFETY: caller guarantees `vec` is a valid Vec.
3700 let len = unsafe { vec_payload(vec) }.items.len();
3701 // Every `size`th position starts a chunk; the last one stops at the end
3702 // rather than past it, which is the short tail.
3703 let groups = (0..len)
3704 .step_by(size)
3705 .map(move |s| (s, (s + size).min(len)));
3706 unsafe { vec_of_groups(ctx, vec, groups) }
3707 })
3708}
3709
3710/// `seq.windows(n)` — every consecutive run of exactly `n`, each starting one
3711/// element after the last (ADR-149). The receiver is not touched.
3712///
3713/// `[1, 2, 3, 4].windows(2)` is `[[1, 2], [2, 3], [3, 4]]`. Elements are shared,
3714/// not copied: the `2` in the first window and the `2` in the second are one
3715/// object, which is the language's reference semantics rather than a rule of
3716/// this wrapper.
3717///
3718/// **A window that does not fit is dropped rather than shortened**, which is the
3719/// one place this and [`praxis_vec_chunks`] answer differently: `[1, 2].windows(5)`
3720/// is `[]`, because a run of five is a run of five. It is not the fault below
3721/// arriving late — "which runs of five are there" has an answer for a sequence
3722/// of two, and that answer is none.
3723///
3724/// Raises `FaultKind::InvalidSize` and answers Unit when `n` is not positive,
3725/// for [`praxis_vec_chunks`]'s reason.
3726///
3727/// # Safety
3728/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `n` a
3729/// valid `Int` `GcRef`.
3730#[unsafe(no_mangle)]
3731pub unsafe extern "C" fn praxis_vec_windows(
3732 ctx: *mut RuntimeContext,
3733 vec: GcRef,
3734 n: GcRef,
3735) -> GcRef {
3736 abi_guard!("praxis_vec_windows", ctx, {
3737 // SAFETY: caller guarantees `n` is a valid Int.
3738 let Some(size) = group_size(unsafe { int_payload(n) }) else {
3739 unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
3740 return unsafe { unit_sentinel(ctx) };
3741 };
3742 // SAFETY: caller guarantees `vec` is a valid Vec.
3743 let len = unsafe { vec_payload(vec) }.items.len();
3744 // Written as a subtraction guarded by its own comparison rather than a
3745 // `saturating_sub`: `len - size` saturating to zero would answer *one*
3746 // window for a receiver too short to hold any, and the empty answer is
3747 // the whole point of the branch.
3748 let starts = if size <= len { len - size + 1 } else { 0 };
3749 let groups = (0..starts).map(move |s| (s, s + size));
3750 unsafe { vec_of_groups(ctx, vec, groups) }
3751 })
3752}
3753
3754/// `seq.join(sep)` — these `Text` elements concatenated with `sep` between them
3755/// (ADR-144). An empty sequence answers `""`; a one-element sequence answers
3756/// that element's characters and no separator.
3757///
3758/// Raises `FaultKind::TypeMismatch` and answers Unit when an element is not a
3759/// `Text`. The catalog row bounds the item to `Text`, so reaching that is a
3760/// compiler bug — reported the way `praxis_vec_sorted` reports its own, rather
3761/// than reading a foreign payload as a pointer-and-length pair.
3762///
3763/// This does **not** render: a `Vec[Int]` is refused at `praxis check` rather
3764/// than stringified here, which is what keeps `join` from being a back door
3765/// around ADR-143's decision about which types have a `to_text`.
3766///
3767/// # Safety
3768/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef` and `sep` a
3769/// valid `Text` `GcRef`.
3770#[unsafe(no_mangle)]
3771pub unsafe extern "C" fn praxis_vec_join(
3772 ctx: *mut RuntimeContext,
3773 vec: GcRef,
3774 sep: GcRef,
3775) -> GcRef {
3776 abi_guard!("praxis_vec_join", ctx, {
3777 // SAFETY: caller guarantees `vec` is a valid Vec and `sep` a valid Text.
3778 let p = unsafe { vec_payload(vec) };
3779 if !p
3780 .items
3781 .iter()
3782 .all(|item| std::ptr::eq(item.descriptor(), &crate::text::TEXT))
3783 {
3784 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3785 return unsafe { unit_sentinel(ctx) };
3786 }
3787 let separator = unsafe { text_str(sep) };
3788 let mut joined = String::new();
3789 for (i, item) in p.items.iter().enumerate() {
3790 if i > 0 {
3791 joined.push_str(separator);
3792 }
3793 // SAFETY: the loop above proved every element's descriptor is TEXT.
3794 joined.push_str(unsafe { text_str(*item) });
3795 }
3796 // SAFETY: `joined` is valid UTF-8; ctx/heap valid.
3797 unsafe { text_ref(ctx, joined) }
3798 })
3799}
3800
3801/// `chars.to_text()` — these `Char`s as one `Text`, with nothing between them
3802/// (ADR-144). The inverse of walking a `Text`, and what renders a `Grid` row
3803/// back as the line it was read from.
3804///
3805/// Each code point is read through [`read_scalar`] with the `Char` handle, never
3806/// a bare payload read: the payload is **four** bytes and an `i64` read would
3807/// take eight of them. A foreign element is `TypeMismatch` and the Unit
3808/// sentinel, for [`praxis_vec_join`]'s reason.
3809///
3810/// # Safety
3811/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3812#[unsafe(no_mangle)]
3813pub unsafe extern "C" fn praxis_vec_to_text(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3814 abi_guard!("praxis_vec_to_text", ctx, {
3815 // SAFETY: caller guarantees `vec` is a valid Vec.
3816 let p = unsafe { vec_payload(vec) };
3817 let mut rendered = String::new();
3818 for item in &p.items {
3819 // SAFETY: `read_scalar` proves the descriptor is `CHAR` before
3820 // reading its four bytes, and answers `None` otherwise.
3821 let Some(code) = (unsafe { read_scalar(*item, scalars::CHAR_PAYLOAD) }) else {
3822 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
3823 return unsafe { unit_sentinel(ctx) };
3824 };
3825 // The descriptor's own writer, so a line rebuilt from a `Grid` row
3826 // holds the characters `out` would have written one at a time.
3827 scalars::write_char(&mut rendered, code);
3828 }
3829 // SAFETY: `rendered` is valid UTF-8; ctx/heap valid.
3830 unsafe { text_ref(ctx, rendered) }
3831 })
3832}
3833
3834/// `v.frequencies()` — a `Counter[T]` holding how many times each element of
3835/// `vec` occurs (§6.3, §6.2).
3836///
3837/// The first combinator whose result is a **keyed** collection, which is why the
3838/// catalog row carries a `HashStable` bound of its own:
3839/// `require_collection_invariants` is applied to a method's *receiver*, and the
3840/// receiver here is an ordinary `Vec` that may legitimately hold anything. It is
3841/// the result that has a key rule.
3842///
3843/// The counter's key descriptor is the source `Vec`'s element label, which may
3844/// be null when the construction site knew no element type — the same null
3845/// [`praxis_counter_new`] already accepts and means "not told yet".
3846///
3847/// # Safety
3848/// `ctx` must be live and wired; `vec` must be a valid `Vec` `GcRef`.
3849#[unsafe(no_mangle)]
3850pub unsafe extern "C" fn praxis_vec_frequencies(ctx: *mut RuntimeContext, vec: GcRef) -> GcRef {
3851 abi_guard!("praxis_vec_frequencies", ctx, {
3852 let scope = unsafe { NativeScope::new(ctx) };
3853 // The receiver is rooted **explicitly**, unlike the one-allocation
3854 // wrappers around it. The keys below are `GcRef`s into this `Vec`'s
3855 // items and they are held across one allocation *per distinct element*,
3856 // which is a much longer window than `praxis_set_items`' single
3857 // `vec_of`; relying on the caller's shadow frame alone for that long is
3858 // an assumption worth not making.
3859 let _receiver = scope.root(vec);
3860 // SAFETY: caller guarantees `vec` is a valid Vec.
3861 let p = unsafe { vec_payload(vec) };
3862 // Count first, with no allocation at all, so the tally cannot be
3863 // disturbed by a collection mid-loop. The tally is a `Vec` with a side
3864 // index rather than a bare map so the counts come out in
3865 // first-occurrence order, which makes the *allocation* order a function
3866 // of the input; the `Counter` itself is unordered either way.
3867 let mut counts: Vec<(DynamicKey, i64)> = Vec::new();
3868 let mut index: std::collections::HashMap<DynamicKey, usize> =
3869 std::collections::HashMap::new();
3870 for item in &p.items {
3871 let key = DynamicKey::new(*item);
3872 match index.get(&key) {
3873 Some(at) => counts[*at].1 += 1,
3874 None => {
3875 index.insert(key, counts.len());
3876 counts.push((key, 1));
3877 }
3878 }
3879 }
3880 let counter = unsafe { praxis_counter_new(ctx, p.element_descriptor) };
3881 let rooted = scope.root(counter);
3882 for (key, count) in counts {
3883 // Allocate first, then take the payload borrow — the boxed `Int`
3884 // allocation can collect, and the counter has to be reachable
3885 // through the native root store rather than through a `&mut` this
3886 // frame is holding across it.
3887 let boxed = unsafe { int_ref(ctx, count) };
3888 unsafe { counter_payload_mut(rooted) }
3889 .entries
3890 .insert(key, boxed);
3891 }
3892 counter
3893 })
3894}
3895
3896// ---------------------------------------------------------------------------
3897// Deque[T] methods (§6.1). Mirrors the Vec surface but adds the
3898// front/back distinction: `push_front`/`push_back`/`pop_front`/`pop_back`.
3899// `pop_*` fault on an empty deque (§9.1 `EmptyCollection`).
3900// ---------------------------------------------------------------------------
3901
3902use crate::collections::DequePayload;
3903
3904/// Read the `DequePayload` out of a `GcRef` as a shared ref, asserting Deque.
3905///
3906/// # Safety
3907/// `r` must be a valid `Deque` `GcRef`.
3908unsafe fn deque_payload(r: GcRef) -> &'static DequePayload {
3909 // SAFETY: caller guarantees `r` is a Deque; see `payload_ref`.
3910 unsafe { payload_ref::<DequePayload>(r) }
3911}
3912
3913/// Read the `DequePayload` out of a `GcRef` as a mutable ref, asserting Deque.
3914///
3915/// # Safety
3916/// `r` must be a valid `Deque` `GcRef`, rooted for `'s`.
3917unsafe fn deque_payload_mut<'s>(r: Rooted<'s>) -> &'s mut DequePayload {
3918 // SAFETY: caller guarantees `r` is a Deque; see `payload_mut`.
3919 unsafe { payload_mut::<DequePayload>(r) }
3920}
3921
3922/// Allocate a new empty `Deque[T]` with the given element descriptor (§11.2).
3923/// A null descriptor stays null — "not told yet" — exactly as `praxis_vec_new`.
3924///
3925/// # Safety
3926/// `ctx` must be live and wired. `element_descriptor` must be a valid pointer to
3927/// a `'static TypeDescriptor` (or null).
3928#[unsafe(no_mangle)]
3929pub unsafe extern "C" fn praxis_deque_new(
3930 ctx: *mut RuntimeContext,
3931 element_descriptor: *const TypeDescriptor,
3932) -> GcRef {
3933 abi_guard!("praxis_deque_new", ctx, {
3934 // SAFETY: DequePayload is DEQUE's payload type.
3935 unsafe {
3936 gc_alloc_owned(ctx, &crate::collections::DEQUE, || DequePayload {
3937 element_descriptor,
3938 items: std::collections::VecDeque::new(),
3939 })
3940 }
3941 })
3942}
3943
3944/// Prepend `value` to the front of `deque`; returns Unit (§6.1).
3945///
3946/// # Safety
3947/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
3948/// `value` must be a valid `GcRef`.
3949#[unsafe(no_mangle)]
3950pub unsafe extern "C" fn praxis_deque_push_front(
3951 ctx: *mut RuntimeContext,
3952 deque: GcRef,
3953 value: GcRef,
3954) -> GcRef {
3955 abi_guard!("praxis_deque_push_front", ctx, {
3956 unsafe { maybe_collect(ctx) };
3957 let scope = unsafe { NativeScope::new(ctx) };
3958 let p = unsafe { deque_payload_mut(scope.root(deque)) };
3959 if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3960 return unsafe { unit_sentinel(ctx) };
3961 }
3962 let before = p.owned_bytes();
3963 p.items.push_front(value);
3964 charge_growth(ctx, before, p.owned_bytes());
3965 unsafe { unit_sentinel(ctx) }
3966 })
3967}
3968
3969/// Append `value` to the back of `deque`; returns Unit (§6.1).
3970///
3971/// # Safety
3972/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
3973/// `value` must be a valid `GcRef`.
3974#[unsafe(no_mangle)]
3975pub unsafe extern "C" fn praxis_deque_push_back(
3976 ctx: *mut RuntimeContext,
3977 deque: GcRef,
3978 value: GcRef,
3979) -> GcRef {
3980 abi_guard!("praxis_deque_push_back", ctx, {
3981 unsafe { maybe_collect(ctx) };
3982 let scope = unsafe { NativeScope::new(ctx) };
3983 let p = unsafe { deque_payload_mut(scope.root(deque)) };
3984 if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
3985 return unsafe { unit_sentinel(ctx) };
3986 }
3987 let before = p.owned_bytes();
3988 p.items.push_back(value);
3989 charge_growth(ctx, before, p.owned_bytes());
3990 unsafe { unit_sentinel(ctx) }
3991 })
3992}
3993
3994/// Remove and return the front element; faults `EmptyCollection` if empty.
3995///
3996/// # Safety
3997/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
3998#[unsafe(no_mangle)]
3999pub unsafe extern "C" fn praxis_deque_pop_front(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
4000 abi_guard!("praxis_deque_pop_front", ctx, {
4001 // No allocation in the common case, but `pop_front` on a VecDeque does not
4002 // allocate Rust heap, so no collection is needed; `deque` stays live.
4003 let scope = unsafe { NativeScope::new(ctx) };
4004 let p = unsafe { deque_payload_mut(scope.root(deque)) };
4005 match p.items.pop_front() {
4006 Some(v) => v,
4007 None => {
4008 unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4009 unsafe { unit_sentinel(ctx) }
4010 }
4011 }
4012 })
4013}
4014
4015/// Remove and return the back element; faults `EmptyCollection` if empty.
4016///
4017/// # Safety
4018/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
4019#[unsafe(no_mangle)]
4020pub unsafe extern "C" fn praxis_deque_pop_back(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
4021 abi_guard!("praxis_deque_pop_back", ctx, {
4022 let scope = unsafe { NativeScope::new(ctx) };
4023 let p = unsafe { deque_payload_mut(scope.root(deque)) };
4024 match p.items.pop_back() {
4025 Some(v) => v,
4026 None => {
4027 unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4028 unsafe { unit_sentinel(ctx) }
4029 }
4030 }
4031 })
4032}
4033
4034/// The number of elements in `deque`, as a boxed `Int` (§6.1).
4035///
4036/// # Safety
4037/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
4038#[unsafe(no_mangle)]
4039pub unsafe extern "C" fn praxis_deque_len(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
4040 abi_guard!("praxis_deque_len", ctx, {
4041 let p = unsafe { deque_payload(deque) };
4042 let len = p.items.len() as i64;
4043 unsafe { int_ref(ctx, len) }
4044 })
4045}
4046
4047/// The element at `index` (0-based from the front); faults `IndexOutOfBounds`.
4048///
4049/// # Safety
4050/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
4051/// `index` must be a valid `Int` `GcRef`.
4052#[unsafe(no_mangle)]
4053pub unsafe extern "C" fn praxis_deque_get(
4054 ctx: *mut RuntimeContext,
4055 deque: GcRef,
4056 index: GcRef,
4057) -> GcRef {
4058 abi_guard!("praxis_deque_get", ctx, {
4059 let p = unsafe { deque_payload(deque) };
4060 let idx = unsafe { int_payload(index) };
4061 let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
4062 return unsafe { unit_sentinel(ctx) };
4063 };
4064 p.items[idx]
4065 })
4066}
4067
4068/// Replace the element at `index` (0-based from the front); faults
4069/// `IndexOutOfBounds` if out of range. Returns the Unit sentinel.
4070///
4071/// A replacement and never an insertion, and the element descriptor is
4072/// reconciled the same way, for [`praxis_vec_set`]'s reasons.
4073///
4074/// # Safety
4075/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`;
4076/// `index` must be a valid `Int` `GcRef`; `value` must be a valid `GcRef`.
4077#[unsafe(no_mangle)]
4078pub unsafe extern "C" fn praxis_deque_set(
4079 ctx: *mut RuntimeContext,
4080 deque: GcRef,
4081 index: GcRef,
4082 value: GcRef,
4083) -> GcRef {
4084 abi_guard!("praxis_deque_set", ctx, {
4085 let scope = unsafe { NativeScope::new(ctx) };
4086 let p = unsafe { deque_payload_mut(scope.root(deque)) };
4087 let idx = unsafe { int_payload(index) };
4088 let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
4089 return unsafe { unit_sentinel(ctx) };
4090 };
4091 if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
4092 return unsafe { unit_sentinel(ctx) };
4093 }
4094 p.items[idx] = value;
4095 unsafe { unit_sentinel(ctx) }
4096 })
4097}
4098
4099/// `d[i] min= candidate` and `d[i] max= candidate` — [`vec_update`] over a
4100/// `Deque`, indexing 0-based from the front as [`praxis_deque_set`] does.
4101///
4102/// # Safety
4103/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`,
4104/// `index` a valid `Int`, and `value` of the deque's element type.
4105unsafe fn deque_update(
4106 ctx: *mut RuntimeContext,
4107 deque: GcRef,
4108 index: GcRef,
4109 value: GcRef,
4110 keep: std::cmp::Ordering,
4111) -> GcRef {
4112 // SAFETY: as `praxis_deque_set`, whose body this is with a comparison in
4113 // front of the store.
4114 let scope = unsafe { NativeScope::new(ctx) };
4115 let p = unsafe { deque_payload_mut(scope.root(deque)) };
4116 let idx = unsafe { int_payload(index) };
4117 let Some(idx) = (unsafe { checked_index(ctx, idx, p.items.len()) }) else {
4118 return unsafe { unit_sentinel(ctx) };
4119 };
4120 if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
4121 return unsafe { unit_sentinel(ctx) };
4122 }
4123 if unsafe { update_cmp(value, p.items[idx]) } == keep {
4124 p.items[idx] = value;
4125 }
4126 unsafe { unit_sentinel(ctx) }
4127}
4128
4129/// `d[i] min= candidate` — keep the smaller of the two. See [`deque_update`].
4130///
4131/// # Safety
4132/// As [`deque_update`].
4133#[unsafe(no_mangle)]
4134pub unsafe extern "C" fn praxis_deque_update_min(
4135 ctx: *mut RuntimeContext,
4136 deque: GcRef,
4137 index: GcRef,
4138 value: GcRef,
4139) -> GcRef {
4140 abi_guard!("praxis_deque_update_min", ctx, {
4141 unsafe { deque_update(ctx, deque, index, value, std::cmp::Ordering::Less) }
4142 })
4143}
4144
4145/// `d[i] max= candidate` — keep the larger of the two. See [`deque_update`].
4146///
4147/// # Safety
4148/// As [`deque_update`].
4149#[unsafe(no_mangle)]
4150pub unsafe extern "C" fn praxis_deque_update_max(
4151 ctx: *mut RuntimeContext,
4152 deque: GcRef,
4153 index: GcRef,
4154 value: GcRef,
4155) -> GcRef {
4156 abi_guard!("praxis_deque_update_max", ctx, {
4157 unsafe { deque_update(ctx, deque, index, value, std::cmp::Ordering::Greater) }
4158 })
4159}
4160
4161/// True iff `deque` has no elements, as a boxed `Bool` (§6.1).
4162///
4163/// # Safety
4164/// `ctx` must be live and wired; `deque` must be a valid `Deque` `GcRef`.
4165#[unsafe(no_mangle)]
4166pub unsafe extern "C" fn praxis_deque_is_empty(ctx: *mut RuntimeContext, deque: GcRef) -> GcRef {
4167 abi_guard!("praxis_deque_is_empty", ctx, {
4168 let p = unsafe { deque_payload(deque) };
4169 let empty = p.items.is_empty();
4170 unsafe { bool_ref(ctx, empty) }
4171 })
4172}
4173
4174// ---------------------------------------------------------------------------
4175// Map[K, V] / Set[T] / Counter[T] (§6.1, §11.3).
4176//
4177// All three reuse Rust hash collections behind opaque GC objects. Keys are
4178// wrapped in `DynamicKey`, which delegates Rust `Hash`/`Eq` to the descriptor's
4179// structural callbacks — this is what makes tuples/records/enums/nested
4180// collections work as keys (§19.7 criterion). Counter's absent keys read as
4181// zero (§6.2); `min=`/`max=` update a map entry in place (§6.2).
4182// ---------------------------------------------------------------------------
4183
4184use crate::maps::{CounterPayload, MapPayload, SetPayload};
4185
4186/// Read a `MapPayload` as a shared ref. See `payload_ref` for the safety model.
4187unsafe fn map_payload(r: GcRef) -> &'static MapPayload {
4188 unsafe { payload_ref::<MapPayload>(r) }
4189}
4190
4191unsafe fn map_payload_mut<'s>(r: Rooted<'s>) -> &'s mut MapPayload {
4192 unsafe { payload_mut::<MapPayload>(r) }
4193}
4194
4195unsafe fn set_payload(r: GcRef) -> &'static SetPayload {
4196 unsafe { payload_ref::<SetPayload>(r) }
4197}
4198
4199unsafe fn set_payload_mut<'s>(r: Rooted<'s>) -> &'s mut SetPayload {
4200 unsafe { payload_mut::<SetPayload>(r) }
4201}
4202
4203unsafe fn counter_payload(r: GcRef) -> &'static CounterPayload {
4204 unsafe { payload_ref::<CounterPayload>(r) }
4205}
4206
4207unsafe fn counter_payload_mut<'s>(r: Rooted<'s>) -> &'s mut CounterPayload {
4208 unsafe { payload_mut::<CounterPayload>(r) }
4209}
4210
4211/// Allocate an empty `Map[K, V]`. `key_descriptor` is the key type the
4212/// construction site knew, or **null** when it knew none — which is kept null,
4213/// the way `praxis_vec_new` keeps it. Spelling an unknown type `INT` is a claim,
4214/// and every reader that believed it would read the wrong type.
4215///
4216/// # Safety
4217/// `ctx` must be live and wired. `key_descriptor` must be a valid pointer to a
4218/// `'static TypeDescriptor` (or null).
4219#[unsafe(no_mangle)]
4220pub unsafe extern "C" fn praxis_map_new(
4221 ctx: *mut RuntimeContext,
4222 key_descriptor: *const TypeDescriptor,
4223) -> GcRef {
4224 abi_guard!("praxis_map_new", ctx, {
4225 // The `Map` row carries one type argument, so the value type never reaches
4226 // this wrapper at all — it is unknown here by construction, and says so.
4227 // `praxis_map_insert` adopts the first inserted value's own descriptor,
4228 // which is how a `Vec` learns its element type.
4229 let value_descriptor: *const TypeDescriptor = std::ptr::null();
4230 // SAFETY: MapPayload is MAP's payload type.
4231 unsafe {
4232 gc_alloc_owned(ctx, &crate::maps::MAP, || MapPayload {
4233 key_descriptor,
4234 value_descriptor,
4235 entries: std::collections::HashMap::new(),
4236 })
4237 }
4238 })
4239}
4240
4241/// Insert `(key, value)` into `map`, replacing any prior value; returns Unit.
4242///
4243/// # Safety
4244/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`; `key` and
4245/// `value` must be valid `GcRef`s.
4246#[unsafe(no_mangle)]
4247pub unsafe extern "C" fn praxis_map_insert(
4248 ctx: *mut RuntimeContext,
4249 map: GcRef,
4250 key: GcRef,
4251 value: GcRef,
4252) -> GcRef {
4253 abi_guard!("praxis_map_insert", ctx, {
4254 unsafe { maybe_collect(ctx) };
4255 let scope = unsafe { NativeScope::new(ctx) };
4256 let p = unsafe { map_payload_mut(scope.root(map)) };
4257 // Learn the value type from the first value inserted, the way a `Vec`
4258 // learns its element type from the first `push`. Null is the encoding of
4259 // "never been told", so it is distinguishable from a `Map` that really
4260 // holds `Int`s.
4261 //
4262 // A later value of a different type un-learns it rather than faulting: the
4263 // type checker makes a `Map` homogeneous, so this is unreachable for a
4264 // well-typed program, and `praxis_map_insert` is a non-faulting row (its
4265 // caller emits no fault check). Null is now representable and means "the
4266 // value's own descriptor answers", so forgetting is the safe direction.
4267 let val_desc = value.descriptor();
4268 match p.value() {
4269 None => p.value_descriptor = val_desc,
4270 Some(known) if !std::ptr::eq(known, val_desc) => {
4271 p.value_descriptor = std::ptr::null();
4272 }
4273 Some(_) => {}
4274 }
4275 let before = p.owned_bytes();
4276 p.entries.insert(DynamicKey::new(key), value);
4277 charge_growth(ctx, before, p.owned_bytes());
4278 unsafe { unit_sentinel(ctx) }
4279 })
4280}
4281
4282/// `Some(value)` for `key`, or `None` if absent (§4.7, §5.7).
4283///
4284/// §5.7 writes the signature `Map[K,V].get(K) -> Option[V]` and §4.7 opens
4285/// "Option[T] represents normal domain-level absence. It is not an error
4286/// channel." Answering the Unit sentinel under a `V` static type instead would
4287/// hand the program a value it could not distinguish from a real one without
4288/// `contains`, while the type system insisted it was a `V`.
4289///
4290/// The `Option` is built through the runtime's own `option_schema`, whose
4291/// `Some` slot is unknown — `V` is learned from the value found, never from a
4292/// static type — and which `EnumSchema::same_type` therefore recognizes as the
4293/// same type as the codegen's `Option[Int]`. That is what lets the result match
4294/// against arms the program wrote.
4295///
4296/// # Safety
4297/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`; `key`
4298/// must be a valid `GcRef`.
4299#[unsafe(no_mangle)]
4300pub unsafe extern "C" fn praxis_map_get(ctx: *mut RuntimeContext, map: GcRef, key: GcRef) -> GcRef {
4301 abi_guard!("praxis_map_get", ctx, {
4302 let found = {
4303 let p = unsafe { map_payload(map) };
4304 p.entries.get(&DynamicKey::new(key)).copied()
4305 };
4306 match found {
4307 Some(v) => unsafe { option_some(ctx, v) },
4308 None => unsafe { option_none(ctx) },
4309 }
4310 })
4311}
4312
4313/// `map[key]` (§4.7): the value for `key`, **faulting** if it is absent.
4314///
4315/// A different wrapper from [`praxis_map_get`] because the two answers are the
4316/// language's own choice, not an implementation detail: §4.7 says "indexing a
4317/// missing map key faults instead of returning an option… the user chooses
4318/// between explicit absence with `.get` and assertion-like access with
4319/// indexing". Sharing one wrapper would take that choice away from the user.
4320///
4321/// The fault is [`FaultKind::IndexOutOfBounds`](crate::FaultKind::IndexOutOfBounds)
4322/// — an index the collection does not hold, which is what its doc already
4323/// describes. A dedicated `MissingKey` kind would read better, and adding one is
4324/// a `#[repr(C)]` change that costs an ABI bump (ADR-075).
4325///
4326/// # Safety
4327/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`; `key`
4328/// must be a valid `GcRef`.
4329#[unsafe(no_mangle)]
4330pub unsafe extern "C" fn praxis_map_index(
4331 ctx: *mut RuntimeContext,
4332 map: GcRef,
4333 key: GcRef,
4334) -> GcRef {
4335 abi_guard!("praxis_map_index", ctx, {
4336 let p = unsafe { map_payload(map) };
4337 match p.entries.get(&DynamicKey::new(key)) {
4338 Some(v) => *v,
4339 None => {
4340 unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
4341 unsafe { unit_sentinel(ctx) }
4342 }
4343 }
4344 })
4345}
4346
4347/// True iff `key` is present, as a boxed Bool.
4348///
4349/// # Safety
4350/// `ctx` must be live and wired; `map` and `key` must be valid `GcRef`s.
4351#[unsafe(no_mangle)]
4352pub unsafe extern "C" fn praxis_map_contains(
4353 ctx: *mut RuntimeContext,
4354 map: GcRef,
4355 key: GcRef,
4356) -> GcRef {
4357 abi_guard!("praxis_map_contains", ctx, {
4358 let p = unsafe { map_payload(map) };
4359 let present = p.entries.contains_key(&DynamicKey::new(key));
4360 unsafe { bool_ref(ctx, present) }
4361 })
4362}
4363
4364/// Remove `key`; returns Unit (the removed value, if any, is dropped).
4365///
4366/// # Safety
4367/// `ctx` must be live and wired; `map` and `key` must be valid `GcRef`s.
4368#[unsafe(no_mangle)]
4369pub unsafe extern "C" fn praxis_map_remove(
4370 ctx: *mut RuntimeContext,
4371 map: GcRef,
4372 key: GcRef,
4373) -> GcRef {
4374 abi_guard!("praxis_map_remove", ctx, {
4375 let scope = unsafe { NativeScope::new(ctx) };
4376 let p = unsafe { map_payload_mut(scope.root(map)) };
4377 p.entries.remove(&DynamicKey::new(key));
4378 unsafe { unit_sentinel(ctx) }
4379 })
4380}
4381
4382/// The number of entries, as a boxed Int.
4383///
4384/// # Safety
4385/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4386#[unsafe(no_mangle)]
4387pub unsafe extern "C" fn praxis_map_len(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4388 abi_guard!("praxis_map_len", ctx, {
4389 let p = unsafe { map_payload(map) };
4390 unsafe { int_ref(ctx, p.entries.len() as i64) }
4391 })
4392}
4393
4394/// `m.keys()` — every key, as a `Vec[K]`. Ordered like
4395/// [`praxis_counter_keys`], and index-aligned with [`praxis_map_values`].
4396///
4397/// This and `values()` are the only way to enumerate a `Map`: `for kv in m` has
4398/// no lowering.
4399///
4400/// # Safety
4401/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4402#[unsafe(no_mangle)]
4403pub unsafe extern "C" fn praxis_map_keys(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4404 abi_guard!("praxis_map_keys", ctx, {
4405 let key_desc = unsafe { map_payload(map) }.key_descriptor;
4406 let rows = unsafe { crate::maps::ordered_entries(&map_payload(map).entries) };
4407 unsafe { vec_of(ctx, key_desc, rows.into_iter().map(|(k, _)| k)) }
4408 })
4409}
4410
4411/// `m.values()` — every value, as a `Vec[V]`. See [`praxis_map_keys`].
4412///
4413/// # Safety
4414/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4415#[unsafe(no_mangle)]
4416pub unsafe extern "C" fn praxis_map_values(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4417 abi_guard!("praxis_map_values", ctx, {
4418 let val_desc = unsafe { map_payload(map) }.value_descriptor;
4419 let rows = unsafe { crate::maps::ordered_entries(&map_payload(map).entries) };
4420 unsafe { vec_of(ctx, val_desc, rows.into_iter().map(|(_, v)| v)) }
4421 })
4422}
4423
4424/// True iff the map is empty, as a boxed Bool.
4425///
4426/// # Safety
4427/// `ctx` must be live and wired; `map` must be a valid `Map` `GcRef`.
4428#[unsafe(no_mangle)]
4429pub unsafe extern "C" fn praxis_map_is_empty(ctx: *mut RuntimeContext, map: GcRef) -> GcRef {
4430 abi_guard!("praxis_map_is_empty", ctx, {
4431 let p = unsafe { map_payload(map) };
4432 unsafe { bool_ref(ctx, p.entries.is_empty()) }
4433 })
4434}
4435
4436/// Order a candidate against the value already stored, for `min=` and `max=`.
4437///
4438/// Through the descriptor each value carries (ADR-045) — the same `compare`
4439/// callback `sorted`, a heap and `<` order through, which is what makes
4440/// `d[k] min= v` keep the value `[cur, v].sorted()` puts first. Reading the two
4441/// payloads as `i64`s instead is what this replaced, and it is wrong for three
4442/// of the four orderable types a program can write: it compares a `Text` by its
4443/// *address*, puts `-2.0` after `-1.0`, and reads four bytes past a `Char`.
4444///
4445/// **`Equal` is the answer to both degenerate cases**, and for
4446/// [`HeapEntry::cmp`](crate::heaps::HeapEntry)'s reason. Descriptors that differ
4447/// cannot happen — a `Map[K, V]`'s values are all one type — and a `V` with no
4448/// order at all is refused at `praxis check` by the row's `Ord` bound, so each
4449/// is a miscompile rather than a program. An updating store that compares
4450/// `Equal` keeps the entry it has, which is a consistent semantics one step from
4451/// the bug; dispatching a callback on a foreign payload is not.
4452///
4453/// # Safety
4454/// Both must be valid `GcRef`s, so each payload matches the descriptor in its
4455/// own header.
4456unsafe fn update_cmp(candidate: GcRef, current: GcRef) -> std::cmp::Ordering {
4457 let desc = candidate.descriptor();
4458 if !std::ptr::eq(desc, current.descriptor()) {
4459 return std::cmp::Ordering::Equal;
4460 }
4461 let Some(compare) = desc.compare else {
4462 return std::cmp::Ordering::Equal;
4463 };
4464 // SAFETY: both values carry `desc` (checked above), so both payloads are
4465 // values of its type.
4466 unsafe {
4467 compare(
4468 candidate.payload::<u8>() as *const u8,
4469 current.payload::<u8>() as *const u8,
4470 )
4471 }
4472}
4473
4474/// `distance[key] min= candidate` (§6.2): keep the smaller value, or insert if
4475/// absent (an absent entry accepts the first value). The value is of any
4476/// orderable type (§5.4 `SupportsOrd`), ordered by [`update_cmp`]; returns Unit.
4477///
4478/// **A tie keeps the entry that is there**, which is what the `<` this replaced
4479/// did and is now observable rather than academic: `+0.0` and `-0.0` compare
4480/// equal and print differently, so a tie that replaced would let `d[k] min= v`
4481/// answer by how many times it had been written.
4482///
4483/// # Safety
4484/// `ctx` must be live and wired; `map`, `key`, `value` must be valid `GcRef`s,
4485/// and `value` must be of the map's value type.
4486#[unsafe(no_mangle)]
4487pub unsafe extern "C" fn praxis_map_update_min(
4488 ctx: *mut RuntimeContext,
4489 map: GcRef,
4490 key: GcRef,
4491 value: GcRef,
4492) -> GcRef {
4493 abi_guard!("praxis_map_update_min", ctx, {
4494 unsafe { maybe_collect(ctx) };
4495 let scope = unsafe { NativeScope::new(ctx) };
4496 let p = unsafe { map_payload_mut(scope.root(map)) };
4497 match p.entries.get_mut(&DynamicKey::new(key)) {
4498 Some(existing) => {
4499 // SAFETY: both are values this map holds, so each payload
4500 // matches the descriptor in its own header.
4501 if unsafe { update_cmp(value, *existing) } == std::cmp::Ordering::Less {
4502 *existing = value;
4503 }
4504 }
4505 None => {
4506 p.entries.insert(DynamicKey::new(key), value);
4507 }
4508 }
4509 unsafe { unit_sentinel(ctx) }
4510 })
4511}
4512
4513/// `best[key] max= score` (§6.2): keep the larger value, or insert if absent.
4514/// The dual of [`praxis_map_update_min`], including its tie rule.
4515///
4516/// # Safety
4517/// `ctx` must be live and wired; `map`, `key`, `value` must be valid `GcRef`s,
4518/// and `value` must be of the map's value type.
4519#[unsafe(no_mangle)]
4520pub unsafe extern "C" fn praxis_map_update_max(
4521 ctx: *mut RuntimeContext,
4522 map: GcRef,
4523 key: GcRef,
4524 value: GcRef,
4525) -> GcRef {
4526 abi_guard!("praxis_map_update_max", ctx, {
4527 unsafe { maybe_collect(ctx) };
4528 let scope = unsafe { NativeScope::new(ctx) };
4529 let p = unsafe { map_payload_mut(scope.root(map)) };
4530 match p.entries.get_mut(&DynamicKey::new(key)) {
4531 Some(existing) => {
4532 // SAFETY: as `praxis_map_update_min`.
4533 if unsafe { update_cmp(value, *existing) } == std::cmp::Ordering::Greater {
4534 *existing = value;
4535 }
4536 }
4537 None => {
4538 p.entries.insert(DynamicKey::new(key), value);
4539 }
4540 }
4541 unsafe { unit_sentinel(ctx) }
4542 })
4543}
4544
4545// --- Set[T] -----------------------------------------------------------------
4546
4547/// Allocate an empty `Set[T]`. `element_descriptor` is the element type the
4548/// construction site knew, or **null** when it knew none — kept null.
4549///
4550/// # Safety
4551/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
4552/// a `'static TypeDescriptor` (or null).
4553#[unsafe(no_mangle)]
4554pub unsafe extern "C" fn praxis_set_new(
4555 ctx: *mut RuntimeContext,
4556 element_descriptor: *const TypeDescriptor,
4557) -> GcRef {
4558 abi_guard!("praxis_set_new", ctx, {
4559 // SAFETY: SetPayload is SET's payload type.
4560 unsafe {
4561 gc_alloc_owned(ctx, &crate::maps::SET, || SetPayload {
4562 element_descriptor,
4563 entries: std::collections::HashSet::new(),
4564 })
4565 }
4566 })
4567}
4568
4569/// Insert `value` into `set`; returns Unit.
4570///
4571/// # Safety
4572/// `ctx` must be live and wired; `set` and `value` must be valid `GcRef`s.
4573#[unsafe(no_mangle)]
4574pub unsafe extern "C" fn praxis_set_insert(
4575 ctx: *mut RuntimeContext,
4576 set: GcRef,
4577 value: GcRef,
4578) -> GcRef {
4579 abi_guard!("praxis_set_insert", ctx, {
4580 unsafe { maybe_collect(ctx) };
4581 let scope = unsafe { NativeScope::new(ctx) };
4582 let p = unsafe { set_payload_mut(scope.root(set)) };
4583 let before = p.owned_bytes();
4584 p.entries.insert(DynamicKey::new(value));
4585 charge_growth(ctx, before, p.owned_bytes());
4586 unsafe { unit_sentinel(ctx) }
4587 })
4588}
4589
4590/// Remove `value` from `set`; returns Unit.
4591///
4592/// # Safety
4593/// `ctx` must be live and wired; `set` and `value` must be valid `GcRef`s.
4594#[unsafe(no_mangle)]
4595pub unsafe extern "C" fn praxis_set_remove(
4596 ctx: *mut RuntimeContext,
4597 set: GcRef,
4598 value: GcRef,
4599) -> GcRef {
4600 abi_guard!("praxis_set_remove", ctx, {
4601 let scope = unsafe { NativeScope::new(ctx) };
4602 let p = unsafe { set_payload_mut(scope.root(set)) };
4603 p.entries.remove(&DynamicKey::new(value));
4604 unsafe { unit_sentinel(ctx) }
4605 })
4606}
4607
4608/// True iff `value` is in the set, as a boxed Bool.
4609///
4610/// # Safety
4611/// `ctx` must be live and wired; `set` and `value` must be valid `GcRef`s.
4612#[unsafe(no_mangle)]
4613pub unsafe extern "C" fn praxis_set_contains(
4614 ctx: *mut RuntimeContext,
4615 set: GcRef,
4616 value: GcRef,
4617) -> GcRef {
4618 abi_guard!("praxis_set_contains", ctx, {
4619 let p = unsafe { set_payload(set) };
4620 let present = p.entries.contains(&DynamicKey::new(value));
4621 unsafe { bool_ref(ctx, present) }
4622 })
4623}
4624
4625/// The number of elements, as a boxed Int.
4626///
4627/// # Safety
4628/// `ctx` must be live and wired; `set` must be a valid `Set` `GcRef`.
4629#[unsafe(no_mangle)]
4630pub unsafe extern "C" fn praxis_set_len(ctx: *mut RuntimeContext, set: GcRef) -> GcRef {
4631 abi_guard!("praxis_set_len", ctx, {
4632 let p = unsafe { set_payload(set) };
4633 unsafe { int_ref(ctx, p.entries.len() as i64) }
4634 })
4635}
4636
4637/// True iff the set is empty, as a boxed Bool.
4638///
4639/// # Safety
4640/// `ctx` must be live and wired; `set` must be a valid `Set` `GcRef`.
4641#[unsafe(no_mangle)]
4642pub unsafe extern "C" fn praxis_set_is_empty(ctx: *mut RuntimeContext, set: GcRef) -> GcRef {
4643 abi_guard!("praxis_set_is_empty", ctx, {
4644 let p = unsafe { set_payload(set) };
4645 unsafe { bool_ref(ctx, p.entries.is_empty()) }
4646 })
4647}
4648
4649/// Every member, as a `Vec[T]` in [`crate::maps::ordered_members`] order — the
4650/// snapshot `for x in s` iterates (ADR-066).
4651///
4652/// There is no `praxis_set_get`, and this is why: a `HashSet` has no nth member,
4653/// so an indexed accessor would be a linear scan per step and the loop would be
4654/// quadratic. The snapshot is one pass, and it is what makes the order
4655/// deterministic — which for `for` is the program's *answer* and not only its
4656/// printing.
4657///
4658/// # Safety
4659/// `ctx` must be live and wired; `set` must be a valid `Set` `GcRef`.
4660#[unsafe(no_mangle)]
4661pub unsafe extern "C" fn praxis_set_items(ctx: *mut RuntimeContext, set: GcRef) -> GcRef {
4662 abi_guard!("praxis_set_items", ctx, {
4663 let elem_desc = unsafe { set_payload(set) }.element_descriptor;
4664 let members = unsafe { crate::maps::ordered_members(&set_payload(set).entries) };
4665 unsafe { vec_of(ctx, elem_desc, members.into_iter()) }
4666 })
4667}
4668
4669// --- Counter[T] -------------------------------------------------------------
4670
4671/// Allocate an empty `Counter[T]`. `key_descriptor` is the key type the
4672/// construction site knew, or **null** when it knew none — kept null.
4673///
4674/// # Safety
4675/// `ctx` must be live and wired; `key_descriptor` must be a valid pointer to a
4676/// `'static TypeDescriptor` (or null).
4677#[unsafe(no_mangle)]
4678pub unsafe extern "C" fn praxis_counter_new(
4679 ctx: *mut RuntimeContext,
4680 key_descriptor: *const TypeDescriptor,
4681) -> GcRef {
4682 abi_guard!("praxis_counter_new", ctx, {
4683 // SAFETY: CounterPayload is COUNTER's payload type.
4684 unsafe {
4685 gc_alloc_owned(ctx, &crate::maps::COUNTER, || CounterPayload {
4686 key_descriptor,
4687 entries: std::collections::HashMap::new(),
4688 })
4689 }
4690 })
4691}
4692
4693/// The count for `key`, or zero if absent (§6.2: "absent values read as zero").
4694/// Never faults. Returns a boxed Int.
4695///
4696/// # Safety
4697/// `ctx` must be live and wired; `counter` and `key` must be valid `GcRef`s.
4698#[unsafe(no_mangle)]
4699pub unsafe extern "C" fn praxis_counter_get(
4700 ctx: *mut RuntimeContext,
4701 counter: GcRef,
4702 key: GcRef,
4703) -> GcRef {
4704 abi_guard!("praxis_counter_get", ctx, {
4705 let p = unsafe { counter_payload(counter) };
4706 let count = match p.entries.get(&DynamicKey::new(key)) {
4707 Some(v) => unsafe { int_payload(*v) },
4708 None => 0, // §6.2: absent reads as zero.
4709 };
4710 unsafe { int_ref(ctx, count) }
4711 })
4712}
4713
4714/// Increment the count for `key` by one (inserting 1 if absent); returns Unit.
4715///
4716/// # Safety
4717/// `ctx` must be live and wired; `counter` and `key` must be valid `GcRef`s.
4718#[unsafe(no_mangle)]
4719pub unsafe extern "C" fn praxis_counter_inc(
4720 ctx: *mut RuntimeContext,
4721 counter: GcRef,
4722 key: GcRef,
4723) -> GcRef {
4724 abi_guard!("praxis_counter_inc", ctx, {
4725 let scope = unsafe { NativeScope::new(ctx) };
4726 let p = unsafe { counter_payload_mut(scope.root(counter)) };
4727 let dk = DynamicKey::new(key);
4728 match p.entries.get_mut(&dk) {
4729 Some(v) => {
4730 let cur = unsafe { int_payload(*v) };
4731 // Checked, like every other integer computation in this file
4732 // (§4.12): a raw `cur + 1` panics across `extern "C"` in debug — the
4733 // non-unwinding panic §10.4 forbids — and wraps to `i64::MIN` in
4734 // release, which is a silently wrong count. A `Counter`'s values are
4735 // set to arbitrary `Int`s by `c[k] = n`, so this is reachable from
4736 // source and not only from `i64::MAX` increments.
4737 let Some(next) = cur.checked_add(1) else {
4738 unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
4739 return unsafe { unit_sentinel(ctx) };
4740 };
4741 // SAFETY: ctx is wired; alloc a fresh Int for the incremented value.
4742 *v = unsafe { int_ref(ctx, next) };
4743 }
4744 None => {
4745 let one = unsafe { int_ref(ctx, 1_i64) };
4746 p.entries.insert(dk, one);
4747 }
4748 }
4749 unsafe { unit_sentinel(ctx) }
4750 })
4751}
4752
4753/// `counts[key] = value` (§6.2): set the count for `key`, replacing any prior
4754/// one; returns Unit.
4755///
4756/// [`praxis_counter_inc`] adds exactly one, so it cannot express
4757/// `counts[key] += n` or `counts[key] = n`. A subscript assignment is a
4758/// read-modify-write over the pair (`praxis_counter_get`, this), which is what
4759/// makes every assignment operator work on a `Counter` rather than only `+= 1`.
4760///
4761/// # Safety
4762/// `ctx` must be live and wired; `counter` and `key` must be valid `GcRef`s and
4763/// `value` must be an `Int`.
4764#[unsafe(no_mangle)]
4765pub unsafe extern "C" fn praxis_counter_set(
4766 ctx: *mut RuntimeContext,
4767 counter: GcRef,
4768 key: GcRef,
4769 value: GcRef,
4770) -> GcRef {
4771 abi_guard!("praxis_counter_set", ctx, {
4772 unsafe { maybe_collect(ctx) };
4773 let scope = unsafe { NativeScope::new(ctx) };
4774 let p = unsafe { counter_payload_mut(scope.root(counter)) };
4775 let before = p.owned_bytes();
4776 p.entries.insert(DynamicKey::new(key), value);
4777 charge_growth(ctx, before, p.owned_bytes());
4778 unsafe { unit_sentinel(ctx) }
4779 })
4780}
4781
4782/// `c.keys()` — every key, as a `Vec[T]`.
4783///
4784/// Ordered by the key's own `compare` (ADR-138), so it is the *same* order
4785/// [`praxis_counter_values`] uses and the two are index-aligned. A `HashMap`'s
4786/// own order is randomized per process, so returning it would make the same
4787/// program answer differently on two runs — and here the order is the program's
4788/// *answer*, not only its printing.
4789///
4790/// # Safety
4791/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4792#[unsafe(no_mangle)]
4793pub unsafe extern "C" fn praxis_counter_keys(ctx: *mut RuntimeContext, counter: GcRef) -> GcRef {
4794 abi_guard!("praxis_counter_keys", ctx, {
4795 let key_desc = unsafe { counter_payload(counter) }.key_descriptor;
4796 let rows = unsafe { crate::maps::ordered_entries(&counter_payload(counter).entries) };
4797 unsafe { vec_of(ctx, key_desc, rows.into_iter().map(|(k, _)| k)) }
4798 })
4799}
4800
4801/// `c.values()` — every count, as a `Vec[Int]`.
4802///
4803/// §3.3's representative program is `counts.values().count(|n| n >= 2)`. Ordered
4804/// like [`praxis_counter_keys`]; see it for why the order is fixed.
4805///
4806/// # Safety
4807/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4808#[unsafe(no_mangle)]
4809pub unsafe extern "C" fn praxis_counter_values(ctx: *mut RuntimeContext, counter: GcRef) -> GcRef {
4810 abi_guard!("praxis_counter_values", ctx, {
4811 let rows = unsafe { crate::maps::ordered_entries(&counter_payload(counter).entries) };
4812 unsafe { vec_of(ctx, &scalars::INT, rows.into_iter().map(|(_, v)| v)) }
4813 })
4814}
4815
4816/// The number of distinct keys, as a boxed Int.
4817///
4818/// # Safety
4819/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4820#[unsafe(no_mangle)]
4821pub unsafe extern "C" fn praxis_counter_len(ctx: *mut RuntimeContext, counter: GcRef) -> GcRef {
4822 abi_guard!("praxis_counter_len", ctx, {
4823 let p = unsafe { counter_payload(counter) };
4824 unsafe { int_ref(ctx, p.entries.len() as i64) }
4825 })
4826}
4827
4828/// True iff the counter has no keys, as a boxed Bool.
4829///
4830/// # Safety
4831/// `ctx` must be live and wired; `counter` must be a valid `Counter` `GcRef`.
4832#[unsafe(no_mangle)]
4833pub unsafe extern "C" fn praxis_counter_is_empty(
4834 ctx: *mut RuntimeContext,
4835 counter: GcRef,
4836) -> GcRef {
4837 abi_guard!("praxis_counter_is_empty", ctx, {
4838 let p = unsafe { counter_payload(counter) };
4839 unsafe { bool_ref(ctx, p.entries.is_empty()) }
4840 })
4841}
4842
4843// ---------------------------------------------------------------------------
4844// MinHeap[T] / MaxHeap[T] (§6.1, §11.2).
4845//
4846// `MaxHeap` maps directly to Rust's max-`BinaryHeap`; `MinHeap` wraps entries in
4847// `Reverse` so the smallest surfaces first. `pop`/`peek` fault `EmptyCollection`
4848// on an empty heap.
4849// ---------------------------------------------------------------------------
4850
4851use crate::heaps::{HeapEntry, MaxHeapPayload, MinHeapPayload};
4852use std::collections::BinaryHeap;
4853
4854unsafe fn max_heap_payload_mut<'s>(r: Rooted<'s>) -> &'s mut MaxHeapPayload {
4855 unsafe { payload_mut::<MaxHeapPayload>(r) }
4856}
4857
4858unsafe fn max_heap_payload(r: GcRef) -> &'static MaxHeapPayload {
4859 unsafe { payload_ref::<MaxHeapPayload>(r) }
4860}
4861
4862unsafe fn min_heap_payload_mut<'s>(r: Rooted<'s>) -> &'s mut MinHeapPayload {
4863 unsafe { payload_mut::<MinHeapPayload>(r) }
4864}
4865
4866unsafe fn min_heap_payload(r: GcRef) -> &'static MinHeapPayload {
4867 unsafe { payload_ref::<MinHeapPayload>(r) }
4868}
4869
4870/// Allocate an empty `MaxHeap[T]`. A null `element_descriptor` — the codegen's
4871/// "no static element type" — is kept null.
4872///
4873/// # Safety
4874/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
4875/// a `'static TypeDescriptor` (or null).
4876#[unsafe(no_mangle)]
4877pub unsafe extern "C" fn praxis_max_heap_new(
4878 ctx: *mut RuntimeContext,
4879 element_descriptor: *const TypeDescriptor,
4880) -> GcRef {
4881 abi_guard!("praxis_max_heap_new", ctx, {
4882 // SAFETY: MaxHeapPayload is MAX_HEAP's payload type.
4883 unsafe {
4884 gc_alloc_owned(ctx, &crate::heaps::MAX_HEAP, || MaxHeapPayload {
4885 element_descriptor,
4886 items: BinaryHeap::new(),
4887 })
4888 }
4889 })
4890}
4891
4892/// Push `value` onto the max-heap; returns Unit.
4893///
4894/// # Safety
4895/// `ctx` must be live and wired; `heap` and `value` must be valid `GcRef`s.
4896#[unsafe(no_mangle)]
4897pub unsafe extern "C" fn praxis_max_heap_push(
4898 ctx: *mut RuntimeContext,
4899 heap_ref: GcRef,
4900 value: GcRef,
4901) -> GcRef {
4902 abi_guard!("praxis_max_heap_push", ctx, {
4903 unsafe { maybe_collect(ctx) };
4904 let scope = unsafe { NativeScope::new(ctx) };
4905 let p = unsafe { max_heap_payload_mut(scope.root(heap_ref)) };
4906 let before = p.owned_bytes();
4907 p.items.push(HeapEntry {
4908 value,
4909 descriptor: value.descriptor(),
4910 });
4911 charge_growth(ctx, before, p.owned_bytes());
4912 unsafe { unit_sentinel(ctx) }
4913 })
4914}
4915
4916/// Remove and return the largest element; faults `EmptyCollection` if empty.
4917///
4918/// # Safety
4919/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4920#[unsafe(no_mangle)]
4921pub unsafe extern "C" fn praxis_max_heap_pop(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4922 abi_guard!("praxis_max_heap_pop", ctx, {
4923 let scope = unsafe { NativeScope::new(ctx) };
4924 let p = unsafe { max_heap_payload_mut(scope.root(heap_ref)) };
4925 match p.items.pop() {
4926 Some(e) => e.value,
4927 None => {
4928 unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4929 unsafe { unit_sentinel(ctx) }
4930 }
4931 }
4932 })
4933}
4934
4935/// The largest element without removing it; faults `EmptyCollection` if empty.
4936///
4937/// # Safety
4938/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4939#[unsafe(no_mangle)]
4940pub unsafe extern "C" fn praxis_max_heap_peek(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4941 abi_guard!("praxis_max_heap_peek", ctx, {
4942 let p = unsafe { max_heap_payload(heap_ref) };
4943 match p.items.peek() {
4944 Some(e) => e.value,
4945 None => {
4946 unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
4947 unsafe { unit_sentinel(ctx) }
4948 }
4949 }
4950 })
4951}
4952
4953/// The number of elements, as a boxed Int.
4954///
4955/// # Safety
4956/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4957#[unsafe(no_mangle)]
4958pub unsafe extern "C" fn praxis_max_heap_len(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4959 abi_guard!("praxis_max_heap_len", ctx, {
4960 let p = unsafe { max_heap_payload(heap_ref) };
4961 unsafe { int_ref(ctx, p.items.len() as i64) }
4962 })
4963}
4964
4965/// Every element, as a `Vec[T]` in [`crate::heaps::in_pop_order`] — the snapshot
4966/// `for x in h` iterates (ADR-066). The heap is **not** drained.
4967///
4968/// A heap's backing array is heap-ordered only at its root, so an indexed
4969/// accessor over it would answer in insertion-history order — reading the array
4970/// as if it were a `Vec`'s.
4971///
4972/// # Safety
4973/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4974#[unsafe(no_mangle)]
4975pub unsafe extern "C" fn praxis_max_heap_items(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
4976 abi_guard!("praxis_max_heap_items", ctx, {
4977 let p = unsafe { max_heap_payload(heap_ref) };
4978 let items = crate::heaps::in_pop_order(&p.items, |e| e.value);
4979 unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
4980 })
4981}
4982
4983/// True iff the heap is empty, as a boxed Bool.
4984///
4985/// # Safety
4986/// `ctx` must be live and wired; `heap_ref` must be a valid `MaxHeap` `GcRef`.
4987#[unsafe(no_mangle)]
4988pub unsafe extern "C" fn praxis_max_heap_is_empty(
4989 ctx: *mut RuntimeContext,
4990 heap_ref: GcRef,
4991) -> GcRef {
4992 abi_guard!("praxis_max_heap_is_empty", ctx, {
4993 let p = unsafe { max_heap_payload(heap_ref) };
4994 unsafe { bool_ref(ctx, p.items.is_empty()) }
4995 })
4996}
4997
4998// --- MinHeap (mirrors MaxHeap with Reverse wrapping) -----------------------
4999
5000/// Allocate an empty `MinHeap[T]`. A null `element_descriptor` — the codegen's
5001/// "no static element type" — is kept null.
5002///
5003/// # Safety
5004/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
5005/// a `'static TypeDescriptor` (or null).
5006#[unsafe(no_mangle)]
5007pub unsafe extern "C" fn praxis_min_heap_new(
5008 ctx: *mut RuntimeContext,
5009 element_descriptor: *const TypeDescriptor,
5010) -> GcRef {
5011 abi_guard!("praxis_min_heap_new", ctx, {
5012 // SAFETY: MinHeapPayload is MIN_HEAP's payload type.
5013 unsafe {
5014 gc_alloc_owned(ctx, &crate::heaps::MIN_HEAP, || MinHeapPayload {
5015 element_descriptor,
5016 items: BinaryHeap::new(),
5017 })
5018 }
5019 })
5020}
5021
5022/// Push `value` onto the min-heap; returns Unit.
5023///
5024/// # Safety
5025/// `ctx` must be live and wired; `heap` and `value` must be valid `GcRef`s.
5026#[unsafe(no_mangle)]
5027pub unsafe extern "C" fn praxis_min_heap_push(
5028 ctx: *mut RuntimeContext,
5029 heap_ref: GcRef,
5030 value: GcRef,
5031) -> GcRef {
5032 abi_guard!("praxis_min_heap_push", ctx, {
5033 unsafe { maybe_collect(ctx) };
5034 let scope = unsafe { NativeScope::new(ctx) };
5035 let p = unsafe { min_heap_payload_mut(scope.root(heap_ref)) };
5036 let before = p.owned_bytes();
5037 p.items.push(std::cmp::Reverse(HeapEntry {
5038 value,
5039 descriptor: value.descriptor(),
5040 }));
5041 charge_growth(ctx, before, p.owned_bytes());
5042 unsafe { unit_sentinel(ctx) }
5043 })
5044}
5045
5046/// Remove and return the smallest element; faults `EmptyCollection` if empty.
5047///
5048/// # Safety
5049/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5050#[unsafe(no_mangle)]
5051pub unsafe extern "C" fn praxis_min_heap_pop(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
5052 abi_guard!("praxis_min_heap_pop", ctx, {
5053 let scope = unsafe { NativeScope::new(ctx) };
5054 let p = unsafe { min_heap_payload_mut(scope.root(heap_ref)) };
5055 match p.items.pop() {
5056 Some(e) => e.0.value,
5057 None => {
5058 unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
5059 unsafe { unit_sentinel(ctx) }
5060 }
5061 }
5062 })
5063}
5064
5065/// The smallest element without removing it; faults `EmptyCollection` if empty.
5066///
5067/// # Safety
5068/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5069#[unsafe(no_mangle)]
5070pub unsafe extern "C" fn praxis_min_heap_peek(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
5071 abi_guard!("praxis_min_heap_peek", ctx, {
5072 let p = unsafe { min_heap_payload(heap_ref) };
5073 match p.items.peek() {
5074 Some(e) => e.0.value,
5075 None => {
5076 unsafe { set_fault(ctx, RaisedFault::EMPTY_COLLECTION) };
5077 unsafe { unit_sentinel(ctx) }
5078 }
5079 }
5080 })
5081}
5082
5083/// The number of elements, as a boxed Int.
5084///
5085/// # Safety
5086/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5087#[unsafe(no_mangle)]
5088pub unsafe extern "C" fn praxis_min_heap_len(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
5089 abi_guard!("praxis_min_heap_len", ctx, {
5090 let p = unsafe { min_heap_payload(heap_ref) };
5091 unsafe { int_ref(ctx, p.items.len() as i64) }
5092 })
5093}
5094
5095/// Every element, as a `Vec[T]` in [`crate::heaps::in_pop_order`] — ascending,
5096/// because the stored entry is a `Reverse<HeapEntry>`. See
5097/// [`praxis_max_heap_items`].
5098///
5099/// # Safety
5100/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5101#[unsafe(no_mangle)]
5102pub unsafe extern "C" fn praxis_min_heap_items(ctx: *mut RuntimeContext, heap_ref: GcRef) -> GcRef {
5103 abi_guard!("praxis_min_heap_items", ctx, {
5104 let p = unsafe { min_heap_payload(heap_ref) };
5105 let items = crate::heaps::in_pop_order(&p.items, |e| e.0.value);
5106 unsafe { vec_of(ctx, p.element_descriptor, items.into_iter()) }
5107 })
5108}
5109
5110/// True iff the heap is empty, as a boxed Bool.
5111///
5112/// # Safety
5113/// `ctx` must be live and wired; `heap_ref` must be a valid `MinHeap` `GcRef`.
5114#[unsafe(no_mangle)]
5115pub unsafe extern "C" fn praxis_min_heap_is_empty(
5116 ctx: *mut RuntimeContext,
5117 heap_ref: GcRef,
5118) -> GcRef {
5119 abi_guard!("praxis_min_heap_is_empty", ctx, {
5120 let p = unsafe { min_heap_payload(heap_ref) };
5121 unsafe { bool_ref(ctx, p.items.is_empty()) }
5122 })
5123}
5124
5125// ---------------------------------------------------------------------------
5126// BitSet (§6.1). A compact set of non-negative integers.
5127// ---------------------------------------------------------------------------
5128
5129use crate::bitset::{BitIndex, BitSetPayload};
5130
5131unsafe fn bitset_payload(r: GcRef) -> &'static BitSetPayload {
5132 unsafe { payload_ref::<BitSetPayload>(r) }
5133}
5134
5135unsafe fn bitset_payload_mut<'s>(r: Rooted<'s>) -> &'s mut BitSetPayload {
5136 unsafe { payload_mut::<BitSetPayload>(r) }
5137}
5138
5139/// Allocate an empty `BitSet` (§6.1). Nullary — no element descriptor.
5140///
5141/// # Safety
5142/// `ctx` must be live and wired.
5143#[unsafe(no_mangle)]
5144pub unsafe extern "C" fn praxis_bitset_new(ctx: *mut RuntimeContext) -> GcRef {
5145 abi_guard!("praxis_bitset_new", ctx, {
5146 // SAFETY: BitSetPayload is BITSET's payload type.
5147 unsafe {
5148 gc_alloc_owned(ctx, &crate::bitset::BITSET, || BitSetPayload {
5149 words: ReprCVec::new(),
5150 })
5151 }
5152 })
5153}
5154
5155/// Set bit `value`; returns Unit. Faults `InvalidSize` if `value` is negative
5156/// or above [`BitIndex::MAX`] — a member this set cannot hold.
5157///
5158/// # Safety
5159/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`; `value`
5160/// must be a valid `Int` `GcRef`.
5161#[unsafe(no_mangle)]
5162pub unsafe extern "C" fn praxis_bitset_insert(
5163 ctx: *mut RuntimeContext,
5164 bs: GcRef,
5165 value: GcRef,
5166) -> GcRef {
5167 abi_guard!("praxis_bitset_insert", ctx, {
5168 unsafe { maybe_collect(ctx) };
5169 let scope = unsafe { NativeScope::new(ctx) };
5170 let p = unsafe { bitset_payload_mut(scope.root(bs)) };
5171 let i = unsafe { int_payload(value) };
5172 // An insert that cannot be honoured is a fault, not a silent no-op: the
5173 // caller asked the set to contain something, and it will not.
5174 let Some(index) = BitIndex::new(i) else {
5175 unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
5176 return unsafe { unit_sentinel(ctx) };
5177 };
5178 // A `BitSet` grows its word vector to reach the index, so an insert far
5179 // past the current high-water is a large uncharged allocation — the
5180 // shape `bfs` has, one visited-set per search.
5181 let before = p.owned_bytes();
5182 p.insert(index);
5183 charge_growth(ctx, before, p.owned_bytes());
5184 unsafe { unit_sentinel(ctx) }
5185 })
5186}
5187
5188/// Clear bit `value`; returns Unit. A value the set cannot hold is a value it
5189/// does not contain, so removing one is a no-op rather than a fault.
5190///
5191/// # Safety
5192/// `ctx` must be live and wired; `bs` and `value` must be valid `GcRef`s.
5193#[unsafe(no_mangle)]
5194pub unsafe extern "C" fn praxis_bitset_remove(
5195 ctx: *mut RuntimeContext,
5196 bs: GcRef,
5197 value: GcRef,
5198) -> GcRef {
5199 abi_guard!("praxis_bitset_remove", ctx, {
5200 let scope = unsafe { NativeScope::new(ctx) };
5201 let p = unsafe { bitset_payload_mut(scope.root(bs)) };
5202 let i = unsafe { int_payload(value) };
5203 if let Some(index) = BitIndex::new(i) {
5204 p.remove(index);
5205 }
5206 unsafe { unit_sentinel(ctx) }
5207 })
5208}
5209
5210/// True iff bit `value` is set, as a raw `0`/`1` in the scalar channel. A value
5211/// the set cannot hold is simply absent — the query is total.
5212///
5213/// **It answers an `i64` and not a boxed `Bool` (ADR-118 decision 6.)** A boxed
5214/// answer would be unboxed again on the next instruction — `if bs.contains(x)`
5215/// as a `Materialize{Bool}`, an `ExtractScalar{Bool}` and then the branch that
5216/// wanted the predicate. `praxis_struct_eq` and `praxis_value_cmp` answer the
5217/// scalar channel for the same reason, and MIR carries this one as
5218/// [`Inst::BitsetContains`](praxis_mir::Inst::BitsetContains) — a
5219/// `Scalar(Bool)` result, and, because it neither allocates nor faults, not a
5220/// GC safepoint.
5221///
5222/// `0` and `1` and nothing else, which is what the `Bool` payload byte holds
5223/// and what `emit_inline_bool` re-boxes with a `!= 0` test.
5224///
5225/// # Safety
5226/// `ctx` must be live and wired; `bs` and `value` must be valid `GcRef`s.
5227#[unsafe(no_mangle)]
5228pub unsafe extern "C" fn praxis_bitset_contains(
5229 ctx: *mut RuntimeContext,
5230 bs: GcRef,
5231 value: GcRef,
5232) -> i64 {
5233 abi_guard!("praxis_bitset_contains", ctx, {
5234 let p = unsafe { bitset_payload(bs) };
5235 let i = unsafe { int_payload(value) };
5236 let present = BitIndex::new(i).is_some_and(|index| p.contains(index));
5237 i64::from(present)
5238 })
5239}
5240
5241/// The number of set bits, as a boxed Int.
5242///
5243/// # Safety
5244/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`.
5245#[unsafe(no_mangle)]
5246pub unsafe extern "C" fn praxis_bitset_len(ctx: *mut RuntimeContext, bs: GcRef) -> GcRef {
5247 abi_guard!("praxis_bitset_len", ctx, {
5248 let p = unsafe { bitset_payload(bs) };
5249 unsafe { int_ref(ctx, p.count() as i64) }
5250 })
5251}
5252
5253/// Every member, as a `Vec[Int]` **ascending** — the snapshot `for i in b`
5254/// iterates (ADR-066).
5255///
5256/// This is the one iterable whose members are not objects: they are bit
5257/// positions, so each one is boxed here rather than copied from the payload.
5258///
5259/// # Safety
5260/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`.
5261#[unsafe(no_mangle)]
5262pub unsafe extern "C" fn praxis_bitset_items(ctx: *mut RuntimeContext, bs: GcRef) -> GcRef {
5263 abi_guard!("praxis_bitset_items", ctx, {
5264 // The members are read out before the first allocation: `vec_of` allocates
5265 // per element, and a collection during the walk would move nothing here
5266 // (the bits are not objects) but would leave the borrow of the payload
5267 // spanning a safepoint, which is not allowed.
5268 let members: Vec<i64> = unsafe { bitset_payload(bs) }.members().collect();
5269 let result = unsafe { praxis_vec_new(ctx, &scalars::INT as *const _) };
5270 let scope = unsafe { NativeScope::new(ctx) };
5271 let rooted = scope.root(result);
5272 for value in members {
5273 let boxed = unsafe { int_ref(ctx, value) };
5274 unsafe { vec_payload_mut(rooted) }.items.push(boxed);
5275 }
5276 result
5277 })
5278}
5279
5280/// True iff the bitset is empty, as a boxed Bool.
5281///
5282/// # Safety
5283/// `ctx` must be live and wired; `bs` must be a valid `BitSet` `GcRef`.
5284#[unsafe(no_mangle)]
5285pub unsafe extern "C" fn praxis_bitset_is_empty(ctx: *mut RuntimeContext, bs: GcRef) -> GcRef {
5286 abi_guard!("praxis_bitset_is_empty", ctx, {
5287 let p = unsafe { bitset_payload(bs) };
5288 unsafe { bool_ref(ctx, p.count() == 0) }
5289 })
5290}
5291
5292// ---------------------------------------------------------------------------
5293// Grid[T] methods (§6.4). `GridPayload` is a row-major `Vec<GcRef>` plus a
5294// width. Coordinates are (x, y) with x rightward, y downward (§6.4). Indexing
5295// stays behind runtime wrappers (§11.5 realloc safety).
5296// ---------------------------------------------------------------------------
5297
5298use crate::collections::{GridExtent, GridPayload};
5299
5300unsafe fn grid_payload(r: GcRef) -> &'static GridPayload {
5301 unsafe { payload_ref::<GridPayload>(r) }
5302}
5303
5304unsafe fn grid_payload_mut<'s>(r: Rooted<'s>) -> &'s mut GridPayload {
5305 unsafe { payload_mut::<GridPayload>(r) }
5306}
5307
5308/// Allocate a `(x, y)` point tuple from two `i64` coordinates. The schema is
5309/// the cached `(Int, Int)` point schema; elements are filled via
5310/// `praxis_tuple_set`. Returns the point `GcRef`.
5311///
5312/// Three allocations, and each one may collect: the tuple must survive the two
5313/// coordinate allocations, and the x coordinate must survive the y's. Nothing
5314/// generated is on the stack here — the caller is a runtime helper — so the
5315/// only thing that can root them is a native scope.
5316unsafe fn alloc_point(ctx: *mut RuntimeContext, x: i64, y: i64) -> GcRef {
5317 let scope = unsafe { NativeScope::new(ctx) };
5318 let schema = crate::tuples::point_schema();
5319 let schema_ptr = schema as *const crate::tuples::TupleSchema;
5320 let tup = scope.root(unsafe { praxis_alloc_tuple(ctx, schema_ptr) });
5321 let x_ref = scope.root(unsafe { int_ref(ctx, x) });
5322 unsafe { praxis_tuple_set(ctx, tup.get(), 0, x_ref.get()) };
5323 let y_ref = unsafe { int_ref(ctx, y) };
5324 unsafe { praxis_tuple_set(ctx, tup.get(), 1, y_ref) };
5325 tup.get()
5326}
5327
5328/// The (x, y) coordinates of a flat `idx` in a grid of `width`.
5329fn grid_xy(idx: usize, width: usize) -> (i64, i64) {
5330 ((idx % width) as i64, (idx / width) as i64)
5331}
5332
5333/// Read the two coordinates out of a `(Int, Int)` point tuple.
5334///
5335/// The inverse of [`alloc_point`], and the one place the grid wrappers unpack a
5336/// point: every `Grid` method taking a position takes it as this tuple, so
5337/// there is one shape to read and no reason for two readings of it.
5338///
5339/// # Safety
5340/// `point` must be a valid `(Int, Int)` tuple `GcRef` — which the type checker
5341/// guarantees for every catalog row whose parameter is `Tuple[Int, Int]`.
5342unsafe fn point_xy(point: GcRef) -> (i64, i64) {
5343 let tp = point.payload::<crate::tuples::TuplePayload>() as *const crate::tuples::TuplePayload;
5344 // SAFETY: caller guarantees `point` is a tuple, so its payload is a
5345 // `TuplePayload`, and a `(Int, Int)` shape has both slots filled with `Int`s.
5346 let pt = unsafe { &*tp };
5347 unsafe { (int_payload(pt.items[0]), int_payload(pt.items[1])) }
5348}
5349
5350/// The height (row count) of a grid: `items.len() / width`, or 0 if width is 0
5351/// (avoids division by zero on a degenerate empty grid).
5352fn grid_height(items_len: usize, width: usize) -> usize {
5353 items_len.checked_div(width).unwrap_or(0)
5354}
5355
5356/// The in-bounds neighbour at `(px + dx, py + dy)`, or `None` if it falls
5357/// outside a `width × height` grid.
5358///
5359/// The offsets are `checked_add` because `px`/`py` come out of a user-supplied
5360/// point tuple, so `(i64::MAX, 0).neighbors4()` would otherwise overflow the
5361/// addition and panic *inside* `extern "C"`. A coordinate that overflows is
5362/// outside every grid — `GridExtent` bounds the extents far below `i64::MAX` —
5363/// so "outside" is the whole answer, not a special case.
5364fn grid_neighbor(
5365 px: i64,
5366 py: i64,
5367 dx: i64,
5368 dy: i64,
5369 width: usize,
5370 height: usize,
5371) -> Option<(i64, i64)> {
5372 let nx = px.checked_add(dx)?;
5373 let ny = py.checked_add(dy)?;
5374 // Both non-negative below, so the casts are exact.
5375 (nx >= 0 && ny >= 0 && (nx as usize) < width && (ny as usize) < height).then_some((nx, ny))
5376}
5377
5378/// The zero value of the type `descriptor` names, or `None` if that type has no
5379/// natural default.
5380///
5381/// Only the scalars and `Unit` have one. A `Grid[Vec[Int]](3, 3)` would need
5382/// nine distinct empty vectors and, worse, no way to know their element type —
5383/// so it is refused rather than filled with something of the wrong type. A null
5384/// descriptor means the caller never said what the cells are, which is likewise
5385/// nothing this can invent.
5386///
5387/// # Safety
5388/// `ctx` must be live and wired.
5389unsafe fn default_cell(
5390 ctx: *mut RuntimeContext,
5391 descriptor: *const TypeDescriptor,
5392) -> Option<GcRef> {
5393 use crate::descriptor::BuiltinTypeId as B;
5394 // SAFETY: a non-null descriptor is a valid `&'static`.
5395 let builtin = unsafe { descriptor.as_ref() }?.as_builtin()?;
5396 unsafe {
5397 match builtin {
5398 B::Unit => Some(unit_sentinel(ctx)),
5399 B::Bool => Some(bool_ref(ctx, false)),
5400 B::Int => Some(int_ref(ctx, 0_i64)),
5401 B::Byte => Some(gc_alloc(ctx, scalars::BYTE_PAYLOAD, 0_u8)),
5402 // `0_u32`, not `'\0'`: a `Char`'s payload is the scalar *value*,
5403 // and a Rust `char` only fits because it shares `u32`'s layout. NUL
5404 // is inside the interned range, so this is the immortal, like the
5405 // `Int` arm above.
5406 B::Char => Some(char_ref(ctx, 0_u32)),
5407 B::Float => Some(gc_alloc(ctx, scalars::FLOAT_PAYLOAD, 0.0_f64)),
5408 // `(null, 0)` meets `praxis_alloc_text`'s UTF-8 precondition
5409 // trivially: the wrapper's own `bytes.is_null() || len == 0` branch
5410 // turns it into the empty slice, and the empty slice is UTF-8.
5411 // The precondition is load-bearing (ADR-111): a violation here
5412 // would abort, not fault. `alloc_text_empty_string_round_trips`
5413 // pins the branch this depends on.
5414 B::Text => Some(praxis_alloc_text(ctx, std::ptr::null(), 0)),
5415 // A composite has no zero value the runtime can invent: a
5416 // `Grid[Vec[Int]]` must be filled by the program that knows what its
5417 // cells are.
5418 B::Vec
5419 | B::Deque
5420 | B::Grid
5421 | B::Map
5422 | B::Set
5423 | B::Counter
5424 | B::MinHeap
5425 | B::MaxHeap
5426 | B::BitSet
5427 // A `Range`'s zero value would be a pair of bounds nobody chose;
5428 // `0..0` is *a* range but it is not "the empty one" in any sense a
5429 // `Grid[Range]` cell wants.
5430 | B::Range
5431 | B::Tuple
5432 | B::Record
5433 | B::Enum
5434 | B::Closure
5435 | B::VarCell => None,
5436 }
5437 }
5438}
5439
5440/// Allocate an empty `Grid[T]` with the given element descriptor, width, and
5441/// height, all cells initialized to the cell type's zero value. (The input parser also constructs
5442/// grids directly; this wrapper is for source `Grid[T]()` + a follow-up fill.)
5443///
5444/// Faults `InvalidSize` if either extent is negative or the grid would exceed
5445/// [`GridExtent::MAX_CELLS`] — the sizes arrive from source, where a negative
5446/// value would otherwise land near `usize::MAX` on the cast.
5447///
5448/// # Safety
5449/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
5450/// a `'static TypeDescriptor` (or null).
5451#[unsafe(no_mangle)]
5452pub unsafe extern "C" fn praxis_grid_new(
5453 ctx: *mut RuntimeContext,
5454 element_descriptor: *const TypeDescriptor,
5455 width: i64,
5456 height: i64,
5457) -> GcRef {
5458 abi_guard!("praxis_grid_new", ctx, {
5459 let Some(extent) = GridExtent::new(width, height) else {
5460 unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
5461 return unsafe { unit_sentinel(ctx) };
5462 };
5463 // Every cell of a `Grid[T]` must *be* a `T`. Filling with the Unit sentinel
5464 // under a `T` element descriptor is the same lie as a mislabelled element
5465 // descriptor, one level down: `get`, `format`, `equals` and `hash` all
5466 // dispatch `T`'s callbacks against a zero-sized Unit payload.
5467 let cells = if extent.cells() == 0 {
5468 Vec::new()
5469 } else {
5470 let Some(fill) = (unsafe { default_cell(ctx, element_descriptor) }) else {
5471 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
5472 return unsafe { unit_sentinel(ctx) };
5473 };
5474 vec![fill; extent.cells()]
5475 };
5476 // SAFETY: GridPayload is GRID's payload type.
5477 unsafe {
5478 gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
5479 element_descriptor,
5480 items: cells,
5481 width: extent.width(),
5482 })
5483 }
5484 })
5485}
5486
5487/// Allocate a `Grid[T]` of `width` × `height` cells, every one holding `fill`
5488/// (ADR-146's `Grid(w, h, fill)`) — the working grid an algorithm allocates for
5489/// itself: an occupancy board, a visited mask, a distance table.
5490///
5491/// Faults `InvalidSize` on the extents [`praxis_grid_new`] refuses, through the
5492/// same [`GridExtent::new`], since this is the very allocation ADR-041 was
5493/// written about and a fill changes nothing about the arithmetic.
5494///
5495/// **It does not call [`default_cell`], and that is the whole difference.**
5496/// `praxis_grid_new` has to invent a zero value for the cell type and has none
5497/// for a composite, so it raises `TypeMismatch` for a `Grid[Vec[Int]]` rather
5498/// than filling it with Unit sentinels under a `Vec` descriptor. An explicit
5499/// fill removes the question — the caller supplied a value of the cell type —
5500/// so a grid of collections is constructible here and not there. The descriptor
5501/// is still reconciled through [`adopt_or_reject`], so a *declared* cell type
5502/// the fill does not match is `TypeMismatch` rather than a silent retag.
5503///
5504/// Every cell is the same `GcRef`, exactly as [`praxis_vec_filled`]'s are; see
5505/// its comment for why that is the language's existing rule rather than a new
5506/// one. The extents arrive boxed for the reason stated there too.
5507///
5508/// # Safety
5509/// `ctx` must be live and wired; `element_descriptor` must be a valid pointer to
5510/// a `'static TypeDescriptor` (or null); `width` and `height` must be valid
5511/// `Int` `GcRef`s; `fill` must be a valid `GcRef`.
5512#[unsafe(no_mangle)]
5513pub unsafe extern "C" fn praxis_grid_filled(
5514 ctx: *mut RuntimeContext,
5515 element_descriptor: *const TypeDescriptor,
5516 width: GcRef,
5517 height: GcRef,
5518 fill: GcRef,
5519) -> GcRef {
5520 abi_guard!("praxis_grid_filled", ctx, {
5521 // SAFETY: caller guarantees `width` and `height` are valid Ints.
5522 let (w, h) = unsafe { (int_payload(width), int_payload(height)) };
5523 let Some(extent) = GridExtent::new(w, h) else {
5524 unsafe { set_fault(ctx, RaisedFault::INVALID_SIZE) };
5525 return unsafe { unit_sentinel(ctx) };
5526 };
5527 let mut descriptor = element_descriptor;
5528 if !unsafe { adopt_or_reject(ctx, &mut descriptor, fill) } {
5529 return unsafe { unit_sentinel(ctx) };
5530 }
5531 let scope = unsafe { NativeScope::new(ctx) };
5532 let fill = scope.root(fill).get();
5533 // The cells are built inside the initializer, which `gc_alloc_owned` runs
5534 // *after* the safepoint — `praxis_vec_filled`'s rule, for the same
5535 // untraced `Vec<GcRef>`.
5536 // SAFETY: GridPayload is GRID's payload type.
5537 unsafe {
5538 gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
5539 element_descriptor: descriptor,
5540 items: vec![fill; extent.cells()],
5541 width: extent.width(),
5542 })
5543 }
5544 })
5545}
5546
5547/// The grid width (number of columns), as a boxed Int.
5548///
5549/// # Safety
5550/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5551#[unsafe(no_mangle)]
5552pub unsafe extern "C" fn praxis_grid_width(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5553 abi_guard!("praxis_grid_width", ctx, {
5554 let p = unsafe { grid_payload(grid) };
5555 unsafe { int_ref(ctx, p.width as i64) }
5556 })
5557}
5558
5559/// The grid height (number of rows), as a boxed Int.
5560///
5561/// # Safety
5562/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
5563#[unsafe(no_mangle)]
5564pub unsafe extern "C" fn praxis_grid_height(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
5565 abi_guard!("praxis_grid_height", ctx, {
5566 let p = unsafe { grid_payload(grid) };
5567 // height = items.len() / width.
5568 let height = grid_height(p.items.len(), p.width);
5569 unsafe { int_ref(ctx, height as i64) }
5570 })
5571}
5572
5573/// The cell at `(x, y)`; faults `IndexOutOfBounds` if out of range.
5574///
5575/// # Safety
5576/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`/`y`
5577/// must be valid `Int` `GcRef`s.
5578#[unsafe(no_mangle)]
5579pub unsafe extern "C" fn praxis_grid_get(
5580 ctx: *mut RuntimeContext,
5581 grid: GcRef,
5582 x: GcRef,
5583 y: GcRef,
5584) -> GcRef {
5585 abi_guard!("praxis_grid_get", ctx, {
5586 let p = unsafe { grid_payload(grid) };
5587 let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5588 let height = grid_height(p.items.len(), p.width);
5589 let Some(idx) = (unsafe { checked_cell(ctx, xi, yi, p.width, height) }) else {
5590 return unsafe { unit_sentinel(ctx) };
5591 };
5592 p.items[idx]
5593 })
5594}
5595
5596/// Set the cell at `(x, y)`; faults `IndexOutOfBounds` if out of range.
5597///
5598/// # Safety
5599/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`/`y`
5600/// must be valid `Int` `GcRef`s; `value` must be a valid `GcRef`.
5601#[unsafe(no_mangle)]
5602pub unsafe extern "C" fn praxis_grid_set(
5603 ctx: *mut RuntimeContext,
5604 grid: GcRef,
5605 x: GcRef,
5606 y: GcRef,
5607 value: GcRef,
5608) -> GcRef {
5609 abi_guard!("praxis_grid_set", ctx, {
5610 let scope = unsafe { NativeScope::new(ctx) };
5611 let p = unsafe { grid_payload_mut(scope.root(grid)) };
5612 let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5613 let height = grid_height(p.items.len(), p.width);
5614 let Some(idx) = (unsafe { checked_cell(ctx, xi, yi, p.width, height) }) else {
5615 return unsafe { unit_sentinel(ctx) };
5616 };
5617 if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
5618 return unsafe { unit_sentinel(ctx) };
5619 }
5620 p.items[idx] = value;
5621 unsafe { unit_sentinel(ctx) }
5622 })
5623}
5624
5625/// `g[x, y] min= candidate` and `g[x, y] max= candidate` — [`vec_update`] over a
5626/// `Grid`, bounds-checked at the cell as [`praxis_grid_set`] is.
5627///
5628/// This is the shape the operator was wanted for most: a `Grid[Int]` of best
5629/// costs relaxed cell by cell, where the grid is already sized and every cell
5630/// already holds its `fill`.
5631///
5632/// # Safety
5633/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`, `x` and
5634/// `y` valid `Int`s, and `value` of the grid's cell type.
5635unsafe fn grid_update(
5636 ctx: *mut RuntimeContext,
5637 grid: GcRef,
5638 x: GcRef,
5639 y: GcRef,
5640 value: GcRef,
5641 keep: std::cmp::Ordering,
5642) -> GcRef {
5643 // SAFETY: as `praxis_grid_set`, whose body this is with a comparison in
5644 // front of the store.
5645 let scope = unsafe { NativeScope::new(ctx) };
5646 let p = unsafe { grid_payload_mut(scope.root(grid)) };
5647 let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5648 let height = grid_height(p.items.len(), p.width);
5649 let Some(idx) = (unsafe { checked_cell(ctx, xi, yi, p.width, height) }) else {
5650 return unsafe { unit_sentinel(ctx) };
5651 };
5652 if !unsafe { adopt_or_reject(ctx, &mut p.element_descriptor, value) } {
5653 return unsafe { unit_sentinel(ctx) };
5654 }
5655 if unsafe { update_cmp(value, p.items[idx]) } == keep {
5656 p.items[idx] = value;
5657 }
5658 unsafe { unit_sentinel(ctx) }
5659}
5660
5661/// `g[x, y] min= candidate` — keep the smaller of the two. See [`grid_update`].
5662///
5663/// # Safety
5664/// As [`grid_update`].
5665#[unsafe(no_mangle)]
5666pub unsafe extern "C" fn praxis_grid_update_min(
5667 ctx: *mut RuntimeContext,
5668 grid: GcRef,
5669 x: GcRef,
5670 y: GcRef,
5671 value: GcRef,
5672) -> GcRef {
5673 abi_guard!("praxis_grid_update_min", ctx, {
5674 unsafe { grid_update(ctx, grid, x, y, value, std::cmp::Ordering::Less) }
5675 })
5676}
5677
5678/// `g[x, y] max= candidate` — keep the larger of the two. See [`grid_update`].
5679///
5680/// # Safety
5681/// As [`grid_update`].
5682#[unsafe(no_mangle)]
5683pub unsafe extern "C" fn praxis_grid_update_max(
5684 ctx: *mut RuntimeContext,
5685 grid: GcRef,
5686 x: GcRef,
5687 y: GcRef,
5688 value: GcRef,
5689) -> GcRef {
5690 abi_guard!("praxis_grid_update_max", ctx, {
5691 unsafe { grid_update(ctx, grid, x, y, value, std::cmp::Ordering::Greater) }
5692 })
5693}
5694
5695/// True iff `(x, y)` is within the grid, as a boxed Bool.
5696///
5697/// # Safety
5698/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`/`y`
5699/// must be valid `Int` `GcRef`s.
5700#[unsafe(no_mangle)]
5701pub unsafe extern "C" fn praxis_grid_contains(
5702 ctx: *mut RuntimeContext,
5703 grid: GcRef,
5704 x: GcRef,
5705 y: GcRef,
5706) -> GcRef {
5707 abi_guard!("praxis_grid_contains", ctx, {
5708 let p = unsafe { grid_payload(grid) };
5709 let (xi, yi) = (unsafe { int_payload(x) }, unsafe { int_payload(y) });
5710 let height = grid_height(p.items.len(), p.width);
5711 // The **pure** [`cell_index`], never `checked_cell`: this wrapper's
5712 // manifest row is `Pure`, so generated code emits no `CheckFault` after
5713 // it and a fault raised on every legitimate `false` would sit pending
5714 // until an unrelated check picked it up.
5715 let inside = cell_index(xi, yi, p.width, height).is_some();
5716 unsafe { bool_ref(ctx, inside) }
5717 })
5718}
5719
5720/// The 4 orthogonal neighbors of `point` that lie inside the grid, as a `Vec`.
5721///
5722/// # Safety
5723/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5724#[unsafe(no_mangle)]
5725pub unsafe extern "C" fn praxis_grid_neighbors4(
5726 ctx: *mut RuntimeContext,
5727 grid: GcRef,
5728 point: GcRef,
5729) -> GcRef {
5730 abi_guard!("praxis_grid_neighbors4", ctx, {
5731 let p = unsafe { grid_payload(grid) };
5732 let (px, py) = unsafe { point_xy(point) };
5733 let height = grid_height(p.items.len(), p.width);
5734 let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
5735 let scope = unsafe { NativeScope::new(ctx) };
5736 let rp = unsafe { vec_payload_mut(scope.root(result)) };
5737 for (dx, dy) in [(0i64, -1), (0, 1), (-1, 0), (1, 0)] {
5738 if let Some((nx, ny)) = grid_neighbor(px, py, dx, dy, p.width, height) {
5739 let pt_ref = unsafe { alloc_point(ctx, nx, ny) };
5740 rp.items.push(pt_ref);
5741 }
5742 }
5743 result
5744 })
5745}
5746
5747/// The 8 neighbors of `point` that lie inside the grid, as a `Vec`.
5748///
5749/// # Safety
5750/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5751#[unsafe(no_mangle)]
5752pub unsafe extern "C" fn praxis_grid_neighbors8(
5753 ctx: *mut RuntimeContext,
5754 grid: GcRef,
5755 point: GcRef,
5756) -> GcRef {
5757 abi_guard!("praxis_grid_neighbors8", ctx, {
5758 let p = unsafe { grid_payload(grid) };
5759 let (px, py) = unsafe { point_xy(point) };
5760 let height = grid_height(p.items.len(), p.width);
5761 let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
5762 let scope = unsafe { NativeScope::new(ctx) };
5763 let rp = unsafe { vec_payload_mut(scope.root(result)) };
5764 for dy in -1i64..=1 {
5765 for dx in -1i64..=1 {
5766 if dx == 0 && dy == 0 {
5767 continue;
5768 }
5769 if let Some((nx, ny)) = grid_neighbor(px, py, dx, dy, p.width, height) {
5770 let pt_ref = unsafe { alloc_point(ctx, nx, ny) };
5771 rp.items.push(pt_ref);
5772 }
5773 }
5774 }
5775 result
5776 })
5777}
5778
5779/// Build one neighbourhood record: a field per direction, in `directions`
5780/// order, each `Some((x, y))` or `None`.
5781///
5782/// # What the field order means, and what it costs to get wrong
5783///
5784/// Slot *i* of the record is `directions[i]`, and the reader's slot index comes
5785/// from the *static* type — the catalog's `Around4`/`Around8` row. The two
5786/// orders are required to agree and nothing derives one from the other, so
5787/// `around_schemas_match_the_catalog` asserts it: a mismatch is a field read
5788/// that quietly answers the wrong direction.
5789///
5790/// # Rooting
5791///
5792/// Every field costs up to four allocations (two `Int`s, a point tuple, a
5793/// `Some`), each a safepoint, so the record is rooted for the whole loop. Each
5794/// field's own value is stored the moment it exists — `praxis_record_set_field`
5795/// allocates nothing — so no second reference is ever live across a safepoint.
5796///
5797/// # Safety
5798/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef` and
5799/// `point` a valid `(Int, Int)` tuple.
5800unsafe fn grid_around(
5801 ctx: *mut RuntimeContext,
5802 grid: GcRef,
5803 point: GcRef,
5804 schema: &'static crate::records::RecordSchema,
5805 directions: &'static [crate::records::Direction],
5806) -> GcRef {
5807 // SAFETY: the caller upholds every argument's validity.
5808 let (width, height) = unsafe {
5809 let p = grid_payload(grid);
5810 (p.width, grid_height(p.items.len(), p.width))
5811 };
5812 let (px, py) = unsafe { point_xy(point) };
5813 let scope = unsafe { NativeScope::new(ctx) };
5814 let record = scope.root(unsafe { praxis_alloc_record(ctx, schema) });
5815 for (i, d) in directions.iter().enumerate() {
5816 let field = match grid_neighbor(px, py, d.dx, d.dy, width, height) {
5817 // `option_some` roots the point across the enum allocation.
5818 Some((nx, ny)) => unsafe { option_some(ctx, alloc_point(ctx, nx, ny)) },
5819 None => unsafe { option_none(ctx) },
5820 };
5821 unsafe { praxis_record_set_field(ctx, record.get(), i as u32, field) };
5822 }
5823 record.get()
5824}
5825
5826/// The four orthogonal neighbours of `point` as an `Around4` record — `up`,
5827/// `left`, `right`, `down`, each `Some((x, y))` or `None` (§6.4).
5828///
5829/// **Not a shorter `neighbors4`.** That wrapper answers a `Vec` clipped to what
5830/// is in bounds, which is what a graph walk wants and what
5831/// `bfs(start, |p| g.neighbors4(p))` passes; it throws away *which* direction
5832/// each neighbour was, and off the edge of the grid it throws away that there
5833/// was a direction at all. This answers exactly that, and the `None` is the
5834/// half a `Vec` cannot carry.
5835///
5836/// # Safety
5837/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5838#[unsafe(no_mangle)]
5839pub unsafe extern "C" fn praxis_grid_around4(
5840 ctx: *mut RuntimeContext,
5841 grid: GcRef,
5842 point: GcRef,
5843) -> GcRef {
5844 abi_guard!("praxis_grid_around4", ctx, {
5845 unsafe {
5846 grid_around(
5847 ctx,
5848 grid,
5849 point,
5850 crate::records::around4_schema(),
5851 crate::records::AROUND4_DIRECTIONS,
5852 )
5853 }
5854 })
5855}
5856
5857/// All eight neighbours of `point` as an `Around8` record, in reading order —
5858/// `up_left`, `up`, `up_right`, `left`, `right`, `down_left`, `down`,
5859/// `down_right` (§6.4). See [`praxis_grid_around4`].
5860///
5861/// # Safety
5862/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s.
5863#[unsafe(no_mangle)]
5864pub unsafe extern "C" fn praxis_grid_around8(
5865 ctx: *mut RuntimeContext,
5866 grid: GcRef,
5867 point: GcRef,
5868) -> GcRef {
5869 abi_guard!("praxis_grid_around8", ctx, {
5870 unsafe {
5871 grid_around(
5872 ctx,
5873 grid,
5874 point,
5875 crate::records::around8_schema(),
5876 crate::records::AROUND8_DIRECTIONS,
5877 )
5878 }
5879 })
5880}
5881
5882/// How many of `point`'s in-bounds neighbours in `directions` hold a cell equal
5883/// to `value`.
5884///
5885/// Equality is the value's own descriptor callback — the same path
5886/// `praxis_grid_find` and `praxis_grid_find_all` take, so "equals" means one
5887/// thing across every `Grid` row (§5.5).
5888///
5889/// A direction that leaves the grid has no cell, so it is not counted. Nothing
5890/// allocates until the answer is boxed, so `grid` needs no root.
5891///
5892/// # Safety
5893/// `ctx` must be live and wired; `grid`, `point` and `value` must be valid
5894/// `GcRef`s.
5895unsafe fn grid_count_equal(
5896 ctx: *mut RuntimeContext,
5897 grid: GcRef,
5898 point: GcRef,
5899 value: GcRef,
5900 directions: &'static [crate::records::Direction],
5901) -> GcRef {
5902 // SAFETY: the caller upholds every argument's validity.
5903 let p = unsafe { grid_payload(grid) };
5904 let height = grid_height(p.items.len(), p.width);
5905 let (px, py) = unsafe { point_xy(point) };
5906 let eq = value.descriptor().equals;
5907 let mut n = 0_i64;
5908 for d in directions {
5909 let Some((nx, ny)) = grid_neighbor(px, py, d.dx, d.dy, p.width, height) else {
5910 continue;
5911 };
5912 let cell = p.items[ny as usize * p.width + nx as usize];
5913 let matches = match eq {
5914 // SAFETY: `equals` came off `value`'s descriptor, and the grid's
5915 // cells are values of the element type the catalog row unified
5916 // `value` with.
5917 Some(equals) => unsafe {
5918 equals(
5919 cell.payload::<u8>() as *const u8,
5920 value.payload::<u8>() as *const u8,
5921 )
5922 },
5923 None => cell == value,
5924 };
5925 n += i64::from(matches);
5926 }
5927 unsafe { int_ref(ctx, n) }
5928}
5929
5930/// `g.count4(p, v)` — how many of the four orthogonal in-bounds neighbours hold
5931/// `v` (§6.4).
5932///
5933/// # Safety
5934/// `ctx` must be live and wired; `grid`, `point` and `value` must be valid
5935/// `GcRef`s.
5936#[unsafe(no_mangle)]
5937pub unsafe extern "C" fn praxis_grid_count4(
5938 ctx: *mut RuntimeContext,
5939 grid: GcRef,
5940 point: GcRef,
5941 value: GcRef,
5942) -> GcRef {
5943 abi_guard!("praxis_grid_count4", ctx, {
5944 unsafe { grid_count_equal(ctx, grid, point, value, crate::records::AROUND4_DIRECTIONS) }
5945 })
5946}
5947
5948/// `g.count8(p, v)` — how many of the eight in-bounds neighbours hold `v`
5949/// (§6.4).
5950///
5951/// # Safety
5952/// `ctx` must be live and wired; `grid`, `point` and `value` must be valid
5953/// `GcRef`s.
5954#[unsafe(no_mangle)]
5955pub unsafe extern "C" fn praxis_grid_count8(
5956 ctx: *mut RuntimeContext,
5957 grid: GcRef,
5958 point: GcRef,
5959 value: GcRef,
5960) -> GcRef {
5961 abi_guard!("praxis_grid_count8", ctx, {
5962 unsafe { grid_count_equal(ctx, grid, point, value, crate::records::AROUND8_DIRECTIONS) }
5963 })
5964}
5965
5966/// How many of `point`'s in-bounds neighbours in `directions` hold a cell the
5967/// closure accepts.
5968///
5969/// A direction that leaves the grid has no cell, so the closure is not called
5970/// for it — the predicate never sees a position that is not on the grid.
5971///
5972/// # Rooting, and what a faulting closure does
5973///
5974/// The closure runs arbitrary Praxis code between iterations, so it allocates
5975/// and it collects. Every cell the loop will hand it is read out of the grid
5976/// and rooted **before the first call**: `grid_payload` hands back a borrow of
5977/// a heap object, and a collection triggered by call *i* would otherwise be
5978/// free to reclaim the cell call *i + 1* is about to receive.
5979///
5980/// A fault stops the count and answers the Unit sentinel, exactly as
5981/// `praxis_vec_sorted_by_key` does: the call site's own fault check is what
5982/// reports, and a half-finished count is not an answer.
5983///
5984/// # Safety
5985/// `ctx` must be live and wired; `grid` and `point` must be valid `GcRef`s and
5986/// `pred` a valid closure `GcRef`.
5987unsafe fn grid_count_where(
5988 ctx: *mut RuntimeContext,
5989 grid: GcRef,
5990 point: GcRef,
5991 pred: GcRef,
5992 directions: &'static [crate::records::Direction],
5993) -> GcRef {
5994 let scope = unsafe { NativeScope::new(ctx) };
5995 // SAFETY: the caller upholds every argument's validity.
5996 let cells: Vec<GcRef> = unsafe {
5997 let p = grid_payload(grid);
5998 let height = grid_height(p.items.len(), p.width);
5999 let (px, py) = point_xy(point);
6000 directions
6001 .iter()
6002 .filter_map(|d| grid_neighbor(px, py, d.dx, d.dy, p.width, height))
6003 .map(|(nx, ny)| {
6004 scope
6005 .root(p.items[ny as usize * p.width + nx as usize])
6006 .get()
6007 })
6008 .collect()
6009 };
6010 let mut n = 0_i64;
6011 for cell in cells {
6012 let Some(answer) = (unsafe { call_unary_closure(ctx, pred, cell) }) else {
6013 // The closure faulted (or is not a closure, which the type checker
6014 // already refused). Leave the fault for the call site's check.
6015 return unsafe { unit_sentinel(ctx) };
6016 };
6017 // A `Bool`'s payload is **one byte**, and `read_scalar` takes the width
6018 // from `BOOL_PAYLOAD`'s own type after checking the descriptor — so a
6019 // closure that answered something else is a `TypeMismatch` rather than
6020 // seven bytes of uninitialized alignment padding.
6021 // SAFETY: `answer` is the `GcRef` the call just produced.
6022 let Some(byte) = (unsafe { read_scalar(answer, scalars::BOOL_PAYLOAD) }) else {
6023 unsafe { set_fault(ctx, RaisedFault::TYPE_MISMATCH) };
6024 return unsafe { unit_sentinel(ctx) };
6025 };
6026 n += i64::from(byte != 0);
6027 }
6028 unsafe { int_ref(ctx, n) }
6029}
6030
6031/// `g.count4_where(p, f)` — how many of the four orthogonal in-bounds
6032/// neighbours hold a cell `f` accepts (§6.4).
6033///
6034/// # Safety
6035/// `ctx` must be live and wired; `grid`, `point` and `pred` must be valid
6036/// `GcRef`s.
6037#[unsafe(no_mangle)]
6038pub unsafe extern "C" fn praxis_grid_count4_where(
6039 ctx: *mut RuntimeContext,
6040 grid: GcRef,
6041 point: GcRef,
6042 pred: GcRef,
6043) -> GcRef {
6044 abi_guard!("praxis_grid_count4_where", ctx, {
6045 unsafe { grid_count_where(ctx, grid, point, pred, crate::records::AROUND4_DIRECTIONS) }
6046 })
6047}
6048
6049/// `g.count8_where(p, f)` — how many of the eight in-bounds neighbours hold a
6050/// cell `f` accepts (§6.4).
6051///
6052/// # Safety
6053/// `ctx` must be live and wired; `grid`, `point` and `pred` must be valid
6054/// `GcRef`s.
6055#[unsafe(no_mangle)]
6056pub unsafe extern "C" fn praxis_grid_count8_where(
6057 ctx: *mut RuntimeContext,
6058 grid: GcRef,
6059 point: GcRef,
6060 pred: GcRef,
6061) -> GcRef {
6062 abi_guard!("praxis_grid_count8_where", ctx, {
6063 unsafe { grid_count_where(ctx, grid, point, pred, crate::records::AROUND8_DIRECTIONS) }
6064 })
6065}
6066
6067/// All `(x, y)` positions in row-major order, as a `Vec`.
6068///
6069/// # Safety
6070/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6071#[unsafe(no_mangle)]
6072pub unsafe extern "C" fn praxis_grid_positions(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6073 abi_guard!("praxis_grid_positions", ctx, {
6074 unsafe { maybe_collect(ctx) };
6075 let p = unsafe { grid_payload(grid) };
6076 let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
6077 let scope = unsafe { NativeScope::new(ctx) };
6078 let rp = unsafe { vec_payload_mut(scope.root(result)) };
6079 for i in 0..p.items.len() {
6080 let (x, y) = grid_xy(i, p.width);
6081 rp.items.push(unsafe { alloc_point(ctx, x, y) });
6082 }
6083 result
6084 })
6085}
6086
6087/// All cells in row-major order, as a `Vec`.
6088///
6089/// # Safety
6090/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6091#[unsafe(no_mangle)]
6092pub unsafe extern "C" fn praxis_grid_cells(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6093 abi_guard!("praxis_grid_cells", ctx, {
6094 let p = unsafe { grid_payload(grid) };
6095 let result = unsafe { praxis_vec_new(ctx, p.element_descriptor) };
6096 let scope = unsafe { NativeScope::new(ctx) };
6097 let rp = unsafe { vec_payload_mut(scope.root(result)) };
6098 for cell in p.items.iter() {
6099 rp.items.push(*cell);
6100 }
6101 result
6102 })
6103}
6104
6105/// Row `y` as a `Vec`; faults `IndexOutOfBounds` if out of range.
6106///
6107/// # Safety
6108/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `y`
6109/// must be a valid `Int` `GcRef`.
6110#[unsafe(no_mangle)]
6111pub unsafe extern "C" fn praxis_grid_row(ctx: *mut RuntimeContext, grid: GcRef, y: GcRef) -> GcRef {
6112 abi_guard!("praxis_grid_row", ctx, {
6113 let p = unsafe { grid_payload(grid) };
6114 let yi = unsafe { int_payload(y) };
6115 let height = grid_height(p.items.len(), p.width);
6116 // One axis of [`cell_index`]'s rule: a row is bounded by the height
6117 // alone, and every `x` in it is in range by construction.
6118 let Some(row) = (unsafe { checked_index(ctx, yi, height) }) else {
6119 return unsafe { unit_sentinel(ctx) };
6120 };
6121 let start = row * p.width;
6122 let result = unsafe { praxis_vec_new(ctx, p.element_descriptor) };
6123 let scope = unsafe { NativeScope::new(ctx) };
6124 let rp = unsafe { vec_payload_mut(scope.root(result)) };
6125 for x in 0..p.width {
6126 rp.items.push(p.items[start + x]);
6127 }
6128 result
6129 })
6130}
6131
6132/// Column `x` as a `Vec`; faults `IndexOutOfBounds` if out of range.
6133///
6134/// # Safety
6135/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`; `x`
6136/// must be a valid `Int` `GcRef`.
6137#[unsafe(no_mangle)]
6138pub unsafe extern "C" fn praxis_grid_column(
6139 ctx: *mut RuntimeContext,
6140 grid: GcRef,
6141 x: GcRef,
6142) -> GcRef {
6143 abi_guard!("praxis_grid_column", ctx, {
6144 let p = unsafe { grid_payload(grid) };
6145 let xi = unsafe { int_payload(x) };
6146 // The other axis: a column is bounded by the width alone, and the
6147 // stride below walks only the rows that exist.
6148 let Some(col) = (unsafe { checked_index(ctx, xi, p.width) }) else {
6149 return unsafe { unit_sentinel(ctx) };
6150 };
6151 let result = unsafe { praxis_vec_new(ctx, p.element_descriptor) };
6152 let scope = unsafe { NativeScope::new(ctx) };
6153 let rp = unsafe { vec_payload_mut(scope.root(result)) };
6154 let mut idx = col;
6155 while idx < p.items.len() {
6156 rp.items.push(p.items[idx]);
6157 idx += p.width;
6158 }
6159 result
6160 })
6161}
6162
6163/// `Some((x, y))` for the first position whose cell equals `value`, or `None`
6164/// (§4.7).
6165///
6166/// An `Option` rather than a sentinel: the Unit sentinel under a `(Int, Int)`
6167/// static type is indistinguishable from a real answer. `find_all` needs no
6168/// equivalent — a `Vec` already encodes "nothing matched" as emptiness.
6169///
6170/// # Safety
6171/// `ctx` must be live and wired; `grid` and `value` must be valid `GcRef`s.
6172#[unsafe(no_mangle)]
6173pub unsafe extern "C" fn praxis_grid_find(
6174 ctx: *mut RuntimeContext,
6175 grid: GcRef,
6176 value: GcRef,
6177) -> GcRef {
6178 abi_guard!("praxis_grid_find", ctx, {
6179 let p = unsafe { grid_payload(grid) };
6180 let val_desc = value.descriptor();
6181 let eq = val_desc.equals;
6182 for (i, cell) in p.items.iter().enumerate() {
6183 let matches = match eq {
6184 Some(equals) => {
6185 let a = cell.payload::<u8>() as *const u8;
6186 let b = value.payload::<u8>() as *const u8;
6187 unsafe { equals(a, b) }
6188 }
6189 None => *cell == value,
6190 };
6191 if matches {
6192 let (x, y) = grid_xy(i, p.width);
6193 // `option_some` roots the point across the enum allocation.
6194 return unsafe { option_some(ctx, alloc_point(ctx, x, y)) };
6195 }
6196 }
6197 unsafe { option_none(ctx) }
6198 })
6199}
6200
6201/// All `(x, y)` positions whose cell equals `value`, as a `Vec`.
6202///
6203/// # Safety
6204/// `ctx` must be live and wired; `grid` and `value` must be valid `GcRef`s.
6205#[unsafe(no_mangle)]
6206pub unsafe extern "C" fn praxis_grid_find_all(
6207 ctx: *mut RuntimeContext,
6208 grid: GcRef,
6209 value: GcRef,
6210) -> GcRef {
6211 abi_guard!("praxis_grid_find_all", ctx, {
6212 unsafe { maybe_collect(ctx) };
6213 let p = unsafe { grid_payload(grid) };
6214 let val_desc = value.descriptor();
6215 let eq = val_desc.equals;
6216 let result = unsafe { praxis_vec_new(ctx, &crate::tuples::TUPLE as *const _) };
6217 let scope = unsafe { NativeScope::new(ctx) };
6218 let rp = unsafe { vec_payload_mut(scope.root(result)) };
6219 for (i, cell) in p.items.iter().enumerate() {
6220 let matches = match eq {
6221 Some(equals) => {
6222 let a = cell.payload::<u8>() as *const u8;
6223 let b = value.payload::<u8>() as *const u8;
6224 unsafe { equals(a, b) }
6225 }
6226 None => *cell == value,
6227 };
6228 if matches {
6229 let (x, y) = grid_xy(i, p.width);
6230 rp.items.push(unsafe { alloc_point(ctx, x, y) });
6231 }
6232 }
6233 result
6234 })
6235}
6236
6237/// A transposed copy of the grid (rows ↔ columns), as a new `Grid`.
6238///
6239/// # Safety
6240/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6241#[unsafe(no_mangle)]
6242pub unsafe extern "C" fn praxis_grid_transpose(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6243 abi_guard!("praxis_grid_transpose", ctx, {
6244 let p = unsafe { grid_payload(grid) };
6245 let height = grid_height(p.items.len(), p.width);
6246 let new_width = height;
6247 let new_height = p.width;
6248 let mut cells = Vec::with_capacity(p.items.len());
6249 for y in 0..new_height {
6250 for x in 0..new_width {
6251 // new[x,y] = old[y,x]
6252 cells.push(p.items[x * p.width + y]);
6253 }
6254 }
6255 let _ = ctx;
6256 // SAFETY: GridPayload is GRID's payload type.
6257 unsafe {
6258 gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
6259 element_descriptor: p.element_descriptor,
6260 items: cells,
6261 width: new_width,
6262 })
6263 }
6264 })
6265}
6266
6267/// A copy of the grid rotated 90° left (counter-clockwise), as a new `Grid`.
6268///
6269/// # Safety
6270/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6271#[unsafe(no_mangle)]
6272pub unsafe extern "C" fn praxis_grid_rotate_left(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6273 abi_guard!("praxis_grid_rotate_left", ctx, {
6274 let p = unsafe { grid_payload(grid) };
6275 let height = grid_height(p.items.len(), p.width);
6276 // Rotate left (90° CCW): result is H×W (width=height, height=width).
6277 // With x rightward and y downward, turning counter-clockwise carries the
6278 // *rightmost* column to the top row, top-to-bottom:
6279 // result[x, y] = original[width-1-y, x], for x in 0..height, y in 0..width.
6280 let new_width = height;
6281 let new_height = p.width;
6282 let mut cells = Vec::with_capacity(p.items.len());
6283 for y in 0..new_height {
6284 for x in 0..new_width {
6285 let ox = p.width - 1 - y;
6286 let oy = x;
6287 cells.push(p.items[oy * p.width + ox]);
6288 }
6289 }
6290 // SAFETY: GridPayload is GRID's payload type.
6291 unsafe {
6292 gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
6293 element_descriptor: p.element_descriptor,
6294 items: cells,
6295 width: new_width,
6296 })
6297 }
6298 })
6299}
6300
6301/// A copy of the grid rotated 90° right (clockwise), as a new `Grid`.
6302///
6303/// # Safety
6304/// `ctx` must be live and wired; `grid` must be a valid `Grid` `GcRef`.
6305#[unsafe(no_mangle)]
6306pub unsafe extern "C" fn praxis_grid_rotate_right(ctx: *mut RuntimeContext, grid: GcRef) -> GcRef {
6307 abi_guard!("praxis_grid_rotate_right", ctx, {
6308 let p = unsafe { grid_payload(grid) };
6309 let height = grid_height(p.items.len(), p.width);
6310 // Rotate right (90° CW): result is H×W (width=height, height=width).
6311 // With x rightward and y downward, turning clockwise carries the *leftmost*
6312 // column to the top row, bottom-to-top:
6313 // result[x, y] = original[y, height-1-x], for x in 0..height, y in 0..width.
6314 let new_width = height;
6315 let new_height = p.width;
6316 let mut cells = Vec::with_capacity(p.items.len());
6317 for y in 0..new_height {
6318 for x in 0..new_width {
6319 let ox = y;
6320 let oy = height - 1 - x;
6321 cells.push(p.items[oy * p.width + ox]);
6322 }
6323 }
6324 // SAFETY: GridPayload is GRID's payload type.
6325 unsafe {
6326 gc_alloc_owned(ctx, &crate::collections::GRID, || GridPayload {
6327 element_descriptor: p.element_descriptor,
6328 items: cells,
6329 width: new_width,
6330 })
6331 }
6332 })
6333}
6334
6335// ---------------------------------------------------------------------------
6336// Text methods (§4.3).
6337//
6338// `Text` is an immutable UTF-8 payload (`Box<str>`). The methods are pure
6339// (no allocation beyond the result object) and never fault.
6340// ---------------------------------------------------------------------------
6341
6342/// Read the `Text` payload of a `GcRef` as a `&str`, following slice owners.
6343///
6344/// # Safety
6345/// `r` must be a valid `Text` `GcRef`. Non-moving GC keeps it stable.
6346unsafe fn text_str(r: GcRef) -> &'static str {
6347 // SAFETY: caller guarantees `r` is Text; payload is a TextPayload.
6348 let payload = r.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload;
6349 unsafe { crate::text::text_str(payload) }
6350}
6351
6352/// The `Text` payload behind a `GcRef`.
6353///
6354/// # Safety
6355/// `r` must be a valid `Text` `GcRef`. Non-moving GC keeps it stable.
6356#[inline]
6357unsafe fn text_payload(r: GcRef) -> *const crate::text::TextPayload {
6358 r.payload::<crate::text::TextPayload>() as *const crate::text::TextPayload
6359}
6360
6361/// The number of Unicode scalar values (chars) in `text`, as a boxed `Int`.
6362///
6363/// **O(1) after the text or its owner has been counted once** (ADR-115). The
6364/// count is cached rather than recomputed as `text_str(text).chars().count()`,
6365/// which is two passes over every byte — `text_str` re-validates the UTF-8 the
6366/// payload is already known to hold, and `chars().count()` then decodes it —
6367/// and this is called *once per iteration* of `for c in t`, because `lower_for`
6368/// puts the plan's `len` call in the loop **header**
6369/// (`praxis-mir/src/build.rs`, `lower_for`).
6370///
6371/// # Safety
6372/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6373#[unsafe(no_mangle)]
6374pub unsafe extern "C" fn praxis_text_len(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6375 abi_guard!("praxis_text_len", ctx, {
6376 // SAFETY: caller guarantees `text` is Text.
6377 let len = unsafe { crate::text::text_char_count(text_payload(text)) } as i64;
6378 unsafe { int_ref(ctx, len) }
6379 })
6380}
6381
6382/// The whole of `text`, trimmed, if `run` accepts all of it — the shared half of
6383/// [`praxis_text_int`] and [`praxis_text_float`] (ADR-136).
6384///
6385/// **`run` is the input parser's own scanner** (`parser::take_int_run`,
6386/// `parser::take_float_run`), and that is the point rather than a convenience.
6387/// `parse(t, int)` and `t.int()` are two spellings of "read a number out of
6388/// text", and a program that gets different answers from them has found a defect
6389/// in one of them. Sharing the scanner makes the disagreement unrepresentable.
6390///
6391/// The difference between the method and the atomic is *how much* must match,
6392/// not what: an atomic stops where its run stops and hands the rest of the line
6393/// to the template, and a method has no rest to hand anywhere — so a run that
6394/// covers less than the whole trimmed text is `None`. That is what makes
6395/// `"1 2"`, `"12abc"` and `"1."` rejections rather than partial answers.
6396///
6397/// Trimming is the one liberty taken, and it is what makes a line read off input
6398/// usable without a second call.
6399fn whole_trimmed(s: &str, run: fn(&[u8]) -> (&str, usize)) -> Option<&str> {
6400 let trimmed = s.trim();
6401 let (text, len) = run(trimmed.as_bytes());
6402 (!text.is_empty() && len == trimmed.len()).then_some(trimmed)
6403}
6404
6405/// The `Int` `text` spells, as `Some(n)`, or `None` when it spells no `Int`
6406/// (ADR-136).
6407///
6408/// `Y001`'s help on `var count: Int = raw` names `.int()`, so this is the method
6409/// that help sends the reader to.
6410///
6411/// `Option[Int]` and not `Int`, for §4.7's reason: a text that is not a number
6412/// is *absence*, not a fault. Input arrives as text and is routinely not what
6413/// the program hoped, so a panicking conversion would make `"abc".int()` a crash
6414/// the program has no way to prevent — where `read lines(int)`, the other half
6415/// of that help, reports at the parser and never produces the value at all.
6416///
6417/// The accepted spelling is **§7.4's `int` atomic** over the whole trimmed text:
6418/// an optional `-` and then digits (see [`whole_trimmed`]). `"1 2"`, `"0x10"`,
6419/// `"1_000"`, `"+5"` and `""` are all `None`, and so is a value outside `Int`'s
6420/// range — for the reason `Y013` exists: a saturated answer is a number nobody
6421/// wrote.
6422///
6423/// # Safety
6424/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6425#[unsafe(no_mangle)]
6426pub unsafe extern "C" fn praxis_text_int(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6427 abi_guard!("praxis_text_int", ctx, {
6428 // SAFETY: caller guarantees `text` is Text.
6429 let s = unsafe { text_str(text) };
6430 match whole_trimmed(s, crate::parser::take_int_run).and_then(|t| t.parse::<i64>().ok()) {
6431 // SAFETY: `ctx` is live and wired; `int_ref` allocates the payload
6432 // and `option_some` roots it across the enum allocation.
6433 Some(n) => unsafe {
6434 let boxed = int_ref(ctx, n);
6435 option_some(ctx, boxed)
6436 },
6437 // SAFETY: `ctx` is live and wired.
6438 None => unsafe { option_none(ctx) },
6439 }
6440 })
6441}
6442
6443/// The `Float` `text` spells, as `Some(x)`, or `None` when it spells no `Float`
6444/// (ADR-136).
6445///
6446/// [`praxis_text_int`]'s twin, over §7.4's `float` atomic: an optional sign,
6447/// digits, an optional `.` **with** a fraction, and an optional complete
6448/// exponent. `"1.5"`, `"-2"`, `"+5.0"` and `"1e10"` are values; `"1."`, `"1e"`,
6449/// `"inf"`, `"nan"` and `""` are `None`, because none of them is a token the
6450/// input parser reads either.
6451///
6452/// `inf` and `nan` are the answer worth stating: Rust's `f64::from_str` accepts
6453/// both, §7.4's `float` accepts neither, and a method that took them would be a
6454/// second opinion about what a number is. `Float` still *has* those values —
6455/// `1.0 / 0.0` is one — and `Float.to_text()` prints them; what has no spelling
6456/// is reading one back out of arbitrary text.
6457///
6458/// The leading `+` this accepts and [`praxis_text_int`] does not is §7.4's own
6459/// asymmetry, carried over rather than papered over: changing an atomic's
6460/// accepted set is a change to the input language.
6461///
6462/// # Safety
6463/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6464#[unsafe(no_mangle)]
6465pub unsafe extern "C" fn praxis_text_float(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6466 abi_guard!("praxis_text_float", ctx, {
6467 // SAFETY: caller guarantees `text` is Text.
6468 let s = unsafe { text_str(text) };
6469 match whole_trimmed(s, crate::parser::take_float_run).and_then(|t| t.parse::<f64>().ok()) {
6470 // SAFETY: `ctx` is live and wired. `praxis_alloc_float` takes the
6471 // bit pattern the uniform scalar ABI carries (§4.3), and
6472 // `option_some` roots the box across the enum allocation.
6473 Some(x) => unsafe {
6474 let boxed = praxis_alloc_float(ctx, x.to_bits() as i64);
6475 option_some(ctx, boxed)
6476 },
6477 // SAFETY: `ctx` is live and wired.
6478 None => unsafe { option_none(ctx) },
6479 }
6480 })
6481}
6482
6483/// True iff `text` has no chars, as a boxed `Bool`.
6484///
6485/// Asks the bytes rather than a `&str`: `text_str` validates the whole payload
6486/// to hand back a `&str`, which would make an O(1) question O(n) (ADR-115). A
6487/// text is empty iff it has no bytes — no scalar encodes to zero of them.
6488///
6489/// # Safety
6490/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`.
6491#[unsafe(no_mangle)]
6492pub unsafe extern "C" fn praxis_text_is_empty(ctx: *mut RuntimeContext, text: GcRef) -> GcRef {
6493 abi_guard!("praxis_text_is_empty", ctx, {
6494 // SAFETY: caller guarantees `text` is Text.
6495 let empty = unsafe { crate::text::text_bytes(text_payload(text)) }.is_empty();
6496 // SAFETY: ctx/heap valid; Bool immortal path.
6497 unsafe { bool_ref(ctx, empty) }
6498 })
6499}
6500
6501/// `a + b` on two `Text`s — a new owned `Text` holding their concatenation
6502/// (ADR-085).
6503///
6504/// Declared `Allocates` rather than `AllocatesAndFaults`, which is
6505/// `praxis_float_to_text`'s row and for the same reason: both payloads are
6506/// UTF-8 by construction, so their concatenation is too, and there is nothing
6507/// for the `InvalidText` fault to check. Since ADR-111 `praxis_alloc_text` is
6508/// `Allocates` on the same footing — every wrapper here trusts its caller about
6509/// encoding, and the one place that cannot (`praxis_get_input`, which holds the
6510/// host's raw bytes) validates and faults there.
6511///
6512/// The result is `Owned` and never a `Slice`: a concatenation has no single
6513/// owner to point into, and a slice of one would be a lie about its extent.
6514///
6515/// # Safety
6516/// `ctx` must be live and wired; `a` and `b` must be valid `Text` `GcRef`s.
6517#[unsafe(no_mangle)]
6518pub unsafe extern "C" fn praxis_text_concat(ctx: *mut RuntimeContext, a: GcRef, b: GcRef) -> GcRef {
6519 abi_guard!("praxis_text_concat", ctx, {
6520 // SAFETY: caller guarantees both are Text.
6521 let left = unsafe { text_str(a) };
6522 let right = unsafe { text_str(b) };
6523 let mut joined = String::with_capacity(left.len() + right.len());
6524 joined.push_str(left);
6525 joined.push_str(right);
6526 // SAFETY: TextPayload matches TEXT's size/align and is fully initialized.
6527 unsafe { text_ref(ctx, joined) }
6528 })
6529}
6530
6531/// Render `value` into a fresh `Text`, **exactly as `out` renders it** (§8.1,
6532/// ADR-147).
6533///
6534/// This is the whole of an interpolation hole. `"{v}"` on a `Vec[Int]` is
6535/// `[1, 2, 3]` because this function and [`praxis_write_stdout`] are the same
6536/// two lines with a different destination: both call [`GcRef::format`], which
6537/// dispatches through the value's type descriptor. There is no second renderer
6538/// here and there must never be one — writing a `write!` inline instead of
6539/// calling `format` is the mistake this wrapper exists to make unnecessary, and
6540/// it is the mistake ADR-143 decision 2 records for the three scalar rows.
6541///
6542/// That is also why a hole may hold **any** type (ADR-147 decision 2). Every
6543/// `GcRef` has a descriptor and every descriptor has a `format` callback, so
6544/// there is no value this can be handed that it cannot render — which is what
6545/// lets inference impose no requirement on a hole at all.
6546///
6547/// Declared `Allocates`, never `AllocatesAndFaults`: nothing above can fail, and
6548/// a `String` built by `format` is valid UTF-8 by construction, so there is
6549/// nothing for an `InvalidText` fault to check. That is `praxis_text_concat`'s
6550/// row exactly.
6551///
6552/// # Safety
6553/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6554#[unsafe(no_mangle)]
6555pub unsafe extern "C" fn praxis_value_to_text(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6556 abi_guard!("praxis_value_to_text", ctx, {
6557 let mut s = String::new();
6558 value.format(&mut s);
6559 // SAFETY: `s` is valid UTF-8; ctx/heap valid.
6560 unsafe { text_ref(ctx, s) }
6561 })
6562}
6563
6564/// The `Char` at `index`, or an `IndexOutOfBounds` fault if out of range
6565/// (ADR-086). `index` counts Unicode scalar values, not bytes.
6566///
6567/// # Safety
6568/// `ctx` must be live and wired; `text` must be a valid `Text` `GcRef`; `index`
6569/// must be a valid `Int` `GcRef`.
6570#[unsafe(no_mangle)]
6571pub unsafe extern "C" fn praxis_text_get(
6572 ctx: *mut RuntimeContext,
6573 text: GcRef,
6574 index: GcRef,
6575) -> GcRef {
6576 abi_guard!("praxis_text_get", ctx, {
6577 // SAFETY: caller guarantees `text` is Text.
6578 let payload = unsafe { text_payload(text) };
6579 // SAFETY: caller guarantees `index` is a valid Int.
6580 let idx = unsafe { int_payload(index) };
6581 if idx < 0 {
6582 unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6583 return unsafe { unit_sentinel(ctx) };
6584 }
6585 // **The byte index is the character index exactly when every scalar is
6586 // one byte, and `text_ascii_bytes` answers that in O(1)** (ADR-115).
6587 // The fallback is `chars().nth(i)`, which is O(i): a multi-byte text
6588 // has no random access without either a wider representation or a
6589 // cursor, and ADR-115 declines the cursor with its arithmetic. `idx` is
6590 // non-negative above, so the `as usize` cannot wrap.
6591 // SAFETY: caller guarantees `text` is Text.
6592 if let Some(bytes) = unsafe { crate::text::text_ascii_bytes(payload) } {
6593 return match bytes.get(idx as usize) {
6594 // One-byte scalars are exactly the ASCII range, so the byte
6595 // *is* the code point (§4.3, ADR-086).
6596 Some(&b) => unsafe { char_ref(ctx, u32::from(b)) },
6597 None => {
6598 unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6599 unsafe { unit_sentinel(ctx) }
6600 }
6601 };
6602 }
6603 // SAFETY: caller guarantees `text` is Text.
6604 let s = unsafe { text_str(text) };
6605 match s.chars().nth(idx as usize) {
6606 Some(ch) => {
6607 // No validity check, and none belongs here: `ch` is a Rust `char`,
6608 // so `ch as u32` is a valid Unicode scalar by construction. The
6609 // check `praxis_int_to_char` needs is for the values that did not
6610 // come from one — which is why this goes to `char_ref` directly
6611 // rather than through `checked_alloc_char`.
6612 //
6613 // This is the interning's largest site (ADR-107): the same call
6614 // is `t[i]` and every step of `for c in t` (the `iter_plan`
6615 // lowering), so a program that walks a line of ASCII text would
6616 // otherwise box one object per character.
6617 unsafe { char_ref(ctx, ch as u32) }
6618 }
6619 None => {
6620 unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6621 unsafe { unit_sentinel(ctx) }
6622 }
6623 }
6624 })
6625}
6626
6627// ---------------------------------------------------------------------------
6628// `out(...)` — write a value to stdout followed by a newline (§16.1).
6629// ---------------------------------------------------------------------------
6630
6631/// Format `value` through its descriptor and write it to stdout followed by a
6632/// newline. Returns the Unit sentinel (§4.3), matching `out`'s `(T) -> Unit`
6633/// type. Never faults.
6634///
6635/// # Safety
6636/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6637#[unsafe(no_mangle)]
6638pub unsafe extern "C" fn praxis_write_stdout(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6639 abi_guard!("praxis_write_stdout", ctx, {
6640 use std::io::Write;
6641 let mut out = String::new();
6642 value.format(&mut out);
6643 let _ = std::io::stdout().write_all(out.as_bytes());
6644 let _ = std::io::stdout().write_all(b"\n");
6645 // `out` is `(T) -> Unit`: return the Unit sentinel so a Unit-typed value
6646 // flows out, not the printed argument (which would otherwise leak as the
6647 // function's result and be printed a second time by the host).
6648 unsafe { unit_sentinel(ctx) }
6649 })
6650}
6651
6652// ---------------------------------------------------------------------------
6653// `dbg(...)`, `panic(...)`, `assert(...)` — the rest of §16.1's control names.
6654// ---------------------------------------------------------------------------
6655
6656/// Format `value` through its descriptor, write it to stderr followed by a
6657/// newline, and hand **the same reference back** (§8.1). `dbg` is `forall T.
6658/// (T) -> T`, so it can be wrapped around any subexpression without changing
6659/// what the program computes. Never faults, never allocates.
6660///
6661/// # Safety
6662/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6663#[unsafe(no_mangle)]
6664pub unsafe extern "C" fn praxis_dbg(_ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6665 abi_guard!("praxis_dbg", _ctx, {
6666 use std::io::Write;
6667 let mut rendered = String::new();
6668 value.format(&mut rendered);
6669 let _ = std::io::stderr().write_all(rendered.as_bytes());
6670 let _ = std::io::stderr().write_all(b"\n");
6671 value
6672 })
6673}
6674
6675/// Record `value` as the fault message and raise [`FaultKind::Panic`] (§9.1).
6676///
6677/// The message is rendered **here**, through the value's descriptor, exactly as
6678/// `out` renders its argument. It has to be: the host reads the message after
6679/// the heap the `GcRef` points into has been torn down, so a stored reference
6680/// would outlive what it names.
6681///
6682/// Returns the Unit sentinel. `panic` is `forall T. (T) -> Never`, so no caller
6683/// can use the result — but the ABI returns a `GcRef` on every path, and a
6684/// fault epilogue needs a defined value to carry out (§10.4).
6685///
6686/// # Safety
6687/// `ctx` must be live and wired; `value` must be a valid `GcRef`.
6688#[unsafe(no_mangle)]
6689pub unsafe extern "C" fn praxis_panic(ctx: *mut RuntimeContext, value: GcRef) -> GcRef {
6690 abi_guard!("praxis_panic", ctx, {
6691 let mut message = String::new();
6692 value.format(&mut message);
6693 unsafe { set_fault_message(ctx, message) };
6694 unsafe { set_fault(ctx, RaisedFault::PANIC) };
6695 unsafe { unit_sentinel(ctx) }
6696 })
6697}
6698
6699/// Raise [`FaultKind::AssertFailed`] when `condition` is false (§9.1), and do
6700/// nothing at all when it is true.
6701///
6702/// `assert` is `(Bool) -> Unit`, so the argument is one of the two `Bool`
6703/// immortals and reading its payload needs no descriptor check.
6704///
6705/// It sets **no** message: `assert` takes a condition and nothing else, so the
6706/// only text available would restate the fault kind. `panic` is the name that
6707/// carries words.
6708///
6709/// # Safety
6710/// `ctx` must be live and wired; `condition` must be a valid `Bool` `GcRef`.
6711#[unsafe(no_mangle)]
6712pub unsafe extern "C" fn praxis_assert(ctx: *mut RuntimeContext, condition: GcRef) -> GcRef {
6713 abi_guard!("praxis_assert", ctx, {
6714 // SAFETY: `assert`'s scheme is `(Bool) -> Unit`, so the argument is a Bool.
6715 if !unsafe { crate::immortal::read_bool(condition) } {
6716 unsafe { set_fault(ctx, RaisedFault::ASSERT_FAILED) };
6717 }
6718 unsafe { unit_sentinel(ctx) }
6719 })
6720}
6721
6722// ---------------------------------------------------------------------------
6723// `Range` (§4.11, ADR-059).
6724//
6725// `a..b` and `a..=b` are two symbols rather than one symbol with a flag: the
6726// choice is already a syntactic fact the MIR builder holds, and a boolean
6727// smuggled through an `i64` parameter would have 2^64 spellings for two states.
6728// Both bounds arrive as `Int` `GcRef`s, because a bound is an arbitrary
6729// expression and every other wrapper takes its operands boxed.
6730// ---------------------------------------------------------------------------
6731
6732/// Build the half-open range `start..end` (§4.11). A descending range is
6733/// **empty** — [`RangeVal::new`](crate::range::RangeVal::new) normalizes it, so
6734/// no range with a negative length exists.
6735///
6736/// # Safety
6737/// `ctx` must be live and wired; both bounds must be valid `Int` `GcRef`s.
6738#[unsafe(no_mangle)]
6739pub unsafe extern "C" fn praxis_range_new(
6740 ctx: *mut RuntimeContext,
6741 start: GcRef,
6742 end: GcRef,
6743) -> GcRef {
6744 abi_guard!("praxis_range_new", ctx, {
6745 let a = unsafe { int_payload(start) };
6746 let b = unsafe { int_payload(end) };
6747 unsafe {
6748 gc_alloc(
6749 ctx,
6750 crate::range::RANGE_PAYLOAD,
6751 crate::range::RangeVal::new(a, b),
6752 )
6753 }
6754 })
6755}
6756
6757/// Build the inclusive range `start..=end` (§4.11).
6758///
6759/// # Safety
6760/// `ctx` must be live and wired; both bounds must be valid `Int` `GcRef`s.
6761#[unsafe(no_mangle)]
6762pub unsafe extern "C" fn praxis_range_new_inclusive(
6763 ctx: *mut RuntimeContext,
6764 start: GcRef,
6765 end: GcRef,
6766) -> GcRef {
6767 abi_guard!("praxis_range_new_inclusive", ctx, {
6768 let a = unsafe { int_payload(start) };
6769 let b = unsafe { int_payload(end) };
6770 unsafe {
6771 gc_alloc(
6772 ctx,
6773 crate::range::RANGE_PAYLOAD,
6774 crate::range::RangeVal::new_inclusive(a, b),
6775 )
6776 }
6777 })
6778}
6779
6780/// The number of integers in a range (§4.11) — what a `for` loop reads to
6781/// bound itself.
6782///
6783/// **Faults when the count does not fit an `Int`.** Only the very widest ranges
6784/// reach it (`Int::MIN..Int::MAX` holds `2^64 - 1` integers), and reporting a
6785/// wrapped negative length instead would be a `for` loop that ran zero times
6786/// over every integer there is.
6787///
6788/// The kind is `IntOverflow`, which is what `gcd`, `lcm` and A\*'s path cost
6789/// already answer for a result with no `Int`. It is deliberately not
6790/// `EmptyRange`: the range this fires on is the *fullest* one there is, so that
6791/// message would lie about it (ADR-059, ADR-075).
6792///
6793/// # Safety
6794/// `ctx` must be live and wired; `r` must be a valid `Range` `GcRef`.
6795#[unsafe(no_mangle)]
6796pub unsafe extern "C" fn praxis_range_len(ctx: *mut RuntimeContext, r: GcRef) -> GcRef {
6797 abi_guard!("praxis_range_len", ctx, {
6798 // SAFETY: the compiler only emits this with a Range-typed operand.
6799 let range = unsafe { &*r.payload::<crate::range::RangeVal>() };
6800 match i64::try_from(range.len()) {
6801 Ok(len) => unsafe { int_ref(ctx, len) },
6802 Err(_) => {
6803 unsafe { set_fault(ctx, RaisedFault::INT_OVERFLOW) };
6804 unsafe { unit_sentinel(ctx) }
6805 }
6806 }
6807 })
6808}
6809
6810/// The `index`-th integer of a range (§4.11). Faults when `index` is outside
6811/// it, exactly as `Vec.get` does.
6812///
6813/// # Safety
6814/// `ctx` must be live and wired; `r` must be a valid `Range` `GcRef` and
6815/// `index` a valid `Int` one.
6816#[unsafe(no_mangle)]
6817pub unsafe extern "C" fn praxis_range_get(
6818 ctx: *mut RuntimeContext,
6819 r: GcRef,
6820 index: GcRef,
6821) -> GcRef {
6822 abi_guard!("praxis_range_get", ctx, {
6823 // SAFETY: the compiler only emits this with a Range-typed receiver.
6824 let range = unsafe { &*r.payload::<crate::range::RangeVal>() };
6825 let i = unsafe { int_payload(index) };
6826 match range.get(i) {
6827 Some(value) => unsafe { int_ref(ctx, value) },
6828 None => {
6829 unsafe { set_fault(ctx, RaisedFault::INDEX_OUT_OF_BOUNDS) };
6830 unsafe { unit_sentinel(ctx) }
6831 }
6832 }
6833 })
6834}
6835
6836// ---------------------------------------------------------------------------
6837// Input parser (§7).
6838//
6839// `read` / `parse` lower to runtime calls that fetch the input buffer and run
6840// a compiled parser plan against it. The plan is compiled at HIR time and
6841// registered in a global slab; its index is passed as a boxed Int.
6842// ---------------------------------------------------------------------------
6843
6844/// Return the process-input source buffer (§7.10), reading it the **first**
6845/// time a program asks.
6846///
6847/// A `read` lowers to this call and then to `praxis_run_parser`, so this is
6848/// where §7.10's "the first `read` lazily reads standard input once" happens.
6849/// The host installs a [`crate::input::InputReader`] rather than a buffer; it
6850/// is called at most once — [`crate::input::take_input_reader`] removes it, so
6851/// "once" is structural rather than a flag — and the result is installed as
6852/// `input_source`, which every later `read` reuses.
6853///
6854/// Nothing before a program's first `read` touches the host's input. Reading it
6855/// up front would make a program with no `read` in it still consume standard
6856/// input, so `praxis run` against an open pipe would block forever.
6857///
6858/// A host that installs no reader — every JIT test, and the crash debugger's
6859/// re-run path, which installs the buffer directly to keep re-runs identical
6860/// (§9.7) — reaches the plain `input_source` read below.
6861///
6862/// **A reader that answers zero bytes has given empty input, not no input.** Its
6863/// answer is installed as `input_source` whatever its length, so `read` runs
6864/// against a zero-length buffer and the parser constructors answer from their own
6865/// rules — `lines(int)` over it is `[]` by `split_lines`'s rule, and one that
6866/// requires content faults at `0..0` naming what it expected. That is what §7.11
6867/// asks a mismatch to carry, and a fault raised before any buffer existed can
6868/// carry none of it: it has no input span to name. A zero-byte `--input` file is
6869/// the same decision, made at `praxis-cli/src/run.rs` (ADR-087).
6870///
6871/// The one remaining Unit-source state belongs to a host that installs **neither**
6872/// a buffer nor a reader — every JIT test, every embedder. `praxis_run_parser`'s
6873/// descriptor guard (§6.3) is what keeps that state survivable; no `praxis run`
6874/// reaches it.
6875///
6876/// **This wrapper owns the UTF-8 judgement, and it is the only producer of
6877/// [`FaultKind::InvalidText`](crate::FaultKind::InvalidText)** (ADR-111). A
6878/// reader's bytes are the host's, not the compiler's, so they are checked here
6879/// and `INVALID_TEXT` is raised here — where `lower_read`'s `CheckFault` makes
6880/// it divert at the `read`. Raising it inside `praxis_alloc_text` instead would
6881/// cost a check after every text *literal* for a fault a literal cannot
6882/// produce; that wrapper trusts its caller, and this is the caller that has to
6883/// earn the trust.
6884///
6885/// `praxis run` cannot reach the fault: `lazy_stdin::read` goes through
6886/// `std::io::read_to_string` and exits 2 on non-UTF-8 stdin before the runtime
6887/// sees a byte. An embedder installing its own reader can.
6888///
6889/// # Safety
6890/// `ctx` must be live and wired.
6891#[unsafe(no_mangle)]
6892pub unsafe extern "C" fn praxis_get_input(ctx: *mut RuntimeContext) -> GcRef {
6893 abi_guard!("praxis_get_input", ctx, {
6894 if let Some(read) = crate::input::take_input_reader() {
6895 let bytes = read();
6896 // **This is the one place in the runtime that holds raw host bytes,
6897 // so it is the one place the UTF-8 judgement §4.3 assigns belongs**
6898 // (ADR-111). Here the fault is real: a host's `InputReader` is
6899 // infallible about I/O by design (`crate::input`) and says nothing
6900 // about encoding, so these bytes are exactly as trustworthy as the
6901 // host. `GetInput`'s row is `AllocatesAndFaults` and `lower_read`
6902 // emits the check, so `InvalidText` diverts *at the `read`*.
6903 //
6904 // The Unit sentinel is the defined dummy (§10.4); `input_source`
6905 // holds it until a buffer is installed, so answering it below is
6906 // the same value by a shorter route.
6907 let Ok(text) = std::str::from_utf8(&bytes) else {
6908 unsafe { set_fault(ctx, RaisedFault::INVALID_TEXT) };
6909 return unsafe { (*ctx).input_source };
6910 };
6911 // **The validation is strictly before the allocation, and must
6912 // stay there.** SAFETY: `text` borrows a live, initialized buffer
6913 // for this call, and `ctx` is the caller's live context. The result
6914 // is stored into `input_source` — a root (`RuntimeRoots`) — with no
6915 // allocation in between, so the collection this allocation paces
6916 // cannot reclaim it. `praxis_alloc_text` takes `&[]` for
6917 // `len == 0`, so the empty answer needs no special case here and
6918 // must not get one.
6919 let text = unsafe { praxis_alloc_text(ctx, text.as_ptr(), text.len()) };
6920 unsafe { (*ctx).input_source = text };
6921 }
6922 unsafe { (*ctx).input_source }
6923 })
6924}
6925
6926/// Run a compiled parser plan against `input`, returning the parsed result as a
6927/// `GcRef` (§7.1). `plan_index_gc` is a boxed `Int` whose payload is the
6928/// plan's index in the HIR's global slab.
6929///
6930/// On a parse mismatch (or a non-Text `input`), sets `FaultKind::ParseFailed`
6931/// and returns the Unit sentinel (§7.11). No Rust panic crosses the ABI.
6932///
6933/// The non-Text guard is load-bearing (§6.3 host-safety gap): the parser
6934/// interpreter reinterprets `input`'s payload as a `TextPayload`, so a non-Text
6935/// `input` (e.g. the default Unit singleton when no input buffer was installed)
6936/// would be dereferenced as a Text buffer and segfault. Both `read` (whose
6937/// `input` comes from `praxis_get_input`) and `parse(text, expr)` (whose `input`
6938/// is an arbitrary expression) funnel through here, so guarding at this ABI
6939/// boundary closes the gap regardless of how the input was produced.
6940///
6941/// The guard **clears** the parse detail and records none of its own. It runs no
6942/// parse, so it has nothing to report — and fabricating a [`ParseFail`] there
6943/// would be worse than silence: with no buffer there is no input span, and an
6944/// invented `expected` would make an embedder's host bug read as a parse failure
6945/// at an offset that does not exist. Clearing is also what stops it reporting a
6946/// *previous* parse's offset: this is the one entry into the parser that does
6947/// not go through `run_plan`'s own clear.
6948///
6949/// # Safety
6950/// `ctx` must be live and wired; `plan_index_gc` must be a valid `Int`; `input`
6951/// must be a valid `GcRef` (any descriptor — a non-Text descriptor faults cleanly
6952/// rather than dereferencing garbage).
6953#[unsafe(no_mangle)]
6954pub unsafe extern "C" fn praxis_run_parser(
6955 ctx: *mut RuntimeContext,
6956 plan_index_gc: GcRef,
6957 input: GcRef,
6958) -> GcRef {
6959 abi_guard!("praxis_run_parser", ctx, {
6960 // Guard the parser interpreter against a non-Text input (§6.3). Reaching
6961 // `run_plan` with a non-Text payload would reinterpret foreign bytes as a
6962 // TextPayload and segfault; fault cleanly instead.
6963 if input.descriptor().id() != crate::text::TEXT.id() {
6964 unsafe { crate::parser::clear_parse_detail(ctx) };
6965 unsafe { set_fault(ctx, RaisedFault::PARSE_FAILED) };
6966 return unsafe { unit_sentinel(ctx) };
6967 }
6968 let idx = unsafe { int_payload(plan_index_gc) };
6969 // Delegate to the parser interpreter. It validates the id, reads the
6970 // plan from the process-wide arena, runs it against the input bytes, and
6971 // allocates the result.
6972 // SAFETY: `ctx` is the wrapper's own context argument, and `input` was
6973 // checked to carry a Text payload above.
6974 match unsafe { crate::parser::run_plan_by_id(ctx, idx, input) } {
6975 Some(result) => result,
6976 None => {
6977 // A `None` return means the value named no registered plan (out of
6978 // range, negative, or zero) or the interpreter was not linked.
6979 // Treat as a parse fault.
6980 unsafe { set_fault(ctx, RaisedFault::PARSE_FAILED) };
6981 unsafe { unit_sentinel(ctx) }
6982 }
6983 }
6984 })
6985}
6986
6987// ---------------------------------------------------------------------------
6988// Graph helpers (§6.5, ADR-060).
6989//
6990// Twelve prelude names whose graph is a closure: the caller passes a start
6991// state and a function from a state to its neighbours, and the wrapper walks
6992// whatever that function describes. `crate::graph` owns the walks and never
6993// touches a closure; `ClosureOracle` below is the one thing that does.
6994//
6995// The bare name is the whole walk, `_distance` is the number of the cheapest
6996// route to a goal, and `_path` is the route. The two goal-directed forms of a
6997// family share one walk and project the `cost` or the `states` out of the one
6998// `graph::Route` it answers, so they can never disagree about which route was
6999// found.
7000// ---------------------------------------------------------------------------
7001
7002/// A [`GraphOracle`](crate::graph::GraphOracle) backed by the closures a
7003/// program passed, with every state it is handed rooted in a native frame.
7004///
7005/// The scope is what makes the walks safe: a state lives in a Rust visited set
7006/// or queue, which the collector cannot see, and every closure call may
7007/// allocate. `retain` roots each state the moment the walk decides to remember
7008/// it, so a collection triggered inside the *next* call finds it.
7009struct ClosureOracle<'s, 'c> {
7010 ctx: *mut RuntimeContext,
7011 scope: &'s NativeScope<'c>,
7012 /// `(T) -> Vec[T]`.
7013 neighbours: GcRef,
7014 /// `(T, T) -> Int`, or the Unit sentinel for a helper that has no weights.
7015 weight: GcRef,
7016 /// `(T) -> Int`, or the Unit sentinel for a helper with no heuristic.
7017 heuristic: GcRef,
7018 /// `(T) -> Bool`, or the Unit sentinel for a helper with no goal test.
7019 goal: GcRef,
7020}
7021
7022impl ClosureOracle<'_, '_> {
7023 /// Call `closure` with `args`, or `Err` if it faulted — or if it is not a
7024 /// closure at all.
7025 ///
7026 /// The type checker says every one of these operands is a function, and the
7027 /// only runtime representation of a function value is a closure object. The
7028 /// descriptor is checked anyway: the alternative to a `TypeMismatch` fault
7029 /// is transmuting whatever the payload's first word happens to be into a
7030 /// function pointer and jumping to it.
7031 unsafe fn call(
7032 &mut self,
7033 closure: GcRef,
7034 args: &[GcRef],
7035 ) -> Result<GcRef, crate::graph::Aborted> {
7036 if !std::ptr::eq(closure.descriptor(), &crate::closures::CLOSURE) {
7037 return Err(self.abort(crate::context::FaultKind::TypeMismatch));
7038 }
7039 // SAFETY: the descriptor check above proves the payload is a
7040 // `ClosurePayload`, so `fn_ptr` is the entry point the codegen wrote
7041 // there (`praxis_alloc_closure`).
7042 let fn_ptr = unsafe { (*closure.payload::<crate::closures::ClosurePayload>()).fn_ptr };
7043 // A closure's entry point is `fn(ctx, closure_self, params...) -> GcRef`
7044 // (§4.10, Approach B): the closure value itself is a hidden first
7045 // explicit argument, and the prologue loads its captures from it. The
7046 // arity is fixed by the helper's signature, which inference has already
7047 // checked, so only the shapes the six helpers use exist here.
7048 let result = match args {
7049 // SAFETY: `fn_ptr` is a finalized JIT entry whose parameter count is
7050 // the one the type checker enforced for this operand; every value
7051 // crossing is a `GcRef`, which is the ABI's only value kind.
7052 [a] => unsafe {
7053 let f: unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef) -> GcRef =
7054 std::mem::transmute(fn_ptr);
7055 f(self.ctx, closure, *a)
7056 },
7057 // SAFETY: as above, at the two-parameter shape.
7058 [a, b] => unsafe {
7059 let f: unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef, GcRef) -> GcRef =
7060 std::mem::transmute(fn_ptr);
7061 f(self.ctx, closure, *a, *b)
7062 },
7063 // Unreachable: `GraphParam` has no shape with another arity, and the
7064 // match on it in `seed_builtin_schemes` is exhaustive. Faulting is
7065 // still the only safe answer, because the alternative is calling
7066 // with the wrong number of arguments.
7067 _ => return Err(self.abort(crate::context::FaultKind::TypeMismatch)),
7068 };
7069 // The closure ran arbitrary Praxis code and may have faulted. Its result
7070 // on that path is the Unit sentinel, so continuing would walk a graph of
7071 // Units; stop instead, leaving the fault for the call site's own check.
7072 if unsafe { praxis_check_fault(self.ctx) } != 0 {
7073 return Err(crate::graph::Aborted);
7074 }
7075 Ok(self.scope.root(result).get())
7076 }
7077
7078 /// The `i64` inside a boxed `Int` a closure returned, or a fault if it is
7079 /// not one.
7080 unsafe fn int_result(&mut self, value: GcRef) -> Result<i64, crate::graph::Aborted> {
7081 if !std::ptr::eq(value.descriptor(), &scalars::INT) {
7082 return Err(self.abort(crate::context::FaultKind::TypeMismatch));
7083 }
7084 Ok(unsafe { int_payload(value) })
7085 }
7086}
7087
7088impl crate::graph::GraphOracle for ClosureOracle<'_, '_> {
7089 fn neighbours(&mut self, state: GcRef) -> Result<Vec<GcRef>, crate::graph::Aborted> {
7090 // SAFETY: `ctx` is live for the wrapper's duration and `neighbours` is
7091 // the operand the type checker typed `(T) -> Vec[T]`.
7092 let result = unsafe { self.call(self.neighbours, &[state])? };
7093 if !std::ptr::eq(result.descriptor(), &crate::collections::VEC) {
7094 return Err(self.abort(crate::context::FaultKind::TypeMismatch));
7095 }
7096 // SAFETY: the descriptor check proves the payload is a `VecPayload`, and
7097 // the result is rooted by `call`, so reading its items cannot race a
7098 // collection — nothing allocates between here and the copy.
7099 let items = unsafe { (*result.payload::<VecPayload>()).items.to_vec() };
7100 for item in &items {
7101 self.scope.root(*item);
7102 }
7103 Ok(items)
7104 }
7105
7106 fn weight(&mut self, from: GcRef, to: GcRef) -> Result<i64, crate::graph::Aborted> {
7107 // SAFETY: as above, at the `(T, T) -> Int` operand.
7108 let result = unsafe { self.call(self.weight, &[from, to])? };
7109 // SAFETY: `result` is a live, rooted `GcRef`.
7110 unsafe { self.int_result(result) }
7111 }
7112
7113 fn heuristic(&mut self, state: GcRef) -> Result<i64, crate::graph::Aborted> {
7114 // SAFETY: as above, at the `(T) -> Int` operand.
7115 let result = unsafe { self.call(self.heuristic, &[state])? };
7116 // SAFETY: `result` is a live, rooted `GcRef`.
7117 unsafe { self.int_result(result) }
7118 }
7119
7120 fn is_goal(&mut self, state: GcRef) -> Result<bool, crate::graph::Aborted> {
7121 // SAFETY: as above, at the `(T) -> Bool` operand.
7122 let result = unsafe { self.call(self.goal, &[state])? };
7123 // A `Bool`'s payload is **one byte**. Reading it as an `i64` would take
7124 // seven further bytes of the block's alignment padding, which the bump
7125 // allocator never initialized. `read_scalar` checks the descriptor and
7126 // takes the width from `BOOL_PAYLOAD`'s own type, so neither half is
7127 // written here.
7128 //
7129 // SAFETY: `result` is a `GcRef` the oracle's own call just produced.
7130 match unsafe { read_scalar(result, scalars::BOOL_PAYLOAD) } {
7131 Some(b) => Ok(b != 0),
7132 None => Err(self.abort(crate::context::FaultKind::TypeMismatch)),
7133 }
7134 }
7135
7136 fn retain(&mut self, state: GcRef) {
7137 self.scope.root(state);
7138 }
7139
7140 fn abort(&mut self, kind: crate::context::FaultKind) -> crate::graph::Aborted {
7141 if let Some(fault) = RaisedFault::new(kind) {
7142 // SAFETY: `ctx` is live and wired for the wrapper's duration.
7143 unsafe { set_fault(self.ctx, fault) };
7144 }
7145 crate::graph::Aborted
7146 }
7147}
7148
7149/// The descriptor every state in this walk shares: the start state's own.
7150///
7151/// The type checker guarantees one state type per call, and a `GcRef` carries
7152/// its descriptor in its header — so the start state is the authority on what
7153/// the result collection holds, and no separate type argument has to cross the
7154/// ABI.
7155#[inline]
7156fn state_descriptor(start: GcRef) -> *const TypeDescriptor {
7157 start.descriptor() as *const TypeDescriptor
7158}
7159
7160/// Build a `Vec[T]` holding `states`, in order.
7161///
7162/// # Safety
7163/// `ctx` must be live and wired; every state must be a valid, rooted `GcRef`.
7164unsafe fn states_as_vec(
7165 ctx: *mut RuntimeContext,
7166 element: *const TypeDescriptor,
7167 states: &[GcRef],
7168) -> GcRef {
7169 let result = unsafe { praxis_vec_new(ctx, element) };
7170 let scope = unsafe { NativeScope::new(ctx) };
7171 let rooted = scope.root(result);
7172 // SAFETY: `result` is the `Vec` just allocated, and `rooted` proves it is in
7173 // the collector's root set for the borrow.
7174 let payload = unsafe { vec_payload_mut(rooted) };
7175 payload.items.extend_from_slice(states);
7176 result
7177}
7178
7179/// `bfs(start, neighbours)` — every reachable state, in breadth-first order
7180/// (§6.5).
7181///
7182/// # Safety
7183/// `ctx` must be live and wired; `start` must be a valid `GcRef` and
7184/// `neighbours` a closure value of type `(T) -> Vec[T]`.
7185#[unsafe(no_mangle)]
7186pub unsafe extern "C" fn praxis_bfs(
7187 ctx: *mut RuntimeContext,
7188 start: GcRef,
7189 neighbours: GcRef,
7190) -> GcRef {
7191 abi_guard!("praxis_bfs", ctx, {
7192 // SAFETY: the caller upholds ctx/operand validity.
7193 unsafe {
7194 let scope = NativeScope::new(ctx);
7195 let mut oracle = ClosureOracle {
7196 ctx,
7197 scope: &scope,
7198 neighbours,
7199 weight: unit_sentinel(ctx),
7200 heuristic: unit_sentinel(ctx),
7201 goal: unit_sentinel(ctx),
7202 };
7203 match crate::graph::bfs_order(&mut oracle, start) {
7204 Ok(states) => states_as_vec(ctx, state_descriptor(start), &states),
7205 Err(_) => unit_sentinel(ctx),
7206 }
7207 }
7208 })
7209}
7210
7211/// `dfs(start, neighbours)` — every reachable state, in depth-first pre-order
7212/// (§6.5).
7213///
7214/// # Safety
7215/// As [`praxis_bfs`].
7216#[unsafe(no_mangle)]
7217pub unsafe extern "C" fn praxis_dfs(
7218 ctx: *mut RuntimeContext,
7219 start: GcRef,
7220 neighbours: GcRef,
7221) -> GcRef {
7222 abi_guard!("praxis_dfs", ctx, {
7223 // SAFETY: the caller upholds ctx/operand validity.
7224 unsafe {
7225 let scope = NativeScope::new(ctx);
7226 let mut oracle = ClosureOracle {
7227 ctx,
7228 scope: &scope,
7229 neighbours,
7230 weight: unit_sentinel(ctx),
7231 heuristic: unit_sentinel(ctx),
7232 goal: unit_sentinel(ctx),
7233 };
7234 match crate::graph::dfs_order(&mut oracle, start) {
7235 Ok(states) => states_as_vec(ctx, state_descriptor(start), &states),
7236 Err(_) => unit_sentinel(ctx),
7237 }
7238 }
7239 })
7240}
7241
7242/// `flood_fill(start, neighbours)` — every reachable state, as a `Set` (§6.5).
7243///
7244/// # Safety
7245/// As [`praxis_bfs`].
7246#[unsafe(no_mangle)]
7247pub unsafe extern "C" fn praxis_flood_fill(
7248 ctx: *mut RuntimeContext,
7249 start: GcRef,
7250 neighbours: GcRef,
7251) -> GcRef {
7252 abi_guard!("praxis_flood_fill", ctx, {
7253 // SAFETY: the caller upholds ctx/operand validity.
7254 unsafe {
7255 let scope = NativeScope::new(ctx);
7256 let mut oracle = ClosureOracle {
7257 ctx,
7258 scope: &scope,
7259 neighbours,
7260 weight: unit_sentinel(ctx),
7261 heuristic: unit_sentinel(ctx),
7262 goal: unit_sentinel(ctx),
7263 };
7264 let states = match crate::graph::reachable(&mut oracle, start) {
7265 Ok(states) => states,
7266 Err(_) => return unit_sentinel(ctx),
7267 };
7268 let result = praxis_set_new(ctx, state_descriptor(start));
7269 let rooted = scope.root(result);
7270 let payload = set_payload_mut(rooted);
7271 for state in states {
7272 payload.entries.insert(DynamicKey::new(state));
7273 }
7274 result
7275 }
7276 })
7277}
7278
7279/// `bfs_distance(start, neighbours, is_goal)` — the fewest steps to a goal, or
7280/// `None` (§6.5).
7281///
7282/// # Safety
7283/// `ctx` must be live and wired; `start` must be a valid `GcRef`, `neighbours` a
7284/// `(T) -> Vec[T]` closure and `goal` a `(T) -> Bool` closure.
7285#[unsafe(no_mangle)]
7286pub unsafe extern "C" fn praxis_bfs_distance(
7287 ctx: *mut RuntimeContext,
7288 start: GcRef,
7289 neighbours: GcRef,
7290 goal: GcRef,
7291) -> GcRef {
7292 abi_guard!("praxis_bfs_distance", ctx, {
7293 // SAFETY: the caller upholds ctx/operand validity.
7294 unsafe {
7295 let scope = NativeScope::new(ctx);
7296 let mut oracle = ClosureOracle {
7297 ctx,
7298 scope: &scope,
7299 neighbours,
7300 weight: unit_sentinel(ctx),
7301 heuristic: unit_sentinel(ctx),
7302 goal,
7303 };
7304 match crate::graph::bfs_route(&mut oracle, start) {
7305 Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7306 Err(_) => unit_sentinel(ctx),
7307 }
7308 }
7309 })
7310}
7311
7312/// `bfs_path(start, neighbours, is_goal)` — a shortest route to a goal, from
7313/// the start to the goal inclusive, or `None` (§6.5).
7314///
7315/// The same walk [`praxis_bfs_distance`] runs; this projects the route out of
7316/// its answer where that one projects the number.
7317///
7318/// # Safety
7319/// As [`praxis_bfs_distance`].
7320#[unsafe(no_mangle)]
7321pub unsafe extern "C" fn praxis_bfs_path(
7322 ctx: *mut RuntimeContext,
7323 start: GcRef,
7324 neighbours: GcRef,
7325 goal: GcRef,
7326) -> GcRef {
7327 abi_guard!("praxis_bfs_path", ctx, {
7328 // SAFETY: the caller upholds ctx/operand validity.
7329 unsafe {
7330 let scope = NativeScope::new(ctx);
7331 let mut oracle = ClosureOracle {
7332 ctx,
7333 scope: &scope,
7334 neighbours,
7335 weight: unit_sentinel(ctx),
7336 heuristic: unit_sentinel(ctx),
7337 goal,
7338 };
7339 match crate::graph::bfs_route(&mut oracle, start) {
7340 Ok(route) => states_as_optional_vec(
7341 ctx,
7342 state_descriptor(start),
7343 route.as_ref().map(|r| r.states.as_slice()),
7344 ),
7345 Err(_) => unit_sentinel(ctx),
7346 }
7347 }
7348 })
7349}
7350
7351/// `dfs_distance(start, neighbours, is_goal)` — the number of edges on the
7352/// route depth-first search found to a goal, or `None` (§6.5).
7353///
7354/// Depth-first arrives by the route it descended into first, which need not be
7355/// a short one, so this and [`praxis_bfs_distance`] answer different numbers on
7356/// the same graph.
7357///
7358/// # Safety
7359/// As [`praxis_bfs_distance`].
7360#[unsafe(no_mangle)]
7361pub unsafe extern "C" fn praxis_dfs_distance(
7362 ctx: *mut RuntimeContext,
7363 start: GcRef,
7364 neighbours: GcRef,
7365 goal: GcRef,
7366) -> GcRef {
7367 abi_guard!("praxis_dfs_distance", ctx, {
7368 // SAFETY: the caller upholds ctx/operand validity.
7369 unsafe {
7370 let scope = NativeScope::new(ctx);
7371 let mut oracle = ClosureOracle {
7372 ctx,
7373 scope: &scope,
7374 neighbours,
7375 weight: unit_sentinel(ctx),
7376 heuristic: unit_sentinel(ctx),
7377 goal,
7378 };
7379 match crate::graph::dfs_route(&mut oracle, start) {
7380 Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7381 Err(_) => unit_sentinel(ctx),
7382 }
7383 }
7384 })
7385}
7386
7387/// `dfs_path(start, neighbours, is_goal)` — the route depth-first search found
7388/// to a goal, from the start to the goal inclusive, or `None` (§6.5).
7389///
7390/// # Safety
7391/// As [`praxis_bfs_distance`].
7392#[unsafe(no_mangle)]
7393pub unsafe extern "C" fn praxis_dfs_path(
7394 ctx: *mut RuntimeContext,
7395 start: GcRef,
7396 neighbours: GcRef,
7397 goal: GcRef,
7398) -> GcRef {
7399 abi_guard!("praxis_dfs_path", ctx, {
7400 // SAFETY: the caller upholds ctx/operand validity.
7401 unsafe {
7402 let scope = NativeScope::new(ctx);
7403 let mut oracle = ClosureOracle {
7404 ctx,
7405 scope: &scope,
7406 neighbours,
7407 weight: unit_sentinel(ctx),
7408 heuristic: unit_sentinel(ctx),
7409 goal,
7410 };
7411 match crate::graph::dfs_route(&mut oracle, start) {
7412 Ok(route) => states_as_optional_vec(
7413 ctx,
7414 state_descriptor(start),
7415 route.as_ref().map(|r| r.states.as_slice()),
7416 ),
7417 Err(_) => unit_sentinel(ctx),
7418 }
7419 }
7420 })
7421}
7422
7423/// `dijkstra(start, neighbours, weight)` — the least cost to every reachable
7424/// state, as a `Map[T, Int]` (§6.5).
7425///
7426/// # Safety
7427/// `ctx` must be live and wired; `start` must be a valid `GcRef`, `neighbours` a
7428/// `(T) -> Vec[T]` closure and `weight` a `(T, T) -> Int` closure.
7429#[unsafe(no_mangle)]
7430pub unsafe extern "C" fn praxis_dijkstra(
7431 ctx: *mut RuntimeContext,
7432 start: GcRef,
7433 neighbours: GcRef,
7434 weight: GcRef,
7435) -> GcRef {
7436 abi_guard!("praxis_dijkstra", ctx, {
7437 // SAFETY: the caller upholds ctx/operand validity.
7438 unsafe {
7439 let scope = NativeScope::new(ctx);
7440 let mut oracle = ClosureOracle {
7441 ctx,
7442 scope: &scope,
7443 neighbours,
7444 weight,
7445 heuristic: unit_sentinel(ctx),
7446 goal: unit_sentinel(ctx),
7447 };
7448 let costs = match crate::graph::dijkstra_costs(&mut oracle, start) {
7449 Ok(costs) => costs,
7450 Err(_) => return unit_sentinel(ctx),
7451 };
7452 let result = scope.root(praxis_map_new(ctx, state_descriptor(start)));
7453 for (state, cost) in costs {
7454 // Each boxed cost is allocated *before* the payload borrow: an
7455 // allocation while a `&mut MapPayload` is live is what `Rooted`
7456 // exists to make impossible, and taking the borrow per entry is
7457 // what keeps that true.
7458 let boxed = scope.root(int_ref(ctx, cost));
7459 map_payload_mut(result)
7460 .entries
7461 .insert(DynamicKey::new(state), boxed.get());
7462 }
7463 result.get()
7464 }
7465 })
7466}
7467
7468/// `dijkstra_distance(start, neighbours, weight, is_goal)` — the cost of the
7469/// cheapest route to a goal, or `None` (§6.5).
7470///
7471/// The search [`praxis_dijkstra`] runs, stopped at the first goal it settles,
7472/// so it makes the same two refusals: a negative edge weight and a cost with no
7473/// `Int`.
7474///
7475/// # Safety
7476/// `ctx` must be live and wired; `start` must be a valid `GcRef`, `neighbours` a
7477/// `(T) -> Vec[T]` closure, `weight` a `(T, T) -> Int` closure and `goal` a
7478/// `(T) -> Bool` closure.
7479#[unsafe(no_mangle)]
7480pub unsafe extern "C" fn praxis_dijkstra_distance(
7481 ctx: *mut RuntimeContext,
7482 start: GcRef,
7483 neighbours: GcRef,
7484 weight: GcRef,
7485 goal: GcRef,
7486) -> GcRef {
7487 abi_guard!("praxis_dijkstra_distance", ctx, {
7488 // SAFETY: the caller upholds ctx/operand validity.
7489 unsafe {
7490 let scope = NativeScope::new(ctx);
7491 let mut oracle = ClosureOracle {
7492 ctx,
7493 scope: &scope,
7494 neighbours,
7495 weight,
7496 heuristic: unit_sentinel(ctx),
7497 goal,
7498 };
7499 match crate::graph::dijkstra_route(&mut oracle, start) {
7500 Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7501 Err(_) => unit_sentinel(ctx),
7502 }
7503 }
7504 })
7505}
7506
7507/// `dijkstra_path(start, neighbours, weight, is_goal)` — the cheapest route to
7508/// a goal, from the start to the goal inclusive, or `None` (§6.5).
7509///
7510/// # Safety
7511/// As [`praxis_dijkstra_distance`].
7512#[unsafe(no_mangle)]
7513pub unsafe extern "C" fn praxis_dijkstra_path(
7514 ctx: *mut RuntimeContext,
7515 start: GcRef,
7516 neighbours: GcRef,
7517 weight: GcRef,
7518 goal: GcRef,
7519) -> GcRef {
7520 abi_guard!("praxis_dijkstra_path", ctx, {
7521 // SAFETY: the caller upholds ctx/operand validity.
7522 unsafe {
7523 let scope = NativeScope::new(ctx);
7524 let mut oracle = ClosureOracle {
7525 ctx,
7526 scope: &scope,
7527 neighbours,
7528 weight,
7529 heuristic: unit_sentinel(ctx),
7530 goal,
7531 };
7532 match crate::graph::dijkstra_route(&mut oracle, start) {
7533 Ok(route) => states_as_optional_vec(
7534 ctx,
7535 state_descriptor(start),
7536 route.as_ref().map(|r| r.states.as_slice()),
7537 ),
7538 Err(_) => unit_sentinel(ctx),
7539 }
7540 }
7541 })
7542}
7543
7544/// `a_star_distance(start, neighbours, weight, heuristic, is_goal)` — the cost
7545/// of the cheapest route to a goal, or `None` (§6.5).
7546///
7547/// # Safety
7548/// `ctx` must be live and wired; `start` must be a valid `GcRef` and each of
7549/// `neighbours`, `weight`, `heuristic` and `goal` a closure value of the type
7550/// the helper's signature declares.
7551#[unsafe(no_mangle)]
7552pub unsafe extern "C" fn praxis_a_star_distance(
7553 ctx: *mut RuntimeContext,
7554 start: GcRef,
7555 neighbours: GcRef,
7556 weight: GcRef,
7557 heuristic: GcRef,
7558 goal: GcRef,
7559) -> GcRef {
7560 abi_guard!("praxis_a_star_distance", ctx, {
7561 // SAFETY: the caller upholds ctx/operand validity.
7562 unsafe {
7563 let scope = NativeScope::new(ctx);
7564 let mut oracle = ClosureOracle {
7565 ctx,
7566 scope: &scope,
7567 neighbours,
7568 weight,
7569 heuristic,
7570 goal,
7571 };
7572 match crate::graph::a_star_route(&mut oracle, start) {
7573 Ok(route) => alloc_optional_int(ctx, route.map(|r| r.cost)),
7574 Err(_) => unit_sentinel(ctx),
7575 }
7576 }
7577 })
7578}
7579
7580/// `a_star_path(start, neighbours, weight, heuristic, is_goal)` — the cheapest
7581/// route to a goal, from the start to the goal inclusive, or `None` (§6.5).
7582///
7583/// # Safety
7584/// As [`praxis_a_star_distance`].
7585#[unsafe(no_mangle)]
7586pub unsafe extern "C" fn praxis_a_star_path(
7587 ctx: *mut RuntimeContext,
7588 start: GcRef,
7589 neighbours: GcRef,
7590 weight: GcRef,
7591 heuristic: GcRef,
7592 goal: GcRef,
7593) -> GcRef {
7594 abi_guard!("praxis_a_star_path", ctx, {
7595 // SAFETY: the caller upholds ctx/operand validity.
7596 unsafe {
7597 let scope = NativeScope::new(ctx);
7598 let mut oracle = ClosureOracle {
7599 ctx,
7600 scope: &scope,
7601 neighbours,
7602 weight,
7603 heuristic,
7604 goal,
7605 };
7606 match crate::graph::a_star_route(&mut oracle, start) {
7607 Ok(route) => states_as_optional_vec(
7608 ctx,
7609 state_descriptor(start),
7610 route.as_ref().map(|r| r.states.as_slice()),
7611 ),
7612 Err(_) => unit_sentinel(ctx),
7613 }
7614 }
7615 })
7616}
7617
7618/// Allocate `Some(n)` or `None` for an `Option[Int]` result.
7619///
7620/// The tags are `Option`'s own declaration order — `Some` first, `None` second
7621/// (`TypeDb::new`) — which is the same order the codegen uses for a `Some(x)`
7622/// the program writes, so a runtime-built `Option` matches against the same
7623/// arms.
7624///
7625/// # Safety
7626/// `ctx` must be live and wired.
7627unsafe fn alloc_optional_int(ctx: *mut RuntimeContext, value: Option<i64>) -> GcRef {
7628 // SAFETY: the caller upholds ctx validity.
7629 unsafe {
7630 match value {
7631 Some(n) => {
7632 let boxed = int_ref(ctx, n);
7633 option_some(ctx, boxed)
7634 }
7635 None => option_none(ctx),
7636 }
7637 }
7638}
7639
7640/// Allocate `Some(states)` as an `Option[Vec[T]]`, or `None` when the search
7641/// found no route.
7642///
7643/// The `Vec` is rooted before the `Some` is built: `option_some` allocates an
7644/// enum, an allocation is a safepoint, and a bare `GcRef` in a local is in
7645/// nobody's root set. That ordering is the whole of what this adds to
7646/// [`states_as_vec`] and [`option_some`].
7647///
7648/// # Safety
7649/// `ctx` must be live and wired; every state must be a valid, rooted `GcRef`.
7650unsafe fn states_as_optional_vec(
7651 ctx: *mut RuntimeContext,
7652 element: *const TypeDescriptor,
7653 states: Option<&[GcRef]>,
7654) -> GcRef {
7655 // SAFETY: the caller upholds ctx/state validity.
7656 unsafe {
7657 match states {
7658 Some(states) => {
7659 let scope = NativeScope::new(ctx);
7660 let vec = scope.root(states_as_vec(ctx, element, states));
7661 option_some(ctx, vec.get())
7662 }
7663 None => option_none(ctx),
7664 }
7665 }
7666}
7667
7668#[cfg(test)]
7669mod tests {
7670 use super::*;
7671 use crate::context::{Fault, FaultKind, Runtime};
7672 use crate::parse_detail::ParseFail;
7673 use crate::shadow_stack::{SlotCount, push_frame};
7674
7675 /// A wired context backed by a real runtime.
7676 pub(super) fn wired_ctx(rt: &mut Runtime) -> *mut RuntimeContext {
7677 let ctx = Box::leak(Box::new(rt.context()));
7678 ctx as *mut RuntimeContext
7679 }
7680
7681 pub(super) unsafe fn drop_ctx(ctx: *mut RuntimeContext) {
7682 // Reclaim the leaked Box. The runtime outlives this call in tests.
7683 let _ = unsafe { Box::from_raw(ctx) };
7684 }
7685
7686 /// The first `Int` value the runtime does **not** intern.
7687 ///
7688 /// Every test below that detects a collection by watching the live registry
7689 /// *shrink* must allocate above this. An interned `Int` never enters the
7690 /// registry, so `praxis_alloc_int(ctx, 5)` in such a loop makes
7691 /// `after < before + 1` true on the first iteration and the test reports
7692 /// success without a collection ever having run — a false pass, which is
7693 /// strictly worse than the failure it replaces.
7694 const UNINTERNED: i64 = crate::small_int::SMALL_INT_MAX + 1;
7695
7696 /// Allocate through a safepointed ABI wrapper until its pre-allocation
7697 /// collection causes the live registry to shrink. Returns the live count
7698 /// immediately after that wrapper allocates its result.
7699 unsafe fn allocate_until_automatic_collection(rt: &Runtime, ctx: *mut RuntimeContext) -> usize {
7700 let mut before = rt.heap().stats().live_count;
7701 for i in 0..10_000_i64 {
7702 // Above the interned range: see `UNINTERNED`.
7703 let _ = unsafe { praxis_alloc_int(ctx, UNINTERNED + i) };
7704 let after = rt.heap().stats().live_count;
7705 if after < before.saturating_add(1) {
7706 return after;
7707 }
7708 before = after;
7709 }
7710 panic!("automatic collection did not run after 10,000 allocations");
7711 }
7712
7713 /// The version number this build declares.
7714 ///
7715 /// Named for the version rather than for any one change, because a version
7716 /// is a statement about a build and several changes share one bump. This
7717 /// pins the numeral so a build cannot ship a layout change without moving
7718 /// it.
7719 ///
7720 /// `gc::tests::the_folded_payload_offset_moved_at_v19_and_is_pinned_here`
7721 /// asserts the other direction, pinning the payload offset *to* a version
7722 /// number, so a layout change and the version that declares it cannot drift
7723 /// apart.
7724 #[test]
7725 fn version_is_twenty_for_the_batch_this_build_ships() {
7726 assert_eq!(RUNTIME_ABI_VERSION, 20);
7727 }
7728
7729 #[test]
7730 fn assert_passes_within_a_single_build() {
7731 assert_abi_version();
7732 }
7733
7734 /// [`int_payload`]'s width check must be a real branch, not a
7735 /// `debug_assert` — the read has to be bounded in the profile users
7736 /// actually run.
7737 ///
7738 /// `debug_assert_eq!` is compiled out of a release build, leaving
7739 /// `unsafe { *r.payload::<i64>() }` against a descriptor that may be zero
7740 /// bytes wide: an 8-byte out-of-bounds heap read, reachable from a program
7741 /// that passes `praxis check`.
7742 ///
7743 /// **This is a source gate on purpose, and it is the only kind that works
7744 /// here.** The defect is a difference *between profiles*, and `cargo test`
7745 /// builds exactly one of them — a behavioural test is green under
7746 /// `debug_assertions` whether the check is conditional or not. The companion
7747 /// below asserts the branch actually refuses; this asserts it is still
7748 /// *there* at `-O`.
7749 ///
7750 /// It reads the file rather than the function because there is nothing in a
7751 /// compiled artifact to ask. `every_no_mangle_wrapper_is_behind_the_panic_guard`
7752 /// is the same technique for the same reason.
7753 #[test]
7754 fn every_scalar_payload_read_goes_through_the_bounded_reader() {
7755 let source = include_str!("abi.rs");
7756
7757 // 1. The reader itself checks before it reads, and the check is an
7758 // ordinary branch — not a `debug_assert`, which compiles out of a
7759 // release build. That distinction is the point: with the check
7760 // compiled out, a `praxis check`-clean program does an out-of-bounds
7761 // read where a debug build aborts cleanly.
7762 const SIGNATURE: &str = "unsafe fn read_scalar<T: Copy>(r: GcRef, handle: crate::descriptor::Payload<T>) -> Option<T> {";
7763 let at = source
7764 .find(SIGNATURE)
7765 .expect("`read_scalar`'s definition moved; this gate names it by signature");
7766 let body_start = at + SIGNATURE.len();
7767 let body_len = source[body_start..]
7768 .find("\n}")
7769 .expect("`read_scalar` has no closing brace in the first column");
7770 let body = &source[body_start..body_start + body_len];
7771
7772 assert!(
7773 !body.contains("debug_assert"),
7774 "`read_scalar`'s type check is a `debug_assert`, which is compiled out of a \
7775 release build — and what is left is an unchecked read off a payload that may \
7776 be narrower (REP-56). Make it an ordinary branch.\nbody was:{body}"
7777 );
7778 assert!(
7779 body.contains("std::ptr::eq(r.descriptor(), handle.descriptor())"),
7780 "`read_scalar` no longer proves the value is the handle's type before reading \
7781 it (REP-37, REP-56).\nbody was:{body}"
7782 );
7783
7784 // 2. Nothing else in this file reads a scalar payload directly. This
7785 // is the half that matters: a gate that names one function can only
7786 // ever gate that function, and every scalar reader needs bounding.
7787 //
7788 // Scanned over the crate's own code only: `include_str!` hands us this
7789 // test too, whose list below would otherwise match itself, and
7790 // comments naming the pattern are describing it rather than doing it.
7791 let code: String = source[..source
7792 .find("#[cfg(test)]")
7793 .expect("abi.rs has no test module marker")]
7794 .lines()
7795 .filter(|l| !l.trim_start().starts_with("//"))
7796 .collect::<Vec<_>>()
7797 .join("\n");
7798 //
7799 // The patterns are the *bare* calls, not the dereferenced ones:
7800 // binding `r.payload::<f64>()` to a local and dereferencing it on the
7801 // next line breaks the spelling `*r.payload::<f64>()` without
7802 // breaking the defect. Forbidding the call means no phrasing of it
7803 // passes. `payload::<u8>()` stays legal: it is how
7804 // `read_scalar` itself reaches the bytes, and how every *compound*
7805 // payload (record, tuple, closure) is reached — those are cast to a
7806 // struct the descriptor already vouched for, not read at a width.
7807 for forbidden in [
7808 "r.payload::<i64>()",
7809 "r.payload::<f64>()",
7810 "r.payload::<u32>()",
7811 "r.payload::<bool>()",
7812 ] {
7813 assert!(
7814 !code.contains(forbidden),
7815 "a scalar payload is read directly as `{forbidden}` instead of through \
7816 `read_scalar`, so its type is unchecked in release (REP-56). Route it \
7817 through `read_scalar(r, scalars::…_PAYLOAD)` instead."
7818 );
7819 }
7820
7821 // 3. And no Rust `bool` is ever materialized from a payload byte: a
7822 // `bool` whose byte is not 0 or 1 is an *invalid value*, which is
7823 // undefined behaviour independently of whether the read was in
7824 // bounds. `BoolPayload` is a `u8` precisely so it never has to be.
7825 assert!(
7826 !code.contains("Payload<bool>") && !code.contains("read_scalar::<bool>"),
7827 "a `bool` is read straight out of a payload; read `scalars::BOOL_PAYLOAD` \
7828 (a `u8`) and compare it instead (REP-56)."
7829 );
7830 }
7831
7832 /// **ADR-111.** `praxis_alloc_text`'s UTF-8 backstop is unconditional in
7833 /// every profile, and it never becomes an unchecked read.
7834 ///
7835 /// The same source-gate technique as
7836 /// [`every_scalar_payload_read_goes_through_the_bounded_reader`], for the
7837 /// same reason and against a sharper temptation. Making the row `Allocates`
7838 /// says the caller promises UTF-8; the next tidy-up reads that as licence to
7839 /// delete the check — either into a `debug_assert` (which compiles out of a
7840 /// release build, so debug aborts and release builds a `Box<str>` of
7841 /// non-UTF-8 bytes that `text_str` later hands out as a `&str`) or into
7842 /// `from_utf8_unchecked` (the same hole, with the check deleted rather than
7843 /// compiled out). Both give two profiles two answers, and `just ci` never
7844 /// builds the one users get.
7845 ///
7846 /// A behavioural test cannot see this: under `cfg(debug_assertions)` a
7847 /// `debug_assert` version passes every test the branch version passes.
7848 #[test]
7849 fn the_text_precondition_backstop_is_unconditional_in_every_profile() {
7850 let source = include_str!("abi.rs");
7851 const SIGNATURE: &str = "pub unsafe extern \"C\" fn praxis_alloc_text(";
7852 let at = source
7853 .find(SIGNATURE)
7854 .expect("`praxis_alloc_text`'s definition moved; this gate names it by signature");
7855 let body_len = source[at..]
7856 .find("\n}")
7857 .expect("`praxis_alloc_text` has no closing brace in the first column");
7858 let body = &source[at..at + body_len];
7859
7860 assert!(
7861 body.contains("std::str::from_utf8(slice)"),
7862 "`praxis_alloc_text` no longer validates its buffer. The check is the \
7863 backstop on a raw read, not an optimization the `Allocates` row traded \
7864 away (ADR-111).\nbody was:{body}"
7865 );
7866 assert!(
7867 !body.contains("debug_assert"),
7868 "`praxis_alloc_text`'s UTF-8 check is a `debug_assert`, which is compiled \
7869 out of a release build — leaving a `Box<str>` built from bytes that are \
7870 not UTF-8 (REP-56's shape). Make it an ordinary branch.\nbody was:{body}"
7871 );
7872 assert!(
7873 !body.contains("from_utf8_unchecked"),
7874 "`praxis_alloc_text` skips the check outright. A precondition is not a \
7875 licence to read unvalidated bytes as a `str` — the refusal is \
7876 `text_bytes_are_not_utf8`, which costs a never-taken branch \
7877 (ADR-111).\nbody was:{body}"
7878 );
7879 // And the refusal is not a fault. If it were, the fault sweep would
7880 // classify the wrapper as faulting and correctly refuse the
7881 // `Allocates` row — this says so at the site rather than leaving the
7882 // failure to be diagnosed three tests away.
7883 assert!(
7884 !body.contains("set_fault"),
7885 "`praxis_alloc_text` sets a fault. Its row is `Effect::Allocates`, so no \
7886 `CheckFault` follows the call and nothing would ever observe it \
7887 (ADR-088, ADR-111).\nbody was:{body}"
7888 );
7889 }
7890
7891 /// The companion to the source gate above: the branch it insists on is real,
7892 /// and it refuses rather than reading.
7893 ///
7894 /// A `Unit` is zero bytes wide, which is the shape that must be refused.
7895 /// The refusal is a panic, which is ADR-080's defined path — inside a
7896 /// wrapper `abi_guard!` turns it into a `Panic` fault (or a message and an
7897 /// abort where the manifest makes that fault unobservable). What must not
7898 /// happen, in any profile, is the read.
7899 #[test]
7900 fn a_scalar_read_refuses_a_value_that_is_not_its_type() {
7901 let mut rt = Runtime::new();
7902 let ctx = wired_ctx(&mut rt);
7903 // SAFETY: ctx is wired to rt; the Unit immortal is a valid GcRef.
7904 let unit = unsafe { praxis_alloc_unit(ctx) };
7905 assert_eq!(unit.descriptor().size(), 0, "Unit is a zero-width payload");
7906
7907 // The panic is the refusal. `catch_unwind` here is the test standing in
7908 // for `abi_guard!`, which is what catches it in a real wrapper.
7909 let previous = std::panic::take_hook();
7910 std::panic::set_hook(Box::new(|_| {}));
7911 // `AssertUnwindSafe` for the same reason `abi_guard!` uses it: the
7912 // capture is a `Copy` C type and nothing observes a half-finished read.
7913 // SAFETY: `unit` is a valid GcRef into rt's live heap.
7914 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
7915 int_payload(unit)
7916 }));
7917 std::panic::set_hook(previous);
7918 unsafe { drop_ctx(ctx) };
7919
7920 let payload = outcome.expect_err("a zero-width payload must not be read as eight bytes");
7921 let message = payload
7922 .downcast_ref::<String>()
7923 .map(String::as_str)
7924 .or_else(|| payload.downcast_ref::<&str>().copied())
7925 .unwrap_or("");
7926 assert!(
7927 message.contains("int_payload wants a `Int` payload")
7928 && message.contains("this value is a `Unit`"),
7929 "unexpected panic message: {message:?}"
7930 );
7931 }
7932
7933 #[test]
7934 fn alloc_int_and_load_round_trip() {
7935 let mut rt = Runtime::new();
7936 let ctx = wired_ctx(&mut rt);
7937 // SAFETY: ctx is wired to rt.
7938 let r = unsafe { praxis_alloc_int(ctx, 9001) };
7939 // SAFETY: r is a valid Int allocated above.
7940 assert_eq!(unsafe { praxis_int_load(ctx, r) }, 9001);
7941 unsafe { drop_ctx(ctx) };
7942 }
7943
7944 /// The `Int` counterpart of
7945 /// [`bool_and_unit_abi_allocations_reuse_runtime_singletons`]: a small `Int`
7946 /// is one object per value, and an out-of-range one is still a fresh box.
7947 ///
7948 /// Both halves matter. The first is the optimization; the second is the
7949 /// branch a regression would silently delete, leaving every large `Int` in
7950 /// the language reading slot `value - SMALL_INT_MIN` of a table that ends
7951 /// long before it.
7952 #[test]
7953 fn small_ints_are_one_object_per_value_and_large_ones_are_not() {
7954 let mut rt = Runtime::new();
7955 let ctx = wired_ctx(&mut rt);
7956 // SAFETY: ctx is wired to rt throughout.
7957 unsafe {
7958 // In range: two calls, one object — and it is the runtime's own
7959 // table entry, not some other cache.
7960 let a = praxis_alloc_int(ctx, 7);
7961 let b = praxis_alloc_int(ctx, 7);
7962 assert_eq!(a.as_ptr(), b.as_ptr());
7963 assert_eq!(a.as_ptr(), rt.immortals().small_int(7).unwrap().as_ptr());
7964 assert_eq!(praxis_int_load(ctx, a), 7);
7965
7966 // The four boundary cases, through the ABI: the exact endpoints are
7967 // interned and one step outside either is not.
7968 for v in [
7969 crate::small_int::SMALL_INT_MIN,
7970 crate::small_int::SMALL_INT_MAX,
7971 ] {
7972 assert_eq!(
7973 praxis_alloc_int(ctx, v).as_ptr(),
7974 praxis_alloc_int(ctx, v).as_ptr(),
7975 "{v} is the edge of the range and must be interned"
7976 );
7977 }
7978 for v in [
7979 crate::small_int::SMALL_INT_MIN - 1,
7980 crate::small_int::SMALL_INT_MAX + 1,
7981 ] {
7982 let x = praxis_alloc_int(ctx, v);
7983 let y = praxis_alloc_int(ctx, v);
7984 assert_ne!(
7985 x.as_ptr(),
7986 y.as_ptr(),
7987 "{v} is outside the range and must still allocate"
7988 );
7989 assert_eq!(praxis_int_load(ctx, x), v, "and still hold its value");
7990 assert_eq!(praxis_int_load(ctx, y), v);
7991 }
7992
7993 // Distinct in-range values are distinct objects: interning shares
7994 // an object across *calls*, never across values.
7995 assert_ne!(
7996 praxis_alloc_int(ctx, 7).as_ptr(),
7997 praxis_alloc_int(ctx, 8).as_ptr()
7998 );
7999
8000 // The host helper and the ABI wrapper answer the same object, as
8001 // `Runtime::alloc_bool` and `praxis_alloc_bool` already do.
8002 assert_eq!(rt.alloc_int(7).as_ptr(), a.as_ptr());
8003 }
8004 unsafe { drop_ctx(ctx) };
8005 }
8006
8007 /// The `Char` counterpart of
8008 /// [`small_ints_are_one_object_per_value_and_large_ones_are_not`] (ADR-107).
8009 ///
8010 /// Both halves matter. The first is the optimization; the second is the
8011 /// branch a regression would silently delete, leaving every non-ASCII `Char`
8012 /// in the language reading slot `code` of a table that ends at 128.
8013 #[test]
8014 fn alloc_char_answers_one_object_per_ascii_code_point_and_a_large_one_still_allocates() {
8015 let mut rt = Runtime::new();
8016 let ctx = wired_ctx(&mut rt);
8017 // SAFETY: ctx is wired to rt throughout.
8018 unsafe {
8019 // In range: two calls, one object — and it is the runtime's own
8020 // table entry, not some other cache.
8021 let a = praxis_alloc_char(ctx, i64::from('a' as u32));
8022 let b = praxis_alloc_char(ctx, i64::from('a' as u32));
8023 assert_eq!(a.as_ptr(), b.as_ptr());
8024 assert_eq!(
8025 a.as_ptr(),
8026 rt.immortals().small_char('a' as u32).unwrap().as_ptr()
8027 );
8028 assert_eq!(a.as_char(), 'a');
8029
8030 // The ceiling is interned; one above it is not. There is no floor
8031 // case — the payload is unsigned and NUL is interned.
8032 let max = i64::from(crate::small_char::SMALL_CHAR_MAX);
8033 assert_eq!(
8034 praxis_alloc_char(ctx, max).as_ptr(),
8035 praxis_alloc_char(ctx, max).as_ptr(),
8036 "the last ASCII scalar is the edge of the range and must be interned"
8037 );
8038 assert_eq!(
8039 praxis_alloc_char(ctx, 0).as_ptr(),
8040 praxis_alloc_char(ctx, 0).as_ptr(),
8041 "NUL is the floor and must be interned"
8042 );
8043 for code in [max + 1, i64::from('é' as u32), 0x10_FFFF] {
8044 let x = praxis_alloc_char(ctx, code);
8045 let y = praxis_alloc_char(ctx, code);
8046 assert_ne!(
8047 x.as_ptr(),
8048 y.as_ptr(),
8049 "{code:#x} is outside the range and must still allocate"
8050 );
8051 assert_eq!(
8052 u32::from(x.as_char()),
8053 code as u32,
8054 "and still hold its code point"
8055 );
8056 }
8057
8058 // Distinct in-range code points are distinct objects: interning
8059 // shares an object across *calls*, never across values.
8060 assert_ne!(
8061 praxis_alloc_char(ctx, i64::from('a' as u32)).as_ptr(),
8062 praxis_alloc_char(ctx, i64::from('b' as u32)).as_ptr()
8063 );
8064
8065 // The validity rule is untouched by the table: an interned slot is
8066 // reached only after `checked_alloc_char` has approved the value, so
8067 // a code point that is not a scalar still faults.
8068 for bad in [-1_i64, 0xD800, 0x11_0000, 0x1_0000_0041] {
8069 let _ = praxis_alloc_char(ctx, bad);
8070 assert_eq!(
8071 rt.take_fault(),
8072 Some(FaultKind::InvalidChar),
8073 "{bad:#x} is not a scalar value"
8074 );
8075 }
8076
8077 // The host helper and the ABI wrapper answer the same object, as
8078 // `Runtime::alloc_int`/`praxis_alloc_int` already do. This is what
8079 // makes "a `Char` is interned" one fact rather than two.
8080 assert_eq!(rt.alloc_char('a' as u32).as_ptr(), a.as_ptr());
8081 // …and the out-of-range halves still disagree as objects, which is
8082 // the same statement from the other side.
8083 assert_ne!(
8084 rt.alloc_char('é' as u32).as_ptr(),
8085 rt.alloc_char('é' as u32).as_ptr()
8086 );
8087 }
8088 unsafe { drop_ctx(ctx) };
8089 }
8090
8091 /// `praxis_text_get` is the interning's largest site: it is `t[i]` *and*
8092 /// every step of `for c in t`. It reaches [`char_ref`] directly rather than
8093 /// through [`checked_alloc_char`] — a Rust `char` needs no validity check —
8094 /// so it is its own door and must be pinned as one.
8095 ///
8096 /// `text_get_answers_a_char_object` covers the uninterned half (`é`) and the
8097 /// descriptor; this covers the identity.
8098 #[test]
8099 fn text_get_answers_the_interned_char() {
8100 let mut rt = Runtime::new();
8101 let ctx = wired_ctx(&mut rt);
8102 // SAFETY: ctx wired.
8103 unsafe {
8104 let s = "abca";
8105 let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
8106 let zero = praxis_alloc_int(ctx, 0);
8107 let three = praxis_alloc_int(ctx, 3);
8108 let first = praxis_text_get(ctx, text, zero);
8109 let last = praxis_text_get(ctx, text, three);
8110 assert!(!rt.has_pending_fault());
8111
8112 // Two reads of the same character are one object, and it is the same
8113 // object every other door answers.
8114 assert_eq!(first.as_ptr(), last.as_ptr());
8115 assert_eq!(
8116 first.as_ptr(),
8117 praxis_alloc_char(ctx, i64::from('a' as u32)).as_ptr()
8118 );
8119 assert_eq!(
8120 first.as_ptr(),
8121 rt.immortals().small_char('a' as u32).unwrap().as_ptr()
8122 );
8123 assert_eq!(first.as_char(), 'a');
8124
8125 // The non-ASCII half still allocates, and still answers the right
8126 // scalar — the branch a regression would delete.
8127 let u = "éé";
8128 let utext = praxis_alloc_text(ctx, u.as_ptr(), u.len());
8129 let one = praxis_alloc_int(ctx, 1);
8130 let x = praxis_text_get(ctx, utext, zero);
8131 let y = praxis_text_get(ctx, utext, one);
8132 assert_ne!(x.as_ptr(), y.as_ptr(), "`é` is outside the interned range");
8133 assert_eq!(x.as_char(), 'é');
8134 assert_eq!(y.as_char(), 'é');
8135 }
8136 unsafe { drop_ctx(ctx) };
8137 }
8138
8139 /// The two doors into [`checked_alloc_char`] answer the same object, which
8140 /// is that helper's whole reason for existing — a rule stated at both goes
8141 /// stale at one, and now the rule includes which object.
8142 #[test]
8143 fn int_to_char_answers_the_same_object_as_alloc_char() {
8144 let mut rt = Runtime::new();
8145 let ctx = wired_ctx(&mut rt);
8146 // SAFETY: ctx wired.
8147 unsafe {
8148 let code = praxis_alloc_int(ctx, i64::from('Z' as u32));
8149 let via_to_char = praxis_int_to_char(ctx, code);
8150 let via_alloc = praxis_alloc_char(ctx, i64::from('Z' as u32));
8151 assert!(!rt.has_pending_fault());
8152 assert_eq!(via_to_char.as_ptr(), via_alloc.as_ptr());
8153 assert_eq!(via_to_char.as_char(), 'Z');
8154
8155 // Outside the range both still allocate, and still not each other.
8156 let big = praxis_alloc_int(ctx, i64::from('é' as u32));
8157 assert_ne!(
8158 praxis_int_to_char(ctx, big).as_ptr(),
8159 praxis_alloc_char(ctx, i64::from('é' as u32)).as_ptr()
8160 );
8161 }
8162 unsafe { drop_ctx(ctx) };
8163 }
8164
8165 /// **ADR-143.** `Int.to_text()` answers exactly what `out` writes, and the
8166 /// assertion is against `out`'s own path rather than a literal.
8167 ///
8168 /// Comparing to `"1660"` would pass while the two renderers disagreed about
8169 /// everything else; comparing to `GcRef::format`'s output cannot, because
8170 /// that is the function `praxis_write_stdout` calls. `i64::MIN` is in the
8171 /// list because it is the one value whose negation does not fit, and
8172 /// therefore the first thing a hand-rolled renderer gets wrong.
8173 #[test]
8174 fn int_to_text_renders_exactly_what_out_renders() {
8175 let mut rt = Runtime::new();
8176 let ctx = wired_ctx(&mut rt);
8177 // SAFETY: ctx wired; every receiver is an Int.
8178 unsafe {
8179 for v in [0_i64, 1, -1, 1660, i64::MAX, i64::MIN, UNINTERNED] {
8180 let receiver = praxis_alloc_int(ctx, v);
8181 let answer = praxis_int_to_text(ctx, receiver);
8182 assert!(!rt.has_pending_fault(), "{v} faulted");
8183 let mut printed = String::new();
8184 receiver.format(&mut printed);
8185 assert_eq!(answer.as_text(), printed, "to_text and out disagree on {v}");
8186 }
8187 }
8188 unsafe { drop_ctx(ctx) };
8189 }
8190
8191 /// **ADR-143.** The same claim for `Char.to_text()`, at an interned ASCII
8192 /// character and an uninterned multi-byte one.
8193 ///
8194 /// The multi-byte case is the one that would catch reading the four-byte
8195 /// payload as an `i64`: `'é'` is `0xE9`, and eight bytes from a four-byte
8196 /// payload picks up whatever follows it.
8197 #[test]
8198 fn char_to_text_renders_exactly_what_out_renders() {
8199 let mut rt = Runtime::new();
8200 let ctx = wired_ctx(&mut rt);
8201 // SAFETY: ctx wired; every receiver is a Char.
8202 unsafe {
8203 for c in ['#', 'a', 'é', '☃', '\u{10FFFF}'] {
8204 let receiver = praxis_alloc_char(ctx, i64::from(u32::from(c)));
8205 let answer = praxis_char_to_text(ctx, receiver);
8206 assert!(!rt.has_pending_fault(), "{c} faulted");
8207 let mut printed = String::new();
8208 receiver.format(&mut printed);
8209 assert_eq!(answer.as_text(), printed, "to_text and out disagree on {c}");
8210 assert_eq!(answer.as_text(), c.to_string());
8211 }
8212 }
8213 unsafe { drop_ctx(ctx) };
8214 }
8215
8216 /// **ADR-147.** An interpolation hole renders exactly what `out` writes, for
8217 /// **every** type — including the ones with no `to_text()` row.
8218 ///
8219 /// This is the wrapper-level half of ADR-147 decision 2, and it is asserted
8220 /// against `GcRef::format` — `praxis_write_stdout`'s own call — rather than
8221 /// against a literal, for `int_to_text_renders_exactly_what_out_renders`'s
8222 /// reason: a literal comparison passes while the two agree by coincidence.
8223 ///
8224 /// The receivers deliberately span a scalar, a `Text` (whose rendering is
8225 /// its own characters and not a quoted form), a collection and a tuple, so a
8226 /// wrapper that reached for a scalar payload instead of the descriptor fails
8227 /// on the last two rather than on none of them.
8228 #[test]
8229 fn value_to_text_renders_exactly_what_out_renders() {
8230 let mut rt = Runtime::new();
8231 let ctx = wired_ctx(&mut rt);
8232 // SAFETY: ctx wired; every receiver below is freshly allocated here.
8233 unsafe {
8234 let empty = praxis_alloc_text(ctx, std::ptr::null(), 0);
8235 let hello = "hello";
8236 let text = praxis_alloc_text(ctx, hello.as_ptr(), hello.len());
8237 let vec = praxis_vec_new(ctx, &scalars::INT as *const _);
8238 for n in [1_i64, 2, 3] {
8239 let _ = praxis_vec_push(ctx, vec, praxis_alloc_int(ctx, n));
8240 }
8241 let receivers = [
8242 praxis_alloc_int(ctx, UNINTERNED),
8243 praxis_alloc_int(ctx, 0),
8244 praxis_alloc_bool(ctx, 1),
8245 praxis_alloc_char(ctx, i64::from(u32::from('☃'))),
8246 empty,
8247 text,
8248 vec,
8249 ];
8250 for receiver in receivers {
8251 let answer = praxis_value_to_text(ctx, receiver);
8252 assert!(!rt.has_pending_fault(), "value_to_text faulted");
8253 let mut printed = String::new();
8254 receiver.format(&mut printed);
8255 assert_eq!(
8256 answer.as_text(),
8257 printed,
8258 "a hole and `out` must write the same characters"
8259 );
8260 }
8261 // The `Text` rows pin the shape a caller is most likely to assume
8262 // wrong: `out("hello")` writes `hello`, not `"hello"`, so `"{s}"`
8263 // must not add quotes either.
8264 assert_eq!(praxis_value_to_text(ctx, text).as_text(), "hello");
8265 assert_eq!(praxis_value_to_text(ctx, empty).as_text(), "");
8266 }
8267 unsafe { drop_ctx(ctx) };
8268 }
8269
8270 /// **ADR-144.** `join` puts the separator *between* elements and nowhere
8271 /// else, which is the whole of the specification and the whole of what an
8272 /// off-by-one gets wrong.
8273 #[test]
8274 fn vec_join_puts_the_separator_between_and_nowhere_else() {
8275 let mut rt = Runtime::new();
8276 let ctx = wired_ctx(&mut rt);
8277 // SAFETY: ctx wired; every element and separator is a Text.
8278 unsafe {
8279 let cases: [(&[&str], &str, &str); 5] = [
8280 (&[], ", ", ""),
8281 (&["only"], ", ", "only"),
8282 (&["a", "b", "c"], ", ", "a, b, c"),
8283 (&["a", "b", "c"], "", "abc"),
8284 (&["é", "☃"], " — ", "é — ☃"),
8285 ];
8286 for (items, sep, want) in cases {
8287 let members: Vec<GcRef> = items.iter().map(|s| rt.alloc_text(s)).collect();
8288 let vec = rt.alloc_vec(&crate::text::TEXT, members);
8289 let separator = rt.alloc_text(sep);
8290 let answer = praxis_vec_join(ctx, vec, separator);
8291 assert!(!rt.has_pending_fault(), "{items:?} faulted");
8292 assert_eq!(answer.as_text(), want);
8293 }
8294 }
8295 unsafe { drop_ctx(ctx) };
8296 }
8297
8298 /// **ADR-144.** A non-`Text` element is `TypeMismatch` and the Unit
8299 /// sentinel, not a `Text` payload read out of an `Int`.
8300 ///
8301 /// The catalog row's `Text` bound means only a compiler bug gets here, and
8302 /// this is what that bug looks like when it does: a fault the program can
8303 /// see, rather than a pointer-and-length pair read from eight bytes of
8304 /// integer.
8305 #[test]
8306 fn vec_join_refuses_a_non_text_element() {
8307 let mut rt = Runtime::new();
8308 let ctx = wired_ctx(&mut rt);
8309 // SAFETY: ctx wired.
8310 unsafe {
8311 let mixed = rt.alloc_vec(&scalars::INT, vec![rt.alloc_text("a"), rt.alloc_int(1)]);
8312 let sep = rt.alloc_text(",");
8313 let answer = praxis_vec_join(ctx, mixed, sep);
8314 assert!(rt.has_pending_fault());
8315 assert!(std::ptr::eq(answer.descriptor(), &scalars::UNIT));
8316 }
8317 unsafe { drop_ctx(ctx) };
8318 }
8319
8320 /// **ADR-144.** `Vec[Char].to_text()` is the characters with nothing between
8321 /// them, and it agrees with `out` on each of them for ADR-143's reason: it
8322 /// goes through `scalars::write_char` too.
8323 #[test]
8324 fn vec_to_text_renders_every_char() {
8325 let mut rt = Runtime::new();
8326 let ctx = wired_ctx(&mut rt);
8327 // SAFETY: ctx wired; every element is a Char.
8328 unsafe {
8329 for want in ["", ".", "..|", "héllo", "☃☃"] {
8330 let members: Vec<GcRef> = want
8331 .chars()
8332 .map(|c| praxis_alloc_char(ctx, i64::from(u32::from(c))))
8333 .collect();
8334 let vec = rt.alloc_vec(&scalars::CHAR, members);
8335 let answer = praxis_vec_to_text(ctx, vec);
8336 assert!(!rt.has_pending_fault(), "{want:?} faulted");
8337 assert_eq!(answer.as_text(), want);
8338 }
8339 }
8340 unsafe { drop_ctx(ctx) };
8341 }
8342
8343 /// **ADR-144.** A non-`Char` element faults rather than being read as four
8344 /// bytes of something else.
8345 #[test]
8346 fn vec_to_text_refuses_a_non_char_element() {
8347 let mut rt = Runtime::new();
8348 let ctx = wired_ctx(&mut rt);
8349 // SAFETY: ctx wired.
8350 unsafe {
8351 let mixed = rt.alloc_vec(&scalars::CHAR, vec![rt.alloc_int(65)]);
8352 let answer = praxis_vec_to_text(ctx, mixed);
8353 assert!(rt.has_pending_fault());
8354 assert!(std::ptr::eq(answer.descriptor(), &scalars::UNIT));
8355 }
8356 unsafe { drop_ctx(ctx) };
8357 }
8358
8359 /// **ADR-145.** `reversed` answers a **new** `Vec` and leaves the receiver
8360 /// alone — the rule every barrier in this block states, and the one a
8361 /// wrapper that reversed in place would break invisibly for a caller still
8362 /// holding `v`.
8363 ///
8364 /// The empty case is here because `praxis_vec_sorted` needs a `len() > 1`
8365 /// guard for the analogous one and this needs none: there is no callback to
8366 /// avoid calling.
8367 #[test]
8368 fn vec_reversed_answers_a_new_vec_and_leaves_the_receiver_alone() {
8369 let mut rt = Runtime::new();
8370 let ctx = wired_ctx(&mut rt);
8371 // SAFETY: ctx wired.
8372 unsafe {
8373 let source = rt.alloc_vec(
8374 &scalars::INT,
8375 vec![rt.alloc_int(3), rt.alloc_int(1), rt.alloc_int(2)],
8376 );
8377 let answer = praxis_vec_reversed(ctx, source);
8378 assert!(!rt.has_pending_fault());
8379 let got: Vec<i64> = answer.as_vec().iter().map(|r| r.as_int()).collect();
8380 assert_eq!(got, vec![2, 1, 3]);
8381 let still: Vec<i64> = source.as_vec().iter().map(|r| r.as_int()).collect();
8382 assert_eq!(still, vec![3, 1, 2], "the receiver is not touched");
8383 assert_ne!(answer.as_ptr(), source.as_ptr());
8384
8385 let empty = rt.alloc_vec(&scalars::INT, vec![]);
8386 assert!(praxis_vec_reversed(ctx, empty).as_vec().is_empty());
8387 assert!(!rt.has_pending_fault());
8388 }
8389 unsafe { drop_ctx(ctx) };
8390 }
8391
8392 /// **ADR-145.** Reversal reads no descriptor callback, so a `Vec` of a type
8393 /// with no `compare` reverses where `sorted` faults.
8394 ///
8395 /// This is the runtime half of the catalog row carrying no capability bound.
8396 /// A `Unit` has no `compare` — `praxis_vec_sorted` raises `TypeMismatch` on
8397 /// one — and it reverses without a word.
8398 #[test]
8399 fn vec_reversed_needs_no_callback_where_sorted_needs_compare() {
8400 let mut rt = Runtime::new();
8401 let ctx = wired_ctx(&mut rt);
8402 // SAFETY: ctx wired.
8403 unsafe {
8404 let closures = rt.alloc_vec(
8405 &crate::closures::CLOSURE,
8406 vec![
8407 praxis_alloc_closure(ctx, std::ptr::null(), 0),
8408 praxis_alloc_closure(ctx, std::ptr::null(), 0),
8409 ],
8410 );
8411 assert_eq!(praxis_vec_reversed(ctx, closures).as_vec().len(), 2);
8412 assert!(!rt.has_pending_fault(), "reversal asks for no callback");
8413
8414 praxis_vec_sorted(ctx, closures);
8415 assert!(rt.has_pending_fault(), "ordering still asks for `compare`");
8416 }
8417 unsafe { drop_ctx(ctx) };
8418 }
8419
8420 /// The shape of both groupings, read back as nested `Int`s.
8421 ///
8422 /// # Safety
8423 /// `answer` must be a valid `Vec[Vec[Int]]` `GcRef`.
8424 unsafe fn groups_of_int(answer: GcRef) -> Vec<Vec<i64>> {
8425 answer
8426 .as_vec()
8427 .iter()
8428 .map(|inner| inner.as_vec().iter().map(|r| r.as_int()).collect())
8429 .collect()
8430 }
8431
8432 /// **ADR-149.** `chunks` partitions: every element appears once, in order,
8433 /// and a length the size does not divide leaves a *short last chunk* rather
8434 /// than dropping the tail or padding it.
8435 #[test]
8436 fn vec_chunks_partitions_and_keeps_a_short_tail() {
8437 let mut rt = Runtime::new();
8438 let ctx = wired_ctx(&mut rt);
8439 // SAFETY: ctx wired.
8440 unsafe {
8441 let ints: Vec<GcRef> = (1..=5).map(|n| rt.alloc_int(n)).collect();
8442 let source = rt.alloc_vec(&scalars::INT, ints);
8443
8444 let two = rt.alloc_int(2);
8445 let answer = praxis_vec_chunks(ctx, source, two);
8446 assert!(!rt.has_pending_fault());
8447 assert_eq!(groups_of_int(answer), vec![vec![1, 2], vec![3, 4], vec![5]]);
8448
8449 // A size that divides leaves no short chunk, which is the same rule
8450 // and is worth pinning beside the one that does.
8451 let five = rt.alloc_int(5);
8452 assert_eq!(
8453 groups_of_int(praxis_vec_chunks(ctx, source, five)),
8454 vec![vec![1, 2, 3, 4, 5]],
8455 );
8456
8457 // Wider than the receiver is not a fault: it is one short chunk.
8458 let nine = rt.alloc_int(9);
8459 assert_eq!(
8460 groups_of_int(praxis_vec_chunks(ctx, source, nine)),
8461 vec![vec![1, 2, 3, 4, 5]],
8462 );
8463
8464 let still: Vec<i64> = source.as_vec().iter().map(|r| r.as_int()).collect();
8465 assert_eq!(still, vec![1, 2, 3, 4, 5], "the receiver is not touched");
8466 }
8467 unsafe { drop_ctx(ctx) };
8468 }
8469
8470 /// **ADR-149.** `windows` slides by one and keeps only the runs that fit, so
8471 /// a receiver shorter than the size answers `[]` rather than one short run —
8472 /// the one place the two groupings differ.
8473 #[test]
8474 fn vec_windows_slide_by_one_and_drop_a_run_that_does_not_fit() {
8475 let mut rt = Runtime::new();
8476 let ctx = wired_ctx(&mut rt);
8477 // SAFETY: ctx wired.
8478 unsafe {
8479 let ints: Vec<GcRef> = (1..=4).map(|n| rt.alloc_int(n)).collect();
8480 let source = rt.alloc_vec(&scalars::INT, ints);
8481
8482 let two = rt.alloc_int(2);
8483 assert_eq!(
8484 groups_of_int(praxis_vec_windows(ctx, source, two)),
8485 vec![vec![1, 2], vec![2, 3], vec![3, 4]],
8486 );
8487 assert!(!rt.has_pending_fault());
8488
8489 // Exactly the length is one window; one past it is none. Off by one
8490 // here is the whole difference between `[]` and a wrong answer.
8491 let four = rt.alloc_int(4);
8492 assert_eq!(
8493 groups_of_int(praxis_vec_windows(ctx, source, four)),
8494 vec![vec![1, 2, 3, 4]],
8495 );
8496 let five = rt.alloc_int(5);
8497 let none = praxis_vec_windows(ctx, source, five);
8498 assert!(
8499 none.as_vec().is_empty(),
8500 "a run of five does not fit in four"
8501 );
8502 assert!(
8503 !rt.has_pending_fault(),
8504 "not fitting is an answer, not a fault"
8505 );
8506
8507 // Windows share their elements rather than copying them, which is
8508 // the language's reference semantics and not a rule of this wrapper.
8509 let answer = praxis_vec_windows(ctx, source, two);
8510 let first = answer.as_vec()[0].as_vec()[1].as_ptr();
8511 let second = answer.as_vec()[1].as_vec()[0].as_ptr();
8512 assert_eq!(first, second, "the overlapping element is one object");
8513 }
8514 unsafe { drop_ctx(ctx) };
8515 }
8516
8517 /// **ADR-149.** The only thing either grouping refuses: a run of `n <= 0` is
8518 /// not a short run, it is not a run, so there is no sequence of them to
8519 /// answer with.
8520 ///
8521 /// The empty receiver is here beside it because it is the case that looks
8522 /// like a fault and is not — `[].chunks(2)` is `[]`, the same way `[]`
8523 /// reverses to `[]`.
8524 #[test]
8525 fn a_group_size_of_zero_or_less_is_an_invalid_size_fault() {
8526 for size in [0i64, -1, i64::MIN] {
8527 for (name, wrapper) in [
8528 (
8529 "chunks",
8530 praxis_vec_chunks as unsafe extern "C" fn(_, _, _) -> _,
8531 ),
8532 ("windows", praxis_vec_windows),
8533 ] {
8534 let mut rt = Runtime::new();
8535 let ctx = wired_ctx(&mut rt);
8536 // SAFETY: ctx wired.
8537 unsafe {
8538 let source = rt.alloc_vec(&scalars::INT, vec![rt.alloc_int(1)]);
8539 let n = rt.alloc_int(size);
8540 let answer = wrapper(ctx, source, n);
8541 assert!(rt.has_pending_fault(), "{name}({size}) must fault");
8542 assert_eq!(rt.fault(), crate::FaultKind::InvalidSize, "{name}({size})");
8543 assert!(
8544 std::ptr::eq(answer.descriptor(), &scalars::UNIT),
8545 "{name}({size}) answers the Unit sentinel"
8546 );
8547 }
8548 unsafe { drop_ctx(ctx) };
8549 }
8550 }
8551
8552 let mut rt = Runtime::new();
8553 let ctx = wired_ctx(&mut rt);
8554 // SAFETY: ctx wired.
8555 unsafe {
8556 let empty = rt.alloc_vec(&scalars::INT, vec![]);
8557 let two = rt.alloc_int(2);
8558 assert!(praxis_vec_chunks(ctx, empty, two).as_vec().is_empty());
8559 assert!(praxis_vec_windows(ctx, empty, two).as_vec().is_empty());
8560 assert!(
8561 !rt.has_pending_fault(),
8562 "an empty receiver is an empty answer"
8563 );
8564 }
8565 unsafe { drop_ctx(ctx) };
8566 }
8567
8568 /// **ADR-149 decision 1.** The outer `Vec` is labelled `VEC` at every length
8569 /// and the inner ones carry the receiver's element descriptor.
8570 ///
8571 /// The **empty** answer is the whole reason this test exists, and it is the
8572 /// only part that is a choice: `VEC` is what `outer.push(inner)` already
8573 /// produces, so a non-empty grouping could hardly answer anything else and
8574 /// asserting it proves little. With the label inferred from the first group
8575 /// there would be none to read, and `[1, 2].windows(5)` would carry a null
8576 /// where `[1, 2].windows(2)` carries `VEC` — one type with two labels, and
8577 /// the null is the one `vec_format` renders as `[]` and `push` treats as
8578 /// "adopt whatever arrives".
8579 #[test]
8580 fn a_grouping_labels_the_outer_vec_even_when_it_is_empty() {
8581 let mut rt = Runtime::new();
8582 let ctx = wired_ctx(&mut rt);
8583 // SAFETY: ctx wired.
8584 unsafe {
8585 let source = rt.alloc_vec(&scalars::INT, vec![rt.alloc_int(1), rt.alloc_int(2)]);
8586 let two = rt.alloc_int(2);
8587 let five = rt.alloc_int(5);
8588
8589 for answer in [
8590 praxis_vec_chunks(ctx, source, two),
8591 praxis_vec_windows(ctx, source, two),
8592 // The two that come out empty, and the reason this test exists.
8593 praxis_vec_windows(ctx, source, five),
8594 praxis_vec_chunks(ctx, rt.alloc_vec(&scalars::INT, vec![]), two),
8595 ] {
8596 let p = vec_payload(answer);
8597 assert!(
8598 std::ptr::eq(p.element_descriptor, &crate::collections::VEC),
8599 "the outer Vec holds Vecs whether or not it holds any"
8600 );
8601 for inner in p.items.iter() {
8602 assert!(std::ptr::eq(
8603 vec_payload(*inner).element_descriptor,
8604 &scalars::INT
8605 ));
8606 }
8607 }
8608 }
8609 unsafe { drop_ctx(ctx) };
8610 }
8611
8612 /// **ADR-149.** A grouping reads no descriptor callback, so a `Vec` of a
8613 /// type with no `compare` groups where `sorted` faults — `reversed`'s claim,
8614 /// and the runtime half of these two rows carrying no capability bound.
8615 #[test]
8616 fn a_grouping_needs_no_callback_where_sorted_needs_compare() {
8617 let mut rt = Runtime::new();
8618 let ctx = wired_ctx(&mut rt);
8619 // SAFETY: ctx wired.
8620 unsafe {
8621 let closures = rt.alloc_vec(
8622 &crate::closures::CLOSURE,
8623 vec![
8624 praxis_alloc_closure(ctx, std::ptr::null(), 0),
8625 praxis_alloc_closure(ctx, std::ptr::null(), 0),
8626 praxis_alloc_closure(ctx, std::ptr::null(), 0),
8627 ],
8628 );
8629 let two = rt.alloc_int(2);
8630 assert_eq!(praxis_vec_chunks(ctx, closures, two).as_vec().len(), 2);
8631 assert_eq!(praxis_vec_windows(ctx, closures, two).as_vec().len(), 2);
8632 assert!(!rt.has_pending_fault(), "grouping asks for no callback");
8633
8634 praxis_vec_sorted(ctx, closures);
8635 assert!(rt.has_pending_fault(), "ordering still asks for `compare`");
8636 }
8637 unsafe { drop_ctx(ctx) };
8638 }
8639
8640 /// **ADR-107's pacing half, and ADR-100 §3's analogue.** [`char_ref`] must
8641 /// give the collector its turn on the path where it allocates *nothing*.
8642 ///
8643 /// `TextGet` is `AllocatesAndFaults` in the manifest, which is generated
8644 /// code's contract that the call site is a GC safepoint. A `for c in line`
8645 /// loop over ASCII touches nothing else that bumps the pacing counter, and
8646 /// the counter is the collector's only trigger — so an early return here
8647 /// would make such a loop run arbitrarily long with no collection at all.
8648 ///
8649 /// **The interleaved allocation is load-bearing and must stay unpaced.** The
8650 /// observable is the live registry *shrinking*, and an interned `Char` never
8651 /// enters it, so a loop of nothing but `praxis_text_get` could not shrink
8652 /// anything however well it paced — a guaranteed false pass (see
8653 /// `UNINTERNED`). `Runtime::alloc_int` is the one helper that grows the heap
8654 /// **without** pacing, so it supplies the pressure and the population while
8655 /// leaving `praxis_text_get` as the only safepoint in the loop. Swapping it
8656 /// for `praxis_alloc_int` would make the test pass with this function's
8657 /// safepoint deleted.
8658 #[test]
8659 fn char_ref_paces_the_collector_even_when_it_answers_from_the_table() {
8660 let mut rt = Runtime::new();
8661 let ctx = wired_ctx(&mut rt);
8662 // SAFETY: ctx wired throughout; `text` and `index` are rooted below.
8663 unsafe {
8664 let s = "abcdefgh";
8665 let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
8666 let index = praxis_alloc_int(ctx, 3);
8667 let mut frame = push_frame(ctx, SlotCount::new(2).unwrap());
8668 frame.set(0, text);
8669 frame.set(1, index);
8670
8671 let mut before = rt.heap().stats().live_count;
8672 let mut paced = false;
8673 for i in 0..100_000_i64 {
8674 // Registered, unrooted, and *unpaced*: pressure the collector can
8675 // see and reclaim, contributed by something that never offers a
8676 // turn itself.
8677 let _ = rt.alloc_int(UNINTERNED + i);
8678 // The wrapper under test. Every character of `s` is ASCII, so
8679 // this answers from the table and allocates nothing.
8680 let c = praxis_text_get(ctx, text, index);
8681 assert_eq!(c.as_char(), 'd');
8682 let after = rt.heap().stats().live_count;
8683 if after < before.saturating_add(1) {
8684 paced = true;
8685 break;
8686 }
8687 before = after;
8688 }
8689 drop(frame);
8690 assert!(
8691 paced,
8692 "praxis_text_get never gave the collector a turn on the interned path"
8693 );
8694 }
8695 unsafe { drop_ctx(ctx) };
8696 }
8697
8698 /// `default_cell`'s `Char` arm, the fourth boxing site. A `Grid[Char]`'s
8699 /// fill is NUL, which is inside the range.
8700 #[test]
8701 fn a_grid_of_char_fills_with_the_interned_nul() {
8702 let mut rt = Runtime::new();
8703 let ctx = wired_ctx(&mut rt);
8704 // SAFETY: ctx wired.
8705 unsafe {
8706 let grid = praxis_grid_new(ctx, &crate::scalars::CHAR, 3, 2);
8707 assert!(!rt.has_pending_fault());
8708 let nul = rt.immortals().small_char(0).expect("NUL is interned");
8709 for y in 0..2 {
8710 for x in 0..3 {
8711 let xi = praxis_alloc_int(ctx, x);
8712 let yi = praxis_alloc_int(ctx, y);
8713 let cell = praxis_grid_get(ctx, grid, xi, yi);
8714 assert_eq!(
8715 cell.as_ptr(),
8716 nul.as_ptr(),
8717 "every cell of a fresh Grid[Char] is the one interned NUL"
8718 );
8719 }
8720 }
8721 }
8722 unsafe { drop_ctx(ctx) };
8723 }
8724
8725 /// The executable form of "nothing in the language can observe `Char`
8726 /// identity", and the [`crate::dynamic_key::DynamicKey`] leg of ADR-107's
8727 /// argument.
8728 ///
8729 /// `DynamicKey::eq` opens with a pointer comparison, and that is the line
8730 /// interning could in principle have moved — but it is a fast path *for*
8731 /// structural equality and `char_equals` is a reflexive `u32 ==`, so sharing
8732 /// can only make it fire more often. This asserts the consequence rather than
8733 /// the argument: the same shape is run twice, once with interned keys and
8734 /// once with keys the runtime does not intern, and the two must agree.
8735 #[test]
8736 fn interning_a_char_does_not_change_keyed_collection_behaviour() {
8737 // (key, a different key, label): once inside the ASCII range and once
8738 // outside it. `é` and `ü` are two scalars the table does not hold.
8739 for (a_ch, b_ch, label) in [('a', 'b', "interned"), ('é', 'ü', "allocated")] {
8740 let mut rt = Runtime::new();
8741 let ctx = wired_ctx(&mut rt);
8742 // SAFETY: ctx wired; every ref below comes from the ABI.
8743 unsafe {
8744 let a = praxis_alloc_char(ctx, i64::from(a_ch as u32));
8745 // A *second* reference to the same value, built separately.
8746 // Interned it is `a`; uninterned it is a different object with
8747 // the same payload. Both must key the same slot.
8748 let a_again = praxis_alloc_char(ctx, i64::from(a_ch as u32));
8749 let b = praxis_alloc_char(ctx, i64::from(b_ch as u32));
8750 assert_eq!(
8751 std::ptr::eq(a.as_ptr(), a_again.as_ptr()),
8752 label == "interned",
8753 "the fixture must actually be {label}"
8754 );
8755
8756 let set = praxis_set_new(ctx, &crate::scalars::CHAR);
8757 let _ = praxis_set_insert(ctx, set, a);
8758 assert_eq!(
8759 praxis_bool_load(ctx, praxis_set_contains(ctx, set, a_again)),
8760 1,
8761 "{label}: an equal Char is the same set member"
8762 );
8763 assert_eq!(
8764 praxis_bool_load(ctx, praxis_set_contains(ctx, set, b)),
8765 0,
8766 "{label}: a different Char is not"
8767 );
8768 // Inserting the equal-but-possibly-distinct object must not add
8769 // a second member — the property that would break if the pointer
8770 // fast path and `char_equals` ever disagreed.
8771 let _ = praxis_set_insert(ctx, set, a_again);
8772 assert_eq!(praxis_int_load(ctx, praxis_set_len(ctx, set)), 1, "{label}");
8773
8774 let counter = praxis_counter_new(ctx, &crate::scalars::CHAR);
8775 let _ = praxis_counter_inc(ctx, counter, a);
8776 let _ = praxis_counter_inc(ctx, counter, a_again);
8777 assert_eq!(
8778 praxis_int_load(ctx, praxis_counter_get(ctx, counter, a)),
8779 2,
8780 "{label}: two bumps of an equal key are one key"
8781 );
8782 assert_eq!(
8783 praxis_int_load(ctx, praxis_counter_len(ctx, counter)),
8784 1,
8785 "{label}"
8786 );
8787 }
8788 unsafe { drop_ctx(ctx) };
8789 }
8790 }
8791
8792 /// An interned `Char` is never registered, so a collection cannot reclaim it
8793 /// however unrooted it is — the `Char` half of
8794 /// [`an_interned_int_survives_collection_unrooted`].
8795 #[test]
8796 fn an_interned_char_survives_collection_unrooted() {
8797 let mut rt = Runtime::new();
8798 let ctx = wired_ctx(&mut rt);
8799 // SAFETY: ctx is wired to rt throughout.
8800 unsafe {
8801 let _ = praxis_alloc_char(ctx, i64::from('q' as u32));
8802 }
8803 assert_eq!(
8804 rt.heap().stats().live_count,
8805 0,
8806 "an interned Char must not enter the live registry"
8807 );
8808 // Nothing roots `'q'`: no shadow frame, no native scope, no Rust local
8809 // the collector can see. A registered object here would be swept.
8810 rt.collect_now();
8811 // SAFETY: ctx is still wired; the reference must still be readable.
8812 unsafe {
8813 let q = praxis_alloc_char(ctx, i64::from('q' as u32));
8814 assert!(!q.header().is_poisoned(), "an immortal is never swept");
8815 assert_eq!(q.as_char(), 'q');
8816 }
8817 unsafe { drop_ctx(ctx) };
8818 }
8819
8820 /// An interned `Int` is never registered, so a collection cannot reclaim it
8821 /// however unrooted it is. The `Int` analogue of
8822 /// `runtime_collect_keeps_immortals_alive_unrooted`.
8823 #[test]
8824 fn an_interned_int_survives_collection_unrooted() {
8825 let mut rt = Runtime::new();
8826 let ctx = wired_ctx(&mut rt);
8827 // SAFETY: ctx is wired to rt throughout.
8828 unsafe {
8829 let _ = praxis_alloc_int(ctx, 5);
8830 }
8831 assert_eq!(
8832 rt.heap().stats().live_count,
8833 0,
8834 "an interned Int must not enter the live registry"
8835 );
8836 // Nothing roots `5`: no shadow frame, no native scope, no Rust local the
8837 // collector can see. A registered object here would be swept.
8838 rt.collect_now();
8839 // SAFETY: ctx is still wired; the reference must still be readable.
8840 unsafe {
8841 let five = praxis_alloc_int(ctx, 5);
8842 assert!(!five.header().is_poisoned(), "an immortal is never swept");
8843 assert_eq!(praxis_int_load(ctx, five), 5);
8844 }
8845 unsafe { drop_ctx(ctx) };
8846 }
8847
8848 /// The executable form of "nothing in the language can observe `Int`
8849 /// identity": the three keyed collections must behave identically whether
8850 /// their keys are shared objects or distinct ones.
8851 ///
8852 /// [`crate::dynamic_key::DynamicKey`]'s `eq` opens with a pointer
8853 /// comparison, and that is the line interning could in principle have moved
8854 /// — but it is a fast path *for* structural equality and `int_equals` is
8855 /// reflexive, so sharing can only make it fire more often. This asserts the
8856 /// consequence rather than the argument: every operation below is run twice
8857 /// at the same shape, once with interned keys and once with uninterned ones,
8858 /// and the two must agree.
8859 #[test]
8860 fn interning_does_not_change_keyed_collection_behaviour() {
8861 // (key_a, key_b) pairs: two distinct keys, once inside the interned
8862 // range and once outside it.
8863 for (a_val, b_val, label) in [
8864 (5_i64, 6_i64, "interned"),
8865 (UNINTERNED, UNINTERNED + 1, "allocated"),
8866 ] {
8867 let mut rt = Runtime::new();
8868 let ctx = wired_ctx(&mut rt);
8869 // SAFETY: ctx wired; every ref below comes from the ABI.
8870 unsafe {
8871 let a = praxis_alloc_int(ctx, a_val);
8872 // A *second* reference to the same value, allocated separately.
8873 // Interned it is `a`; uninterned it is a different object with
8874 // the same payload. Both must key the same slot.
8875 let a_again = praxis_alloc_int(ctx, a_val);
8876 let b = praxis_alloc_int(ctx, b_val);
8877
8878 let map = praxis_map_new(ctx, &scalars::INT);
8879 let one = praxis_alloc_int(ctx, 1);
8880 let _ = praxis_map_insert(ctx, map, a, one);
8881 // `map_index` rather than `map_get`: the subscript answers the
8882 // value where `.get` answers an `Option` (ADR-076), and the
8883 // value is what has to match.
8884 assert_eq!(
8885 praxis_int_load(ctx, praxis_map_index(ctx, map, a_again)),
8886 1,
8887 "{label}: an equal key must find the entry"
8888 );
8889 assert_eq!(
8890 rt.fault(),
8891 FaultKind::None,
8892 "{label}: an equal key is a present key"
8893 );
8894 assert_eq!(
8895 praxis_bool_load(ctx, praxis_map_contains(ctx, map, b)),
8896 0,
8897 "{label}: a different key must not"
8898 );
8899 assert_eq!(praxis_int_load(ctx, praxis_map_len(ctx, map)), 1);
8900
8901 let set = praxis_set_new(ctx, &scalars::INT);
8902 let _ = praxis_set_insert(ctx, set, a);
8903 let _ = praxis_set_insert(ctx, set, a_again);
8904 assert_eq!(
8905 praxis_int_load(ctx, praxis_set_len(ctx, set)),
8906 1,
8907 "{label}: re-inserting an equal value must not grow the set"
8908 );
8909 assert_eq!(praxis_bool_load(ctx, praxis_set_contains(ctx, set, b)), 0);
8910
8911 let counter = praxis_counter_new(ctx, &scalars::INT);
8912 let _ = praxis_counter_inc(ctx, counter, a);
8913 let _ = praxis_counter_inc(ctx, counter, a_again);
8914 assert_eq!(
8915 praxis_int_load(ctx, praxis_counter_get(ctx, counter, a)),
8916 2,
8917 "{label}: two bumps of an equal key are two bumps of one key"
8918 );
8919 assert_eq!(praxis_int_load(ctx, praxis_counter_len(ctx, counter)), 1);
8920 }
8921 unsafe { drop_ctx(ctx) };
8922 }
8923 }
8924
8925 #[test]
8926 fn bool_and_unit_abi_allocations_reuse_runtime_singletons() {
8927 let mut rt = Runtime::new();
8928 let ctx = wired_ctx(&mut rt);
8929 let (true_ref, false_ref, unit_ref) = unsafe {
8930 (
8931 praxis_alloc_bool(ctx, 1),
8932 praxis_alloc_bool(ctx, 0),
8933 praxis_alloc_unit(ctx),
8934 )
8935 };
8936 let expected = (
8937 rt.immortals().true_(),
8938 rt.immortals().false_(),
8939 rt.immortals().unit(),
8940 );
8941 unsafe { drop_ctx(ctx) };
8942
8943 assert_eq!(true_ref.as_ptr(), expected.0.as_ptr());
8944 assert_eq!(false_ref.as_ptr(), expected.1.as_ptr());
8945 assert_eq!(unit_ref.as_ptr(), expected.2.as_ptr());
8946 }
8947
8948 #[test]
8949 fn repeated_bool_allocation_mints_no_new_objects() {
8950 // A fresh *immortal* per call would be unregistered storage no
8951 // collection can reclaim, leaking one Bool per loop iteration. There
8952 // are two Bools; a hundred calls must name two objects.
8953 let mut rt = Runtime::new();
8954 let ctx = wired_ctx(&mut rt);
8955 let mut seen = std::collections::HashSet::new();
8956 // SAFETY: ctx wired.
8957 unsafe {
8958 for i in 0..100_i64 {
8959 seen.insert(praxis_alloc_bool(ctx, i % 2).as_ptr());
8960 seen.insert(praxis_alloc_unit(ctx).as_ptr());
8961 }
8962 }
8963 unsafe { drop_ctx(ctx) };
8964 assert_eq!(seen.len(), 3, "true, false and unit — and nothing else");
8965 }
8966
8967 /// Every wrapper that answers a *predicate* hands back one of the two `Bool`
8968 /// singletons — the comparisons and the `is_empty`/`contains` family, which
8969 /// are what a real program calls in a loop. It is also what makes their
8970 /// `Effect::Pure` rows honest: nothing here can collect, so the call site is
8971 /// not a safepoint.
8972 #[test]
8973 fn predicate_wrappers_return_bool_singletons_and_allocate_nothing() {
8974 let mut rt = Runtime::new();
8975 let ctx = wired_ctx(&mut rt);
8976 let (immortal_true, immortal_false) = (rt.immortals().true_(), rt.immortals().false_());
8977 // SAFETY: ctx wired; every argument below is allocated through the ABI.
8978 unsafe {
8979 let one = praxis_alloc_int(ctx, 1);
8980 let two = praxis_alloc_int(ctx, 2);
8981 let empty_vec = praxis_vec_new(ctx, &scalars::INT);
8982 let empty_text = praxis_alloc_text(ctx, std::ptr::null(), 0);
8983 let live_before = rt.heap().stats().live_count;
8984
8985 let answers = [
8986 (praxis_int_eq(ctx, one, two), false),
8987 (praxis_int_ne(ctx, one, two), true),
8988 (praxis_int_lt(ctx, one, two), true),
8989 (praxis_int_gt(ctx, one, two), false),
8990 (praxis_int_le(ctx, one, one), true),
8991 (praxis_int_ge(ctx, one, two), false),
8992 (praxis_vec_is_empty(ctx, empty_vec), true),
8993 (praxis_text_is_empty(ctx, empty_text), true),
8994 ];
8995
8996 assert_eq!(
8997 rt.heap().stats().live_count,
8998 live_before,
8999 "a predicate wrapper must not allocate"
9000 );
9001 for (answer, expected) in answers {
9002 let want = if expected {
9003 immortal_true
9004 } else {
9005 immortal_false
9006 };
9007 assert_eq!(
9008 answer.as_ptr(),
9009 want.as_ptr(),
9010 "predicate answered with a fresh Bool instead of the singleton"
9011 );
9012 }
9013 }
9014 unsafe { drop_ctx(ctx) };
9015 }
9016
9017 /// Every wrapper that boxes a *derived* scalar — `Text` construction, the
9018 /// `.len()` family, `Grid` extents, checked arithmetic — paces the
9019 /// collector. Without that, a program whose pressure comes from those (a
9020 /// text-processing loop, say) could run arbitrarily long with the collector
9021 /// never offered a turn. Each is driven here until its own pacing collects.
9022 ///
9023 /// **Every receiver is sized past the interned range on purpose.** The first
9024 /// four wrappers answer a *length*, and a length inside
9025 /// [`crate::small_int`]'s range is an immortal that never enters the live
9026 /// registry — so with a five-byte `Text` or a 2×2 `Grid` the shrink test
9027 /// below is true on the first iteration and the test passes without a
9028 /// collection ever running (see `UNINTERNED`). Interning removes the
9029 /// allocation, not the pacing, and it is the pacing this test is about; the
9030 /// oversized receivers are what keep the observable in place.
9031 #[test]
9032 fn every_scalar_boxing_wrapper_paces_the_collector() {
9033 // (name, a closure that performs one allocating call)
9034 type Call = unsafe extern "C" fn(*mut RuntimeContext, GcRef) -> GcRef;
9035 let cases: [(&str, Call); 7] = [
9036 ("praxis_text_len", praxis_text_len),
9037 ("praxis_vec_len", praxis_vec_len),
9038 ("praxis_grid_width", praxis_grid_width),
9039 ("praxis_grid_height", praxis_grid_height),
9040 ("praxis_float_to_text", praxis_float_to_text),
9041 ("praxis_int_to_text", praxis_int_to_text),
9042 ("praxis_char_to_text", praxis_char_to_text),
9043 ];
9044 // One past the interned range, in whatever unit the receiver measures.
9045 let big = UNINTERNED as usize;
9046 for (name, call) in cases {
9047 let mut rt = Runtime::new();
9048 let ctx = wired_ctx(&mut rt);
9049 // SAFETY: ctx wired; each receiver matches its wrapper.
9050 unsafe {
9051 let text = "x".repeat(big);
9052 let receiver = match name {
9053 "praxis_text_len" => praxis_alloc_text(ctx, text.as_ptr(), big),
9054 "praxis_vec_len" => {
9055 // The elements are all the same interned `0`, so the Vec
9056 // costs one allocation regardless of its length — only
9057 // its `len()` matters here.
9058 rt.alloc_vec(&scalars::INT, vec![rt.alloc_int(0); big])
9059 }
9060 "praxis_float_to_text" => praxis_alloc_float(ctx, 1.5_f64.to_bits() as i64),
9061 // The two `to_text` rows answer a fresh owned `Text` every
9062 // call whatever the receiver is, so an interned receiver is
9063 // the honest case: the allocation is the *answer*, not the
9064 // argument.
9065 "praxis_int_to_text" => praxis_alloc_int(ctx, big as i64),
9066 "praxis_char_to_text" => praxis_alloc_char(ctx, i64::from(u32::from('e'))),
9067 // `width` reads the first dimension and `height` the second,
9068 // so each case makes *its own* answer uninterned and leaves
9069 // the other dimension at one cell.
9070 "praxis_grid_width" => praxis_grid_new(ctx, &scalars::INT, big as i64, 1),
9071 _ => praxis_grid_new(ctx, &scalars::INT, 1, big as i64),
9072 };
9073 let mut frame = push_frame(ctx, SlotCount::new(1).unwrap());
9074 frame.set(0, receiver);
9075
9076 let mut before = rt.heap().stats().live_count;
9077 let mut paced = false;
9078 for _ in 0..10_000 {
9079 let _ = call(ctx, receiver);
9080 let after = rt.heap().stats().live_count;
9081 if after < before.saturating_add(1) {
9082 paced = true;
9083 break;
9084 }
9085 before = after;
9086 }
9087 drop(frame);
9088 assert!(paced, "{name} never gave the collector a turn");
9089 }
9090 unsafe { drop_ctx(ctx) };
9091 }
9092 }
9093
9094 #[test]
9095 fn checked_add_returns_sum() {
9096 let mut rt = Runtime::new();
9097 let ctx = wired_ctx(&mut rt);
9098 // SAFETY: ctx wired; operands allocated as Ints.
9099 unsafe {
9100 let a = praxis_alloc_int(ctx, 40);
9101 let b = praxis_alloc_int(ctx, 2);
9102 let s = praxis_int_add(ctx, a, b);
9103 assert_eq!(praxis_int_load(ctx, s), 42);
9104 assert!(!rt.has_pending_fault());
9105 }
9106 unsafe { drop_ctx(ctx) };
9107 }
9108
9109 #[test]
9110 fn float_sign_of_zero_is_zero() {
9111 let mut rt = Runtime::new();
9112 let ctx = wired_ctx(&mut rt);
9113 let signed = unsafe {
9114 let zero = praxis_alloc_float(ctx, 0.0_f64.to_bits() as i64);
9115 let result = praxis_float_sign(ctx, zero);
9116 f64::from_bits(praxis_float_load(ctx, result) as u64)
9117 };
9118 unsafe { drop_ctx(ctx) };
9119
9120 assert_eq!(signed, 0.0);
9121 }
9122
9123 /// `-0.0` is still zero: `signum` reports the sign *bit* and would answer
9124 /// `-1.0` here.
9125 #[test]
9126 fn float_sign_of_negative_zero_is_zero() {
9127 let mut rt = Runtime::new();
9128 let ctx = wired_ctx(&mut rt);
9129 let signed = unsafe {
9130 let zero = praxis_alloc_float(ctx, (-0.0_f64).to_bits() as i64);
9131 let result = praxis_float_sign(ctx, zero);
9132 f64::from_bits(praxis_float_load(ctx, result) as u64)
9133 };
9134 unsafe { drop_ctx(ctx) };
9135
9136 assert_eq!(signed, 0.0);
9137 }
9138
9139 #[test]
9140 fn float_sign_of_nan_is_nan() {
9141 let mut rt = Runtime::new();
9142 let ctx = wired_ctx(&mut rt);
9143 let signed = unsafe {
9144 let nan = praxis_alloc_float(ctx, f64::NAN.to_bits() as i64);
9145 let result = praxis_float_sign(ctx, nan);
9146 f64::from_bits(praxis_float_load(ctx, result) as u64)
9147 };
9148 unsafe { drop_ctx(ctx) };
9149
9150 assert!(signed.is_nan());
9151 }
9152
9153 /// `min`/`max`/`clamp` hand back **the reference they were given**, not an
9154 /// equal copy (ADR-058). That is what makes them `Effect::Pure` — no
9155 /// allocation, so their call site is not a safepoint — and a version that
9156 /// allocated would pass every value test while quietly making three of the
9157 /// seven helpers collect.
9158 #[test]
9159 fn the_selecting_helpers_return_an_operand_and_allocate_nothing() {
9160 let mut rt = Runtime::new();
9161 let ctx = wired_ctx(&mut rt);
9162 // SAFETY: ctx wired; every operand is a valid Int.
9163 unsafe {
9164 let lo = praxis_alloc_int(ctx, 3);
9165 let hi = praxis_alloc_int(ctx, 7);
9166 assert_eq!(praxis_int_min(ctx, lo, hi).as_ptr(), lo.as_ptr());
9167 assert_eq!(praxis_int_min(ctx, hi, lo).as_ptr(), lo.as_ptr());
9168 assert_eq!(praxis_int_max(ctx, lo, hi).as_ptr(), hi.as_ptr());
9169 assert_eq!(praxis_int_max(ctx, hi, lo).as_ptr(), hi.as_ptr());
9170 // Equal operands pick the left one — arbitrary but fixed, so the
9171 // choice is a decision and not a coin flip.
9172 let three = praxis_alloc_int(ctx, 3);
9173 assert_eq!(praxis_int_min(ctx, lo, three).as_ptr(), lo.as_ptr());
9174 assert_eq!(praxis_int_max(ctx, lo, three).as_ptr(), lo.as_ptr());
9175 // `clamp` returns whichever of its three operands is the answer.
9176 let v = praxis_alloc_int(ctx, 5);
9177 assert_eq!(praxis_int_clamp(ctx, v, lo, hi).as_ptr(), v.as_ptr());
9178 let below = praxis_alloc_int(ctx, 1);
9179 assert_eq!(praxis_int_clamp(ctx, below, lo, hi).as_ptr(), lo.as_ptr());
9180 let above = praxis_alloc_int(ctx, 9);
9181 assert_eq!(praxis_int_clamp(ctx, above, lo, hi).as_ptr(), hi.as_ptr());
9182 assert!(!rt.has_pending_fault());
9183 }
9184 unsafe { drop_ctx(ctx) };
9185 }
9186
9187 /// An inverted `clamp` range is empty, so there is no operand to return and
9188 /// no answer that is not invented. It faults (ADR-058) and returns the Unit
9189 /// sentinel, like every other faulting wrapper.
9190 #[test]
9191 fn an_inverted_clamp_range_faults_rather_than_guessing() {
9192 let mut rt = Runtime::new();
9193 let ctx = wired_ctx(&mut rt);
9194 // SAFETY: ctx wired; every operand is a valid Int.
9195 unsafe {
9196 let v = praxis_alloc_int(ctx, 5);
9197 let lo = praxis_alloc_int(ctx, 10);
9198 let hi = praxis_alloc_int(ctx, 0);
9199 let r = praxis_int_clamp(ctx, v, lo, hi);
9200 assert!(rt.has_pending_fault());
9201 assert_eq!(rt.fault(), FaultKind::EmptyRange);
9202 assert_eq!(r.as_ptr(), rt.immortals().unit().as_ptr());
9203 }
9204 let _ = rt.take_fault();
9205 // A degenerate but *legal* range — one value wide — is not inverted.
9206 // SAFETY: ctx wired; every operand is a valid Int.
9207 unsafe {
9208 let v = praxis_alloc_int(ctx, 5);
9209 let same = praxis_alloc_int(ctx, 4);
9210 let r = praxis_int_clamp(ctx, v, same, same);
9211 assert!(!rt.has_pending_fault());
9212 assert_eq!(r.as_ptr(), same.as_ptr());
9213 }
9214 unsafe { drop_ctx(ctx) };
9215 }
9216
9217 /// A range whose member count has no `Int` faults with `IntOverflow`,
9218 /// answering the Unit sentinel like every other faulting wrapper.
9219 ///
9220 /// The kind is `IntOverflow` and not `EmptyRange` (ADR-059, ADR-075):
9221 /// `Int::MIN..Int::MAX` is the *widest* range expressible, so "empty range"
9222 /// would be a fault message that contradicts the input. `gcd`, `lcm` and
9223 /// A\*'s path cost answer `IntOverflow` for a result with no `Int` too.
9224 #[test]
9225 fn a_range_whose_count_has_no_int_faults_rather_than_wrapping_negative() {
9226 let mut rt = Runtime::new();
9227 let ctx = wired_ctx(&mut rt);
9228 // SAFETY: ctx wired; both bounds are valid Ints.
9229 unsafe {
9230 let lo = praxis_alloc_int(ctx, i64::MIN);
9231 let hi = praxis_alloc_int(ctx, i64::MAX);
9232 let r = praxis_range_new(ctx, lo, hi);
9233 let len = praxis_range_len(ctx, r);
9234 assert!(rt.has_pending_fault());
9235 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9236 assert_eq!(len.as_ptr(), rt.immortals().unit().as_ptr());
9237 }
9238 let _ = rt.take_fault();
9239 // A range one narrower is countable, so the refusal is the edge and not
9240 // the rule.
9241 // SAFETY: ctx wired; both bounds are valid Ints.
9242 unsafe {
9243 let lo = praxis_alloc_int(ctx, 0);
9244 let hi = praxis_alloc_int(ctx, i64::MAX);
9245 let r = praxis_range_new(ctx, lo, hi);
9246 let len = praxis_range_len(ctx, r);
9247 assert!(!rt.has_pending_fault());
9248 assert_eq!(praxis_int_load(ctx, len), i64::MAX);
9249 }
9250 unsafe { drop_ctx(ctx) };
9251 }
9252
9253 /// `gcd` and `lcm` at the edges of what an `Int` can hold. Both are computed
9254 /// in `i128` and range-checked on the way out, so the only refusal is a
9255 /// result that genuinely has no `Int` — and `gcd`'s is exactly one input
9256 /// pair, which a naive `i64` implementation would have wrapped instead.
9257 #[test]
9258 fn gcd_and_lcm_are_non_negative_and_refuse_only_what_has_no_int() {
9259 let mut rt = Runtime::new();
9260 let ctx = wired_ctx(&mut rt);
9261 // SAFETY: ctx wired; every operand is a valid Int.
9262 unsafe {
9263 let load = |r: GcRef| praxis_int_load(ctx, r);
9264 // `Int::MIN`'s divisors: |Int::MIN| is out of range, but every gcd
9265 // *with* it that is not itself is in range.
9266 let min = praxis_alloc_int(ctx, i64::MIN);
9267 let two = praxis_alloc_int(ctx, 2);
9268 assert_eq!(load(praxis_int_gcd(ctx, min, two)), 2);
9269 assert!(!rt.has_pending_fault());
9270 // …and the one pair whose answer is 2^63 faults.
9271 let min2 = praxis_alloc_int(ctx, i64::MIN);
9272 let _ = praxis_int_gcd(ctx, min, min2);
9273 assert!(rt.has_pending_fault());
9274 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9275 }
9276 let _ = rt.take_fault();
9277 // SAFETY: ctx wired; every operand is a valid Int.
9278 unsafe {
9279 let load = |r: GcRef| praxis_int_load(ctx, r);
9280 // Both signs, one answer: the lcm is non-negative.
9281 let neg = praxis_alloc_int(ctx, -4);
9282 let six = praxis_alloc_int(ctx, 6);
9283 assert_eq!(load(praxis_int_lcm(ctx, neg, six)), 12);
9284 let neg6 = praxis_alloc_int(ctx, -6);
9285 assert_eq!(load(praxis_int_lcm(ctx, neg, neg6)), 12);
9286 // `lcm(n, 0)` is 0, and the pair `(0, 0)` does not divide by zero.
9287 let zero = praxis_alloc_int(ctx, 0);
9288 assert_eq!(load(praxis_int_lcm(ctx, six, zero)), 0);
9289 assert_eq!(load(praxis_int_lcm(ctx, zero, zero)), 0);
9290 assert_eq!(load(praxis_int_gcd(ctx, zero, zero)), 0);
9291 assert!(!rt.has_pending_fault());
9292 // An lcm that does not fit: two coprime halves of the range.
9293 let big = praxis_alloc_int(ctx, i64::MAX);
9294 let three = praxis_alloc_int(ctx, 3);
9295 let _ = praxis_int_lcm(ctx, big, three);
9296 assert!(rt.has_pending_fault());
9297 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9298 }
9299 let _ = rt.take_fault();
9300 unsafe { drop_ctx(ctx) };
9301 }
9302
9303 /// `abs` faults on the one input with no positive counterpart, and `sign` is
9304 /// total on the same input — the distinction the manifest records as
9305 /// `AllocatesAndFaults` versus `Allocates`.
9306 #[test]
9307 fn abs_faults_on_the_value_with_no_positive_and_sign_does_not() {
9308 let mut rt = Runtime::new();
9309 let ctx = wired_ctx(&mut rt);
9310 // SAFETY: ctx wired; every operand is a valid Int.
9311 unsafe {
9312 let min = praxis_alloc_int(ctx, i64::MIN);
9313 let r = praxis_int_abs(ctx, min);
9314 assert!(rt.has_pending_fault());
9315 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9316 assert_eq!(r.as_ptr(), rt.immortals().unit().as_ptr());
9317 }
9318 let _ = rt.take_fault();
9319 // SAFETY: ctx wired; every operand is a valid Int.
9320 unsafe {
9321 let min = praxis_alloc_int(ctx, i64::MIN);
9322 assert_eq!(praxis_int_load(ctx, praxis_int_sign(ctx, min)), -1);
9323 let max = praxis_alloc_int(ctx, i64::MAX);
9324 assert_eq!(praxis_int_load(ctx, praxis_int_abs(ctx, max)), i64::MAX);
9325 assert_eq!(praxis_int_load(ctx, praxis_int_sign(ctx, max)), 1);
9326 assert!(!rt.has_pending_fault());
9327 }
9328 unsafe { drop_ctx(ctx) };
9329 }
9330
9331 #[test]
9332 fn overflow_sets_fault_and_returns_sentinel() {
9333 let mut rt = Runtime::new();
9334 let ctx = wired_ctx(&mut rt);
9335 // SAFETY: ctx wired; operands are valid Ints.
9336 unsafe {
9337 let a = praxis_alloc_int(ctx, i64::MAX);
9338 let b = praxis_alloc_int(ctx, 1);
9339 let s = praxis_int_add(ctx, a, b);
9340 // The fault is set; the return is the Unit sentinel.
9341 assert!(rt.has_pending_fault());
9342 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9343 assert_eq!(s.as_ptr(), rt.immortals().unit().as_ptr());
9344 }
9345 let _ = rt.take_fault();
9346 unsafe { drop_ctx(ctx) };
9347 }
9348
9349 #[test]
9350 fn division_by_zero_sets_fault() {
9351 let mut rt = Runtime::new();
9352 let ctx = wired_ctx(&mut rt);
9353 // SAFETY: ctx wired.
9354 unsafe {
9355 let a = praxis_alloc_int(ctx, 10);
9356 let b = praxis_alloc_int(ctx, 0);
9357 let _ = praxis_int_div(ctx, a, b);
9358 assert!(rt.has_pending_fault());
9359 assert_eq!(rt.fault(), FaultKind::DivByZero);
9360 }
9361 let _ = rt.take_fault();
9362 unsafe { drop_ctx(ctx) };
9363 }
9364
9365 #[test]
9366 fn remainder_by_zero_sets_fault() {
9367 let mut rt = Runtime::new();
9368 let ctx = wired_ctx(&mut rt);
9369 // SAFETY: ctx wired.
9370 unsafe {
9371 let a = praxis_alloc_int(ctx, 10);
9372 let b = praxis_alloc_int(ctx, 0);
9373 let _ = praxis_int_rem(ctx, a, b);
9374 assert!(rt.has_pending_fault());
9375 assert_eq!(rt.fault(), FaultKind::DivByZero);
9376 }
9377 let _ = rt.take_fault();
9378 unsafe { drop_ctx(ctx) };
9379 }
9380
9381 #[test]
9382 fn subtraction_overflow_sets_fault() {
9383 // The add/sub/mul overflow paths are symmetric. Sub: `Int::MIN - 1`
9384 // overflows.
9385 let mut rt = Runtime::new();
9386 let ctx = wired_ctx(&mut rt);
9387 // SAFETY: ctx wired; operands are valid Ints.
9388 unsafe {
9389 let a = praxis_alloc_int(ctx, i64::MIN);
9390 let b = praxis_alloc_int(ctx, 1);
9391 let _ = praxis_int_sub(ctx, a, b);
9392 assert!(rt.has_pending_fault());
9393 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9394 }
9395 let _ = rt.take_fault();
9396 unsafe { drop_ctx(ctx) };
9397 }
9398
9399 #[test]
9400 fn multiplication_overflow_sets_fault() {
9401 // `Int::MIN * -1` is the canonical mul overflow (same magnitude as
9402 // `Int::MAX + 1`).
9403 let mut rt = Runtime::new();
9404 let ctx = wired_ctx(&mut rt);
9405 // SAFETY: ctx wired; operands are valid Ints.
9406 unsafe {
9407 let a = praxis_alloc_int(ctx, i64::MIN);
9408 let b = praxis_alloc_int(ctx, -1);
9409 let _ = praxis_int_mul(ctx, a, b);
9410 assert!(rt.has_pending_fault());
9411 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9412 }
9413 let _ = rt.take_fault();
9414 unsafe { drop_ctx(ctx) };
9415 }
9416
9417 #[test]
9418 fn division_truncates_toward_zero() {
9419 // §4.12 / abi.rs comment: division truncates toward zero, so -7 / 2 == -3
9420 // (not -4 as floor division would give). Remainder takes the sign of the
9421 // dividend.
9422 let mut rt = Runtime::new();
9423 let ctx = wired_ctx(&mut rt);
9424 // SAFETY: ctx wired.
9425 unsafe {
9426 let a = praxis_alloc_int(ctx, -7);
9427 let b = praxis_alloc_int(ctx, 2);
9428 let q = praxis_int_div(ctx, a, b);
9429 assert!(!rt.has_pending_fault());
9430 assert_eq!(praxis_int_load(ctx, q), -3);
9431 }
9432 unsafe { drop_ctx(ctx) };
9433 }
9434
9435 #[test]
9436 fn remainder_truncates_toward_zero() {
9437 // -7 % 2 == -1 (remainder takes the dividend's sign under truncation).
9438 let mut rt = Runtime::new();
9439 let ctx = wired_ctx(&mut rt);
9440 // SAFETY: ctx wired.
9441 unsafe {
9442 let a = praxis_alloc_int(ctx, -7);
9443 let b = praxis_alloc_int(ctx, 2);
9444 let r = praxis_int_rem(ctx, a, b);
9445 assert!(!rt.has_pending_fault());
9446 assert_eq!(praxis_int_load(ctx, r), -1);
9447 }
9448 unsafe { drop_ctx(ctx) };
9449 }
9450
9451 #[test]
9452 fn division_min_div_minus_one_overflows() {
9453 // Regression for the §10.4 no-panic-across-ABI contract: `Int::MIN / -1`
9454 // is the sole signed-division case that overflows. The raw `/` panics in
9455 // debug builds; the wrapper must instead fault `IntOverflow`.
9456 let mut rt = Runtime::new();
9457 let ctx = wired_ctx(&mut rt);
9458 // SAFETY: ctx wired; operands are valid Ints.
9459 unsafe {
9460 let a = praxis_alloc_int(ctx, i64::MIN);
9461 let b = praxis_alloc_int(ctx, -1);
9462 let _ = praxis_int_div(ctx, a, b);
9463 assert!(rt.has_pending_fault());
9464 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9465 }
9466 let _ = rt.take_fault();
9467 unsafe { drop_ctx(ctx) };
9468 }
9469
9470 #[test]
9471 fn remainder_min_div_minus_one_overflows() {
9472 // Companion to the division regression: `Int::MIN % -1` traps in debug
9473 // builds even though the mathematical remainder is 0, because the
9474 // corresponding quotient overflows. The wrapper must fault instead.
9475 let mut rt = Runtime::new();
9476 let ctx = wired_ctx(&mut rt);
9477 // SAFETY: ctx wired; operands are valid Ints.
9478 unsafe {
9479 let a = praxis_alloc_int(ctx, i64::MIN);
9480 let b = praxis_alloc_int(ctx, -1);
9481 let _ = praxis_int_rem(ctx, a, b);
9482 assert!(rt.has_pending_fault());
9483 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9484 }
9485 let _ = rt.take_fault();
9486 unsafe { drop_ctx(ctx) };
9487 }
9488
9489 #[test]
9490 fn comparisons_yield_bools() {
9491 let mut rt = Runtime::new();
9492 let ctx = wired_ctx(&mut rt);
9493 // SAFETY: ctx wired.
9494 unsafe {
9495 let one = praxis_alloc_int(ctx, 1);
9496 let two = praxis_alloc_int(ctx, 2);
9497 assert_eq!(praxis_bool_load(ctx, praxis_int_lt(ctx, one, two)), 1);
9498 assert_eq!(praxis_bool_load(ctx, praxis_int_gt(ctx, one, two)), 0);
9499 assert_eq!(praxis_bool_load(ctx, praxis_int_eq(ctx, one, one)), 1);
9500 }
9501 unsafe { drop_ctx(ctx) };
9502 }
9503
9504 #[test]
9505 fn neg_of_min_overflows() {
9506 let mut rt = Runtime::new();
9507 let ctx = wired_ctx(&mut rt);
9508 // SAFETY: ctx wired.
9509 unsafe {
9510 let min = praxis_alloc_int(ctx, i64::MIN);
9511 let _ = praxis_int_neg(ctx, min);
9512 assert!(rt.has_pending_fault());
9513 assert_eq!(rt.fault(), FaultKind::IntOverflow);
9514 }
9515 let _ = rt.take_fault();
9516 unsafe { drop_ctx(ctx) };
9517 }
9518
9519 #[test]
9520 fn check_fault_reports_pending() {
9521 let mut rt = Runtime::new();
9522 let ctx = wired_ctx(&mut rt);
9523 // SAFETY: ctx wired.
9524 unsafe {
9525 assert_eq!(praxis_check_fault(ctx), 0);
9526 let a = praxis_alloc_int(ctx, 1);
9527 let b = praxis_alloc_int(ctx, 0);
9528 let _ = praxis_int_div(ctx, a, b);
9529 assert_eq!(praxis_check_fault(ctx), 1);
9530 }
9531 let _ = rt.take_fault();
9532 unsafe { drop_ctx(ctx) };
9533 }
9534
9535 #[test]
9536 fn alloc_text_round_trips() {
9537 let mut rt = Runtime::new();
9538 let ctx = wired_ctx(&mut rt);
9539 let s = "héllo";
9540 // SAFETY: ctx wired; `bytes` is a valid UTF-8 buffer for the call.
9541 unsafe {
9542 let r = praxis_alloc_text(ctx, s.as_ptr(), s.len());
9543 assert_eq!(r.as_text(), "héllo");
9544 }
9545 unsafe { drop_ctx(ctx) };
9546 }
9547
9548 #[test]
9549 fn alloc_bool_round_trips_value() {
9550 // This pins the *value*; `bool_and_unit_abi_allocations_reuse_runtime_singletons`
9551 // pins the identity. Bool equality is structural (§5.5).
9552 let mut rt = Runtime::new();
9553 let ctx = wired_ctx(&mut rt);
9554 // SAFETY: ctx wired.
9555 unsafe {
9556 let t = praxis_alloc_bool(ctx, 1);
9557 let f = praxis_alloc_bool(ctx, 0);
9558 assert_eq!(praxis_bool_load(ctx, t), 1);
9559 assert_eq!(praxis_bool_load(ctx, f), 0);
9560 }
9561 unsafe { drop_ctx(ctx) };
9562 }
9563
9564 #[test]
9565 fn fault_clear_default_is_none() {
9566 let f = Fault::clear();
9567 assert!(!f.is_pending());
9568 assert_eq!(f.kind(), FaultKind::None);
9569 }
9570
9571 // --- Vec[T] collection wrappers ----------------------------------------
9572
9573 #[test]
9574 fn vec_new_is_empty() {
9575 let mut rt = Runtime::new();
9576 let ctx = wired_ctx(&mut rt);
9577 // SAFETY: ctx wired; INT is a valid static descriptor.
9578 unsafe {
9579 let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9580 assert_eq!(praxis_bool_load(ctx, praxis_vec_is_empty(ctx, v)), 1);
9581 assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 0);
9582 }
9583 unsafe { drop_ctx(ctx) };
9584 }
9585
9586 #[test]
9587 fn vec_push_grows_and_get_reads_back() {
9588 let mut rt = Runtime::new();
9589 let ctx = wired_ctx(&mut rt);
9590 // SAFETY: ctx wired; push mutates the vec in place (returns Unit), so we
9591 // keep using the same `v` GcRef throughout.
9592 unsafe {
9593 let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9594 let a = praxis_alloc_int(ctx, 10);
9595 let b = praxis_alloc_int(ctx, 20);
9596 let c = praxis_alloc_int(ctx, 30);
9597 let _ = praxis_vec_push(ctx, v, a);
9598 let _ = praxis_vec_push(ctx, v, b);
9599 let _ = praxis_vec_push(ctx, v, c);
9600 assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 3);
9601 let i0 = praxis_alloc_int(ctx, 0);
9602 let i2 = praxis_alloc_int(ctx, 2);
9603 assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, i0)), 10);
9604 assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, i2)), 30);
9605 }
9606 unsafe { drop_ctx(ctx) };
9607 }
9608
9609 #[test]
9610 fn vec_get_out_of_bounds_faults() {
9611 let mut rt = Runtime::new();
9612 let ctx = wired_ctx(&mut rt);
9613 // SAFETY: ctx wired.
9614 unsafe {
9615 let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9616 let one = praxis_alloc_int(ctx, 1);
9617 let _ = praxis_vec_get(ctx, v, one); // empty vec, index 0
9618 assert!(rt.has_pending_fault());
9619 assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9620 }
9621 let _ = rt.take_fault();
9622 unsafe { drop_ctx(ctx) };
9623 }
9624
9625 /// `praxis_vec_set` and `praxis_deque_set` **replace** — the property that
9626 /// separates them from the push beside them, and the one a store row pointed
9627 /// at the wrong wrapper would break silently.
9628 ///
9629 /// So each assertion is about what appending would get wrong: the length is
9630 /// unchanged, the neighbours are unchanged, an index one past the end faults
9631 /// instead of growing the collection, and a value of the wrong type is
9632 /// refused rather than retagging an explicitly typed collection.
9633 #[test]
9634 fn a_sequence_store_replaces_and_never_appends() {
9635 let mut rt = Runtime::new();
9636 let ctx = wired_ctx(&mut rt);
9637 // SAFETY: ctx wired; the stores mutate in place, so the same `GcRef`s
9638 // stay valid throughout.
9639 unsafe {
9640 let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9641 for n in [10, 20, 30] {
9642 let _ = praxis_vec_push(ctx, v, praxis_alloc_int(ctx, n));
9643 }
9644 let d = praxis_deque_new(ctx, &crate::scalars::INT as *const _);
9645 for n in [10, 20] {
9646 let _ = praxis_deque_push_back(ctx, d, praxis_alloc_int(ctx, n));
9647 }
9648
9649 let one = praxis_alloc_int(ctx, 1);
9650 let ninety_nine = praxis_alloc_int(ctx, 99);
9651 let _ = praxis_vec_set(ctx, v, one, ninety_nine);
9652 let _ = praxis_deque_set(ctx, d, one, ninety_nine);
9653 assert!(!rt.has_pending_fault());
9654
9655 assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 3);
9656 assert_eq!(praxis_int_load(ctx, praxis_deque_len(ctx, d)), 2);
9657 let zero = praxis_alloc_int(ctx, 0);
9658 let two = praxis_alloc_int(ctx, 2);
9659 assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, zero)), 10);
9660 assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, one)), 99);
9661 assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, two)), 30);
9662 assert_eq!(praxis_int_load(ctx, praxis_deque_get(ctx, d, zero)), 10);
9663 assert_eq!(praxis_int_load(ctx, praxis_deque_get(ctx, d, one)), 99);
9664
9665 // One past the end, and a negative index, are both out of range —
9666 // and neither grows the collection, which is what a store that fell
9667 // through to the appending wrapper would do.
9668 let three = praxis_alloc_int(ctx, 3);
9669 let neg = praxis_alloc_int(ctx, -1);
9670 type Store = unsafe extern "C" fn(*mut RuntimeContext, GcRef, GcRef, GcRef) -> GcRef;
9671 type Len = unsafe extern "C" fn(*mut RuntimeContext, GcRef) -> GcRef;
9672 for (recv, idx, store, len_of) in [
9673 (v, three, praxis_vec_set as Store, praxis_vec_len as Len),
9674 (v, neg, praxis_vec_set as Store, praxis_vec_len as Len),
9675 (d, two, praxis_deque_set as Store, praxis_deque_len as Len),
9676 (d, neg, praxis_deque_set as Store, praxis_deque_len as Len),
9677 ] {
9678 let before = praxis_int_load(ctx, len_of(ctx, recv));
9679 let _ = store(ctx, recv, idx, ninety_nine);
9680 assert!(rt.has_pending_fault(), "an out-of-range store must fault");
9681 assert_eq!(rt.take_fault(), Some(FaultKind::IndexOutOfBounds));
9682 assert_eq!(
9683 praxis_int_load(ctx, len_of(ctx, recv)),
9684 before,
9685 "a faulting store must not have grown the collection"
9686 );
9687 }
9688
9689 // A `Vec[Int]` refuses a `Float` rather than retagging itself.
9690 let float = praxis_alloc_float(ctx, 1.5_f64.to_bits() as i64);
9691 let _ = praxis_vec_set(ctx, v, zero, float);
9692 assert_eq!(rt.take_fault(), Some(FaultKind::TypeMismatch));
9693 assert_eq!(praxis_int_load(ctx, praxis_vec_get(ctx, v, zero)), 10);
9694 }
9695 unsafe { drop_ctx(ctx) };
9696 }
9697
9698 #[test]
9699 fn vec_push_many_survive_collection() {
9700 // Stress: root the receiver/current element exactly as generated code
9701 // does, push enough elements to force multiple automatic collections,
9702 // and leave one unrooted allocation per iteration so collection is
9703 // observable as a live-registry shrink.
9704 //
9705 // Both the pushed element and the deliberately-unrooted allocation are
9706 // offset past the interned range: an interned `Int` is never registered,
9707 // so an in-range element would trip the shrink test on iteration zero
9708 // and neither the collection nor the rooting would be exercised (see
9709 // `UNINTERNED`). The offset is carried through the spot checks below so
9710 // the values read back are still the values pushed.
9711 let mut rt = Runtime::new();
9712 let ctx = wired_ctx(&mut rt);
9713 // SAFETY: ctx wired; push mutates in place so `v` stays valid throughout.
9714 unsafe {
9715 let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9716 let mut frame = push_frame(ctx, SlotCount::new(2).unwrap());
9717 frame.set(0, v);
9718 let mut observed_reclamation = false;
9719 for i in 0..5000_i64 {
9720 let before_alloc = rt.heap().stats().live_count;
9721 let elem = praxis_alloc_int(ctx, UNINTERNED + i);
9722 if rt.heap().stats().live_count < before_alloc.saturating_add(1) {
9723 observed_reclamation = true;
9724 }
9725 frame.set(1, elem);
9726 let before_push = rt.heap().stats().live_count;
9727 let _ = praxis_vec_push(ctx, v, elem);
9728 if rt.heap().stats().live_count < before_push {
9729 observed_reclamation = true;
9730 }
9731 frame.clear(1);
9732 let _ = rt.alloc_int(-UNINTERNED - i - 1);
9733 }
9734 assert!(
9735 observed_reclamation,
9736 "the test must observe an automatic collection, not merely allocation pressure"
9737 );
9738 assert_eq!(praxis_int_load(ctx, praxis_vec_len(ctx, v)), 5000);
9739 // Spot-check first/middle/last. The *indices* stay small (they are
9740 // interned, which is fine — nothing here watches them); the values
9741 // carry the offset the elements were pushed with.
9742 let zero = praxis_alloc_int(ctx, 0);
9743 assert_eq!(
9744 praxis_int_load(ctx, praxis_vec_get(ctx, v, zero)),
9745 UNINTERNED
9746 );
9747 let middle = praxis_alloc_int(ctx, 2500);
9748 assert_eq!(
9749 praxis_int_load(ctx, praxis_vec_get(ctx, v, middle)),
9750 UNINTERNED + 2500
9751 );
9752 let last = praxis_alloc_int(ctx, 4999);
9753 assert_eq!(
9754 praxis_int_load(ctx, praxis_vec_get(ctx, v, last)),
9755 UNINTERNED + 4999
9756 );
9757 drop(frame);
9758 }
9759 unsafe { drop_ctx(ctx) };
9760 }
9761
9762 #[test]
9763 fn vec_get_negative_index_faults() {
9764 // The `idx < 0` guard in `praxis_vec_get`: a negative index is out of
9765 // bounds, not a wrapped-around large one.
9766 let mut rt = Runtime::new();
9767 let ctx = wired_ctx(&mut rt);
9768 // SAFETY: ctx wired.
9769 unsafe {
9770 let v = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9771 let a = praxis_alloc_int(ctx, 1);
9772 let _ = praxis_vec_push(ctx, v, a); // non-empty vec, so only the sign can fail
9773 let neg = praxis_alloc_int(ctx, -1);
9774 let _ = praxis_vec_get(ctx, v, neg);
9775 assert!(rt.has_pending_fault());
9776 assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9777 }
9778 let _ = rt.take_fault();
9779 unsafe { drop_ctx(ctx) };
9780 }
9781
9782 #[test]
9783 fn text_get_negative_index_faults() {
9784 // Companion to `vec_get_negative_index_faults`, for `praxis_text_get`'s
9785 // own `idx < 0` guard.
9786 let mut rt = Runtime::new();
9787 let ctx = wired_ctx(&mut rt);
9788 // SAFETY: ctx wired.
9789 unsafe {
9790 let s = "ab";
9791 let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
9792 let neg = praxis_alloc_int(ctx, -1);
9793 let _ = praxis_text_get(ctx, text, neg);
9794 assert!(rt.has_pending_fault());
9795 assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9796 }
9797 let _ = rt.take_fault();
9798 unsafe { drop_ctx(ctx) };
9799 }
9800
9801 /// **ADR-086, the runtime half.** `praxis_text_get` allocates a `Char`.
9802 ///
9803 /// The catalog's twin (`the_two_text_reads_answer_a_char`) is pure data and
9804 /// cannot see this; this is pure runtime and cannot see that. Both halves
9805 /// are needed, and they must hold together: with only one, a `Char`-typed
9806 /// value routes into `praxis_char_load`, whose `read_scalar` answers `None`
9807 /// against the `INT` descriptor and panics.
9808 #[test]
9809 fn text_get_answers_a_char_object() {
9810 let mut rt = Runtime::new();
9811 let ctx = wired_ctx(&mut rt);
9812 // SAFETY: ctx wired.
9813 unsafe {
9814 // `"sddddd"[4]` must be `'d'` and not `100`: an object carrying the
9815 // `INT` descriptor would have the right value and the wrong type.
9816 let s = "sddddd";
9817 let text = praxis_alloc_text(ctx, s.as_ptr(), s.len());
9818 let four = praxis_alloc_int(ctx, 4);
9819 let got = praxis_text_get(ctx, text, four);
9820 assert!(!rt.has_pending_fault());
9821 assert!(
9822 std::ptr::eq(got.descriptor(), &crate::scalars::CHAR),
9823 "ADR-086: the read answers a Char, not the char's scalar value"
9824 );
9825 assert_eq!(got.as_char(), 'd');
9826
9827 // Scalar-not-byte indexing, pinned at the runtime level too: `é` is
9828 // one scalar and two UTF-8 bytes, so a byte index would answer 0xC3.
9829 let u = "héllo";
9830 let utext = praxis_alloc_text(ctx, u.as_ptr(), u.len());
9831 let one = praxis_alloc_int(ctx, 1);
9832 let got = praxis_text_get(ctx, utext, one);
9833 assert!(!rt.has_pending_fault());
9834 assert_eq!(got.as_char(), 'é');
9835 }
9836 unsafe { drop_ctx(ctx) };
9837 }
9838
9839 /// **ADR-115 at the ABI, on the shape that decides it.** `t.len()` and
9840 /// `t[i]` are defined on scalars (§4.3, ADR-086); indexing bytes is an
9841 /// optimization licensed by the count, and the licence must be refused on
9842 /// every text where it would be wrong.
9843 ///
9844 /// The cases are chosen to break a byte-indexing implementation that only
9845 /// looked at the text's own leading byte or only at its first scalar: a
9846 /// multi-byte scalar at the start, in the middle, at the end, a four-byte
9847 /// one, and a slice whose own bytes are all one-byte but whose owner's are
9848 /// not.
9849 #[test]
9850 fn a_text_reads_by_scalar_wherever_the_multi_byte_scalar_sits() {
9851 let mut rt = Runtime::new();
9852 let ctx = wired_ctx(&mut rt);
9853 // SAFETY: ctx wired for every call in this block.
9854 unsafe {
9855 for src in [
9856 "",
9857 "abc",
9858 "\u{0}\u{7f}",
9859 "éabc",
9860 "abéc",
9861 "abcé",
9862 "a\u{1F600}b",
9863 "\u{20AC}\u{20AC}",
9864 "héllo wörld",
9865 ] {
9866 let text = praxis_alloc_text(ctx, src.as_ptr(), src.len());
9867 let expected: Vec<char> = src.chars().collect();
9868
9869 let len = praxis_text_len(ctx, text);
9870 assert!(!rt.has_pending_fault(), "{src:?}");
9871 assert_eq!(len.as_int(), expected.len() as i64, "{src:?}");
9872
9873 let empty = praxis_text_is_empty(ctx, text);
9874 assert_eq!(empty.as_bool(), expected.is_empty(), "{src:?}");
9875
9876 for (i, want) in expected.iter().enumerate() {
9877 let idx = praxis_alloc_int(ctx, i as i64);
9878 let got = praxis_text_get(ctx, text, idx);
9879 assert!(!rt.has_pending_fault(), "{src:?}[{i}]");
9880 assert!(
9881 std::ptr::eq(got.descriptor(), &crate::scalars::CHAR),
9882 "{src:?}[{i}] answers a Char (ADR-086)"
9883 );
9884 assert_eq!(got.as_char(), *want, "{src:?}[{i}]");
9885 }
9886
9887 // One past the end faults, whichever path answered above.
9888 let past = praxis_alloc_int(ctx, expected.len() as i64);
9889 let _ = praxis_text_get(ctx, text, past);
9890 assert!(rt.has_pending_fault(), "{src:?}[{}]", expected.len());
9891 assert_eq!(rt.fault(), FaultKind::IndexOutOfBounds);
9892 let _ = rt.take_fault();
9893 }
9894
9895 // A view whose own bytes are all one-byte, inside an owner whose
9896 // are not. The answers are the same as the owner's corresponding
9897 // scalars; the byte-index path is refused because the licence is
9898 // the owner's to give.
9899 let owner_src = "héllo wörld";
9900 let owner = praxis_alloc_text(ctx, owner_src.as_ptr(), owner_src.len());
9901 // "llo " — bytes [3, 7) of the owner, all below 0x80.
9902 let view = rt
9903 .alloc_text_slice(owner, 3, 4)
9904 .expect("[3, 7) is on scalar boundaries");
9905 let len = praxis_text_len(ctx, view);
9906 assert_eq!(len.as_int(), 4);
9907 for (i, want) in "llo ".chars().enumerate() {
9908 let idx = praxis_alloc_int(ctx, i as i64);
9909 let got = praxis_text_get(ctx, view, idx);
9910 assert!(!rt.has_pending_fault());
9911 assert_eq!(got.as_char(), want);
9912 }
9913 }
9914 unsafe { drop_ctx(ctx) };
9915 }
9916
9917 /// **ADR-086's narrowing half.** `Int.to_char()` reaches the same
9918 /// range check `praxis_alloc_char` does, because they share one helper.
9919 ///
9920 /// A wrapper that forwarded `value as u32` without the check would answer
9921 /// `'A'` for `0x1_0000_0041` instead of faulting, which is the case that
9922 /// proves this door reaches the shared guard rather than restating it.
9923 #[test]
9924 fn int_to_char_rejects_what_is_not_a_scalar_value() {
9925 let mut rt = Runtime::new();
9926 let ctx = wired_ctx(&mut rt);
9927 // SAFETY: ctx wired.
9928 unsafe {
9929 for bad in [-1_i64, 0xD800, 0x11_0000, 0x1_0000_0041] {
9930 let n = praxis_alloc_int(ctx, bad);
9931 let got = praxis_int_to_char(ctx, n);
9932 assert!(rt.has_pending_fault(), "{bad} must not answer a Char");
9933 assert_eq!(rt.fault(), FaultKind::InvalidChar, "{bad}");
9934 assert!(std::ptr::eq(got.descriptor(), &crate::scalars::UNIT));
9935 let _ = rt.take_fault();
9936 }
9937
9938 // …and the round trip holds for one that is.
9939 let n = praxis_alloc_int(ctx, 233);
9940 let got = praxis_int_to_char(ctx, n);
9941 assert!(!rt.has_pending_fault());
9942 assert_eq!(got.as_char(), 'é');
9943 let back = praxis_char_to_int(ctx, got);
9944 assert!(!rt.has_pending_fault());
9945 assert_eq!(back.as_int(), 233);
9946 }
9947 unsafe { drop_ctx(ctx) };
9948 }
9949
9950 #[test]
9951 fn alloc_text_empty_string_round_trips() {
9952 // The `len == 0` branch in `praxis_alloc_text` treats an empty buffer as
9953 // the empty slice. An empty Text must format as "".
9954 let mut rt = Runtime::new();
9955 let ctx = wired_ctx(&mut rt);
9956 // SAFETY: ctx wired; null pointer + zero length is the documented empty path.
9957 unsafe {
9958 let r = praxis_alloc_text(ctx, std::ptr::null(), 0);
9959 assert_eq!(r.as_text(), "");
9960 }
9961 unsafe { drop_ctx(ctx) };
9962 }
9963
9964 #[test]
9965 fn vec_new_with_null_descriptor_defaults_to_int() {
9966 // A null element descriptor is kept null — "the caller has no static
9967 // element type" — and the vec must still be usable.
9968 let mut rt = Runtime::new();
9969 let ctx = wired_ctx(&mut rt);
9970 // SAFETY: ctx wired; null descriptor is the handled default case.
9971 unsafe {
9972 let v = praxis_vec_new(ctx, std::ptr::null());
9973 assert_eq!(praxis_bool_load(ctx, praxis_vec_is_empty(ctx, v)), 1);
9974 }
9975 unsafe { drop_ctx(ctx) };
9976 }
9977
9978 #[test]
9979 fn vec_push_rejects_a_value_with_the_wrong_descriptor() {
9980 let mut rt = Runtime::new();
9981 let ctx = wired_ctx(&mut rt);
9982 let length_after;
9983 unsafe {
9984 let ints = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
9985 let float = praxis_alloc_float(ctx, 1.5_f64.to_bits() as i64);
9986 let _ = praxis_vec_push(ctx, ints, float);
9987 length_after = ints.as_vec().len();
9988 }
9989 unsafe { drop_ctx(ctx) };
9990
9991 assert_eq!(
9992 length_after, 0,
9993 "an ABI type mismatch must not silently retag and mutate an explicitly typed Vec[Int]"
9994 );
9995 }
9996
9997 #[test]
9998 fn alloc_char_rejects_values_that_only_become_valid_after_truncation() {
9999 let mut rt = Runtime::new();
10000 let ctx = wired_ctx(&mut rt);
10001 let result = unsafe { praxis_alloc_char(ctx, 0x1_0000_0041) };
10002 let unit = rt.immortals().unit();
10003 unsafe { drop_ctx(ctx) };
10004
10005 assert_eq!(
10006 result.as_ptr(),
10007 unit.as_ptr(),
10008 "the ABI must range-check the i64 code point before converting it to u32"
10009 );
10010 // And the fault it raises must name itself: a `FaultKind::None` here
10011 // would have the host report "no fault" while generated code took its
10012 // fault path.
10013 assert_eq!(rt.fault(), FaultKind::InvalidChar);
10014 assert!(rt.has_pending_fault());
10015 }
10016
10017 /// A negative code point is out of range for the same reason a too-large
10018 /// one is, and `as u32` wraps it into the valid range just as silently.
10019 #[test]
10020 fn alloc_char_rejects_a_negative_code_point() {
10021 let mut rt = Runtime::new();
10022 let ctx = wired_ctx(&mut rt);
10023 let result = unsafe { praxis_alloc_char(ctx, -1) };
10024 let unit = rt.immortals().unit();
10025 unsafe { drop_ctx(ctx) };
10026
10027 assert_eq!(result.as_ptr(), unit.as_ptr());
10028 assert_eq!(rt.fault(), FaultKind::InvalidChar);
10029 }
10030
10031 /// **ADR-111.** Input that is not UTF-8 faults at the `read`, because that
10032 /// is where the bytes stop being the compiler's and start being the host's.
10033 ///
10034 /// Asserted through `praxis_get_input` and never by feeding
10035 /// `praxis_alloc_text` bad bytes directly: that is a violated precondition,
10036 /// so it panics through `abi_guard!`, and `praxis_alloc_text`'s
10037 /// `Allocates` row makes the resulting `Panic` fault unobservable — the
10038 /// process aborts instead of failing. The property is that a program
10039 /// reading non-UTF-8 input gets `InvalidText` at its `read`.
10040 ///
10041 /// `praxis run` cannot reach this: `lazy_stdin::read` goes through
10042 /// `std::io::read_to_string` and exits 2 on non-UTF-8 stdin before the
10043 /// runtime sees a byte (`praxis-cli/src/run.rs`). The reachable caller is an
10044 /// embedder that installs its own `InputReader`, which is exactly what this
10045 /// test is.
10046 #[test]
10047 fn input_that_is_not_utf8_faults_at_the_read() {
10048 fn not_utf8() -> Vec<u8> {
10049 vec![0xF0, 0x28, 0x8C, 0x28]
10050 }
10051 let mut rt = Runtime::new();
10052 let ctx = wired_ctx(&mut rt);
10053 crate::input::install_input_reader(not_utf8);
10054 let result = unsafe { praxis_get_input(ctx) };
10055 let unit = rt.immortals().unit();
10056 crate::input::clear_input_reader();
10057 unsafe { drop_ctx(ctx) };
10058
10059 assert_eq!(rt.fault(), FaultKind::InvalidText);
10060 assert!(rt.has_pending_fault());
10061 assert_eq!(
10062 result.as_ptr(),
10063 unit.as_ptr(),
10064 "the fault path answers §10.4's defined dummy, not a half-built Text"
10065 );
10066 }
10067
10068 /// The mutation companion, and it is required: a `praxis_get_input` that
10069 /// faulted on *every* input would pass the gate above.
10070 ///
10071 /// Multi-byte on purpose — a validation that accepted only ASCII would also
10072 /// pass a test written with `"hi"`.
10073 #[test]
10074 fn input_that_is_utf8_still_becomes_the_buffer() {
10075 fn multibyte() -> Vec<u8> {
10076 "héllo wörld".as_bytes().to_vec()
10077 }
10078 let mut rt = Runtime::new();
10079 let ctx = wired_ctx(&mut rt);
10080 crate::input::install_input_reader(multibyte);
10081 let result = unsafe { praxis_get_input(ctx) };
10082 let contents = result.as_text().to_string();
10083 let descriptor = result.descriptor().name;
10084 crate::input::clear_input_reader();
10085 unsafe { drop_ctx(ctx) };
10086
10087 assert!(!rt.has_pending_fault(), "fault: {:?}", rt.fault());
10088 assert_eq!(descriptor, "Text");
10089 assert_eq!(contents, "héllo wörld");
10090 }
10091
10092 #[test]
10093 fn grid_cell_vectors_preserve_the_grid_element_descriptor() {
10094 let mut rt = Runtime::new();
10095 let ctx = wired_ctx(&mut rt);
10096 let cell = rt.alloc_text("x");
10097 let grid = rt.alloc_grid(&crate::text::TEXT, vec![cell], 1);
10098 let descriptors;
10099 unsafe {
10100 let zero = praxis_alloc_int(ctx, 0);
10101 let cells = praxis_grid_cells(ctx, grid);
10102 let row = praxis_grid_row(ctx, grid, zero);
10103 let column = praxis_grid_column(ctx, grid, zero);
10104 descriptors = [
10105 (*vec_payload(cells).element_descriptor).id(),
10106 (*vec_payload(row).element_descriptor).id(),
10107 (*vec_payload(column).element_descriptor).id(),
10108 ];
10109 }
10110 unsafe { drop_ctx(ctx) };
10111
10112 assert!(
10113 descriptors.iter().all(|id| *id == crate::text::TEXT.id()),
10114 "cells(), row(), and column() must return Vec values tagged with the Grid cell type"
10115 );
10116 }
10117
10118 #[test]
10119 fn constructed_grid_cells_satisfy_the_declared_element_descriptor() {
10120 let mut rt = Runtime::new();
10121 let ctx = wired_ctx(&mut rt);
10122 let cell_descriptor;
10123 unsafe {
10124 let grid = praxis_grid_new(ctx, &crate::scalars::INT as *const _, 1, 1);
10125 cell_descriptor = grid_payload(grid).items[0].descriptor().id();
10126 }
10127 unsafe { drop_ctx(ctx) };
10128
10129 assert_eq!(
10130 cell_descriptor,
10131 crate::scalars::INT.id(),
10132 "a live Grid[Int] must never contain a Unit placeholder observable through get/format/hash"
10133 );
10134 }
10135
10136 #[test]
10137 fn grid_position_vectors_use_the_point_tuple_descriptor() {
10138 let mut rt = Runtime::new();
10139 let ctx = wired_ctx(&mut rt);
10140 let cell = rt.alloc_int(1);
10141 let grid = rt.alloc_grid(&crate::scalars::INT, vec![cell], 1);
10142 let descriptors;
10143 unsafe {
10144 let point = alloc_point(ctx, 0, 0);
10145 let positions = praxis_grid_positions(ctx, grid);
10146 let neighbors4 = praxis_grid_neighbors4(ctx, grid, point);
10147 let neighbors8 = praxis_grid_neighbors8(ctx, grid, point);
10148 let matches = praxis_grid_find_all(ctx, grid, cell);
10149 descriptors = [
10150 (*vec_payload(positions).element_descriptor).id(),
10151 (*vec_payload(neighbors4).element_descriptor).id(),
10152 (*vec_payload(neighbors8).element_descriptor).id(),
10153 (*vec_payload(matches).element_descriptor).id(),
10154 ];
10155 }
10156 unsafe { drop_ctx(ctx) };
10157
10158 assert!(
10159 descriptors
10160 .iter()
10161 .all(|id| *id == crate::tuples::TUPLE.id()),
10162 "position-producing Grid methods must return Vec[Tuple[Int, Int]] at runtime"
10163 );
10164 }
10165
10166 /// An extent must be validated before it becomes a `usize`. Unchecked,
10167 /// `vec![unit; (w as usize) * (h as usize)]` turns `-1` into `usize::MAX`,
10168 /// and the products either overflow (a capacity panic across `extern "C"`)
10169 /// or ask the host for terabytes (an OOM abort). The wrapper must answer
10170 /// with a fault, and the heap must be untouched — a partly-built grid is as
10171 /// bad as a crash.
10172 #[test]
10173 fn a_negative_or_absurd_grid_extent_faults_instead_of_allocating() {
10174 let absurd = GridExtent::MAX_CELLS as i64 + 1;
10175 for (width, height) in [
10176 (-1_i64, 4_i64),
10177 (4, -1),
10178 (-1, -1),
10179 (i64::MIN, 1),
10180 // Overflows the `usize` multiplication outright.
10181 (i64::MAX, 2),
10182 (1 << 40, 1 << 40),
10183 // Multiplies cleanly and is still an allocation no host can serve.
10184 (absurd, 1),
10185 (1, absurd),
10186 ] {
10187 let mut rt = Runtime::new();
10188 let ctx = wired_ctx(&mut rt);
10189 let live_before = rt.heap().stats().live_count;
10190 let result =
10191 unsafe { praxis_grid_new(ctx, &crate::scalars::INT as *const _, width, height) };
10192 let live_after = rt.heap().stats().live_count;
10193 let unit = rt.immortals().unit();
10194 unsafe { drop_ctx(ctx) };
10195
10196 assert_eq!(
10197 rt.fault(),
10198 FaultKind::InvalidSize,
10199 "Grid[Int]({width}, {height}) must fault"
10200 );
10201 assert_eq!(
10202 result.as_ptr(),
10203 unit.as_ptr(),
10204 "a faulted Grid[Int]({width}, {height}) returns the Unit sentinel"
10205 );
10206 assert_eq!(
10207 live_after, live_before,
10208 "a rejected Grid[Int]({width}, {height}) allocates nothing"
10209 );
10210 }
10211 }
10212
10213 /// The other side of the same gate: an extent the runtime *can* serve still
10214 /// builds the grid it asked for, including the degenerate zero cases.
10215 #[test]
10216 fn an_in_range_grid_extent_still_builds_its_cells() {
10217 let mut rt = Runtime::new();
10218 let ctx = wired_ctx(&mut rt);
10219 let shapes: Vec<(i64, i64, usize, usize)> = vec![(0, 0, 0, 0), (0, 5, 0, 0), (3, 2, 6, 3)];
10220 let mut observed = Vec::new();
10221 for (width, height, _, _) in &shapes {
10222 let grid =
10223 unsafe { praxis_grid_new(ctx, &crate::scalars::INT as *const _, *width, *height) };
10224 let p = unsafe { grid_payload(grid) };
10225 observed.push((p.items.len(), p.width));
10226 }
10227 unsafe { drop_ctx(ctx) };
10228
10229 assert_eq!(rt.fault(), FaultKind::None, "no in-range extent faults");
10230 for ((w, h, cells, width), (got_cells, got_width)) in shapes.iter().zip(observed) {
10231 assert_eq!(
10232 (got_cells, got_width),
10233 (*cells, *width),
10234 "Grid[Int]({w}, {h}) shape"
10235 );
10236 }
10237 }
10238
10239 /// **ADR-146.** `Vec(n, fill)` builds `n` slots, all of them the fill, and
10240 /// the empty case is a `Vec` and not a fault.
10241 #[test]
10242 fn vec_filled_builds_n_copies_of_one_value() {
10243 let mut rt = Runtime::new();
10244 let ctx = wired_ctx(&mut rt);
10245 let observed: Vec<(usize, bool)> = [0_i64, 1, 7]
10246 .into_iter()
10247 .map(|n| unsafe {
10248 let count = praxis_alloc_int(ctx, n);
10249 let fill = praxis_alloc_int(ctx, 42);
10250 let v = praxis_vec_filled(ctx, &crate::scalars::INT as *const _, count, fill);
10251 let p = vec_payload(v);
10252 // Every slot is the *same* reference, which is the aliasing
10253 // ADR-146 decision 4 states rather than n copies of a value.
10254 let all_same = p.items.iter().all(|item| item.as_ptr() == fill.as_ptr());
10255 (p.items.len(), all_same)
10256 })
10257 .collect();
10258 unsafe { drop_ctx(ctx) };
10259
10260 assert_eq!(rt.fault(), FaultKind::None, "no in-range count faults");
10261 assert_eq!(observed, vec![(0, true), (1, true), (7, true)]);
10262 }
10263
10264 /// The other half of ADR-041 decision 1, for the newtype it added: a count
10265 /// the runtime cannot serve is a fault and not an allocation, and the heap
10266 /// is untouched — a half-built `Vec` is as bad as a crash.
10267 #[test]
10268 fn vec_filled_refuses_a_negative_or_absurd_count() {
10269 let absurd = crate::collections::VecExtent::MAX_ITEMS as i64 + 1;
10270 for n in [-1_i64, i64::MIN, absurd, i64::MAX] {
10271 let mut rt = Runtime::new();
10272 let ctx = wired_ctx(&mut rt);
10273 let (result, live_before, live_after, unit) = unsafe {
10274 let count = praxis_alloc_int(ctx, n);
10275 let fill = praxis_alloc_int(ctx, 0);
10276 let before = rt.heap().stats().live_count;
10277 let r = praxis_vec_filled(ctx, &crate::scalars::INT as *const _, count, fill);
10278 (
10279 r,
10280 before,
10281 rt.heap().stats().live_count,
10282 rt.immortals().unit(),
10283 )
10284 };
10285 unsafe { drop_ctx(ctx) };
10286
10287 assert_eq!(rt.fault(), FaultKind::InvalidSize, "Vec({n}, 0) must fault");
10288 assert_eq!(
10289 result.as_ptr(),
10290 unit.as_ptr(),
10291 "a faulted Vec({n}, 0) returns the Unit sentinel"
10292 );
10293 assert_eq!(
10294 live_after, live_before,
10295 "a rejected Vec({n}, 0) allocates nothing"
10296 );
10297 }
10298 }
10299
10300 /// A declared element type the fill is not is a `TypeMismatch`, through the
10301 /// same `adopt_or_reject` a `push` goes through — not a silent retag of the
10302 /// collection to the fill's type, which is the mislabelling defect one
10303 /// level down.
10304 /// A *null* static descriptor adopts instead, which is what "the caller has
10305 /// no static element type" already means for `praxis_vec_new`.
10306 #[test]
10307 fn vec_filled_reconciles_its_element_descriptor() {
10308 let mut rejecting = Runtime::new();
10309 let ctx = wired_ctx(&mut rejecting);
10310 unsafe {
10311 let count = praxis_alloc_int(ctx, 3);
10312 let text = praxis_alloc_text(ctx, b"x".as_ptr(), 1);
10313 praxis_vec_filled(ctx, &crate::scalars::INT as *const _, count, text);
10314 drop_ctx(ctx);
10315 }
10316 assert_eq!(
10317 rejecting.fault(),
10318 FaultKind::TypeMismatch,
10319 "a `Vec[Int]` filled with a `Text` is a mislabelled element descriptor"
10320 );
10321
10322 let mut adopting = Runtime::new();
10323 let ctx = wired_ctx(&mut adopting);
10324 let adopted = unsafe {
10325 let count = praxis_alloc_int(ctx, 3);
10326 let text = praxis_alloc_text(ctx, b"x".as_ptr(), 1);
10327 let v = praxis_vec_filled(ctx, std::ptr::null(), count, text);
10328 let matches = std::ptr::eq(vec_payload(v).element_descriptor, text.descriptor());
10329 drop_ctx(ctx);
10330 matches
10331 };
10332 assert_eq!(adopting.fault(), FaultKind::None);
10333 assert!(
10334 adopted,
10335 "a null static descriptor adopts the fill's, as `praxis_vec_new` already does"
10336 );
10337 }
10338
10339 /// **ADR-146 decision 6.** `Grid(w, h, fill)` accepts a fill
10340 /// `praxis_grid_new` cannot invent: `default_cell` has no zero value for a
10341 /// composite and answers `TypeMismatch`, and the explicit fill is exactly
10342 /// what removes the question. The contrast is the assertion — both calls
10343 /// are in one test so a later change that reintroduced `default_cell` here
10344 /// fails rather than passes quietly.
10345 #[test]
10346 fn grid_filled_accepts_a_composite_fill_where_grid_new_cannot() {
10347 let mut inventing = Runtime::new();
10348 let ctx = wired_ctx(&mut inventing);
10349 unsafe {
10350 praxis_grid_new(ctx, &crate::collections::VEC as *const _, 2, 2);
10351 drop_ctx(ctx);
10352 }
10353 assert_eq!(
10354 inventing.fault(),
10355 FaultKind::TypeMismatch,
10356 "`praxis_grid_new` still has no zero value for a `Vec` cell"
10357 );
10358
10359 let mut supplied = Runtime::new();
10360 let ctx = wired_ctx(&mut supplied);
10361 let (cells, all_same) = unsafe {
10362 let inner = praxis_vec_new(ctx, &crate::scalars::INT as *const _);
10363 let (w, h) = (praxis_alloc_int(ctx, 2), praxis_alloc_int(ctx, 2));
10364 let g = praxis_grid_filled(ctx, &crate::collections::VEC as *const _, w, h, inner);
10365 let p = grid_payload(g);
10366 let same = p.items.iter().all(|c| c.as_ptr() == inner.as_ptr());
10367 let len = p.items.len();
10368 drop_ctx(ctx);
10369 (len, same)
10370 };
10371 assert_eq!(supplied.fault(), FaultKind::None);
10372 assert_eq!(cells, 4, "an explicit fill builds all four cells");
10373 assert!(
10374 all_same,
10375 "the four cells are one `Vec`, not four (ADR-146 decision 4)"
10376 );
10377 }
10378
10379 /// `Grid(w, h, fill)` takes the extents `praxis_grid_new` refuses, through
10380 /// the same `GridExtent::new` — a fill changes nothing about the
10381 /// arithmetic.
10382 #[test]
10383 fn grid_filled_refuses_the_extents_grid_new_refuses() {
10384 let absurd = GridExtent::MAX_CELLS as i64 + 1;
10385 for (width, height) in [(-1_i64, 4_i64), (4, -1), (i64::MAX, 2), (absurd, 1)] {
10386 let mut rt = Runtime::new();
10387 let ctx = wired_ctx(&mut rt);
10388 let (result, live_before, live_after, unit) = unsafe {
10389 let (w, h) = (praxis_alloc_int(ctx, width), praxis_alloc_int(ctx, height));
10390 let fill = praxis_alloc_int(ctx, 0);
10391 let before = rt.heap().stats().live_count;
10392 let r = praxis_grid_filled(ctx, &crate::scalars::INT as *const _, w, h, fill);
10393 (
10394 r,
10395 before,
10396 rt.heap().stats().live_count,
10397 rt.immortals().unit(),
10398 )
10399 };
10400 unsafe { drop_ctx(ctx) };
10401
10402 assert_eq!(
10403 rt.fault(),
10404 FaultKind::InvalidSize,
10405 "Grid({width}, {height}, 0) must fault"
10406 );
10407 assert_eq!(
10408 result.as_ptr(),
10409 unit.as_ptr(),
10410 "a faulted Grid({width}, {height}, 0) returns the Unit sentinel"
10411 );
10412 assert_eq!(
10413 live_after, live_before,
10414 "a rejected Grid({width}, {height}, 0) allocates nothing"
10415 );
10416 }
10417 }
10418
10419 /// A member the set cannot hold is a fault, and a negative one does not
10420 /// vanish silently. Unchecked, `bs.insert(10^18)` would ask `Vec::resize`
10421 /// for 10^16 words — an OOM abort from inside `extern "C"`.
10422 #[test]
10423 fn a_bitset_member_outside_the_representable_range_faults() {
10424 for member in [-1_i64, i64::MIN, i64::MAX, BitIndex::MAX + 1] {
10425 let mut rt = Runtime::new();
10426 let ctx = wired_ctx(&mut rt);
10427 let words;
10428 unsafe {
10429 let bs = praxis_bitset_new(ctx);
10430 let value = praxis_alloc_int(ctx, member);
10431 let _ = praxis_bitset_insert(ctx, bs, value);
10432 words = bitset_payload(bs).words.len();
10433 }
10434 unsafe { drop_ctx(ctx) };
10435
10436 assert_eq!(
10437 rt.fault(),
10438 FaultKind::InvalidSize,
10439 "BitSet.insert({member}) must fault"
10440 );
10441 assert_eq!(words, 0, "BitSet.insert({member}) must allocate no words");
10442 }
10443 }
10444
10445 /// The words of a **live, heap-allocated** `BitSet`, reached the way
10446 /// generated code reaches them: through
10447 /// [`INLINE_BITSET_SITE`](crate::bitset::INLINE_BITSET_SITE), from the
10448 /// object base, with no knowledge of the payload beyond what the site
10449 /// carries (ADR-118 part 2).
10450 ///
10451 /// `a_backend_can_read_the_length_and_the_elements_out_of_a_live_payload`
10452 /// in `collections.rs` is this test for `Vec`; this is the second payload's
10453 /// copy of the same agreement between the emitted load and the layout.
10454 ///
10455 /// Compiled out under `std-vec-payload`: that arm has no site to name, so
10456 /// naming one fails the *build* rather than miscompiling a load.
10457 #[cfg(not(feature = "std-vec-payload"))]
10458 #[test]
10459 fn the_inline_bitset_site_addresses_a_live_bitsets_words() {
10460 use crate::bitset::INLINE_BITSET_SITE;
10461
10462 let mut rt = Runtime::new();
10463 let ctx = wired_ctx(&mut rt);
10464 let (words, len) = unsafe {
10465 let bs = praxis_bitset_new(ctx);
10466 assert!(
10467 std::ptr::eq(INLINE_BITSET_SITE.type_id().descriptor(), bs.descriptor()),
10468 "the site names the descriptor the inline proof compares against"
10469 );
10470 for member in [0_i64, 63, 64, 200] {
10471 let value = praxis_alloc_int(ctx, member);
10472 let _ = praxis_bitset_insert(ctx, bs, value);
10473 }
10474 let base = bs.as_ptr().cast::<u8>().cast_const();
10475 (
10476 base.add(INLINE_BITSET_SITE.elements_offset())
10477 .cast::<*const u64>()
10478 .read(),
10479 base.add(INLINE_BITSET_SITE.len_offset())
10480 .cast::<usize>()
10481 .read(),
10482 )
10483 };
10484
10485 assert_eq!(len, 4, "bit 200 lives in the fourth word");
10486 assert_eq!(
10487 INLINE_BITSET_SITE.element_shift(),
10488 3,
10489 "a word is eight bytes"
10490 );
10491 for member in [0_u64, 63, 64, 200] {
10492 // SAFETY: `member >> 6 < len`, and `words` is the live buffer the
10493 // site's displacement just answered.
10494 let w = unsafe { *words.add((member >> 6) as usize) };
10495 assert!(
10496 (w >> (member & 63)) & 1 == 1,
10497 "bit {member} read back through the site's displacements"
10498 );
10499 }
10500 unsafe { drop_ctx(ctx) };
10501 }
10502
10503 /// Queries stay total: a value the set cannot hold is a value it does not
10504 /// contain, and removing one is a no-op. Neither may fault, and neither may
10505 /// grow the word vector.
10506 #[test]
10507 fn bitset_queries_outside_the_range_are_absent_rather_than_faults() {
10508 let mut rt = Runtime::new();
10509 let ctx = wired_ctx(&mut rt);
10510 let (present, words) = unsafe {
10511 let bs = praxis_bitset_new(ctx);
10512 let huge = praxis_alloc_int(ctx, i64::MAX);
10513 let _ = praxis_bitset_remove(ctx, bs, huge);
10514 // The answer is the scalar channel's `0`/`1` (ADR-118 decision 6),
10515 // so there is no box to load it back out of.
10516 let answer = praxis_bitset_contains(ctx, bs, huge);
10517 (answer != 0, bitset_payload(bs).words.len())
10518 };
10519 unsafe { drop_ctx(ctx) };
10520
10521 assert!(!present, "an unrepresentable member is absent");
10522 assert_eq!(words, 0, "a query allocates no words");
10523 assert_eq!(rt.fault(), FaultKind::None, "a query does not fault");
10524 }
10525
10526 /// `(i64::MAX, i64::MAX).neighbors4()` must not overflow the offset addition
10527 /// and panic across `extern "C"`. Every such neighbour is outside every
10528 /// grid, so the answer is an empty Vec.
10529 #[test]
10530 fn neighbors_of_an_extreme_point_are_empty_rather_than_a_panic() {
10531 let mut rt = Runtime::new();
10532 let ctx = wired_ctx(&mut rt);
10533 let cell = rt.alloc_int(1);
10534 let grid = rt.alloc_grid(&crate::scalars::INT, vec![cell], 1);
10535 let counts = unsafe {
10536 let mut counts = Vec::new();
10537 for (x, y) in [
10538 (i64::MAX, i64::MAX),
10539 (i64::MIN, i64::MIN),
10540 (i64::MAX, 0),
10541 (0, i64::MIN),
10542 ] {
10543 let point = alloc_point(ctx, x, y);
10544 counts.push((
10545 vec_payload(praxis_grid_neighbors4(ctx, grid, point))
10546 .items
10547 .len(),
10548 vec_payload(praxis_grid_neighbors8(ctx, grid, point))
10549 .items
10550 .len(),
10551 ));
10552 }
10553 counts
10554 };
10555 unsafe { drop_ctx(ctx) };
10556
10557 assert!(
10558 counts.iter().all(|(n4, n8)| *n4 == 0 && *n8 == 0),
10559 "an out-of-range point has no in-grid neighbours: {counts:?}"
10560 );
10561 assert_eq!(rt.fault(), FaultKind::None);
10562 }
10563
10564 /// A 3×3 grid of `Int`s holding `1..=9` in reading order, so a neighbour's
10565 /// cell names its own position.
10566 fn nine_grid(rt: &mut Runtime) -> GcRef {
10567 let cells: Vec<GcRef> = (1..=9).map(|n| rt.alloc_int(n)).collect();
10568 rt.alloc_grid(&crate::scalars::INT, cells, 3)
10569 }
10570
10571 /// Every field of a neighbourhood record as `(name, Some((x, y)) | None)`,
10572 /// **in slot order** — which is what a field read indexes, so a test that
10573 /// reads through this is a test of the order and not only of the values.
10574 ///
10575 /// # Safety
10576 /// `record` must be an `Around4`/`Around8` `GcRef`.
10577 unsafe fn around_fields(record: GcRef) -> Vec<(&'static str, Option<(i64, i64)>)> {
10578 // SAFETY: the caller guarantees a record built under one of the two
10579 // neighbourhood schemas, so every field is an `Option[(Int, Int)]`.
10580 unsafe {
10581 let rp = &*(record.payload::<u8>() as *const crate::records::RecordPayload);
10582 let schema = &*rp.schema;
10583 schema
10584 .fields
10585 .iter()
10586 .zip(&rp.items)
10587 .map(|(field, value)| {
10588 let ep = &*(value.payload::<u8>() as *const crate::enums::EnumPayload);
10589 let point = (i64::from(ep.tag) == crate::enums::OPTION_SOME_TAG)
10590 .then(|| point_xy(ep.items[0]));
10591 (field.name, point)
10592 })
10593 .collect()
10594 }
10595 }
10596
10597 /// `around4` names all four directions, and a direction that leaves the
10598 /// grid is `None` rather than absent.
10599 ///
10600 /// That is the whole difference from `neighbors4`, which answers a clipped
10601 /// `Vec` in which the corner case and the interior case are two lists of
10602 /// different lengths with no way to tell which entry was which direction.
10603 #[test]
10604 fn around4_answers_every_direction_and_the_missing_ones_are_none() {
10605 let mut rt = Runtime::new();
10606 let ctx = wired_ctx(&mut rt);
10607 let grid = nine_grid(&mut rt);
10608 let (middle, corner, far_corner) = unsafe {
10609 let m = around_fields(praxis_grid_around4(ctx, grid, alloc_point(ctx, 1, 1)));
10610 let c = around_fields(praxis_grid_around4(ctx, grid, alloc_point(ctx, 0, 0)));
10611 let f = around_fields(praxis_grid_around4(ctx, grid, alloc_point(ctx, 2, 2)));
10612 (m, c, f)
10613 };
10614 unsafe { drop_ctx(ctx) };
10615
10616 assert_eq!(
10617 middle,
10618 vec![
10619 ("up", Some((1, 0))),
10620 ("left", Some((0, 1))),
10621 ("right", Some((2, 1))),
10622 ("down", Some((1, 2))),
10623 ],
10624 "the plus in reading order, centre skipped"
10625 );
10626 assert_eq!(
10627 corner,
10628 vec![
10629 ("up", None),
10630 ("left", None),
10631 ("right", Some((1, 0))),
10632 ("down", Some((0, 1))),
10633 ]
10634 );
10635 assert_eq!(
10636 far_corner,
10637 vec![
10638 ("up", Some((2, 1))),
10639 ("left", Some((1, 2))),
10640 ("right", None),
10641 ("down", None),
10642 ]
10643 );
10644 assert_eq!(rt.fault(), FaultKind::None);
10645 }
10646
10647 /// `around8`'s slots are the 3×3 block in reading order, centre skipped —
10648 /// which is what makes a printed `Around8` look like the block it
10649 /// describes, and what a diagonal read depends on.
10650 #[test]
10651 fn around8_is_a_3x3_block_in_reading_order() {
10652 let mut rt = Runtime::new();
10653 let ctx = wired_ctx(&mut rt);
10654 let grid = nine_grid(&mut rt);
10655 let (middle, edge) = unsafe {
10656 let m = around_fields(praxis_grid_around8(ctx, grid, alloc_point(ctx, 1, 1)));
10657 let e = around_fields(praxis_grid_around8(ctx, grid, alloc_point(ctx, 1, 0)));
10658 (m, e)
10659 };
10660 unsafe { drop_ctx(ctx) };
10661
10662 assert_eq!(
10663 middle,
10664 vec![
10665 ("up_left", Some((0, 0))),
10666 ("up", Some((1, 0))),
10667 ("up_right", Some((2, 0))),
10668 ("left", Some((0, 1))),
10669 ("right", Some((2, 1))),
10670 ("down_left", Some((0, 2))),
10671 ("down", Some((1, 2))),
10672 ("down_right", Some((2, 2))),
10673 ]
10674 );
10675 // The top edge: the whole first row of the block is off the grid, and
10676 // says so three times rather than shortening the answer.
10677 assert_eq!(
10678 edge,
10679 vec![
10680 ("up_left", None),
10681 ("up", None),
10682 ("up_right", None),
10683 ("left", Some((0, 0))),
10684 ("right", Some((2, 0))),
10685 ("down_left", Some((0, 1))),
10686 ("down", Some((1, 1))),
10687 ("down_right", Some((2, 1))),
10688 ]
10689 );
10690 assert_eq!(rt.fault(), FaultKind::None);
10691 }
10692
10693 /// `neighbors_of_an_extreme_point_are_empty_rather_than_a_panic`'s case for
10694 /// the record and the counts: `grid_neighbor`'s `checked_add` is what keeps
10695 /// `(i64::MAX, i64::MAX)` from overflowing inside `extern "C"`, and every
10696 /// wrapper that steps a point has to be behind it.
10697 #[test]
10698 fn a_neighbourhood_of_an_extreme_point_is_all_absent_rather_than_a_panic() {
10699 let mut rt = Runtime::new();
10700 let ctx = wired_ctx(&mut rt);
10701 let grid = nine_grid(&mut rt);
10702 let one = rt.alloc_int(1);
10703 let observed = unsafe {
10704 let mut observed = Vec::new();
10705 for (x, y) in [
10706 (i64::MAX, i64::MAX),
10707 (i64::MIN, i64::MIN),
10708 (i64::MAX, 0),
10709 (0, i64::MIN),
10710 ] {
10711 let point = alloc_point(ctx, x, y);
10712 let four = around_fields(praxis_grid_around4(ctx, grid, point));
10713 let eight = around_fields(praxis_grid_around8(ctx, grid, point));
10714 observed.push((
10715 four.iter().filter(|(_, p)| p.is_some()).count(),
10716 eight.len(),
10717 eight.iter().filter(|(_, p)| p.is_some()).count(),
10718 int_payload(praxis_grid_count4(ctx, grid, point, one)),
10719 int_payload(praxis_grid_count8(ctx, grid, point, one)),
10720 ));
10721 }
10722 observed
10723 };
10724 unsafe { drop_ctx(ctx) };
10725
10726 for row in &observed {
10727 assert_eq!(
10728 *row,
10729 (0, 8, 0, 0, 0),
10730 "an out-of-range point has no in-grid neighbours, and its \
10731 record still has all eight fields: {observed:?}"
10732 );
10733 }
10734 assert_eq!(rt.fault(), FaultKind::None);
10735 }
10736
10737 /// `count4`/`count8` count cells, so a direction with no cell is not one of
10738 /// them — a corner counts over three neighbours, not eight.
10739 ///
10740 /// Equality is the descriptor's, which is `praxis_grid_find`'s path: the
10741 /// grid holds `1..=9`, so each count is the size of a known subset.
10742 #[test]
10743 fn a_neighbourhood_count_counts_only_the_cells_that_are_there() {
10744 let mut rt = Runtime::new();
10745 let ctx = wired_ctx(&mut rt);
10746 let grid = nine_grid(&mut rt);
10747 let counts = unsafe {
10748 let centre = alloc_point(ctx, 1, 1);
10749 let corner = alloc_point(ctx, 0, 0);
10750 let two = praxis_alloc_int(ctx, 2);
10751 let five = praxis_alloc_int(ctx, 5);
10752 let nine = praxis_alloc_int(ctx, 9);
10753 [
10754 // `2` is the cell above the centre: one orthogonal hit.
10755 int_payload(praxis_grid_count4(ctx, grid, centre, two)),
10756 // `9` is diagonal from the centre, so only the eight sees it.
10757 int_payload(praxis_grid_count4(ctx, grid, centre, nine)),
10758 int_payload(praxis_grid_count8(ctx, grid, centre, nine)),
10759 // The centre is never its own neighbour.
10760 int_payload(praxis_grid_count8(ctx, grid, centre, five)),
10761 // A corner's neighbourhood is two cells of four, three of eight.
10762 int_payload(praxis_grid_count4(ctx, grid, corner, five)),
10763 int_payload(praxis_grid_count8(ctx, grid, corner, five)),
10764 ]
10765 };
10766 unsafe { drop_ctx(ctx) };
10767
10768 assert_eq!(counts, [1, 0, 1, 0, 0, 1]);
10769 assert_eq!(rt.fault(), FaultKind::None);
10770 }
10771
10772 /// `map[key]` faults on an absent key where `.get` answers, and
10773 /// `praxis_counter_set` replaces a count where `praxis_counter_inc` only
10774 /// adds one.
10775 ///
10776 /// Here as well as in the JIT tests because §4.7's choice is the *runtime's*
10777 /// to make: the two map wrappers differ in one line, and a compiler that
10778 /// pointed both rows at `praxis_map_get` would still pass every type test.
10779 #[test]
10780 fn a_map_index_faults_where_get_answers_and_a_counter_set_replaces() {
10781 let mut rt = Runtime::new();
10782 let ctx = wired_ctx(&mut rt);
10783 let (present, absent_get) = unsafe {
10784 let map = praxis_map_new(ctx, &crate::scalars::INT as *const _);
10785 let key = praxis_alloc_int(ctx, 1);
10786 let val = praxis_alloc_int(ctx, 42);
10787 praxis_map_insert(ctx, map, key, val);
10788 let present = int_payload(praxis_map_index(ctx, map, key));
10789 assert_eq!(rt.fault(), FaultKind::None, "a present key does not fault");
10790 // `.get` on an absent key is `None` and no fault…
10791 let other = praxis_alloc_int(ctx, 2);
10792 let absent_get = praxis_map_get(ctx, map, other);
10793 assert_eq!(rt.fault(), FaultKind::None, "`.get` does not fault");
10794 // …and the subscript on the same key faults.
10795 praxis_map_index(ctx, map, other);
10796 (present, absent_get)
10797 };
10798 assert_eq!(present, 42);
10799 assert_eq!(
10800 absent_get.descriptor().id(),
10801 crate::enums::ENUM.id(),
10802 "`.get` answers with absence, and absence is an `Option` value"
10803 );
10804 assert_eq!(
10805 rt.fault(),
10806 FaultKind::IndexOutOfBounds,
10807 "§4.7: indexing a missing key faults"
10808 );
10809 unsafe { drop_ctx(ctx) };
10810
10811 // The counter half: `set` replaces, where `inc` adds one.
10812 let mut rt = Runtime::new();
10813 let ctx = wired_ctx(&mut rt);
10814 let (after_inc, after_set, len) = unsafe {
10815 let c = praxis_counter_new(ctx, &crate::scalars::INT as *const _);
10816 let key = praxis_alloc_int(ctx, 7);
10817 praxis_counter_inc(ctx, c, key);
10818 let after_inc = int_payload(praxis_counter_get(ctx, c, key));
10819 let five = praxis_alloc_int(ctx, 5);
10820 praxis_counter_set(ctx, c, key, five);
10821 let after_set = int_payload(praxis_counter_get(ctx, c, key));
10822 let len = int_payload(praxis_counter_len(ctx, c));
10823 (after_inc, after_set, len)
10824 };
10825 unsafe { drop_ctx(ctx) };
10826 assert_eq!(after_inc, 1);
10827 assert_eq!(after_set, 5, "a set replaces rather than adds");
10828 assert_eq!(len, 1, "and does not add a second entry for the same key");
10829 assert_eq!(rt.fault(), FaultKind::None);
10830 }
10831
10832 /// `min=` and `max=` order by the value's **own type** (ADR-045), which is
10833 /// the half no type test can see.
10834 ///
10835 /// Every case here is one the `int_payload` comparison this replaced answers
10836 /// *differently*, so this is a regression test rather than a demonstration —
10837 /// three of the four orderable types a program can write come out wrong when
10838 /// both payloads are read as `i64`s:
10839 ///
10840 /// * A `Text` payload is a pointer, so an `i64` compare orders two strings
10841 /// by the addresses the allocator happened to hand out.
10842 /// * A negative `Float`'s bits *ascend* as the number descends, so `-2.0`
10843 /// reads as the larger value.
10844 /// * A `Char` payload is four bytes wide, so an `i64` read takes four bytes
10845 /// that are not the character's.
10846 #[test]
10847 fn an_updating_store_orders_by_the_values_own_type() {
10848 let mut rt = Runtime::new();
10849 let ctx = wired_ctx(&mut rt);
10850 // SAFETY: `ctx` is live and wired, and every ref below is freshly
10851 // allocated through the wrappers.
10852 let (text, float, ch, first) = unsafe {
10853 let key = text_ref(ctx, "k");
10854
10855 // `Text`: lexicographic, whichever order the candidates arrive in.
10856 let texts = praxis_map_new(ctx, &crate::text::TEXT as *const _);
10857 praxis_map_update_min(ctx, texts, key, text_ref(ctx, "pear"));
10858 praxis_map_update_min(ctx, texts, key, text_ref(ctx, "apple"));
10859 praxis_map_update_min(ctx, texts, key, text_ref(ctx, "quince"));
10860 let text = praxis_map_index(ctx, texts, key).as_text().to_string();
10861
10862 // `Float`: numeric, and the pair is chosen on the negative side of
10863 // zero where the bit order runs backwards.
10864 let floats = praxis_map_new(ctx, &crate::scalars::FLOAT as *const _);
10865 let minus_one = praxis_alloc_float(ctx, (-1.0f64).to_bits() as i64);
10866 let minus_two = praxis_alloc_float(ctx, (-2.0f64).to_bits() as i64);
10867 praxis_map_update_min(ctx, floats, key, minus_one);
10868 praxis_map_update_min(ctx, floats, key, minus_two);
10869 let float = praxis_map_index(ctx, floats, key).as_float();
10870
10871 // `Char`: by scalar value, out of a payload narrower than a word.
10872 let chars = praxis_map_new(ctx, &crate::scalars::CHAR as *const _);
10873 praxis_map_update_max(ctx, chars, key, praxis_alloc_char(ctx, 'b' as i64));
10874 praxis_map_update_max(ctx, chars, key, praxis_alloc_char(ctx, 'z' as i64));
10875 let ch = praxis_map_index(ctx, chars, key).as_char();
10876
10877 // …and an absent entry still accepts the first value it is handed,
10878 // which is the rule no comparison is reached for.
10879 let fresh = praxis_map_new(ctx, &crate::text::TEXT as *const _);
10880 praxis_map_update_max(ctx, fresh, key, text_ref(ctx, "only"));
10881 let first = praxis_map_index(ctx, fresh, key).as_text().to_string();
10882
10883 (text, float, ch, first)
10884 };
10885 assert_eq!(rt.fault(), FaultKind::None);
10886 unsafe { drop_ctx(ctx) };
10887
10888 assert_eq!(
10889 text, "apple",
10890 "a Text orders lexicographically, not by address"
10891 );
10892 assert_eq!(
10893 float, -2.0,
10894 "-2.0 is the smaller number, and the larger of the two bit patterns"
10895 );
10896 assert_eq!(ch, 'z');
10897 assert_eq!(first, "only", "an absent entry accepts the first value");
10898 }
10899
10900 /// The indexed receivers' updating stores (ADR-161): they order by the
10901 /// element's own type as the `Map` one does, and they part company with it
10902 /// on the index — there is no absent entry to accept a first value, so an
10903 /// index that is not there is the plain store's `IndexOutOfBounds`.
10904 ///
10905 /// Three properties no type test can see, and one of them is what separates
10906 /// this row from the `Map` row: **nothing is inserted**. A `Vec` that grew
10907 /// by one here would be `push` wearing the operator's spelling.
10908 #[test]
10909 fn an_indexed_updating_store_replaces_in_range_and_faults_outside_it() {
10910 let mut rt = Runtime::new();
10911 let ctx = wired_ctx(&mut rt);
10912 // SAFETY: `ctx` is live and wired; every ref below is freshly allocated
10913 // through the wrappers.
10914 let (kept, tied, cell, front, len_after) = unsafe {
10915 // A `Vec[Text]`, so the ordering is the one an `i64` compare cannot
10916 // do, and the loser leaves the element alone.
10917 let v = praxis_vec_new(ctx, &crate::text::TEXT as *const _);
10918 praxis_vec_push(ctx, v, text_ref(ctx, "pear"));
10919 praxis_vec_push(ctx, v, text_ref(ctx, "fig"));
10920 let zero = praxis_alloc_int(ctx, 0);
10921 praxis_vec_update_min(ctx, v, zero, text_ref(ctx, "apple"));
10922 praxis_vec_update_min(ctx, v, zero, text_ref(ctx, "quince"));
10923 let kept = praxis_vec_get(ctx, v, zero).as_text().to_string();
10924
10925 // A tie keeps the incumbent, which is only visible through identity:
10926 // the two are equal `Text`s in different objects.
10927 let one = praxis_alloc_int(ctx, 1);
10928 let incumbent = praxis_vec_get(ctx, v, one);
10929 praxis_vec_update_min(ctx, v, one, text_ref(ctx, "fig"));
10930 let tied = praxis_vec_get(ctx, v, one) == incumbent;
10931
10932 // A `Grid[Int]`: the relaxation shape, bounds-checked at the cell.
10933 let g = praxis_grid_new(ctx, &crate::scalars::INT as *const _, 2, 2);
10934 praxis_grid_set(ctx, g, zero, zero, praxis_alloc_int(ctx, 99));
10935 praxis_grid_update_min(ctx, g, zero, zero, praxis_alloc_int(ctx, 7));
10936 praxis_grid_update_min(ctx, g, zero, zero, praxis_alloc_int(ctx, 40));
10937 let cell = praxis_grid_get(ctx, g, zero, zero).as_int();
10938
10939 // A `Deque`, indexed 0-based from the front as its plain store is.
10940 let d = praxis_deque_new(ctx, &crate::scalars::INT as *const _);
10941 praxis_deque_push_back(ctx, d, praxis_alloc_int(ctx, 3));
10942 praxis_deque_push_front(ctx, d, praxis_alloc_int(ctx, 8));
10943 praxis_deque_update_max(ctx, d, zero, praxis_alloc_int(ctx, 20));
10944 let front = praxis_deque_get(ctx, d, zero).as_int();
10945
10946 assert_eq!(rt.fault(), FaultKind::None, "nothing so far faults");
10947
10948 // …and an index the vector does not hold is the plain store's
10949 // refusal, not a `Map`-style insert.
10950 let past_end = praxis_alloc_int(ctx, 2);
10951 praxis_vec_update_min(ctx, v, past_end, text_ref(ctx, "aardvark"));
10952 let len_after = praxis_vec_len(ctx, v).as_int();
10953
10954 (kept, tied, cell, front, len_after)
10955 };
10956 assert_eq!(
10957 rt.fault(),
10958 FaultKind::IndexOutOfBounds,
10959 "an index that is not there faults; it does not grow the vector"
10960 );
10961 unsafe { drop_ctx(ctx) };
10962
10963 assert_eq!(kept, "apple", "a Text element orders lexicographically");
10964 assert!(tied, "a tie keeps the element that is there");
10965 assert_eq!(cell, 7, "the grid cell relaxed and then held");
10966 assert_eq!(front, 20);
10967 assert_eq!(
10968 len_after, 2,
10969 "`min=` never appends — `push` is that spelling"
10970 );
10971 }
10972
10973 /// `Map.get` is statically value-typed, so an absent key cannot answer the
10974 /// Unit sentinel: a value whose static type is `V` and whose runtime
10975 /// descriptor is `Unit` is a type confusion the program cannot detect.
10976 ///
10977 /// The assertion names the variant, not merely `!= UNIT`: the answer is the
10978 /// `None` of the runtime's own `option_schema`, which is what makes it match
10979 /// a program's `None` arm.
10980 #[test]
10981 fn absent_map_get_does_not_return_an_untyped_unit_sentinel() {
10982 let mut rt = Runtime::new();
10983 let ctx = wired_ctx(&mut rt);
10984 let (missing, found);
10985 unsafe {
10986 let map = praxis_map_new(ctx, &crate::scalars::INT as *const _);
10987 let key = praxis_alloc_int(ctx, 1);
10988 missing = praxis_map_get(ctx, map, key);
10989 let value = praxis_alloc_int(ctx, 42);
10990 praxis_map_insert(ctx, map, key, value);
10991 found = praxis_map_get(ctx, map, key);
10992 }
10993
10994 assert_ne!(
10995 missing.descriptor().id(),
10996 crate::scalars::UNIT.id(),
10997 "Map.get is statically value-typed; absence needs Option or a checked fault, not Unit"
10998 );
10999 assert_eq!(missing.descriptor().id(), crate::enums::ENUM.id());
11000 assert_eq!(
11001 enum_tag_of(missing),
11002 crate::enums::OPTION_NONE_TAG as u32,
11003 "absence is `None`"
11004 );
11005 assert_eq!(enum_tag_of(found), crate::enums::OPTION_SOME_TAG as u32);
11006 // …and the `Some` carries the value, rather than merely not being Unit.
11007 let payload = unsafe { praxis_enum_payload(ctx, found, 0) };
11008 assert_eq!(unsafe { praxis_int_load(ctx, payload) }, 42);
11009 unsafe { drop_ctx(ctx) };
11010 }
11011
11012 /// A goal predicate that is `false` at every state, as a closure entry
11013 /// point. Rust-side `extern "C"` stands in for a JIT'd closure body: the
11014 /// oracle only cares that the pointer has the closure calling convention.
11015 ///
11016 /// # Safety
11017 /// Called only through [`ClosureOracle::call`], which upholds the ABI.
11018 unsafe extern "C" fn always_false(
11019 ctx: *mut RuntimeContext,
11020 _closure: GcRef,
11021 _state: GcRef,
11022 ) -> GcRef {
11023 // SAFETY: the oracle passes its own wired ctx.
11024 unsafe { bool_ref(ctx, false) }
11025 }
11026
11027 /// A neighbour function with no neighbours: the walk visits the start and
11028 /// stops, so the only thing that can decide the answer is the goal test.
11029 ///
11030 /// # Safety
11031 /// As [`always_false`].
11032 unsafe extern "C" fn no_neighbours(
11033 ctx: *mut RuntimeContext,
11034 _closure: GcRef,
11035 _state: GcRef,
11036 ) -> GcRef {
11037 // SAFETY: the oracle passes its own wired ctx.
11038 unsafe { praxis_vec_new(ctx, &crate::scalars::INT as *const _) }
11039 }
11040
11041 /// A `Bool` object laid out exactly the way [`Heap::alloc_raw`] lays one
11042 /// out — a `GcHeader` followed by its payload at
11043 /// `GcHeader::payload_offset_for(1)` — but in memory *this module* owns,
11044 /// and with the seven bytes after the one-byte payload set to `0xFF`.
11045 ///
11046 /// The heap cannot be asked for this shape. Under the page allocator
11047 /// (ADR-103) a `Bool` lands on the ladder's bottom rung, so its block is
11048 /// rounded up to an 8-byte boundary and the seven bytes after the one-byte
11049 /// payload are slack no object ever writes — and a fresh page is `mmap`ped
11050 /// zero, so an eight-byte read of a heap `Bool` answers *correctly* by
11051 /// accident. Owning the storage turns the padding from an accident into a
11052 /// fixture, which is the only way the read can be measured rather than
11053 /// sampled.
11054 ///
11055 /// The header carries a **freshly minted** `HeapId`, so the collector's
11056 /// provenance check (`Heap::mark`) skips this object instead of colouring
11057 /// it: the oracle roots every closure result, and a root the heap did not
11058 /// allocate is not the heap's to touch.
11059 #[repr(C)]
11060 struct DirtyPaddedBool {
11061 header: crate::gc::GcHeader,
11062 /// Byte 0 is the `BoolPayload`; bytes 1..8 are the neighbours a
11063 /// wrong-width or wrong-offset read would consume.
11064 payload: [u8; 8],
11065 }
11066
11067 /// A `false` whose seven following bytes are `0xFF`, leaked once per thread
11068 /// so a closure entry point can answer it. See [`DirtyPaddedBool`].
11069 fn dirty_padded_false() -> GcRef {
11070 thread_local! {
11071 static CELL: std::cell::Cell<*mut crate::gc::GcHeader> =
11072 const { std::cell::Cell::new(std::ptr::null_mut()) };
11073 }
11074 CELL.with(|cell| {
11075 if cell.get().is_null() {
11076 let object = Box::leak(Box::new(DirtyPaddedBool {
11077 header: crate::gc::GcHeader::new(
11078 &scalars::BOOL,
11079 crate::gc::GcHeader::payload_offset_for(scalars::BOOL.align()) as u16,
11080 crate::gc::HeapId::mint(),
11081 ),
11082 payload: [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
11083 }));
11084 cell.set(&mut object.header as *mut crate::gc::GcHeader);
11085 }
11086 // SAFETY: the pointer heads a leaked, correctly-laid-out `Bool`
11087 // object that lives for the rest of the process.
11088 unsafe { GcRef::from_raw(cell.get()) }
11089 })
11090 }
11091
11092 /// As [`always_false`], but answering the [`dirty_padded_false`] fixture.
11093 ///
11094 /// # Safety
11095 /// As [`always_false`].
11096 unsafe extern "C" fn always_dirty_false(
11097 _ctx: *mut RuntimeContext,
11098 _closure: GcRef,
11099 _state: GcRef,
11100 ) -> GcRef {
11101 dirty_padded_false()
11102 }
11103
11104 /// `ClosureOracle::is_goal` must read the closure's `Bool` answer at a
11105 /// `Bool`'s width: the payload is **one** byte (`BoolPayload = u8`, and
11106 /// `BOOL` is built from it), so an eight-byte read would take seven further
11107 /// bytes from past the object.
11108 ///
11109 /// Every goal predicate of every goal-directed helper goes
11110 /// through that read, so this walk is the whole class: the goal answers
11111 /// `false` at the only reachable state, and the answer must be `None`.
11112 ///
11113 /// The `false` it answers is [`dirty_padded_false`], whose payload byte is
11114 /// `0x00` and whose next seven bytes are `0xFF`. That is what makes this a
11115 /// gate rather than a walk: an eight-byte read answers
11116 /// `0xFFFF_FFFF_FFFF_FF00`, which is "goal reached at the start state" and
11117 /// therefore `Some(0)`; a read at *any* offset past byte zero answers
11118 /// `0xFF`, likewise `Some(0)`. Only one byte at offset zero answers `None`,
11119 /// so the test fails if either the width or the offset is wrong. Asking the
11120 /// heap for this shape does not work — see [`DirtyPaddedBool`].
11121 ///
11122 /// [`read_scalar`] is what makes both mistakes unspellable at the call
11123 /// site, and `int_payload`'s width check — an ordinary branch, so it holds
11124 /// in every profile — is what stops the next site from making them;
11125 /// `read_scalar_answers_none_for_a_foreign_descriptor` pins the reader
11126 /// itself.
11127 #[test]
11128 fn a_graph_goal_predicate_reads_a_bool_at_a_bool_s_width() {
11129 // The fixture is only a gate while its padding is dirty: state that
11130 // here, so a later edit that zeroes it fails loudly rather than
11131 // silently turning this back into the walk it replaced.
11132 let fixture = dirty_padded_false();
11133 assert!(std::ptr::eq(fixture.descriptor(), &scalars::BOOL));
11134 assert_eq!(
11135 unsafe { read_scalar(fixture, scalars::BOOL_PAYLOAD) },
11136 Some(0u8),
11137 "the fixture is `false` at a Bool's width"
11138 );
11139 assert_ne!(
11140 unsafe { *fixture.payload::<i64>() },
11141 0,
11142 "…and non-zero at an Int's, which is what the wrong read consumed"
11143 );
11144
11145 let mut rt = Runtime::new();
11146 let ctx = wired_ctx(&mut rt);
11147 let answer = unsafe {
11148 let goal = praxis_alloc_closure(ctx, always_dirty_false as *const u8, 0);
11149 let neighbours = praxis_alloc_closure(ctx, no_neighbours as *const u8, 0);
11150 let start = praxis_alloc_int(ctx, 0);
11151 praxis_bfs_distance(ctx, start, neighbours, goal)
11152 };
11153 assert!(!rt.has_pending_fault(), "fault: {:?}", rt.fault());
11154 assert_eq!(answer.descriptor().id(), crate::enums::ENUM.id());
11155 assert_eq!(
11156 enum_tag_of(answer),
11157 crate::enums::OPTION_NONE_TAG as u32,
11158 "the goal answered `false` at every state, so no distance was found"
11159 );
11160 unsafe { drop_ctx(ctx) };
11161 }
11162
11163 /// The same walk against the immortal `false` every real program's closure
11164 /// answers — a companion, not a gate: a wrong-width read passes it, because
11165 /// the allocator leaves a `Bool`'s slack bytes zero. It rules out a reader
11166 /// that only handles the fixture correctly.
11167 #[test]
11168 fn a_graph_goal_predicate_that_is_false_everywhere_finds_nothing() {
11169 let mut rt = Runtime::new();
11170 let ctx = wired_ctx(&mut rt);
11171 let answer = unsafe {
11172 let goal = praxis_alloc_closure(ctx, always_false as *const u8, 0);
11173 let neighbours = praxis_alloc_closure(ctx, no_neighbours as *const u8, 0);
11174 let start = praxis_alloc_int(ctx, 0);
11175 praxis_bfs_distance(ctx, start, neighbours, goal)
11176 };
11177 assert!(!rt.has_pending_fault(), "fault: {:?}", rt.fault());
11178 assert_eq!(answer.descriptor().id(), crate::enums::ENUM.id());
11179 assert_eq!(enum_tag_of(answer), crate::enums::OPTION_NONE_TAG as u32);
11180 unsafe { drop_ctx(ctx) };
11181 }
11182
11183 /// The reader itself, both directions. `read_scalar` is what makes a
11184 /// wrong-type or wrong-width read unspellable at a call site, so its own
11185 /// contract is pinned here: the right type reads at the right width, and a
11186 /// foreign type answers `None` instead of reinterpreting the bytes.
11187 #[test]
11188 fn read_scalar_answers_none_for_a_foreign_descriptor() {
11189 let mut rt = Runtime::new();
11190 let ctx = wired_ctx(&mut rt);
11191 unsafe {
11192 let t = bool_ref(ctx, true);
11193 let f = bool_ref(ctx, false);
11194 assert_eq!(read_scalar(t, crate::scalars::BOOL_PAYLOAD), Some(1u8));
11195 assert_eq!(read_scalar(f, crate::scalars::BOOL_PAYLOAD), Some(0u8));
11196 // An `Int` is not a `Bool`, and the answer is absence rather than
11197 // the first byte of the `i64`.
11198 let n = praxis_alloc_int(ctx, 1);
11199 assert_eq!(read_scalar(n, crate::scalars::BOOL_PAYLOAD), None);
11200 assert_eq!(read_scalar(n, crate::scalars::INT_PAYLOAD), Some(1i64));
11201 drop_ctx(ctx);
11202 }
11203 }
11204
11205 /// The variant tag of an enum value, read the way the runtime's own
11206 /// `enum_format` reads it.
11207 fn enum_tag_of(value: GcRef) -> u32 {
11208 // SAFETY: the caller passes an ENUM-descriptor object.
11209 unsafe { (*(value.payload::<u8>() as *const crate::enums::EnumPayload)).tag }
11210 }
11211
11212 /// The same rule under a *tuple* static type:
11213 /// `Grid.find` answers `(Int, Int)`, so "nothing matched" cannot be the Unit
11214 /// sentinel wearing that type.
11215 #[test]
11216 fn absent_grid_find_does_not_return_an_untyped_unit_sentinel() {
11217 let mut rt = Runtime::new();
11218 let ctx = wired_ctx(&mut rt);
11219 let (missing, found);
11220 unsafe {
11221 let cell = praxis_alloc_int(ctx, 1);
11222 let sought = praxis_alloc_int(ctx, 2);
11223 let grid = rt.alloc_grid(&crate::scalars::INT, vec![cell], 1);
11224 missing = praxis_grid_find(ctx, grid, sought);
11225 let present = praxis_alloc_int(ctx, 1);
11226 found = praxis_grid_find(ctx, grid, present);
11227 }
11228
11229 assert_ne!(
11230 missing.descriptor().id(),
11231 crate::scalars::UNIT.id(),
11232 "Grid.find is statically point-typed; absence needs Option or a checked fault, not Unit"
11233 );
11234 assert_eq!(missing.descriptor().id(), crate::enums::ENUM.id());
11235 assert_eq!(enum_tag_of(missing), crate::enums::OPTION_NONE_TAG as u32);
11236 // …and a hit is `Some((x, y))`, still a real point inside the option.
11237 assert_eq!(enum_tag_of(found), crate::enums::OPTION_SOME_TAG as u32);
11238 let point = unsafe { praxis_enum_payload(ctx, found, 0) };
11239 assert_eq!(point.descriptor().id(), crate::tuples::TUPLE.id());
11240 unsafe { drop_ctx(ctx) };
11241 }
11242
11243 // --- GC pacing (§12.4, ADR-019) ----------------------------------------
11244 //
11245 // `maybe_collect` is the load-bearing mechanism for the shadow-stack
11246 // spill: the alloc wrappers call it so collection happens automatically
11247 // inside JIT'd code.
11248
11249 #[test]
11250 fn maybe_collect_skips_below_threshold() {
11251 // A fresh heap with a single small allocation is well under the 64 KiB
11252 // threshold, so `maybe_collect` must report no collection ran.
11253 let mut rt = Runtime::new();
11254 let ctx = wired_ctx(&mut rt);
11255 // SAFETY: ctx wired.
11256 unsafe {
11257 let _ = praxis_alloc_int(ctx, 1);
11258 // Nothing live matters here; we only ask whether collection *ran*.
11259 let roots = crate::roots::RuntimeRoots::from_context(ctx);
11260 let ran = rt.heap().maybe_collect(&roots);
11261 assert!(
11262 !ran,
11263 "a single small Int must not trip the 64 KiB threshold"
11264 );
11265 }
11266 unsafe { drop_ctx(ctx) };
11267 }
11268
11269 #[test]
11270 fn maybe_collect_runs_under_pressure() {
11271 // Allocating past the 64 KiB threshold collects on its own, with no
11272 // generated frame on the stack and no hand-written `maybe_collect`
11273 // call. The helper asserts it happens within 10,000 allocations.
11274 let mut rt = Runtime::new();
11275 let ctx = wired_ctx(&mut rt);
11276 // SAFETY: ctx wired.
11277 unsafe {
11278 let _ = allocate_until_automatic_collection(&rt, ctx);
11279 // After a collection the pacing counter resets, so an immediate
11280 // call (no new allocations) does not collect again.
11281 let roots = crate::roots::RuntimeRoots::from_context(ctx);
11282 assert!(
11283 !rt.heap().maybe_collect(&roots),
11284 "counter must reset after a collection"
11285 );
11286 }
11287 unsafe { drop_ctx(ctx) };
11288 }
11289
11290 #[test]
11291 fn checked_int_add_is_an_automatic_gc_safepoint() {
11292 let mut rt = Runtime::new();
11293 let ctx = wired_ctx(&mut rt);
11294 let collected;
11295 unsafe {
11296 // The *sum* has to be uninterned, not just the operands: what is
11297 // watched below is whether `praxis_int_add`'s result enters the live
11298 // registry, and an interned sum never does (see `UNINTERNED`).
11299 let lhs = praxis_alloc_int(ctx, UNINTERNED);
11300 let rhs = praxis_alloc_int(ctx, 22);
11301 let mut frame = push_frame(ctx, SlotCount::new(2).unwrap());
11302 frame.set(0, lhs);
11303 frame.set(1, rhs);
11304
11305 let mut before = rt.heap().stats().live_count;
11306 let mut observed = false;
11307 for _ in 0..10_000 {
11308 let _ = praxis_int_add(ctx, lhs, rhs);
11309 let after = rt.heap().stats().live_count;
11310 if after < before.saturating_add(1) {
11311 observed = true;
11312 break;
11313 }
11314 before = after;
11315 }
11316 collected = observed;
11317 drop(frame);
11318 }
11319 unsafe { drop_ctx(ctx) };
11320
11321 assert!(
11322 collected,
11323 "every allocating ABI wrapper must participate in automatic GC pacing"
11324 );
11325 }
11326
11327 #[test]
11328 fn automatic_gc_roots_the_ambient_input_buffer() {
11329 let mut rt = Runtime::new();
11330 let ctx = wired_ctx(&mut rt);
11331 let live_after_collection;
11332 unsafe {
11333 (*ctx).input_source = rt.alloc_text("input that main has not read yet");
11334 let frame = push_frame(ctx, SlotCount::new(0).unwrap());
11335 live_after_collection = allocate_until_automatic_collection(&rt, ctx);
11336 drop(frame);
11337 }
11338 unsafe { drop_ctx(ctx) };
11339
11340 assert!(
11341 live_after_collection >= 2,
11342 "the ambient input Text and the allocation returned after collection must both remain live"
11343 );
11344 }
11345
11346 #[test]
11347 fn automatic_gc_roots_parse_failure_partial_values() {
11348 let mut rt = Runtime::new();
11349 let ctx = wired_ctx(&mut rt);
11350 // Uninterned: what this test watches is a *registered* object surviving
11351 // a collection that roots only through `ParseDetail.partial`, and an
11352 // interned `Int` is never registered, so it would survive whether the
11353 // root set included the slot or not (see `UNINTERNED`).
11354 let partial = rt.alloc_int(UNINTERNED);
11355 rt.parse_detail_mut()
11356 .consider(ParseFail::here(0, "test").with_partial(Some(partial)), b"");
11357 let live_after_collection;
11358 unsafe {
11359 let frame = push_frame(ctx, SlotCount::new(0).unwrap());
11360 live_after_collection = allocate_until_automatic_collection(&rt, ctx);
11361 drop(frame);
11362 }
11363 unsafe { drop_ctx(ctx) };
11364
11365 assert!(
11366 live_after_collection >= 2,
11367 "ParseDetail.partial is runtime-owned and must be included in every automatic root set"
11368 );
11369 }
11370
11371 #[test]
11372 fn automatic_gc_roots_runtime_owned_crash_snapshots() {
11373 let mut rt = Runtime::new();
11374 let ctx = wired_ctx(&mut rt);
11375 // Uninterned, for `automatic_gc_roots_parse_failure_partial_values`'s
11376 // reason: the observable is a registered object surviving because the
11377 // snapshot rooted it.
11378 let captured = rt.alloc_int(UNINTERNED);
11379 let local_name = b"value";
11380 let meta = crate::debug::DebugLocalMeta {
11381 callee_name: std::ptr::null(),
11382 callee_name_len: 0,
11383 source_name: local_name.as_ptr(),
11384 name_len: local_name.len() as u32,
11385 symbol_id: 1,
11386 descriptor: &crate::scalars::INT as *const _,
11387 type_id: 0,
11388 kind: crate::debug::LOCAL_KIND_USER,
11389 span_start: 0,
11390 span_end: 0,
11391 slot_kind: crate::debug::DebugSlotKind::Reference,
11392 };
11393 let metas = [meta];
11394 let func_name = b"main";
11395 let func_meta = crate::debug::FunctionDebugMeta {
11396 func_name: func_name.as_ptr(),
11397 func_name_len: func_name.len() as u32,
11398 local_count: 1,
11399 locals: metas.as_ptr(),
11400 span_start: 0,
11401 span_end: 0,
11402 };
11403 let live_after_collection;
11404 // SAFETY: `ctx` is wired to `rt`; `func_meta`/`metas` outlive the guard,
11405 // and the snapshot is taken while the frame is still claimed — the
11406 // ordering a generated fault epilogue has (ADR-033 decision 1).
11407 unsafe {
11408 let mut debug_frame = crate::debug::push_frame(ctx, &func_meta);
11409 debug_frame.set(0, captured);
11410 crate::crash_snapshot::praxis_snapshot_debug_chain(ctx);
11411 drop(debug_frame);
11412 assert!(rt.crash_snapshot().is_some());
11413
11414 let shadow_frame = push_frame(ctx, SlotCount::new(0).unwrap());
11415 live_after_collection = allocate_until_automatic_collection(&rt, ctx);
11416 drop(shadow_frame);
11417 }
11418 unsafe { drop_ctx(ctx) };
11419
11420 assert!(
11421 live_after_collection >= 2,
11422 "a runtime-owned CrashSnapshot must root its copied local values during automatic GC"
11423 );
11424 }
11425
11426 #[test]
11427 fn nested_allocating_helpers_root_intermediate_results() {
11428 // `Grid.positions` builds its result Vec in a Rust local and fills it by
11429 // calling `alloc_point`, which allocates three times per point. Every
11430 // one of those is a safepoint, and the shadow stack only sees what
11431 // generated code spilled — this is all native code, so the result Vec,
11432 // the points already in it and the tuple `alloc_point` is midway
11433 // through filling are rooted by the helper's own `NativeScope`.
11434 //
11435 // This calls the real helper rather than inlining a sketch of it, and
11436 // reading the points back afterwards is what proves nothing was
11437 // reclaimed.
11438 // The grid is wide enough that the helper's own point allocations cross
11439 // the pacing threshold partway through the loop — the collection has to
11440 // happen *inside* the helper for this to test anything.
11441 const W: usize = 40;
11442 let mut rt = Runtime::new();
11443 let ctx = wired_ctx(&mut rt);
11444 let mut coords: Vec<(i64, i64)> = Vec::new();
11445 let collections_inside_the_helper;
11446 unsafe {
11447 let cells: Vec<GcRef> = (0..(W * W) as i64).map(|i| rt.alloc_int(i)).collect();
11448 let grid = rt.alloc_grid(&scalars::INT, cells, W);
11449 let mut frame = push_frame(ctx, SlotCount::new(1).unwrap());
11450 frame.set(0, grid);
11451
11452 let before = rt.heap().stats().live_count;
11453 let positions = praxis_grid_positions(ctx, grid);
11454 // Every point survived, so the live count only grew; a collection
11455 // that reclaimed the half-built result would show up as a drop.
11456 collections_inside_the_helper = rt.heap().stats().live_count > before;
11457
11458 let items = &(*positions.payload::<VecPayload>()).items;
11459 assert_eq!(items.len(), W * W, "one position per cell");
11460 for point in items {
11461 let tuple = &*point.payload::<crate::tuples::TuplePayload>();
11462 coords.push((int_payload(tuple.items[0]), int_payload(tuple.items[1])));
11463 }
11464
11465 drop(frame);
11466 }
11467 unsafe { drop_ctx(ctx) };
11468
11469 assert!(collections_inside_the_helper);
11470 let expected: Vec<(i64, i64)> = (0..W * W)
11471 .map(|i| ((i % W) as i64, (i / W) as i64))
11472 .collect();
11473 assert_eq!(
11474 coords, expected,
11475 "every point and coordinate the helper allocated must survive the \
11476 collections the helper itself triggers"
11477 );
11478 }
11479
11480 // --- null-context safety (defensive guards, §10.4 spirit) --------------
11481
11482 #[test]
11483 fn check_fault_on_null_context_is_zero() {
11484 // A null/unwired context must report no fault rather than dereferencing
11485 // the null pointer (the guard at `praxis_check_fault`).
11486 // SAFETY: passing a null context is the exact case the guard handles.
11487 assert_eq!(unsafe { praxis_check_fault(std::ptr::null_mut()) }, 0);
11488 }
11489
11490 // The inline prologue deliberately does not null-check the context
11491 // (ADR-101): the check would cost every call in the language, and
11492 // `Runtime::context` is the only producer of a context generated code is
11493 // handed. `RuntimeContext::placeholder` carries the obligation in its doc.
11494
11495 // --- a null element type stays unknown ---------------------------------
11496
11497 /// A collection built with no static element type must not claim to hold
11498 /// `Int`s, and what it holds must render as what it is.
11499 ///
11500 /// The codegen passes a **null** descriptor for `var c = Counter()` — its
11501 /// contract above `collection_element_descriptor_for` says so, and says
11502 /// every `praxis_*_new` wrapper reads it that way. Replacing the null with
11503 /// `&INT` is not a default but a false claim, and the label is dispatched
11504 /// through: a `Text` key would hash and print as an `i64`, a `Float`
11505 /// element as the integer its bits spell.
11506 ///
11507 /// Both halves are asserted because either alone passes a wrong fix: the
11508 /// absent label is the representation, the rendering is the answer.
11509 #[test]
11510 fn a_collection_with_no_static_element_type_does_not_claim_int() {
11511 let mut rt = Runtime::new();
11512 let ctx = wired_ctx(&mut rt);
11513 // SAFETY: `ctx` is wired; every argument below is a wrapper's own.
11514 unsafe {
11515 // Exactly what the codegen passes when the element type is still an
11516 // inference variable.
11517 let null = std::ptr::null::<TypeDescriptor>();
11518 let counter = praxis_counter_new(ctx, null);
11519 let set = praxis_set_new(ctx, null);
11520 let map = praxis_map_new(ctx, null);
11521 let min_heap = praxis_min_heap_new(ctx, null);
11522 let max_heap = praxis_max_heap_new(ctx, null);
11523
11524 assert!(
11525 counter_payload(counter).key().is_none(),
11526 "a Counter with no static key type must not claim one"
11527 );
11528 assert!(set_payload(set).element().is_none());
11529 assert!(map_payload(map).key().is_none());
11530 assert!(min_heap_payload(min_heap).element().is_none());
11531 assert!(max_heap_payload(max_heap).element().is_none());
11532
11533 // …and the values come back out as themselves. A `Text` key through
11534 // a `Counter`, a `Float` through a `MinHeap`: neither is an `Int`,
11535 // and a guessed `Int` label would print both as one.
11536 let key = praxis_alloc_text(ctx, "ab".as_ptr(), 2);
11537 praxis_counter_inc(ctx, counter, key);
11538 let keys = praxis_counter_keys(ctx, counter);
11539 let mut rendered = String::new();
11540 keys.format(&mut rendered);
11541 assert_eq!(rendered, "[ab]", "a Counter's keys are its keys");
11542
11543 let half = praxis_alloc_float(ctx, 1.5f64.to_bits() as i64);
11544 praxis_min_heap_push(ctx, min_heap, half);
11545 let mut rendered = String::new();
11546 min_heap.format(&mut rendered);
11547 assert_eq!(rendered, "[1.5]", "a MinHeap prints the elements it holds");
11548
11549 let member = praxis_alloc_text(ctx, "zz".as_ptr(), 2);
11550 praxis_set_insert(ctx, set, member);
11551 let items = praxis_set_items(ctx, set);
11552 let mut rendered = String::new();
11553 items.format(&mut rendered);
11554 assert_eq!(rendered, "[zz]");
11555 }
11556 // SAFETY: the context was leaked by `wired_ctx` and is unused after this.
11557 unsafe { drop_ctx(ctx) };
11558 }
11559
11560 /// A `Map` does not claim its values are `Int`s.
11561 ///
11562 /// `praxis_map_new` takes one descriptor — the key's — because the `MapNew`
11563 /// row carries one type argument, so the value slot starts **null**.
11564 /// Writing `INT` there would be a claim rather than a default, and it would
11565 /// be the same word as "unknown", so the adoption that follows could not
11566 /// tell a `Map` that really holds `Int`s from one that had never been told
11567 /// anything — and a `Map[Text, Text]`'s value would be read as an `i64`.
11568 #[test]
11569 fn a_map_does_not_claim_its_values_are_ints() {
11570 let mut rt = Runtime::new();
11571 let ctx = wired_ctx(&mut rt);
11572 // SAFETY: `ctx` is wired; every argument below is a wrapper's own.
11573 unsafe {
11574 let empty = praxis_map_new(ctx, &crate::text::TEXT);
11575 assert!(
11576 map_payload(empty).value().is_none(),
11577 "an empty Map has been told nothing about its values"
11578 );
11579 // …so the `Vec` its `values()` answers is not labelled `Int`
11580 // either. A guessed `Int` there would make an empty
11581 // `Map[Text, Text]`'s values unequal to an empty `Vec[Text]`,
11582 // because `vec_equals` compares element labels.
11583 let none_yet = praxis_map_values(ctx, empty);
11584 assert!(vec_payload(none_yet).element().is_none());
11585
11586 // The first insert is what the map learns from.
11587 let k = praxis_alloc_text(ctx, "k".as_ptr(), 1);
11588 let v = praxis_alloc_text(ctx, "vv".as_ptr(), 2);
11589 praxis_map_insert(ctx, empty, k, v);
11590 assert!(
11591 std::ptr::eq(map_payload(empty).value().unwrap(), &crate::text::TEXT),
11592 "a Map learns its value type from the first value inserted"
11593 );
11594 let mut rendered = String::new();
11595 praxis_map_values(ctx, empty).format(&mut rendered);
11596 assert_eq!(rendered, "[vv]");
11597
11598 // A `Map` that really does hold `Int`s says so — an assertion only
11599 // a null "unknown" makes possible, since a hardcoded `INT` would be
11600 // the same word as "never been told".
11601 let ints = praxis_map_new(ctx, &crate::text::TEXT);
11602 let ik = praxis_alloc_text(ctx, "n".as_ptr(), 1);
11603 praxis_map_insert(ctx, ints, ik, praxis_alloc_int(ctx, 7));
11604 assert!(std::ptr::eq(
11605 map_payload(ints).value().unwrap(),
11606 &scalars::INT
11607 ));
11608 }
11609 // SAFETY: the context was leaked by `wired_ctx` and is unused after this.
11610 unsafe { drop_ctx(ctx) };
11611 }
11612
11613 /// An unlearned label is not a label, so it cannot make two empty
11614 /// collections unequal.
11615 ///
11616 /// `same_element` is not pointer identity, because a never-inserted `Map`'s
11617 /// `values()` carries no label and an equally-typed empty `Vec[Int]`
11618 /// carries `Int`. ADR-066 decision 5 is the rule: a null slot means the
11619 /// *value's own* descriptor answers, and a collection with no label has no
11620 /// values, so nothing is left to disagree. Reinstating a guessed descriptor
11621 /// is the fix this rejects.
11622 ///
11623 /// The last case is the limit: two collections that have each been told
11624 /// their element type must still disagree when the types differ.
11625 #[test]
11626 fn an_unlearned_element_label_does_not_make_two_empty_collections_unequal() {
11627 use crate::collections::same_element;
11628 let int: *const crate::descriptor::TypeDescriptor = &scalars::INT;
11629 let text: *const crate::descriptor::TypeDescriptor = &crate::text::TEXT;
11630 let unlearned: *const crate::descriptor::TypeDescriptor = std::ptr::null();
11631
11632 assert!(same_element(unlearned, int), "no label agrees with `Int`");
11633 assert!(same_element(int, unlearned), "and in the other order");
11634 assert!(same_element(unlearned, unlearned));
11635 assert!(same_element(int, int));
11636 // The rule this must not weaken: two *learned* labels that differ are
11637 // two different collections, empty or not.
11638 assert!(!same_element(int, text));
11639
11640 // End to end: a `Map` never inserted into has no value label, and its
11641 // `values()` is an empty `Vec` that is equal to an empty `Vec` however
11642 // that one was labelled.
11643 let mut rt = Runtime::new();
11644 let ctx = wired_ctx(&mut rt);
11645 // SAFETY: `ctx` is wired; every argument below is a wrapper's own.
11646 unsafe {
11647 let never_inserted = praxis_map_new(ctx, &crate::text::TEXT);
11648 let unlabelled = praxis_map_values(ctx, never_inserted);
11649 assert!(vec_payload(unlabelled).element().is_none());
11650
11651 let labelled_ints = praxis_vec_new(ctx, &scalars::INT as *const _);
11652 assert!(
11653 praxis_struct_eq(ctx, unlabelled, labelled_ints) != 0,
11654 "an empty Map's values are an empty Vec[Int]"
11655 );
11656 assert!(
11657 praxis_struct_eq(ctx, labelled_ints, unlabelled) != 0,
11658 "and equality is symmetric"
11659 );
11660
11661 // A non-empty collection is still not an empty one: the length
11662 // check behind `same_element` is what answers, and it must.
11663 praxis_vec_push(ctx, labelled_ints, praxis_alloc_int(ctx, 1));
11664 assert!(praxis_struct_eq(ctx, unlabelled, labelled_ints) == 0);
11665 }
11666 // SAFETY: the context was leaked by `wired_ctx` and is unused after this.
11667 unsafe { drop_ctx(ctx) };
11668 }
11669
11670 // --- the manifest's fault column is checked against the code -----------
11671
11672 /// Every function defined in this file, as `(name, body)`.
11673 ///
11674 /// Line-based on purpose: a definition is a line whose first tokens are one
11675 /// of Rust's `fn` spellings, and its body runs to the line where the brace
11676 /// depth opened by that definition returns to zero. Anything cleverer would
11677 /// be a Rust parser, and anything looser — matching `fn` anywhere — reads
11678 /// the word out of doc comments and glues unrelated bodies together.
11679 fn functions_in_this_file() -> Vec<(String, String)> {
11680 functions_in(include_str!("abi.rs"))
11681 }
11682
11683 /// The code of one line, with any `//` comment removed.
11684 ///
11685 /// The sweep reads what the **compiler** sees, not what a reader wrote
11686 /// beside it. The fixed point matches `set_fault(` as a plain substring, so
11687 /// a comment inside a wrapper naming the helper would otherwise classify
11688 /// that wrapper as faulting. A sweep a comment can fool is a sweep that gets
11689 /// edited around rather than satisfied, which is the failure mode the whole
11690 /// invariant exists to prevent.
11691 ///
11692 /// A `//` inside a string or `char` literal is **not** a comment — `"//"`
11693 /// and `'/'` both occur in this file — so the scan tracks which it is in.
11694 /// It is not a Rust lexer: a raw string's hashes and a block comment are
11695 /// not modelled, because neither appears in a function body here and a
11696 /// half-lexer that claimed to be one would be worse than a stated
11697 /// limitation. It errs toward keeping code, never toward dropping it.
11698 fn code_only(line: &str) -> &str {
11699 let bytes = line.as_bytes();
11700 let (mut in_str, mut in_char, mut escaped) = (false, false, false);
11701 let mut i = 0;
11702 while i < bytes.len() {
11703 let c = bytes[i];
11704 if escaped {
11705 escaped = false;
11706 } else if c == b'\\' && (in_str || in_char) {
11707 escaped = true;
11708 } else if in_str {
11709 in_str = c != b'"';
11710 } else if in_char {
11711 in_char = c != b'\'';
11712 } else if c == b'"' {
11713 in_str = true;
11714 } else if c == b'\'' {
11715 // A lifetime (`'a`) is not a `char` literal; a `char` literal's
11716 // closing quote is at most three bytes away (`'\\n'`, `'\\''`).
11717 in_char = bytes[i + 1..].iter().take(4).any(|b| *b == b'\'');
11718 } else if c == b'/' && bytes.get(i + 1) == Some(&b'/') {
11719 return &line[..i];
11720 }
11721 i += 1;
11722 }
11723 line
11724 }
11725
11726 /// Every function defined in `src`, as `(name, code)` — the code only, with
11727 /// comments stripped by [`code_only`]. Braces are counted on the code too,
11728 /// so a comment holding an unbalanced brace cannot end a body early or run
11729 /// two bodies together.
11730 fn functions_in(src: &str) -> Vec<(String, String)> {
11731 const PREFIXES: [&str; 6] = [
11732 "fn ",
11733 "pub fn ",
11734 "pub(crate) fn ",
11735 "unsafe fn ",
11736 "pub unsafe fn ",
11737 "pub unsafe extern \"C\" fn ",
11738 ];
11739 let mut out: Vec<(String, String)> = Vec::new();
11740 // (name, body so far, brace depth, whether the body has opened at all —
11741 // a multi-line signature spends several lines at depth zero before its
11742 // `{`, and closing there would give every such wrapper a one-line body).
11743 let mut open: Option<(String, String, i32, bool)> = None;
11744 for raw in src.lines() {
11745 let line = code_only(raw);
11746 let depth_change = |l: &str| -> i32 {
11747 l.chars().filter(|c| *c == '{').count() as i32
11748 - l.chars().filter(|c| *c == '}').count() as i32
11749 };
11750 if let Some((name, body, depth, opened)) = open.as_mut() {
11751 body.push_str(line);
11752 body.push('\n');
11753 *depth += depth_change(line);
11754 *opened |= line.contains('{');
11755 if *opened && *depth <= 0 {
11756 out.push((std::mem::take(name), std::mem::take(body)));
11757 open = None;
11758 }
11759 continue;
11760 }
11761 let trimmed = line.trim_start();
11762 let Some(rest) = PREFIXES
11763 .iter()
11764 .find_map(|p| trimmed.strip_prefix(p).filter(|_| trimmed.starts_with(p)))
11765 else {
11766 continue;
11767 };
11768 let name: String = rest
11769 .chars()
11770 .take_while(|c| c.is_alphanumeric() || *c == '_')
11771 .collect();
11772 if name.is_empty() {
11773 continue;
11774 }
11775 let depth = depth_change(line);
11776 let opened = line.contains('{');
11777 // A one-line definition has already opened and closed its body.
11778 if opened && depth <= 0 {
11779 out.push((name, line.to_string()));
11780 } else {
11781 open = Some((name, format!("{line}\n"), depth, opened));
11782 }
11783 }
11784 out
11785 }
11786
11787 /// A wrapper that can raise a fault says so in the manifest.
11788 ///
11789 /// A row declared `Allocates` makes `RuntimeSymbol::faults()` answer
11790 /// `false`, so no `CheckFault` follows the call. If such a wrapper can reach
11791 /// `set_fault` the consequence is not cosmetic: the operation is silently
11792 /// abandoned, the wrapper answers the Unit sentinel, and the fault is
11793 /// observed by some later unrelated check — at the wrong source location,
11794 /// after the program has computed and possibly printed an answer.
11795 ///
11796 /// A hand-corrected row drifts again, so this is the invariant instead: the
11797 /// file is read at compile time, each `praxis_*` wrapper's body is walked,
11798 /// and any body that can reach `set_fault` — directly or through a helper
11799 /// defined here, transitively — must belong to a symbol whose row says
11800 /// `faults()`.
11801 ///
11802 /// One direction only. A row may declare a fault the reader cannot see: the
11803 /// arithmetic wrappers are generated by `checked_int_binop!` and have no
11804 /// textual definition at all, and a future wrapper may fault through a
11805 /// helper in another module. Those are false negatives — this test is weaker
11806 /// than the truth, never stricter — and the direction it does check is the
11807 /// one that produces wrong answers.
11808 /// The fixed point of "can reach `set_fault`" over `defs`: a function
11809 /// faults if it calls `set_fault`, or calls something that does.
11810 fn faulting_functions(defs: &[(String, String)]) -> std::collections::HashSet<String> {
11811 let mut faulting: std::collections::HashSet<String> =
11812 ["set_fault".to_string()].into_iter().collect();
11813 loop {
11814 let mut grew = false;
11815 for (name, body) in defs {
11816 if faulting.contains(name) {
11817 continue;
11818 }
11819 if faulting.iter().any(|f| body.contains(&format!("{f}("))) {
11820 faulting.insert(name.clone());
11821 grew = true;
11822 }
11823 }
11824 if !grew {
11825 break;
11826 }
11827 }
11828 faulting
11829 }
11830
11831 #[test]
11832 fn a_wrapper_that_can_raise_a_fault_declares_that_it_faults() {
11833 let defs = functions_in_this_file();
11834 let faulting = faulting_functions(&defs);
11835
11836 let mut checked = 0usize;
11837 for (name, _) in &defs {
11838 let Some(sym) = praxis_stdlib::abi::RuntimeSymbol::from_name(name) else {
11839 continue;
11840 };
11841 if !faulting.contains(name) {
11842 continue;
11843 }
11844 assert!(
11845 sym.faults(),
11846 "{name} can reach `set_fault`, but its manifest row says it \
11847 cannot fault — so no `CheckFault` follows the call and the \
11848 fault is observed somewhere else entirely"
11849 );
11850 checked += 1;
11851 }
11852 // If the scan stopped finding wrappers, the assertion above would be
11853 // vacuous and this test would pass while saying nothing.
11854 assert!(
11855 checked >= 20,
11856 "expected the fault-raising wrappers to be found; saw {checked}"
11857 );
11858 // These three reach `set_fault` only through a helper, so they are the
11859 // shape the transitive scan has to be able to see.
11860 for name in [
11861 "praxis_vec_push",
11862 "praxis_deque_push_front",
11863 "praxis_deque_push_back",
11864 ] {
11865 assert!(
11866 faulting.contains(name),
11867 "{name} reaches `set_fault` through `adopt_or_reject`; a scan \
11868 that cannot see that cannot hold the invariant"
11869 );
11870 }
11871 // **`InvalidText` lives at exactly one site (ADR-111).**
11872 // `praxis_alloc_text` trusts its caller; the one caller holding raw host
11873 // bytes raises the fault instead. Asserting the destination is visible
11874 // to the scan is what distinguishes a relocated fault from a deleted
11875 // one — without it, deleting the validation outright would leave this
11876 // test just as green.
11877 assert!(
11878 faulting.contains("praxis_get_input"),
11879 "`praxis_get_input` validates the host's input and raises \
11880 `InvalidText` itself (ADR-111); a scan that cannot see that cannot \
11881 tell a relocated fault from a deleted one"
11882 );
11883 assert!(
11884 !faulting.contains("praxis_alloc_text"),
11885 "`praxis_alloc_text` reaches `set_fault` again. Its row is \
11886 `Effect::Allocates`, so nothing observes the fault — a violated \
11887 UTF-8 precondition aborts through `abi_guard!` instead (ADR-111)"
11888 );
11889 }
11890 /// **The sweep above reads code, not prose.** Its classification is a
11891 /// substring match, so without [`code_only`] a *comment* inside a wrapper
11892 /// naming the helper would classify that wrapper as faulting and fail the
11893 /// invariant for a wrapper that cannot fault. A sweep a comment can fool is
11894 /// a sweep that gets edited around rather than satisfied.
11895 ///
11896 /// Synthetic source, because the real file must not contain the shape: the
11897 /// point is that it may, safely.
11898 #[test]
11899 fn the_manifest_sweep_reads_code_and_not_comments() {
11900 let src = r#"
11901pub unsafe extern "C" fn praxis_pretend_pure(ctx: *mut RuntimeContext) -> GcRef {
11902 // It used to call set_fault(ctx, RaisedFault::TYPE_MISMATCH) here, and a
11903 // later edit removed the only path that could. Prose, not code. }
11904 let sep = "//";
11905 let slash = '/';
11906 let _ = (sep, slash);
11907 unit_sentinel(ctx)
11908}
11909
11910pub unsafe extern "C" fn praxis_pretend_faulting(ctx: *mut RuntimeContext) -> GcRef {
11911 set_fault(ctx, RaisedFault::TYPE_MISMATCH);
11912 unit_sentinel(ctx)
11913}
11914"#;
11915 let defs = functions_in(src);
11916 let names: Vec<&str> = defs.iter().map(|(n, _)| n.as_str()).collect();
11917 assert_eq!(names, ["praxis_pretend_pure", "praxis_pretend_faulting"]);
11918 // The unbalanced `}` in that comment must not close the body early:
11919 // braces are counted on the code too, so the whole function is read.
11920 assert!(
11921 defs[0].1.contains("unit_sentinel(ctx)"),
11922 "a brace inside a comment ended the body early: {:?}",
11923 defs[0].1
11924 );
11925
11926 let faulting = faulting_functions(&defs);
11927 assert!(
11928 !faulting.contains("praxis_pretend_pure"),
11929 "a comment naming `set_fault` is not a call to it"
11930 );
11931 assert!(
11932 faulting.contains("praxis_pretend_faulting"),
11933 "and a real call still is — stripping comments must not blind the sweep"
11934 );
11935 }
11936
11937 /// [`code_only`]'s own contract, both directions.
11938 #[test]
11939 fn code_only_keeps_a_slash_inside_a_literal() {
11940 assert_eq!(code_only("let x = 1; // two"), "let x = 1; ");
11941 assert_eq!(code_only(r#"let s = "a//b";"#), r#"let s = "a//b";"#);
11942 assert_eq!(code_only(r"let c = '/'; // gone"), r"let c = '/'; ");
11943 assert_eq!(
11944 code_only(r#"let e = "\"//"; // gone"#),
11945 r#"let e = "\"//"; "#
11946 );
11947 assert_eq!(code_only(" /// a doc comment"), " ");
11948 assert_eq!(code_only("no comment here"), "no comment here");
11949 // A lifetime is not a `char` literal, so the comment after it is still
11950 // a comment.
11951 assert_eq!(
11952 code_only("fn f<'a>(x: &'a str) {} // gone"),
11953 "fn f<'a>(x: &'a str) {} "
11954 );
11955 }
11956
11957 // --- the panic backstop ------------------------------------------------
11958
11959 /// Every `#[unsafe(no_mangle)] extern "C"` function in this crate has its body
11960 /// inside `abi_guard!`.
11961 ///
11962 /// Per-wrapper totality is the contract: a wrapper validates its arguments
11963 /// and reports a bad one as a fault, so the guard never fires. This is the
11964 /// proof that the contract cannot be violated *silently* — a panic
11965 /// unwinding out of `extern "C"` into Cranelift frames is undefined
11966 /// behaviour, and the failure mode of forgetting is a corrupted process at
11967 /// some unrelated later point rather than a message.
11968 ///
11969 /// Read as source text on purpose. The property is "every entry point is
11970 /// wrapped", which is a property of the *set* of entry points; a test that
11971 /// called them one by one would be a test of the ones somebody remembered.
11972 ///
11973 /// **The file set is discovered, not declared.** A hand-written list of
11974 /// files would make the guarantee "every entry point in a file somebody
11975 /// remembered to list", and the `wrappers > 100` floor would still pass on
11976 /// the files that were listed. So the walk covers **every crate's `src/`**,
11977 /// not only this one: nothing says a future `#[unsafe(no_mangle)]` has to live
11978 /// here.
11979 #[test]
11980 fn every_no_mangle_wrapper_is_behind_the_panic_guard() {
11981 /// Every `.rs` file under `dir`, recursively, in a stable order.
11982 fn rust_sources(dir: &std::path::Path, out: &mut Vec<(String, String)>) {
11983 let entries =
11984 std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {}: {e}", dir.display()));
11985 let mut entries: Vec<_> = entries.map(|e| e.expect("dir entry").path()).collect();
11986 entries.sort();
11987 for path in entries {
11988 let name = path.file_name().unwrap_or_default().to_string_lossy();
11989 if name == "target" || name.starts_with('.') {
11990 continue;
11991 }
11992 if path.is_dir() {
11993 rust_sources(&path, out);
11994 } else if path.extension().is_some_and(|e| e == "rs") {
11995 let text = std::fs::read_to_string(&path)
11996 .unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
11997 out.push((path.display().to_string(), text));
11998 }
11999 }
12000 }
12001
12002 // `crates/`, from this crate's manifest directory.
12003 let mut crates_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
12004 crates_dir.pop();
12005 let mut sources: Vec<(String, String)> = Vec::new();
12006 rust_sources(&crates_dir, &mut sources);
12007 // A guard against the walk silently covering nothing — a wrong root
12008 // would otherwise pass by finding no wrapper attribute at all.
12009 assert!(
12010 sources.len() > 50,
12011 "the walk of {} found only {} Rust files, so it is not reading the workspace",
12012 crates_dir.display(),
12013 sources.len()
12014 );
12015
12016 let mut wrappers = 0usize;
12017 let mut unguarded: Vec<String> = Vec::new();
12018 for (file, source) in &sources {
12019 let lines: Vec<&str> = source.lines().collect();
12020 for (n, line) in lines.iter().enumerate() {
12021 // Both spellings: `#[unsafe(no_mangle)]` is the edition-2024
12022 // form and the only one this crate uses, but matching the bare
12023 // attribute too keeps the scan from going blind if a wrapper is
12024 // ever pasted in from older code.
12025 if !matches!(line.trim(), "#[unsafe(no_mangle)]" | "#[no_mangle]") {
12026 continue;
12027 }
12028 wrappers += 1;
12029 // Walk to the line that opens the body, then require the very
12030 // next non-blank line to be the guard.
12031 let mut k = n + 1;
12032 while k < lines.len() && !lines[k].trim_end().ends_with('{') {
12033 k += 1;
12034 }
12035 // Inclusive of `k`: a one-line signature puts `fn name(` on the
12036 // very line that opens the body, so an exclusive range would
12037 // report `<unnamed>` in the message that tells someone which
12038 // wrapper they forgot.
12039 let name = lines[n..=k.min(lines.len() - 1)]
12040 .iter()
12041 .find_map(|l| l.split("fn ").nth(1))
12042 .and_then(|l| l.split('(').next())
12043 .unwrap_or("<unnamed>")
12044 .trim()
12045 .to_string();
12046 let opens_guard = lines
12047 .get(k + 1)
12048 .map(|l| l.trim_start().starts_with("abi_guard!("))
12049 .unwrap_or(false);
12050 if !opens_guard {
12051 unguarded.push(format!("{file}:{} {name}", n + 1));
12052 }
12053 }
12054 }
12055
12056 assert!(
12057 wrappers > 100,
12058 "the scan found only {wrappers} wrappers, so it is not reading the ABI surface"
12059 );
12060 assert!(
12061 unguarded.is_empty(),
12062 "these `extern \"C\"` entry points can let a panic unwind into generated frames: {unguarded:#?}"
12063 );
12064 }
12065
12066 /// The guard's own behaviour: a panic inside a wrapper becomes a fault with
12067 /// a message naming the wrapper, and the wrapper returns its defined dummy.
12068 ///
12069 /// `praxis_dbg` is the one wrapper that can be made to panic on demand
12070 /// without an invalid argument — it formats its value, and a `Text` whose
12071 /// payload is a live `Unit` is a descriptor/payload pairing no validation
12072 /// catches. Every *reachable* panic is a bug to fix in the wrapper; this
12073 /// test is about what happens when one is missed.
12074 #[test]
12075 fn a_panic_inside_a_wrapper_becomes_a_fault_and_a_defined_dummy() {
12076 let value = {
12077 abi_guard!(
12078 "praxis_test_panics",
12079 std::ptr::null_mut::<RuntimeContext>(),
12080 {
12081 #[allow(unreachable_code)]
12082 {
12083 if std::hint::black_box(false) {
12084 panic!("this is the guard under test");
12085 }
12086 7i64
12087 }
12088 }
12089 )
12090 };
12091 assert_eq!(value, 7, "the guard is transparent when nothing panics");
12092
12093 // A **faulting** wrapper: its call sites can carry a `CheckFault`, so
12094 // generated code observes the fault before it looks at the value, and
12095 // the defined dummy is the right answer. The name has to be a real
12096 // manifest symbol — see `panic_fault_is_observable`, which is what
12097 // decides whether the dummy is returned at all.
12098 let mut runtime = crate::Runtime::new();
12099 let mut ctx = runtime.context();
12100 let previous = std::panic::take_hook();
12101 std::panic::set_hook(Box::new(|_| {}));
12102 let dummy: GcRef = abi_guard!("praxis_run_parser", &mut ctx as *mut RuntimeContext, {
12103 panic!("a wrapper that forgot to be total");
12104 });
12105 std::panic::set_hook(previous);
12106
12107 assert_eq!(
12108 runtime.fault(),
12109 crate::FaultKind::Panic,
12110 "an escaped panic is a fault, not an unwind into generated code"
12111 );
12112 assert!(
12113 runtime
12114 .fault_message()
12115 .is_some_and(|m| m.contains("praxis_run_parser")),
12116 "the fault names the wrapper it escaped, which a bare kind could not"
12117 );
12118 assert_eq!(
12119 dummy.descriptor().id(),
12120 crate::scalars::UNIT.id(),
12121 "the dummy is the Unit sentinel §10.4 already specifies"
12122 );
12123 }
12124
12125 /// **The dummy is only returned where the fault will be seen.**
12126 ///
12127 /// Generated code tests the fault slot only where MIR emitted a
12128 /// `CheckFault`, and **`praxis_mir::verify` is what makes that true of a
12129 /// non-faulting wrapper** — its `RedundantFaultCheck` rule rejects a check
12130 /// after an instruction that cannot fault (ADR-088). So for the wrappers the
12131 /// manifest declares non-faulting there is no check, and returning
12132 /// `unit_sentinel` there would hand a `Unit` into a slot generated code
12133 /// believes holds a Record, a Tuple or a closure — a descriptor/payload
12134 /// confusion introduced by the backstop meant to prevent worse. Those abort
12135 /// instead.
12136 ///
12137 /// This test states the classification. The abort itself cannot be asserted
12138 /// in-process, which is exactly why the rule has to be a total function of
12139 /// the manifest rather than a case-by-case judgement.
12140 #[test]
12141 fn a_panic_dummy_is_only_returned_where_a_fault_check_can_follow() {
12142 use praxis_stdlib::abi::RuntimeSymbol;
12143
12144 let mut pure = 0usize;
12145 let mut faulting = 0usize;
12146 for symbol in RuntimeSymbol::ALL.iter().copied() {
12147 let observable = panic_fault_is_observable(symbol.name());
12148 assert_eq!(
12149 observable,
12150 symbol.faults(),
12151 "`{}` is declared {:?}; the panic dummy must be returned iff a \
12152 fault check can follow it",
12153 symbol.name(),
12154 symbol.sig().effect
12155 );
12156 if symbol.faults() {
12157 faulting += 1;
12158 } else {
12159 pure += 1;
12160 }
12161 }
12162 assert!(
12163 pure > 0 && faulting > 0,
12164 "the manifest must contain both classes for this rule to mean anything \
12165 ({pure} non-faulting, {faulting} faulting)"
12166 );
12167
12168 // Every `#[unsafe(no_mangle)]` wrapper in this crate is manifested, so the
12169 // unobservable case left is a name that is not a wrapper at all.
12170 assert!(
12171 !panic_fault_is_observable("praxis_not_a_wrapper_at_all"),
12172 "an unknown name is never treated as observable"
12173 );
12174 }
12175
12176 // ---- Process input (§7.10) ----
12177
12178 /// A reader that answers nothing. A `fn` and not a closure because
12179 /// [`crate::input::InputReader`] is a plain `fn` pointer.
12180 fn no_bytes() -> Vec<u8> {
12181 Vec::new()
12182 }
12183
12184 /// Read the bytes behind a `Text` `GcRef`.
12185 ///
12186 /// # Safety
12187 /// `r` must be a live `Text`.
12188 unsafe fn text_bytes_of(r: GcRef) -> &'static [u8] {
12189 // SAFETY: the caller guarantees `r` is a live Text, so its payload is a
12190 // validly-linked `TextPayload`.
12191 unsafe { crate::text::text_bytes(r.payload::<crate::text::TextPayload>() as *const _) }
12192 }
12193
12194 /// A reader that answers zero bytes has given *empty input*, not no input,
12195 /// so its answer is installed as `input_source` whatever its length.
12196 ///
12197 /// Allocating the buffer only `if !bytes.is_empty()` would leave empty
12198 /// standard input at the immortal Unit, so `praxis_run_parser`'s §6.3
12199 /// descriptor guard would fault *before* the parser ran — a `ParseFailed`
12200 /// with no input span, no `expected` and no `actual`, which is none of the
12201 /// six fields §7.11 says a mismatch carries. A fault raised before any
12202 /// buffer exists cannot carry them; the buffer is what makes the diagnostic
12203 /// possible at all (ADR-087).
12204 #[test]
12205 fn a_reader_that_answers_zero_bytes_installs_an_empty_text() {
12206 let mut rt = Runtime::new();
12207 let ctx = wired_ctx(&mut rt);
12208 crate::input::install_input_reader(no_bytes);
12209 // SAFETY: ctx is wired to rt and live for this call.
12210 let source = unsafe { praxis_get_input(ctx) };
12211 assert_eq!(
12212 source.descriptor().id(),
12213 crate::text::TEXT.id(),
12214 "a zero-byte answer is still an input buffer"
12215 );
12216 // SAFETY: the assertion above proves `source` is a Text.
12217 assert!(
12218 unsafe { text_bytes_of(source) }.is_empty(),
12219 "and the buffer holds exactly what the reader answered"
12220 );
12221 // SAFETY: ctx is wired to rt and live for this call.
12222 assert_eq!(
12223 unsafe { (*ctx).input_source }.as_ptr(),
12224 source.as_ptr(),
12225 "the buffer is installed, not merely returned — §7.10's later \
12226 `read`s reuse it"
12227 );
12228 // SAFETY: ctx came from `wired_ctx` and is not used again.
12229 unsafe { drop_ctx(ctx) };
12230 }
12231
12232 /// **A mutation companion, not a gate.**
12233 ///
12234 /// The cheapest wrong repair is to allocate a `Text` unconditionally in
12235 /// `praxis_get_input`, which passes the gate above and quietly deletes the
12236 /// one state the §6.3 descriptor guard exists for. A host that installs
12237 /// **neither** a buffer nor a reader — every JIT test, every embedder — must
12238 /// still reach `praxis_run_parser` with the Unit source, because
12239 /// `adv_read_against_non_text_input_faults_cleanly` in the codegen crate's
12240 /// `jit.rs` is the probe that a `read` there faults instead of
12241 /// reinterpreting Unit's payload as a `TextPayload` and segfaulting.
12242 ///
12243 /// That is the boundary ADR-087 draws: a reader answering zero bytes is a
12244 /// program state (empty input); no reader at all is a host state (no input),
12245 /// and no `praxis run` reaches it.
12246 #[test]
12247 fn a_host_that_installs_no_reader_keeps_the_unit_source() {
12248 let mut rt = Runtime::new();
12249 let ctx = wired_ctx(&mut rt);
12250 crate::input::clear_input_reader();
12251 // SAFETY: ctx is wired to rt and live for these calls.
12252 let before = unsafe { (*ctx).input_source };
12253 // SAFETY: as above.
12254 let source = unsafe { praxis_get_input(ctx) };
12255 assert_eq!(
12256 source.as_ptr(),
12257 before.as_ptr(),
12258 "with no reader installed there is nothing to call and nothing to \
12259 install; `input_source` is answered untouched"
12260 );
12261 assert_ne!(
12262 source.descriptor().id(),
12263 crate::text::TEXT.id(),
12264 "and it is still the Unit the §6.3 guard is the net under"
12265 );
12266 // SAFETY: ctx came from `wired_ctx` and is not used again.
12267 unsafe { drop_ctx(ctx) };
12268 }
12269
12270 /// **The guard must not report a parse that never ran.**
12271 ///
12272 /// `praxis_run_parser` returns early for a non-Text `input` (§6.3) —
12273 /// `run_plan` would otherwise reinterpret the payload as a `TextPayload` —
12274 /// and that early return must still perform the `clear_parse_detail` every
12275 /// other entry into the parser performs. Without it, a host reaching the
12276 /// guard after an earlier mismatch reports *that* mismatch's offset and
12277 /// expectation for a parse that never started.
12278 ///
12279 /// Not reachable end to end: a fault is terminal within one `praxis run`,
12280 /// so the shape is an embedder calling `main` twice (or the crash debugger's
12281 /// `restart`). This test pins it at the level where the hazard exists.
12282 ///
12283 /// Fabricating a `ParseFail` here instead would be worse than clearing: with
12284 /// no buffer there is no input span, and an invented `expected` would make
12285 /// an embedder's host bug read as a parse failure at an offset that does not
12286 /// exist.
12287 #[test]
12288 fn the_non_text_guard_does_not_report_a_previous_parses_failure() {
12289 let mut rt = Runtime::new();
12290 let ctx = wired_ctx(&mut rt);
12291 rt.parse_detail_mut()
12292 .consider(ParseFail::here(7, "int"), b"0123456789");
12293 assert!(rt.parse_detail().is_set(), "the seed is in place");
12294 // SAFETY: ctx is wired to rt; the plan index is never read, because the
12295 // descriptor guard returns before it.
12296 unsafe {
12297 let plan = praxis_alloc_int(ctx, 1);
12298 let unit = (*ctx).unit_ref;
12299 let result = praxis_run_parser(ctx, plan, unit);
12300 assert_eq!(
12301 result.descriptor().id(),
12302 crate::scalars::UNIT.id(),
12303 "the guard answers the sentinel"
12304 );
12305 }
12306 assert!(rt.has_pending_fault());
12307 assert_eq!(rt.fault(), FaultKind::ParseFailed);
12308 assert!(
12309 !rt.parse_detail().is_set(),
12310 "the §6.3 guard runs no parse, so it has no detail to report — and \
12311 it must not report the previous parse's"
12312 );
12313 // SAFETY: ctx came from `wired_ctx` and is not used again.
12314 unsafe { drop_ctx(ctx) };
12315 }
12316}
12317
12318#[cfg(test)]
12319mod growth_charging_tests {
12320 //! **Every wrapper that can grow a collection's buffer charges the pacer**
12321 //! (ADR-121). See [`super::charge_growth`] for why.
12322 //!
12323 //! The values pushed are all inside `small_int`'s interned range, and that
12324 //! is the whole design of these tests rather than a convenience: an interned
12325 //! `Int` is an immortal the allocator never charges for, so the *only* thing
12326 //! that can move `bytes_since_collect` here is the spine. Push
12327 //! `UNINTERNED + i` instead and every one of these passes whether or not the
12328 //! growth is charged, because the elements would be paying for it.
12329
12330 use super::tests::{drop_ctx, wired_ctx};
12331 use super::*;
12332 use crate::Runtime;
12333
12334 /// Reset the counter, run `body`, and answer what it charged.
12335 fn charged_by(rt: &Runtime, body: impl FnOnce()) -> usize {
12336 // A collection zeroes the counter, so take a reading either side and
12337 // require the run not to have collected; the pushes below are far too
12338 // few to reach any threshold.
12339 let before = rt.heap().bytes_since_collect();
12340 body();
12341 rt.heap().bytes_since_collect().saturating_sub(before)
12342 }
12343
12344 /// Enough pushes that amortized doubling must have reallocated at least
12345 /// once, whatever the initial capacity is.
12346 const PUSHES: i64 = 256;
12347
12348 macro_rules! charges_its_spine {
12349 ($name:ident, $make:expr_2021, $push:expr_2021) => {
12350 #[test]
12351 fn $name() {
12352 let mut rt = Runtime::new();
12353 let ctx = wired_ctx(&mut rt);
12354 // SAFETY: `ctx` is wired to `rt` for the whole test.
12355 unsafe {
12356 let subject = $make(ctx);
12357 let charged = charged_by(&rt, || {
12358 for i in 0..PUSHES {
12359 $push(ctx, subject, i);
12360 }
12361 });
12362 assert!(
12363 charged > 0,
12364 "growing this collection charged the pacer nothing, so a \
12365 program whose memory is this buffer would never collect \
12366 (ADR-121); every value pushed is an interned immortal, so \
12367 the spine is the only thing that could have charged"
12368 );
12369 drop_ctx(ctx);
12370 }
12371 }
12372 };
12373 }
12374
12375 charges_its_spine!(
12376 vec_push_charges_its_spine,
12377 |c| praxis_vec_new(c, &crate::scalars::INT),
12378 |c, s, i| { praxis_vec_push(c, s, praxis_alloc_int(c, i)) }
12379 );
12380 charges_its_spine!(
12381 deque_push_back_charges_its_spine,
12382 |c| praxis_deque_new(c, &crate::scalars::INT),
12383 |c, s, i| praxis_deque_push_back(c, s, praxis_alloc_int(c, i))
12384 );
12385 charges_its_spine!(
12386 deque_push_front_charges_its_spine,
12387 |c| praxis_deque_new(c, &crate::scalars::INT),
12388 |c, s, i| praxis_deque_push_front(c, s, praxis_alloc_int(c, i))
12389 );
12390 charges_its_spine!(
12391 map_insert_charges_its_spine,
12392 |c| praxis_map_new(c, &crate::scalars::INT),
12393 |c, s, i| praxis_map_insert(c, s, praxis_alloc_int(c, i), praxis_alloc_int(c, i))
12394 );
12395 charges_its_spine!(
12396 set_insert_charges_its_spine,
12397 |c| praxis_set_new(c, &crate::scalars::INT),
12398 |c, s, i| praxis_set_insert(c, s, praxis_alloc_int(c, i))
12399 );
12400 charges_its_spine!(
12401 counter_set_charges_its_spine,
12402 |c| praxis_counter_new(c, &crate::scalars::INT),
12403 |c, s, i| praxis_counter_set(c, s, praxis_alloc_int(c, i), praxis_alloc_int(c, i))
12404 );
12405 charges_its_spine!(
12406 bitset_insert_charges_its_spine,
12407 |c| praxis_bitset_new(c),
12408 |c, s, i| praxis_bitset_insert(c, s, praxis_alloc_int(c, i))
12409 );
12410 charges_its_spine!(
12411 max_heap_push_charges_its_spine,
12412 |c| praxis_max_heap_new(c, &crate::scalars::INT),
12413 |c, s, i| praxis_max_heap_push(c, s, praxis_alloc_int(c, i))
12414 );
12415 charges_its_spine!(
12416 min_heap_push_charges_its_spine,
12417 |c| praxis_min_heap_new(c, &crate::scalars::INT),
12418 |c, s, i| praxis_min_heap_push(c, s, praxis_alloc_int(c, i))
12419 );
12420}