Skip to main content

praxis_stdlib/
abi.rs

1//! The runtime ABI manifest: one row per `praxis_*` symbol the JIT can call.
2//!
3//! Everything the compiler needs to know about a runtime wrapper — its exact
4//! symbol name, its parameter and return kinds, and whether calling it can
5//! allocate or fault — is **one row** in [`runtime_symbols!`] below, so no two
6//! places can drift about a symbol's signature or its effects.
7//!
8//! A call target is a [`RuntimeSymbol`], not a string. Adding a wrapper
9//! means adding a row here and one arm to `praxis_runtime::abi::address`; both
10//! are exhaustive matches, so anything else that must change is a compile
11//! error rather than a runtime surprise.
12//!
13//! This crate is the right home because it is the lowest common dependency of
14//! the compiler crates that need the manifest (`praxis-mir`,
15//! `praxis-codegen-cranelift`) and of `praxis-runtime`, which supplies the
16//! addresses.
17
18/// The kind of one ABI parameter — what a value in that position *is*, which
19/// fixes the machine type the caller must pass.
20#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
21pub enum AbiKind {
22    /// `*mut RuntimeContext`. Always the first parameter of every wrapper.
23    Ctx,
24    /// A `GcRef` — a non-null pointer to a `GcHeader`. Pointer-width.
25    Gc,
26    /// A raw, unboxed `i64`. **Not** a GC reference: never rooted, never traced.
27    RawI64,
28    /// A raw, unboxed `u32`. Narrower than a machine word, so passing an `i64`
29    /// here is exactly the mismatch this manifest exists to prevent.
30    RawU32,
31    /// A pointer-width raw word that is not a `GcRef`: a `*const u8`, a
32    /// descriptor or schema pointer, a frame pointer, or a `usize` length.
33    Ptr,
34}
35
36/// What a wrapper returns.
37///
38/// The `Gc`/`GcUnit` split is what relates a wrapper to its catalog row: "a
39/// `GcRef`" alone says nothing about whether the reference can be Unit, so a
40/// wrapper declared `-> Gc` that answers the Unit sentinel on a miss would hand
41/// the program a value whose static type is `V` and whose runtime descriptor is
42/// `Unit`.
43///
44/// **There is deliberately no third arm.** "May be Unit, may be a value" is the
45/// defect, and its absence from this enum is what makes it unrepresentable. A
46/// wrapper whose answer is sometimes absent says so in its result *type* —
47/// `Option[T]` (§4.7) — or it faults.
48#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
49pub enum AbiRet {
50    /// A `GcRef` carrying the wrapper's **answer**: a value of the result type
51    /// its catalog row declares.
52    ///
53    /// The Unit sentinel still comes back on a fault return — that is the ABI's
54    /// universal "a Praxis function returns a valid `GcRef` even when it
55    /// unwinds" — and, in the handful of wrappers the *codegen* calls directly
56    /// (`praxis_alloc_enum` with a null schema, `praxis_tuple_get` with an
57    /// out-of-range index), on a refusal the compiler was responsible for
58    /// having prevented. Neither is "the value is absent", which is the state
59    /// this arm rules out.
60    Gc,
61    /// A `GcRef` that is **always** the Unit sentinel: the wrapper's answer is
62    /// "done", not a value. `Vec.push`, `Map.insert`, `out`, `assert`.
63    ///
64    /// Not `Void`: the call still yields a `GcRef` the caller's uniform value
65    /// channel consumes, and codegen treats it exactly as it treats `Gc`.
66    GcUnit,
67    /// A raw `i64`.
68    RawI64,
69    /// A pointer-width raw word (a frame pointer, a function pointer).
70    Ptr,
71    /// Nothing.
72    Void,
73}
74
75/// The one answer to "does calling this need a root set, or a fault check?"
76///
77/// `Allocates` means the call **may trigger a collection**, so every live
78/// `GcRef` the caller holds must be rooted across it — that is what makes a
79/// call site a safepoint. A wrapper that only hands back an immortal singleton
80/// (`true`, `false`, `unit`) allocates nothing collectable and is therefore not
81/// a safepoint, however "alloc" its name reads.
82#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
83pub enum Effect {
84    /// Neither allocates nor faults.
85    Pure,
86    /// May set a pending fault; cannot allocate.
87    Faults,
88    /// May allocate (and therefore collect); cannot fault.
89    Allocates,
90    /// Both.
91    AllocatesAndFaults,
92}
93
94impl Effect {
95    /// Whether a call to this symbol is a safepoint.
96    #[inline]
97    pub const fn allocates(self) -> bool {
98        matches!(self, Effect::Allocates | Effect::AllocatesAndFaults)
99    }
100
101    /// Whether a call to this symbol needs a fault check afterwards.
102    #[inline]
103    pub const fn faults(self) -> bool {
104        matches!(self, Effect::Faults | Effect::AllocatesAndFaults)
105    }
106}
107
108/// One wrapper's full ABI: what it takes, what it gives back, what it may do.
109#[derive(Clone, Copy, PartialEq, Eq, Debug)]
110pub struct AbiSig {
111    /// Parameter kinds, including the leading [`AbiKind::Ctx`].
112    pub params: &'static [AbiKind],
113    /// Return kind.
114    pub ret: AbiRet,
115    /// Allocation and fault behaviour.
116    pub effect: Effect,
117}
118
119impl AbiSig {
120    /// Parameter count excluding the leading context pointer.
121    #[inline]
122    pub const fn arity(&self) -> usize {
123        self.params.len() - 1
124    }
125}
126
127/// Declare the manifest. One row per symbol:
128/// `Variant = "praxis_name": (ParamKinds…) -> Ret, Effect;`
129macro_rules! runtime_symbols {
130    ($( $variant:ident = $name:literal : ( $($kind:ident),* ) -> $ret:ident , $effect:ident ; )*) => {
131        /// Every `praxis_*` runtime wrapper generated code may call.
132        ///
133        /// A call target in MIR is one of these, so "the compiler emitted a call
134        /// to a symbol that does not exist" is not a representable state.
135        #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
136        pub enum RuntimeSymbol {
137            $(
138                #[doc = concat!("`", $name, "`")]
139                $variant,
140            )*
141        }
142
143        impl RuntimeSymbol {
144            /// Every symbol, in declaration order.
145            pub const ALL: &'static [RuntimeSymbol] = &[$(RuntimeSymbol::$variant),*];
146
147            /// The exact linker symbol name. This is the only place the string
148            /// is written.
149            #[inline]
150            pub const fn name(self) -> &'static str {
151                match self { $(RuntimeSymbol::$variant => $name,)* }
152            }
153
154            /// This symbol's parameter kinds, return kind and effect.
155            #[inline]
156            pub const fn sig(self) -> AbiSig {
157                match self {
158                    $(RuntimeSymbol::$variant => AbiSig {
159                        params: &[$(AbiKind::$kind),*],
160                        ret: AbiRet::$ret,
161                        effect: Effect::$effect,
162                    },)*
163                }
164            }
165
166            /// Recover a symbol from its linker name. The inverse of
167            /// [`RuntimeSymbol::name`]; used where a name crosses a boundary
168            /// that is not yet typed.
169            pub fn from_name(name: &str) -> Option<RuntimeSymbol> {
170                match name {
171                    $($name => Some(RuntimeSymbol::$variant),)*
172                    _ => None,
173                }
174            }
175        }
176    };
177}
178
179impl RuntimeSymbol {
180    /// Whether calling this symbol may trigger a collection (a safepoint).
181    #[inline]
182    pub const fn allocates(self) -> bool {
183        self.sig().effect.allocates()
184    }
185
186    /// Whether calling this symbol may set a pending fault.
187    #[inline]
188    pub const fn faults(self) -> bool {
189        self.sig().effect.faults()
190    }
191
192    /// Parameter count excluding the leading context pointer.
193    #[inline]
194    pub const fn arity(self) -> usize {
195        self.sig().arity()
196    }
197}
198
199impl std::fmt::Display for RuntimeSymbol {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        f.write_str(self.name())
202    }
203}
204
205runtime_symbols! {
206    AllocBool = "praxis_alloc_bool": (Ctx, RawI64) -> Gc, Pure;
207    AllocChar = "praxis_alloc_char": (Ctx, RawI64) -> Gc, AllocatesAndFaults;
208    AllocClosure = "praxis_alloc_closure": (Ctx, Ptr, RawI64) -> Gc, Allocates;
209    AllocEnum = "praxis_alloc_enum": (Ctx, Ptr, RawI64) -> Gc, Allocates;
210    AllocFloat = "praxis_alloc_float": (Ctx, RawI64) -> Gc, Allocates;
211    AllocInt = "praxis_alloc_int": (Ctx, RawI64) -> Gc, Allocates;
212    AllocRecord = "praxis_alloc_record": (Ctx, Ptr) -> Gc, Allocates;
213    AllocText = "praxis_alloc_text": (Ctx, Ptr, Ptr) -> Gc, Allocates;
214    AllocTuple = "praxis_alloc_tuple": (Ctx, Ptr) -> Gc, Allocates;
215    AllocUnit = "praxis_alloc_unit": (Ctx) -> GcUnit, Pure;
216    AllocVarCell = "praxis_alloc_var_cell": (Ctx, Gc) -> Gc, Allocates;
217    Assert = "praxis_assert": (Ctx, Gc) -> GcUnit, Faults;
218    AStarDistance = "praxis_a_star_distance": (Ctx, Gc, Gc, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
219    AStarPath = "praxis_a_star_path": (Ctx, Gc, Gc, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
220    Bfs = "praxis_bfs": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
221    BfsDistance = "praxis_bfs_distance": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
222    BfsPath = "praxis_bfs_path": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
223    // `-> RawI64` and not `-> Gc`, which is what makes `bs.contains(x)` a
224    // scalar-producing MIR instruction rather than a call whose answer has to
225    // be unboxed again (ADR-118 decision 6). `StructEq` and `ValueCmp` are the
226    // two rows this copies, and the shape is the same on all three: a boxed
227    // `Bool` the caller immediately unboxes is a box nobody looks at.
228    BitsetContains = "praxis_bitset_contains": (Ctx, Gc, Gc) -> RawI64, Pure;
229    BitsetInsert = "praxis_bitset_insert": (Ctx, Gc, Gc) -> GcUnit, AllocatesAndFaults;
230    BitsetIsEmpty = "praxis_bitset_is_empty": (Ctx, Gc) -> Gc, Pure;
231    BitsetItems = "praxis_bitset_items": (Ctx, Gc) -> Gc, Allocates;
232    BitsetLen = "praxis_bitset_len": (Ctx, Gc) -> Gc, Allocates;
233    BitsetNew = "praxis_bitset_new": (Ctx) -> Gc, Allocates;
234    // The `:bp` stop (§9.8). `Pure` is the load-bearing column: the handler this
235    // reaches is given a snapshot and no `RuntimeContext`, so it cannot allocate,
236    // cannot collect and cannot raise — which is what lets a breakpoint be a bare
237    // call with no root spill before it and no fault check after. The two
238    // `RawU32`s are the marker's source span, passed as immediates because a
239    // program has nothing to say here: a boxed span would be an allocation at a
240    // site whose whole point is that it does not have one.
241    Breakpoint = "praxis_breakpoint": (Ctx, RawU32, RawU32) -> Void, Pure;
242    BitsetRemove = "praxis_bitset_remove": (Ctx, Gc, Gc) -> GcUnit, Pure;
243    BoolLoad = "praxis_bool_load": (Ctx, Gc) -> RawI64, Pure;
244    CharLoad = "praxis_char_load": (Ctx, Gc) -> RawI64, Pure;
245    CharToInt = "praxis_char_to_int": (Ctx, Gc) -> Gc, Allocates;
246    // The `to_text` family — this row, `FloatToText` and `IntToText` — is
247    // `Allocates` and never `AllocatesAndFaults` (ADR-143). Each answers a fresh
248    // `Text` built from a payload that was validated at construction, so there
249    // is nothing left to check; declaring one faulting would put a `CheckFault`
250    // after every call site that can never fire.
251    CharToText = "praxis_char_to_text": (Ctx, Gc) -> Gc, Allocates;
252    CheckFault = "praxis_check_fault": (Ctx) -> RawI64, Pure;
253    ClosureCapture = "praxis_closure_capture": (Ctx, Gc, RawI64) -> Gc, Pure;
254    ClosureFnPtr = "praxis_closure_fn_ptr": (Ctx, Gc) -> Ptr, Pure;
255    ClosureSetCapture = "praxis_closure_set_capture": (Ctx, Gc, RawI64, Gc) -> Gc, Pure;
256    CounterGet = "praxis_counter_get": (Ctx, Gc, Gc) -> Gc, Allocates;
257    CounterInc = "praxis_counter_inc": (Ctx, Gc, Gc) -> GcUnit, AllocatesAndFaults;
258    CounterKeys = "praxis_counter_keys": (Ctx, Gc) -> Gc, Allocates;
259    CounterSet = "praxis_counter_set": (Ctx, Gc, Gc, Gc) -> GcUnit, Allocates;
260    CounterValues = "praxis_counter_values": (Ctx, Gc) -> Gc, Allocates;
261    CounterIsEmpty = "praxis_counter_is_empty": (Ctx, Gc) -> Gc, Pure;
262    CounterLen = "praxis_counter_len": (Ctx, Gc) -> Gc, Allocates;
263    CounterNew = "praxis_counter_new": (Ctx, Ptr) -> Gc, Allocates;
264    DequeGet = "praxis_deque_get": (Ctx, Gc, Gc) -> Gc, Faults;
265    DequeIsEmpty = "praxis_deque_is_empty": (Ctx, Gc) -> Gc, Pure;
266    DequeLen = "praxis_deque_len": (Ctx, Gc) -> Gc, Allocates;
267    DequeNew = "praxis_deque_new": (Ctx, Ptr) -> Gc, Allocates;
268    DequePopBack = "praxis_deque_pop_back": (Ctx, Gc) -> Gc, Faults;
269    DequePopFront = "praxis_deque_pop_front": (Ctx, Gc) -> Gc, Faults;
270    DequePushBack = "praxis_deque_push_back": (Ctx, Gc, Gc) -> GcUnit, AllocatesAndFaults;
271    DequePushFront = "praxis_deque_push_front": (Ctx, Gc, Gc) -> GcUnit, AllocatesAndFaults;
272    DequeSet = "praxis_deque_set": (Ctx, Gc, Gc, Gc) -> GcUnit, Faults;
273    // The three updating stores over an *indexed* receiver (ADR-161) share one
274    // shape and it is the plain store's, not the map's: the place either exists
275    // or is out of range, so they fault where `MapUpdateMin` allocates. Nothing
276    // is inserted and nothing grows — the slot is already there or the index was
277    // wrong.
278    DequeUpdateMax = "praxis_deque_update_max": (Ctx, Gc, Gc, Gc) -> GcUnit, Faults;
279    DequeUpdateMin = "praxis_deque_update_min": (Ctx, Gc, Gc, Gc) -> GcUnit, Faults;
280    Dbg = "praxis_dbg": (Ctx, Gc) -> Gc, Pure;
281    Dfs = "praxis_dfs": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
282    DfsDistance = "praxis_dfs_distance": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
283    DfsPath = "praxis_dfs_path": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
284    Dijkstra = "praxis_dijkstra": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
285    DijkstraDistance = "praxis_dijkstra_distance": (Ctx, Gc, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
286    DijkstraPath = "praxis_dijkstra_path": (Ctx, Gc, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
287    EnumPayload = "praxis_enum_payload": (Ctx, Gc, RawI64) -> Gc, Pure;
288    EnumSetPayload = "praxis_enum_set_payload": (Ctx, Gc, RawI64, Gc) -> Gc, Pure;
289    EnumTag = "praxis_enum_tag": (Ctx, Gc) -> Gc, Allocates;
290    FloatAbs = "praxis_float_abs": (Ctx, Gc) -> Gc, Allocates;
291    FloatCeil = "praxis_float_ceil": (Ctx, Gc) -> Gc, Allocates;
292    FloatE = "praxis_float_e": (Ctx) -> Gc, Allocates;
293    FloatFloor = "praxis_float_floor": (Ctx, Gc) -> Gc, Allocates;
294    FloatIsInfinite = "praxis_float_is_infinite": (Ctx, Gc) -> Gc, Pure;
295    FloatIsNan = "praxis_float_is_nan": (Ctx, Gc) -> Gc, Pure;
296    FloatLoad = "praxis_float_load": (Ctx, Gc) -> RawI64, Pure;
297    FloatMax = "praxis_float_max": (Ctx, Gc, Gc) -> Gc, Allocates;
298    FloatMin = "praxis_float_min": (Ctx, Gc, Gc) -> Gc, Allocates;
299    FloatPi = "praxis_float_pi": (Ctx) -> Gc, Allocates;
300    FloatRound = "praxis_float_round": (Ctx, Gc) -> Gc, Allocates;
301    FloatSign = "praxis_float_sign": (Ctx, Gc) -> Gc, Allocates;
302    FloatSqrt = "praxis_float_sqrt": (Ctx, Gc) -> Gc, Allocates;
303    FloatToInt = "praxis_float_to_int": (Ctx, Gc) -> Gc, AllocatesAndFaults;
304    FloatToText = "praxis_float_to_text": (Ctx, Gc) -> Gc, Allocates;
305    FloodFill = "praxis_flood_fill": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
306    GetInput = "praxis_get_input": (Ctx) -> Gc, AllocatesAndFaults;
307    // The named-direction neighbourhoods (§6.4). A record of `Option`s, so
308    // every direction is answered — including the ones off the edge, which is
309    // the whole difference from `GridNeighbors4`/`8` and their clipped `Vec`s.
310    // Nothing here can be refused: a point outside the grid has eight outside
311    // neighbours and that is a perfectly good answer, so they allocate and do
312    // not fault.
313    GridAround4 = "praxis_grid_around4": (Ctx, Gc, Gc) -> Gc, Allocates;
314    GridAround8 = "praxis_grid_around8": (Ctx, Gc, Gc) -> Gc, Allocates;
315    GridCells = "praxis_grid_cells": (Ctx, Gc) -> Gc, Allocates;
316    GridColumn = "praxis_grid_column": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
317    GridContains = "praxis_grid_contains": (Ctx, Gc, Gc, Gc) -> Gc, Pure;
318    // The neighbourhood counts (§6.4). The `_where` pair calls back into JIT'd
319    // code, so it declares the fault that closure may raise — without it MIR
320    // emits no `CheckFault` and a faulting predicate hands the program a Unit
321    // sentinel typed as an `Int` (ADR-088). The value-comparing pair calls
322    // nothing and only boxes its answer.
323    GridCount4 = "praxis_grid_count4": (Ctx, Gc, Gc, Gc) -> Gc, Allocates;
324    GridCount4Where = "praxis_grid_count4_where": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
325    GridCount8 = "praxis_grid_count8": (Ctx, Gc, Gc, Gc) -> Gc, Allocates;
326    GridCount8Where = "praxis_grid_count8_where": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
327    // `Grid(w, h, fill)` (ADR-146). The extents arrive boxed where `GridNew`'s
328    // arrive raw, because these two come from lowered argument expressions and
329    // a `RawI64` would cost an `ExtractScalar` apiece; `GridNew`'s are `iconst`
330    // immediates with no local to unbox. It faults for `GridNew`'s reason —
331    // `GridExtent::new` refuses a negative or oversized extent — and for that
332    // reason only, since an explicit fill is the one thing `default_cell`
333    // cannot invent.
334    GridFilled = "praxis_grid_filled": (Ctx, Ptr, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
335    GridFind = "praxis_grid_find": (Ctx, Gc, Gc) -> Gc, Allocates;
336    GridFindAll = "praxis_grid_find_all": (Ctx, Gc, Gc) -> Gc, Allocates;
337    GridGet = "praxis_grid_get": (Ctx, Gc, Gc, Gc) -> Gc, Faults;
338    GridHeight = "praxis_grid_height": (Ctx, Gc) -> Gc, Allocates;
339    GridNeighbors4 = "praxis_grid_neighbors4": (Ctx, Gc, Gc) -> Gc, Allocates;
340    GridNeighbors8 = "praxis_grid_neighbors8": (Ctx, Gc, Gc) -> Gc, Allocates;
341    GridNew = "praxis_grid_new": (Ctx, Ptr, RawI64, RawI64) -> Gc, AllocatesAndFaults;
342    GridPositions = "praxis_grid_positions": (Ctx, Gc) -> Gc, Allocates;
343    GridRotateLeft = "praxis_grid_rotate_left": (Ctx, Gc) -> Gc, Allocates;
344    GridRotateRight = "praxis_grid_rotate_right": (Ctx, Gc) -> Gc, Allocates;
345    GridRow = "praxis_grid_row": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
346    GridSet = "praxis_grid_set": (Ctx, Gc, Gc, Gc, Gc) -> GcUnit, Faults;
347    GridTranspose = "praxis_grid_transpose": (Ctx, Gc) -> Gc, Allocates;
348    GridUpdateMax = "praxis_grid_update_max": (Ctx, Gc, Gc, Gc, Gc) -> GcUnit, Faults;
349    GridUpdateMin = "praxis_grid_update_min": (Ctx, Gc, Gc, Gc, Gc) -> GcUnit, Faults;
350    GridWidth = "praxis_grid_width": (Ctx, Gc) -> Gc, Allocates;
351    IntAbs = "praxis_int_abs": (Ctx, Gc) -> Gc, AllocatesAndFaults;
352    IntAdd = "praxis_int_add": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
353    IntCheckedAdd = "praxis_int_checked_add": (Ctx, Gc, Gc) -> Gc, Allocates;
354    IntCheckedMul = "praxis_int_checked_mul": (Ctx, Gc, Gc) -> Gc, Allocates;
355    IntCheckedSub = "praxis_int_checked_sub": (Ctx, Gc, Gc) -> Gc, Allocates;
356    IntClamp = "praxis_int_clamp": (Ctx, Gc, Gc, Gc) -> Gc, Faults;
357    IntDiv = "praxis_int_div": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
358    IntEq = "praxis_int_eq": (Ctx, Gc, Gc) -> Gc, Pure;
359    IntGcd = "praxis_int_gcd": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
360    IntGe = "praxis_int_ge": (Ctx, Gc, Gc) -> Gc, Pure;
361    IntGt = "praxis_int_gt": (Ctx, Gc, Gc) -> Gc, Pure;
362    IntLcm = "praxis_int_lcm": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
363    IntLe = "praxis_int_le": (Ctx, Gc, Gc) -> Gc, Pure;
364    IntLoad = "praxis_int_load": (Ctx, Gc) -> RawI64, Pure;
365    IntLt = "praxis_int_lt": (Ctx, Gc, Gc) -> Gc, Pure;
366    IntMax = "praxis_int_max": (Ctx, Gc, Gc) -> Gc, Pure;
367    IntMin = "praxis_int_min": (Ctx, Gc, Gc) -> Gc, Pure;
368    IntMul = "praxis_int_mul": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
369    IntNe = "praxis_int_ne": (Ctx, Gc, Gc) -> Gc, Pure;
370    IntNeg = "praxis_int_neg": (Ctx, Gc) -> Gc, AllocatesAndFaults;
371    IntRem = "praxis_int_rem": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
372    IntSaturatingAdd = "praxis_int_saturating_add": (Ctx, Gc, Gc) -> Gc, Allocates;
373    IntSaturatingMul = "praxis_int_saturating_mul": (Ctx, Gc, Gc) -> Gc, Allocates;
374    IntSaturatingSub = "praxis_int_saturating_sub": (Ctx, Gc, Gc) -> Gc, Allocates;
375    IntSign = "praxis_int_sign": (Ctx, Gc) -> Gc, Allocates;
376    IntSub = "praxis_int_sub": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
377    IntToChar = "praxis_int_to_char": (Ctx, Gc) -> Gc, AllocatesAndFaults;
378    IntToFloat = "praxis_int_to_float": (Ctx, Gc) -> Gc, Allocates;
379    // `Allocates`, for the reason recorded on `CharToText`: every `i64` renders.
380    IntToText = "praxis_int_to_text": (Ctx, Gc) -> Gc, Allocates;
381    IntWrappingAdd = "praxis_int_wrapping_add": (Ctx, Gc, Gc) -> Gc, Allocates;
382    IntWrappingMul = "praxis_int_wrapping_mul": (Ctx, Gc, Gc) -> Gc, Allocates;
383    IntWrappingSub = "praxis_int_wrapping_sub": (Ctx, Gc, Gc) -> Gc, Allocates;
384    MapContains = "praxis_map_contains": (Ctx, Gc, Gc) -> Gc, Pure;
385    RangeGet = "praxis_range_get": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
386    RangeLen = "praxis_range_len": (Ctx, Gc) -> Gc, AllocatesAndFaults;
387    RangeNew = "praxis_range_new": (Ctx, Gc, Gc) -> Gc, Allocates;
388    RangeNewInclusive = "praxis_range_new_inclusive": (Ctx, Gc, Gc) -> Gc, Allocates;
389    MapGet = "praxis_map_get": (Ctx, Gc, Gc) -> Gc, Allocates;
390    MapIndex = "praxis_map_index": (Ctx, Gc, Gc) -> Gc, Faults;
391    MapInsert = "praxis_map_insert": (Ctx, Gc, Gc, Gc) -> GcUnit, Allocates;
392    MapIsEmpty = "praxis_map_is_empty": (Ctx, Gc) -> Gc, Pure;
393    MapKeys = "praxis_map_keys": (Ctx, Gc) -> Gc, Allocates;
394    MapLen = "praxis_map_len": (Ctx, Gc) -> Gc, Allocates;
395    MapNew = "praxis_map_new": (Ctx, Ptr) -> Gc, Allocates;
396    MapRemove = "praxis_map_remove": (Ctx, Gc, Gc) -> GcUnit, Pure;
397    MapUpdateMax = "praxis_map_update_max": (Ctx, Gc, Gc, Gc) -> GcUnit, Allocates;
398    MapValues = "praxis_map_values": (Ctx, Gc) -> Gc, Allocates;
399    MapUpdateMin = "praxis_map_update_min": (Ctx, Gc, Gc, Gc) -> GcUnit, Allocates;
400    MaxHeapIsEmpty = "praxis_max_heap_is_empty": (Ctx, Gc) -> Gc, Pure;
401    MaxHeapItems = "praxis_max_heap_items": (Ctx, Gc) -> Gc, Allocates;
402    MaxHeapLen = "praxis_max_heap_len": (Ctx, Gc) -> Gc, Allocates;
403    MaxHeapNew = "praxis_max_heap_new": (Ctx, Ptr) -> Gc, Allocates;
404    MaxHeapPeek = "praxis_max_heap_peek": (Ctx, Gc) -> Gc, Faults;
405    MaxHeapPop = "praxis_max_heap_pop": (Ctx, Gc) -> Gc, Faults;
406    MaxHeapPush = "praxis_max_heap_push": (Ctx, Gc, Gc) -> GcUnit, Allocates;
407    MinHeapIsEmpty = "praxis_min_heap_is_empty": (Ctx, Gc) -> Gc, Pure;
408    MinHeapItems = "praxis_min_heap_items": (Ctx, Gc) -> Gc, Allocates;
409    MinHeapLen = "praxis_min_heap_len": (Ctx, Gc) -> Gc, Allocates;
410    MinHeapNew = "praxis_min_heap_new": (Ctx, Ptr) -> Gc, Allocates;
411    MinHeapPeek = "praxis_min_heap_peek": (Ctx, Gc) -> Gc, Faults;
412    MinHeapPop = "praxis_min_heap_pop": (Ctx, Gc) -> Gc, Faults;
413    MinHeapPush = "praxis_min_heap_push": (Ctx, Gc, Gc) -> GcUnit, Allocates;
414    Panic = "praxis_panic": (Ctx, Gc) -> GcUnit, Faults;
415    RaiseDivByZeroIf = "praxis_raise_div_by_zero_if": (Ctx, RawI64) -> Void, Faults;
416    RaiseEmptyCollection = "praxis_raise_empty_collection": (Ctx) -> GcUnit, Faults;
417    RaiseIntOverflowIf = "praxis_raise_int_overflow_if": (Ctx, RawI64) -> Void, Faults;
418    RaiseStackOverflow = "praxis_raise_stack_overflow": (Ctx) -> Void, Faults;
419    RecordField = "praxis_record_field": (Ctx, Gc, RawU32) -> Gc, Pure;
420    RecordSetField = "praxis_record_set_field": (Ctx, Gc, RawU32, Gc) -> Gc, Pure;
421    RunParser = "praxis_run_parser": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
422    SetContains = "praxis_set_contains": (Ctx, Gc, Gc) -> Gc, Pure;
423    SetInsert = "praxis_set_insert": (Ctx, Gc, Gc) -> GcUnit, Allocates;
424    SetIsEmpty = "praxis_set_is_empty": (Ctx, Gc) -> Gc, Pure;
425    SetItems = "praxis_set_items": (Ctx, Gc) -> Gc, Allocates;
426    SetLen = "praxis_set_len": (Ctx, Gc) -> Gc, Allocates;
427    SetNew = "praxis_set_new": (Ctx, Ptr) -> Gc, Allocates;
428    SetRemove = "praxis_set_remove": (Ctx, Gc, Gc) -> GcUnit, Pure;
429    SnapshotDebugChain = "praxis_snapshot_debug_chain": (Ctx) -> Void, Pure;
430    StructEq = "praxis_struct_eq": (Ctx, Gc, Gc) -> RawI64, Pure;
431    TextConcat = "praxis_text_concat": (Ctx, Gc, Gc) -> Gc, Allocates;
432    TextGet = "praxis_text_get": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
433    TextFloat = "praxis_text_float": (Ctx, Gc) -> Gc, Allocates;
434    TextInt = "praxis_text_int": (Ctx, Gc) -> Gc, Allocates;
435    TextIsEmpty = "praxis_text_is_empty": (Ctx, Gc) -> Gc, Pure;
436    TextLen = "praxis_text_len": (Ctx, Gc) -> Gc, Allocates;
437    TupleGet = "praxis_tuple_get": (Ctx, Gc, RawI64) -> Gc, Pure;
438    TupleSet = "praxis_tuple_set": (Ctx, Gc, RawI64, Gc) -> Gc, Pure;
439    ValueCmp = "praxis_value_cmp": (Ctx, Gc, Gc) -> RawI64, Faults;
440    // `x min= v` and `p.best max= s` — the *place* forms of the updating store,
441    // where there is no collection row to call because there is no collection
442    // (ADR-161). Each answers one of the two `GcRef`s it was handed, so neither
443    // allocates and neither can fault: they are the one shape in the manifest
444    // that picks rather than computes.
445    ValueKeepMax = "praxis_value_keep_max": (Ctx, Gc, Gc) -> Gc, Pure;
446    ValueKeepMin = "praxis_value_keep_min": (Ctx, Gc, Gc) -> Gc, Pure;
447    // The one wrapper an interpolation hole lowers to (ADR-147). `Allocates`
448    // and not `AllocatesAndFaults`: every `GcRef` has a descriptor with a
449    // `format` callback, so there is no value it can be handed that it cannot
450    // render, and a `String` built by `format` is UTF-8 by construction. That is
451    // `TextConcat`'s row, for the same two reasons.
452    ValueToText = "praxis_value_to_text": (Ctx, Gc) -> Gc, Allocates;
453    VarCellGet = "praxis_var_cell_get": (Ctx, Gc) -> Gc, Pure;
454    VarCellSet = "praxis_var_cell_set": (Ctx, Gc, Gc) -> Gc, Pure;
455    // `chunks(n)` and `windows(n)` (ADR-149). The pair that answers `Vec[Vec[T]]`,
456    // and the two rows in this manifest that fault on an **argument** rather than
457    // on an element: a run of `n <= 0` elements is not a short run, it is not a
458    // run, so `InvalidSize` is raised before either walks anything. They read no
459    // descriptor callback — the grouping is by position — so that fault is the
460    // only one either has, which is what makes them `AllocatesAndFaults` where
461    // `VecReversed` beside them is `Allocates`.
462    VecChunks = "praxis_vec_chunks": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
463    // `Vec(n, fill)` (ADR-146). `VecNew` beneath it only allocates; this one
464    // faults, because a count is a runtime `Int` and `VecExtent::new` refuses a
465    // negative or oversized one (ADR-041 decision 1).
466    VecFilled = "praxis_vec_filled": (Ctx, Ptr, Gc, Gc) -> Gc, AllocatesAndFaults;
467    VecFrequencies = "praxis_vec_frequencies": (Ctx, Gc) -> Gc, Allocates;
468    VecGet = "praxis_vec_get": (Ctx, Gc, Gc) -> Gc, Faults;
469    VecIsEmpty = "praxis_vec_is_empty": (Ctx, Gc) -> Gc, Pure;
470    // `join` and `to_text` fault for `praxis_vec_sorted`'s reason and not for
471    // `sorted`'s cause: the catalog row bounds the item to `Text` (or to `Char`),
472    // so an element of another type is a compiler bug — and the honest way to
473    // report one is `TypeMismatch`, not reading a foreign payload as a `Text`
474    // (ADR-144).
475    VecJoin = "praxis_vec_join": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
476    VecLen = "praxis_vec_len": (Ctx, Gc) -> Gc, Allocates;
477    VecNew = "praxis_vec_new": (Ctx, Ptr) -> Gc, Allocates;
478    VecPush = "praxis_vec_push": (Ctx, Gc, Gc) -> GcUnit, AllocatesAndFaults;
479    // `reversed` reads no descriptor callback at all — not `compare`, not
480    // `equals`, not `hash` — so there is nothing it can be handed that it cannot
481    // reverse (ADR-145). That is why it is `Allocates` where `VecSorted` beneath
482    // it is not.
483    VecReversed = "praxis_vec_reversed": (Ctx, Gc) -> Gc, Allocates;
484    VecSet = "praxis_vec_set": (Ctx, Gc, Gc, Gc) -> GcUnit, Faults;
485    // `sorted` faults and `unique` does not, and the difference is derived from
486    // the wrappers rather than guessed: `praxis_vec_sorted` raises
487    // `TypeMismatch` when the element type has no `compare`, while
488    // `praxis_vec_unique` and `praxis_vec_frequencies` go through `DynamicKey`,
489    // which answers "not equal" for a type with no `equals` instead of raising.
490    VecSorted = "praxis_vec_sorted": (Ctx, Gc) -> Gc, AllocatesAndFaults;
491    // The key extractor is called once per element and it is arbitrary Praxis
492    // code, so this faults for two reasons where `praxis_vec_sorted` faults for
493    // one: an unorderable key, and whatever the closure itself raised.
494    VecSortedByKey = "praxis_vec_sorted_by_key": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
495    VecToText = "praxis_vec_to_text": (Ctx, Gc) -> Gc, AllocatesAndFaults;
496    VecUnique = "praxis_vec_unique": (Ctx, Gc) -> Gc, Allocates;
497    VecUpdateMax = "praxis_vec_update_max": (Ctx, Gc, Gc, Gc) -> GcUnit, Faults;
498    VecUpdateMin = "praxis_vec_update_min": (Ctx, Gc, Gc, Gc) -> GcUnit, Faults;
499    // The sliding half of `VecChunks`'s pair; see that row for why it faults.
500    VecWindows = "praxis_vec_windows": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
501    WriteStdout = "praxis_write_stdout": (Ctx, Gc) -> GcUnit, Pure;
502}
503
504/// Build-time coverage of the effect table.
505///
506/// The manifest is the one answer to "does calling this allocate or fault" — a
507/// per-catalog-row `bool` would be a second one, free to drift — and this walks
508/// every row *at compile time* so a symbol can neither be added without an
509/// effect nor left out of [`RuntimeSymbol::ALL`], which is what the rest of the
510/// workspace iterates.
511///
512/// Anything checkable statically is checked here rather than in a test: a
513/// classification error should fail the build, not a test run.
514const _: () = {
515    // `ALL` is generated from the same rows as the enum, so a non-empty `ALL`
516    // that ends at the last variant means every variant is present.
517    assert!(!RuntimeSymbol::ALL.is_empty());
518
519    let mut i = 0;
520    while i < RuntimeSymbol::ALL.len() {
521        let sym = RuntimeSymbol::ALL[i];
522        let sig = sym.sig();
523
524        // Every wrapper leads with the context pointer. Without it there is no
525        // route to the heap, the fault slot or the root set — so a wrapper
526        // lacking one could be neither a safepoint nor a faulting call, and any
527        // effect other than `Pure` would be a lie.
528        assert!(matches!(sig.params[0], AbiKind::Ctx));
529
530        // A wrapper that returns nothing produced no object, so `Allocates`
531        // would misclassify it — and `Allocates` is exactly what makes a call
532        // site a safepoint that the caller must spill its live roots across.
533        assert!(!(matches!(sig.ret, AbiRet::Void) && sig.effect.allocates()));
534
535        // The two queries partition the four variants; `allocates`/`faults`
536        // must agree with the row rather than being independently answerable.
537        assert!(sig.effect.allocates() == sym.allocates());
538        assert!(sig.effect.faults() == sym.faults());
539
540        // `GcUnit` gets no check here on purpose. The invariant it exists for
541        // relates a manifest row to a *catalog* row — a non-faulting wrapper
542        // with a non-`Unit` result type must not be able to answer the sentinel
543        // — and the catalog is built at run time, so the check lives in
544        // `builtins::tests::a_non_faulting_row_with_a_value_result_\
545        // cannot_answer_the_unit_sentinel`.
546
547        i += 1;
548    }
549};
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use std::collections::HashSet;
555
556    /// The manifest is a bijection between variants and linker names. A typo
557    /// that duplicated a name would otherwise make two symbols resolve to one
558    /// address.
559    #[test]
560    fn names_are_unique_and_well_formed() {
561        let mut seen = HashSet::new();
562        for &sym in RuntimeSymbol::ALL {
563            assert!(
564                sym.name().starts_with("praxis_"),
565                "{sym} is not a praxis_* symbol"
566            );
567            assert!(seen.insert(sym.name()), "duplicate symbol name {sym}");
568        }
569        assert_eq!(seen.len(), RuntimeSymbol::ALL.len());
570    }
571
572    /// `ALL` must list every variant. It is generated from the same rows as the
573    /// enum, so this is really a check that the macro was not edited apart.
574    #[test]
575    fn from_name_round_trips_every_symbol() {
576        for &sym in RuntimeSymbol::ALL {
577            assert_eq!(RuntimeSymbol::from_name(sym.name()), Some(sym));
578        }
579        assert_eq!(RuntimeSymbol::from_name("praxis_not_a_symbol"), None);
580    }
581
582    /// Every wrapper takes the context pointer first: the fault slot, the heap
583    /// and the root set all hang off it, so a wrapper without it could not
584    /// allocate, fault or be a safepoint.
585    #[test]
586    fn every_symbol_leads_with_the_context_pointer() {
587        for &sym in RuntimeSymbol::ALL {
588            let sig = sym.sig();
589            assert_eq!(
590                sig.params.first(),
591                Some(&AbiKind::Ctx),
592                "{sym} does not take ctx first"
593            );
594            assert!(
595                !sig.params[1..].contains(&AbiKind::Ctx),
596                "{sym} takes ctx more than once"
597            );
598        }
599    }
600
601    #[test]
602    fn effect_queries_agree_with_the_variants() {
603        assert!(!Effect::Pure.allocates() && !Effect::Pure.faults());
604        assert!(!Effect::Faults.allocates() && Effect::Faults.faults());
605        assert!(Effect::Allocates.allocates() && !Effect::Allocates.faults());
606        assert!(Effect::AllocatesAndFaults.allocates() && Effect::AllocatesAndFaults.faults());
607    }
608
609    /// **A standing invariant:** none of the nine overflow alternatives may be
610    /// declared faulting.
611    ///
612    /// What it catches is an edit marking one `AllocatesAndFaults`, which would
613    /// make MIR emit a `CheckFault` after a call that never faults and quietly
614    /// undo the one property that makes these methods alternatives to a faulting
615    /// operator at all.
616    #[test]
617    fn no_overflow_alternative_declares_that_it_faults() {
618        use RuntimeSymbol::*;
619        for sym in [
620            IntWrappingAdd,
621            IntSaturatingAdd,
622            IntCheckedAdd,
623            IntWrappingSub,
624            IntSaturatingSub,
625            IntCheckedSub,
626            IntWrappingMul,
627            IntSaturatingMul,
628            IntCheckedMul,
629        ] {
630            assert_eq!(
631                sym.sig().effect,
632                Effect::Allocates,
633                "`{}` answers a fresh number and cannot fault (§4.12)",
634                sym.name()
635            );
636        }
637    }
638
639    /// **ADR-143.** All three `to_text` wrappers allocate and none of them
640    /// faults.
641    ///
642    /// Pinned for `no_overflow_alternative_declares_that_it_faults`'s reason:
643    /// the wrong answer here is silent. `MethodEntry::can_fault` reads the
644    /// manifest, so a row copied from `FloatToInt` instead of `FloatToText`
645    /// would make every `n.to_text()` emit a `CheckFault` that can never fire,
646    /// and nothing about the program's behaviour would say so.
647    #[test]
648    fn the_to_text_family_allocates_and_cannot_fault() {
649        for sym in [
650            RuntimeSymbol::IntToText,
651            RuntimeSymbol::FloatToText,
652            RuntimeSymbol::CharToText,
653        ] {
654            assert_eq!(
655                sym.sig().effect,
656                Effect::Allocates,
657                "`{}` renders a payload validated at construction and answers a \
658                 fresh Text; there is nothing for it to fault on",
659                sym.name()
660            );
661        }
662    }
663
664    /// **ADR-147.** An interpolation hole's wrapper allocates and cannot fault,
665    /// and it is `WriteStdout`'s renderer with `TextConcat`'s effect.
666    ///
667    /// The contrast is the assertion. `praxis_write_stdout` is `Pure` because it
668    /// allocates nothing at all; `praxis_value_to_text` does the same rendering
669    /// and then allocates a `Text`, so it is `Allocates` — and it is not
670    /// `AllocatesAndFaults`, because every `GcRef` has a descriptor with a
671    /// `format` callback and a `String` built by one is UTF-8 by construction.
672    /// Declaring it faulting would put a `CheckFault` after every hole in every
673    /// interpolated literal that can never fire, and nothing about the program's
674    /// behaviour would say so.
675    #[test]
676    fn a_holes_renderer_allocates_and_cannot_fault() {
677        assert_eq!(RuntimeSymbol::ValueToText.sig().effect, Effect::Allocates);
678        assert_eq!(RuntimeSymbol::TextConcat.sig().effect, Effect::Allocates);
679        // `out` renders through the same callback and allocates nothing, which
680        // is why the two rows differ at all.
681        assert_eq!(RuntimeSymbol::WriteStdout.sig().effect, Effect::Pure);
682    }
683
684    /// **ADR-145.** `reversed` reads no descriptor callback, so its row is
685    /// `Allocates` where its two neighbours are not.
686    ///
687    /// The contrast is the assertion. `sorted` faults because `compare` may be
688    /// absent; `reversed` has nothing to ask for, and marking it faulting to
689    /// match the barrier beside it would put a dead check after every call.
690    #[test]
691    fn reversal_cannot_fault_where_ordering_can() {
692        assert_eq!(RuntimeSymbol::VecReversed.sig().effect, Effect::Allocates);
693        assert_eq!(
694            RuntimeSymbol::VecSorted.sig().effect,
695            Effect::AllocatesAndFaults,
696            "the neighbour this is contrasted with still orders through `compare`"
697        );
698    }
699
700    /// **ADR-149.** A grouping declares that it faults, where the barrier it
701    /// most resembles does not.
702    ///
703    /// `reversed` and a grouping read the same nothing of their elements, so the
704    /// obvious tidying is to give them the same effect. They must not have it:
705    /// a grouping refuses a size of zero or less, and a row marked `Allocates`
706    /// would emit no `CheckFault` — the fault would be set into a context
707    /// nothing reads, and `chunks(0)` would answer a Unit sentinel typed as a
708    /// `Vec[Vec[T]]` (ADR-088).
709    #[test]
710    fn a_grouping_declares_the_fault_a_reversal_has_not() {
711        for sym in [RuntimeSymbol::VecChunks, RuntimeSymbol::VecWindows] {
712            assert_eq!(sym.sig().effect, Effect::AllocatesAndFaults, "{sym}");
713            assert_eq!(
714                sym.sig().params.len(),
715                3,
716                "{sym} takes the context, the receiver and the size"
717            );
718        }
719        assert_eq!(
720            RuntimeSymbol::VecReversed.sig().effect,
721            Effect::Allocates,
722            "the neighbour this is contrasted with still has nothing to refuse"
723        );
724    }
725
726    /// **ADR-146.** A sized constructor declares that it faults, and its
727    /// nullary neighbour is the contrast.
728    ///
729    /// This row is what makes a negative size *observable*: MIR emits a
730    /// `CheckFault` after an allocation only when a wrapper it reaches declares
731    /// a fault, so a `Vec(n, fill)` marked `Allocates` would set `InvalidSize`
732    /// into a context nothing ever reads and hand the program a Unit sentinel
733    /// typed as a `Vec` (ADR-088).
734    ///
735    /// The arity is asserted too, because the descriptor slot and the fill are
736    /// what distinguish these from the nullary wrappers: a row that grew an
737    /// extent without growing its wrapper would pass garbage in an unfilled
738    /// slot.
739    #[test]
740    fn a_sized_constructor_declares_that_it_faults() {
741        for sym in [RuntimeSymbol::VecFilled, RuntimeSymbol::GridFilled] {
742            assert_eq!(
743                sym.sig().effect,
744                Effect::AllocatesAndFaults,
745                "`{}` refuses a negative or oversized extent, and only a \
746                 declared fault gets a `CheckFault` to observe it",
747                sym.name()
748            );
749        }
750        assert_eq!(
751            RuntimeSymbol::VecNew.sig().effect,
752            Effect::Allocates,
753            "the empty form has no size to refuse, and marking it faulting \
754             would put a dead check after every `Vec()`"
755        );
756        // (ctx, descriptor, count, fill) and (ctx, descriptor, w, h, fill).
757        assert_eq!(
758            RuntimeSymbol::VecFilled.sig().params,
759            &[AbiKind::Ctx, AbiKind::Ptr, AbiKind::Gc, AbiKind::Gc]
760        );
761        assert_eq!(
762            RuntimeSymbol::GridFilled.sig().params,
763            &[
764                AbiKind::Ctx,
765                AbiKind::Ptr,
766                AbiKind::Gc,
767                AbiKind::Gc,
768                AbiKind::Gc
769            ]
770        );
771    }
772
773    /// **ADR-111.** `praxis_alloc_text` trusts its bytes, and the row is where
774    /// that is said.
775    ///
776    /// The UTF-8 requirement is the caller's precondition, not a runtime
777    /// judgement: the compiler's bytes come from a Rust `String` unbroken from
778    /// `Lit::Text` through `Generation::alloc_str`, and the one runtime caller
779    /// that holds raw host bytes (`praxis_get_input`) validates them itself. A
780    /// violation panics into `abi_guard!` and aborts; it never sets a fault.
781    ///
782    /// Written as an assertion rather than left to the manifest because the row
783    /// is read by three things at once and only this one is visible: it decides
784    /// whether `Inst::Alloc { AllocKind::Text }` is followed by a `CheckFault`
785    /// (ADR-088), whether a `Text` literal in a loop is hoisted into the
786    /// preheader (ADR-108 §3), and whether `panic_fault_is_observable` lets the
787    /// wrapper's panic path abort. An edit marking it faulting would silently
788    /// add 41 corpus checks back, un-hoist every `Text` literal, and make the
789    /// abort a fault — this makes it a failing test instead.
790    #[test]
791    fn alloc_text_trusts_its_bytes_and_the_row_says_so() {
792        assert_eq!(
793            RuntimeSymbol::AllocText.sig().effect,
794            Effect::Allocates,
795            "`praxis_alloc_text`'s UTF-8 requirement is its caller's precondition \
796             (ADR-111); declaring it faulting puts a check back after every text \
797             literal and takes `Text` back out of the ADR-108 hoist"
798        );
799        // And the wrapper that owns the fault declares it, so the requirement
800        // is enforced somewhere rather than nowhere.
801        assert!(
802            RuntimeSymbol::GetInput.faults(),
803            "`praxis_get_input` holds raw host bytes and raises `InvalidText` \
804             itself, so the fault still lands at the `read`"
805        );
806    }
807
808    /// Spot-check the rows the compiler is most sensitive to: the two that take
809    /// a narrow `u32`, where passing an `i64` is the mismatch this manifest
810    /// exists to prevent, and the arithmetic wrappers whose fault-and-allocate
811    /// pair drives both the safepoint and the fault check.
812    #[test]
813    fn narrow_and_faulting_rows_are_recorded_exactly() {
814        assert_eq!(
815            RuntimeSymbol::RecordField.sig().params,
816            &[AbiKind::Ctx, AbiKind::Gc, AbiKind::RawU32]
817        );
818        assert_eq!(
819            RuntimeSymbol::RecordSetField.sig().params,
820            &[AbiKind::Ctx, AbiKind::Gc, AbiKind::RawU32, AbiKind::Gc]
821        );
822
823        for sym in [
824            RuntimeSymbol::IntAdd,
825            RuntimeSymbol::IntSub,
826            RuntimeSymbol::IntMul,
827            RuntimeSymbol::IntDiv,
828            RuntimeSymbol::IntRem,
829            RuntimeSymbol::IntNeg,
830        ] {
831            assert_eq!(sym.sig().effect, Effect::AllocatesAndFaults, "{sym}");
832        }
833        // Comparisons hand back an immortal Bool: no collection can happen
834        // inside them, so they are not safepoints.
835        for sym in [
836            RuntimeSymbol::IntEq,
837            RuntimeSymbol::IntLt,
838            RuntimeSymbol::IntGe,
839        ] {
840            assert_eq!(sym.sig().effect, Effect::Pure, "{sym}");
841        }
842    }
843}