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    Dbg = "praxis_dbg": (Ctx, Gc) -> Gc, Pure;
274    Dfs = "praxis_dfs": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
275    DfsDistance = "praxis_dfs_distance": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
276    DfsPath = "praxis_dfs_path": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
277    Dijkstra = "praxis_dijkstra": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
278    DijkstraDistance = "praxis_dijkstra_distance": (Ctx, Gc, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
279    DijkstraPath = "praxis_dijkstra_path": (Ctx, Gc, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
280    EnumPayload = "praxis_enum_payload": (Ctx, Gc, RawI64) -> Gc, Pure;
281    EnumSetPayload = "praxis_enum_set_payload": (Ctx, Gc, RawI64, Gc) -> Gc, Pure;
282    EnumTag = "praxis_enum_tag": (Ctx, Gc) -> Gc, Allocates;
283    FloatAbs = "praxis_float_abs": (Ctx, Gc) -> Gc, Allocates;
284    FloatCeil = "praxis_float_ceil": (Ctx, Gc) -> Gc, Allocates;
285    FloatE = "praxis_float_e": (Ctx) -> Gc, Allocates;
286    FloatFloor = "praxis_float_floor": (Ctx, Gc) -> Gc, Allocates;
287    FloatIsInfinite = "praxis_float_is_infinite": (Ctx, Gc) -> Gc, Pure;
288    FloatIsNan = "praxis_float_is_nan": (Ctx, Gc) -> Gc, Pure;
289    FloatLoad = "praxis_float_load": (Ctx, Gc) -> RawI64, Pure;
290    FloatMax = "praxis_float_max": (Ctx, Gc, Gc) -> Gc, Allocates;
291    FloatMin = "praxis_float_min": (Ctx, Gc, Gc) -> Gc, Allocates;
292    FloatPi = "praxis_float_pi": (Ctx) -> Gc, Allocates;
293    FloatRound = "praxis_float_round": (Ctx, Gc) -> Gc, Allocates;
294    FloatSign = "praxis_float_sign": (Ctx, Gc) -> Gc, Allocates;
295    FloatSqrt = "praxis_float_sqrt": (Ctx, Gc) -> Gc, Allocates;
296    FloatToInt = "praxis_float_to_int": (Ctx, Gc) -> Gc, AllocatesAndFaults;
297    FloatToText = "praxis_float_to_text": (Ctx, Gc) -> Gc, Allocates;
298    FloodFill = "praxis_flood_fill": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
299    GetInput = "praxis_get_input": (Ctx) -> Gc, AllocatesAndFaults;
300    // The named-direction neighbourhoods (§6.4). A record of `Option`s, so
301    // every direction is answered — including the ones off the edge, which is
302    // the whole difference from `GridNeighbors4`/`8` and their clipped `Vec`s.
303    // Nothing here can be refused: a point outside the grid has eight outside
304    // neighbours and that is a perfectly good answer, so they allocate and do
305    // not fault.
306    GridAround4 = "praxis_grid_around4": (Ctx, Gc, Gc) -> Gc, Allocates;
307    GridAround8 = "praxis_grid_around8": (Ctx, Gc, Gc) -> Gc, Allocates;
308    GridCells = "praxis_grid_cells": (Ctx, Gc) -> Gc, Allocates;
309    GridColumn = "praxis_grid_column": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
310    GridContains = "praxis_grid_contains": (Ctx, Gc, Gc, Gc) -> Gc, Pure;
311    // The neighbourhood counts (§6.4). The `_where` pair calls back into JIT'd
312    // code, so it declares the fault that closure may raise — without it MIR
313    // emits no `CheckFault` and a faulting predicate hands the program a Unit
314    // sentinel typed as an `Int` (ADR-088). The value-comparing pair calls
315    // nothing and only boxes its answer.
316    GridCount4 = "praxis_grid_count4": (Ctx, Gc, Gc, Gc) -> Gc, Allocates;
317    GridCount4Where = "praxis_grid_count4_where": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
318    GridCount8 = "praxis_grid_count8": (Ctx, Gc, Gc, Gc) -> Gc, Allocates;
319    GridCount8Where = "praxis_grid_count8_where": (Ctx, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
320    // `Grid(w, h, fill)` (ADR-146). The extents arrive boxed where `GridNew`'s
321    // arrive raw, because these two come from lowered argument expressions and
322    // a `RawI64` would cost an `ExtractScalar` apiece; `GridNew`'s are `iconst`
323    // immediates with no local to unbox. It faults for `GridNew`'s reason —
324    // `GridExtent::new` refuses a negative or oversized extent — and for that
325    // reason only, since an explicit fill is the one thing `default_cell`
326    // cannot invent.
327    GridFilled = "praxis_grid_filled": (Ctx, Ptr, Gc, Gc, Gc) -> Gc, AllocatesAndFaults;
328    GridFind = "praxis_grid_find": (Ctx, Gc, Gc) -> Gc, Allocates;
329    GridFindAll = "praxis_grid_find_all": (Ctx, Gc, Gc) -> Gc, Allocates;
330    GridGet = "praxis_grid_get": (Ctx, Gc, Gc, Gc) -> Gc, Faults;
331    GridHeight = "praxis_grid_height": (Ctx, Gc) -> Gc, Allocates;
332    GridNeighbors4 = "praxis_grid_neighbors4": (Ctx, Gc, Gc) -> Gc, Allocates;
333    GridNeighbors8 = "praxis_grid_neighbors8": (Ctx, Gc, Gc) -> Gc, Allocates;
334    GridNew = "praxis_grid_new": (Ctx, Ptr, RawI64, RawI64) -> Gc, AllocatesAndFaults;
335    GridPositions = "praxis_grid_positions": (Ctx, Gc) -> Gc, Allocates;
336    GridRotateLeft = "praxis_grid_rotate_left": (Ctx, Gc) -> Gc, Allocates;
337    GridRotateRight = "praxis_grid_rotate_right": (Ctx, Gc) -> Gc, Allocates;
338    GridRow = "praxis_grid_row": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
339    GridSet = "praxis_grid_set": (Ctx, Gc, Gc, Gc, Gc) -> GcUnit, Faults;
340    GridTranspose = "praxis_grid_transpose": (Ctx, Gc) -> Gc, Allocates;
341    GridWidth = "praxis_grid_width": (Ctx, Gc) -> Gc, Allocates;
342    IntAbs = "praxis_int_abs": (Ctx, Gc) -> Gc, AllocatesAndFaults;
343    IntAdd = "praxis_int_add": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
344    IntCheckedAdd = "praxis_int_checked_add": (Ctx, Gc, Gc) -> Gc, Allocates;
345    IntCheckedMul = "praxis_int_checked_mul": (Ctx, Gc, Gc) -> Gc, Allocates;
346    IntCheckedSub = "praxis_int_checked_sub": (Ctx, Gc, Gc) -> Gc, Allocates;
347    IntClamp = "praxis_int_clamp": (Ctx, Gc, Gc, Gc) -> Gc, Faults;
348    IntDiv = "praxis_int_div": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
349    IntEq = "praxis_int_eq": (Ctx, Gc, Gc) -> Gc, Pure;
350    IntGcd = "praxis_int_gcd": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
351    IntGe = "praxis_int_ge": (Ctx, Gc, Gc) -> Gc, Pure;
352    IntGt = "praxis_int_gt": (Ctx, Gc, Gc) -> Gc, Pure;
353    IntLcm = "praxis_int_lcm": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
354    IntLe = "praxis_int_le": (Ctx, Gc, Gc) -> Gc, Pure;
355    IntLoad = "praxis_int_load": (Ctx, Gc) -> RawI64, Pure;
356    IntLt = "praxis_int_lt": (Ctx, Gc, Gc) -> Gc, Pure;
357    IntMax = "praxis_int_max": (Ctx, Gc, Gc) -> Gc, Pure;
358    IntMin = "praxis_int_min": (Ctx, Gc, Gc) -> Gc, Pure;
359    IntMul = "praxis_int_mul": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
360    IntNe = "praxis_int_ne": (Ctx, Gc, Gc) -> Gc, Pure;
361    IntNeg = "praxis_int_neg": (Ctx, Gc) -> Gc, AllocatesAndFaults;
362    IntRem = "praxis_int_rem": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
363    IntSaturatingAdd = "praxis_int_saturating_add": (Ctx, Gc, Gc) -> Gc, Allocates;
364    IntSaturatingMul = "praxis_int_saturating_mul": (Ctx, Gc, Gc) -> Gc, Allocates;
365    IntSaturatingSub = "praxis_int_saturating_sub": (Ctx, Gc, Gc) -> Gc, Allocates;
366    IntSign = "praxis_int_sign": (Ctx, Gc) -> Gc, Allocates;
367    IntSub = "praxis_int_sub": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
368    IntToChar = "praxis_int_to_char": (Ctx, Gc) -> Gc, AllocatesAndFaults;
369    IntToFloat = "praxis_int_to_float": (Ctx, Gc) -> Gc, Allocates;
370    // `Allocates`, for the reason recorded on `CharToText`: every `i64` renders.
371    IntToText = "praxis_int_to_text": (Ctx, Gc) -> Gc, Allocates;
372    IntWrappingAdd = "praxis_int_wrapping_add": (Ctx, Gc, Gc) -> Gc, Allocates;
373    IntWrappingMul = "praxis_int_wrapping_mul": (Ctx, Gc, Gc) -> Gc, Allocates;
374    IntWrappingSub = "praxis_int_wrapping_sub": (Ctx, Gc, Gc) -> Gc, Allocates;
375    MapContains = "praxis_map_contains": (Ctx, Gc, Gc) -> Gc, Pure;
376    RangeGet = "praxis_range_get": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
377    RangeLen = "praxis_range_len": (Ctx, Gc) -> Gc, AllocatesAndFaults;
378    RangeNew = "praxis_range_new": (Ctx, Gc, Gc) -> Gc, Allocates;
379    RangeNewInclusive = "praxis_range_new_inclusive": (Ctx, Gc, Gc) -> Gc, Allocates;
380    MapGet = "praxis_map_get": (Ctx, Gc, Gc) -> Gc, Allocates;
381    MapIndex = "praxis_map_index": (Ctx, Gc, Gc) -> Gc, Faults;
382    MapInsert = "praxis_map_insert": (Ctx, Gc, Gc, Gc) -> GcUnit, Allocates;
383    MapIsEmpty = "praxis_map_is_empty": (Ctx, Gc) -> Gc, Pure;
384    MapKeys = "praxis_map_keys": (Ctx, Gc) -> Gc, Allocates;
385    MapLen = "praxis_map_len": (Ctx, Gc) -> Gc, Allocates;
386    MapNew = "praxis_map_new": (Ctx, Ptr) -> Gc, Allocates;
387    MapRemove = "praxis_map_remove": (Ctx, Gc, Gc) -> GcUnit, Pure;
388    MapUpdateMax = "praxis_map_update_max": (Ctx, Gc, Gc, Gc) -> GcUnit, Allocates;
389    MapValues = "praxis_map_values": (Ctx, Gc) -> Gc, Allocates;
390    MapUpdateMin = "praxis_map_update_min": (Ctx, Gc, Gc, Gc) -> GcUnit, Allocates;
391    MaxHeapIsEmpty = "praxis_max_heap_is_empty": (Ctx, Gc) -> Gc, Pure;
392    MaxHeapItems = "praxis_max_heap_items": (Ctx, Gc) -> Gc, Allocates;
393    MaxHeapLen = "praxis_max_heap_len": (Ctx, Gc) -> Gc, Allocates;
394    MaxHeapNew = "praxis_max_heap_new": (Ctx, Ptr) -> Gc, Allocates;
395    MaxHeapPeek = "praxis_max_heap_peek": (Ctx, Gc) -> Gc, Faults;
396    MaxHeapPop = "praxis_max_heap_pop": (Ctx, Gc) -> Gc, Faults;
397    MaxHeapPush = "praxis_max_heap_push": (Ctx, Gc, Gc) -> GcUnit, Allocates;
398    MinHeapIsEmpty = "praxis_min_heap_is_empty": (Ctx, Gc) -> Gc, Pure;
399    MinHeapItems = "praxis_min_heap_items": (Ctx, Gc) -> Gc, Allocates;
400    MinHeapLen = "praxis_min_heap_len": (Ctx, Gc) -> Gc, Allocates;
401    MinHeapNew = "praxis_min_heap_new": (Ctx, Ptr) -> Gc, Allocates;
402    MinHeapPeek = "praxis_min_heap_peek": (Ctx, Gc) -> Gc, Faults;
403    MinHeapPop = "praxis_min_heap_pop": (Ctx, Gc) -> Gc, Faults;
404    MinHeapPush = "praxis_min_heap_push": (Ctx, Gc, Gc) -> GcUnit, Allocates;
405    Panic = "praxis_panic": (Ctx, Gc) -> GcUnit, Faults;
406    RaiseDivByZeroIf = "praxis_raise_div_by_zero_if": (Ctx, RawI64) -> Void, Faults;
407    RaiseEmptyCollection = "praxis_raise_empty_collection": (Ctx) -> GcUnit, Faults;
408    RaiseIntOverflowIf = "praxis_raise_int_overflow_if": (Ctx, RawI64) -> Void, Faults;
409    RaiseStackOverflow = "praxis_raise_stack_overflow": (Ctx) -> Void, Faults;
410    RecordField = "praxis_record_field": (Ctx, Gc, RawU32) -> Gc, Pure;
411    RecordSetField = "praxis_record_set_field": (Ctx, Gc, RawU32, Gc) -> Gc, Pure;
412    RunParser = "praxis_run_parser": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
413    SetContains = "praxis_set_contains": (Ctx, Gc, Gc) -> Gc, Pure;
414    SetInsert = "praxis_set_insert": (Ctx, Gc, Gc) -> GcUnit, Allocates;
415    SetIsEmpty = "praxis_set_is_empty": (Ctx, Gc) -> Gc, Pure;
416    SetItems = "praxis_set_items": (Ctx, Gc) -> Gc, Allocates;
417    SetLen = "praxis_set_len": (Ctx, Gc) -> Gc, Allocates;
418    SetNew = "praxis_set_new": (Ctx, Ptr) -> Gc, Allocates;
419    SetRemove = "praxis_set_remove": (Ctx, Gc, Gc) -> GcUnit, Pure;
420    SnapshotDebugChain = "praxis_snapshot_debug_chain": (Ctx) -> Void, Pure;
421    StructEq = "praxis_struct_eq": (Ctx, Gc, Gc) -> RawI64, Pure;
422    TextConcat = "praxis_text_concat": (Ctx, Gc, Gc) -> Gc, Allocates;
423    TextGet = "praxis_text_get": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
424    TextFloat = "praxis_text_float": (Ctx, Gc) -> Gc, Allocates;
425    TextInt = "praxis_text_int": (Ctx, Gc) -> Gc, Allocates;
426    TextIsEmpty = "praxis_text_is_empty": (Ctx, Gc) -> Gc, Pure;
427    TextLen = "praxis_text_len": (Ctx, Gc) -> Gc, Allocates;
428    TupleGet = "praxis_tuple_get": (Ctx, Gc, RawI64) -> Gc, Pure;
429    TupleSet = "praxis_tuple_set": (Ctx, Gc, RawI64, Gc) -> Gc, Pure;
430    ValueCmp = "praxis_value_cmp": (Ctx, Gc, Gc) -> RawI64, Faults;
431    // The one wrapper an interpolation hole lowers to (ADR-147). `Allocates`
432    // and not `AllocatesAndFaults`: every `GcRef` has a descriptor with a
433    // `format` callback, so there is no value it can be handed that it cannot
434    // render, and a `String` built by `format` is UTF-8 by construction. That is
435    // `TextConcat`'s row, for the same two reasons.
436    ValueToText = "praxis_value_to_text": (Ctx, Gc) -> Gc, Allocates;
437    VarCellGet = "praxis_var_cell_get": (Ctx, Gc) -> Gc, Pure;
438    VarCellSet = "praxis_var_cell_set": (Ctx, Gc, Gc) -> Gc, Pure;
439    // `chunks(n)` and `windows(n)` (ADR-149). The pair that answers `Vec[Vec[T]]`,
440    // and the two rows in this manifest that fault on an **argument** rather than
441    // on an element: a run of `n <= 0` elements is not a short run, it is not a
442    // run, so `InvalidSize` is raised before either walks anything. They read no
443    // descriptor callback — the grouping is by position — so that fault is the
444    // only one either has, which is what makes them `AllocatesAndFaults` where
445    // `VecReversed` beside them is `Allocates`.
446    VecChunks = "praxis_vec_chunks": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
447    // `Vec(n, fill)` (ADR-146). `VecNew` beneath it only allocates; this one
448    // faults, because a count is a runtime `Int` and `VecExtent::new` refuses a
449    // negative or oversized one (ADR-041 decision 1).
450    VecFilled = "praxis_vec_filled": (Ctx, Ptr, Gc, Gc) -> Gc, AllocatesAndFaults;
451    VecFrequencies = "praxis_vec_frequencies": (Ctx, Gc) -> Gc, Allocates;
452    VecGet = "praxis_vec_get": (Ctx, Gc, Gc) -> Gc, Faults;
453    VecIsEmpty = "praxis_vec_is_empty": (Ctx, Gc) -> Gc, Pure;
454    // `join` and `to_text` fault for `praxis_vec_sorted`'s reason and not for
455    // `sorted`'s cause: the catalog row bounds the item to `Text` (or to `Char`),
456    // so an element of another type is a compiler bug — and the honest way to
457    // report one is `TypeMismatch`, not reading a foreign payload as a `Text`
458    // (ADR-144).
459    VecJoin = "praxis_vec_join": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
460    VecLen = "praxis_vec_len": (Ctx, Gc) -> Gc, Allocates;
461    VecNew = "praxis_vec_new": (Ctx, Ptr) -> Gc, Allocates;
462    VecPush = "praxis_vec_push": (Ctx, Gc, Gc) -> GcUnit, AllocatesAndFaults;
463    // `reversed` reads no descriptor callback at all — not `compare`, not
464    // `equals`, not `hash` — so there is nothing it can be handed that it cannot
465    // reverse (ADR-145). That is why it is `Allocates` where `VecSorted` beneath
466    // it is not.
467    VecReversed = "praxis_vec_reversed": (Ctx, Gc) -> Gc, Allocates;
468    VecSet = "praxis_vec_set": (Ctx, Gc, Gc, Gc) -> GcUnit, Faults;
469    // `sorted` faults and `unique` does not, and the difference is derived from
470    // the wrappers rather than guessed: `praxis_vec_sorted` raises
471    // `TypeMismatch` when the element type has no `compare`, while
472    // `praxis_vec_unique` and `praxis_vec_frequencies` go through `DynamicKey`,
473    // which answers "not equal" for a type with no `equals` instead of raising.
474    VecSorted = "praxis_vec_sorted": (Ctx, Gc) -> Gc, AllocatesAndFaults;
475    // The key extractor is called once per element and it is arbitrary Praxis
476    // code, so this faults for two reasons where `praxis_vec_sorted` faults for
477    // one: an unorderable key, and whatever the closure itself raised.
478    VecSortedByKey = "praxis_vec_sorted_by_key": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
479    VecToText = "praxis_vec_to_text": (Ctx, Gc) -> Gc, AllocatesAndFaults;
480    VecUnique = "praxis_vec_unique": (Ctx, Gc) -> Gc, Allocates;
481    // The sliding half of `VecChunks`'s pair; see that row for why it faults.
482    VecWindows = "praxis_vec_windows": (Ctx, Gc, Gc) -> Gc, AllocatesAndFaults;
483    WriteStdout = "praxis_write_stdout": (Ctx, Gc) -> GcUnit, Pure;
484}
485
486/// Build-time coverage of the effect table.
487///
488/// The manifest is the one answer to "does calling this allocate or fault" — a
489/// per-catalog-row `bool` would be a second one, free to drift — and this walks
490/// every row *at compile time* so a symbol can neither be added without an
491/// effect nor left out of [`RuntimeSymbol::ALL`], which is what the rest of the
492/// workspace iterates.
493///
494/// Anything checkable statically is checked here rather than in a test: a
495/// classification error should fail the build, not a test run.
496const _: () = {
497    // `ALL` is generated from the same rows as the enum, so a non-empty `ALL`
498    // that ends at the last variant means every variant is present.
499    assert!(!RuntimeSymbol::ALL.is_empty());
500
501    let mut i = 0;
502    while i < RuntimeSymbol::ALL.len() {
503        let sym = RuntimeSymbol::ALL[i];
504        let sig = sym.sig();
505
506        // Every wrapper leads with the context pointer. Without it there is no
507        // route to the heap, the fault slot or the root set — so a wrapper
508        // lacking one could be neither a safepoint nor a faulting call, and any
509        // effect other than `Pure` would be a lie.
510        assert!(matches!(sig.params[0], AbiKind::Ctx));
511
512        // A wrapper that returns nothing produced no object, so `Allocates`
513        // would misclassify it — and `Allocates` is exactly what makes a call
514        // site a safepoint that the caller must spill its live roots across.
515        assert!(!(matches!(sig.ret, AbiRet::Void) && sig.effect.allocates()));
516
517        // The two queries partition the four variants; `allocates`/`faults`
518        // must agree with the row rather than being independently answerable.
519        assert!(sig.effect.allocates() == sym.allocates());
520        assert!(sig.effect.faults() == sym.faults());
521
522        // `GcUnit` gets no check here on purpose. The invariant it exists for
523        // relates a manifest row to a *catalog* row — a non-faulting wrapper
524        // with a non-`Unit` result type must not be able to answer the sentinel
525        // — and the catalog is built at run time, so the check lives in
526        // `builtins::tests::a_non_faulting_row_with_a_value_result_\
527        // cannot_answer_the_unit_sentinel`.
528
529        i += 1;
530    }
531};
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536    use std::collections::HashSet;
537
538    /// The manifest is a bijection between variants and linker names. A typo
539    /// that duplicated a name would otherwise make two symbols resolve to one
540    /// address.
541    #[test]
542    fn names_are_unique_and_well_formed() {
543        let mut seen = HashSet::new();
544        for &sym in RuntimeSymbol::ALL {
545            assert!(
546                sym.name().starts_with("praxis_"),
547                "{sym} is not a praxis_* symbol"
548            );
549            assert!(seen.insert(sym.name()), "duplicate symbol name {sym}");
550        }
551        assert_eq!(seen.len(), RuntimeSymbol::ALL.len());
552    }
553
554    /// `ALL` must list every variant. It is generated from the same rows as the
555    /// enum, so this is really a check that the macro was not edited apart.
556    #[test]
557    fn from_name_round_trips_every_symbol() {
558        for &sym in RuntimeSymbol::ALL {
559            assert_eq!(RuntimeSymbol::from_name(sym.name()), Some(sym));
560        }
561        assert_eq!(RuntimeSymbol::from_name("praxis_not_a_symbol"), None);
562    }
563
564    /// Every wrapper takes the context pointer first: the fault slot, the heap
565    /// and the root set all hang off it, so a wrapper without it could not
566    /// allocate, fault or be a safepoint.
567    #[test]
568    fn every_symbol_leads_with_the_context_pointer() {
569        for &sym in RuntimeSymbol::ALL {
570            let sig = sym.sig();
571            assert_eq!(
572                sig.params.first(),
573                Some(&AbiKind::Ctx),
574                "{sym} does not take ctx first"
575            );
576            assert!(
577                !sig.params[1..].contains(&AbiKind::Ctx),
578                "{sym} takes ctx more than once"
579            );
580        }
581    }
582
583    #[test]
584    fn effect_queries_agree_with_the_variants() {
585        assert!(!Effect::Pure.allocates() && !Effect::Pure.faults());
586        assert!(!Effect::Faults.allocates() && Effect::Faults.faults());
587        assert!(Effect::Allocates.allocates() && !Effect::Allocates.faults());
588        assert!(Effect::AllocatesAndFaults.allocates() && Effect::AllocatesAndFaults.faults());
589    }
590
591    /// **A standing invariant:** none of the nine overflow alternatives may be
592    /// declared faulting.
593    ///
594    /// What it catches is an edit marking one `AllocatesAndFaults`, which would
595    /// make MIR emit a `CheckFault` after a call that never faults and quietly
596    /// undo the one property that makes these methods alternatives to a faulting
597    /// operator at all.
598    #[test]
599    fn no_overflow_alternative_declares_that_it_faults() {
600        use RuntimeSymbol::*;
601        for sym in [
602            IntWrappingAdd,
603            IntSaturatingAdd,
604            IntCheckedAdd,
605            IntWrappingSub,
606            IntSaturatingSub,
607            IntCheckedSub,
608            IntWrappingMul,
609            IntSaturatingMul,
610            IntCheckedMul,
611        ] {
612            assert_eq!(
613                sym.sig().effect,
614                Effect::Allocates,
615                "`{}` answers a fresh number and cannot fault (§4.12)",
616                sym.name()
617            );
618        }
619    }
620
621    /// **ADR-143.** All three `to_text` wrappers allocate and none of them
622    /// faults.
623    ///
624    /// Pinned for `no_overflow_alternative_declares_that_it_faults`'s reason:
625    /// the wrong answer here is silent. `MethodEntry::can_fault` reads the
626    /// manifest, so a row copied from `FloatToInt` instead of `FloatToText`
627    /// would make every `n.to_text()` emit a `CheckFault` that can never fire,
628    /// and nothing about the program's behaviour would say so.
629    #[test]
630    fn the_to_text_family_allocates_and_cannot_fault() {
631        for sym in [
632            RuntimeSymbol::IntToText,
633            RuntimeSymbol::FloatToText,
634            RuntimeSymbol::CharToText,
635        ] {
636            assert_eq!(
637                sym.sig().effect,
638                Effect::Allocates,
639                "`{}` renders a payload validated at construction and answers a \
640                 fresh Text; there is nothing for it to fault on",
641                sym.name()
642            );
643        }
644    }
645
646    /// **ADR-147.** An interpolation hole's wrapper allocates and cannot fault,
647    /// and it is `WriteStdout`'s renderer with `TextConcat`'s effect.
648    ///
649    /// The contrast is the assertion. `praxis_write_stdout` is `Pure` because it
650    /// allocates nothing at all; `praxis_value_to_text` does the same rendering
651    /// and then allocates a `Text`, so it is `Allocates` — and it is not
652    /// `AllocatesAndFaults`, because every `GcRef` has a descriptor with a
653    /// `format` callback and a `String` built by one is UTF-8 by construction.
654    /// Declaring it faulting would put a `CheckFault` after every hole in every
655    /// interpolated literal that can never fire, and nothing about the program's
656    /// behaviour would say so.
657    #[test]
658    fn a_holes_renderer_allocates_and_cannot_fault() {
659        assert_eq!(RuntimeSymbol::ValueToText.sig().effect, Effect::Allocates);
660        assert_eq!(RuntimeSymbol::TextConcat.sig().effect, Effect::Allocates);
661        // `out` renders through the same callback and allocates nothing, which
662        // is why the two rows differ at all.
663        assert_eq!(RuntimeSymbol::WriteStdout.sig().effect, Effect::Pure);
664    }
665
666    /// **ADR-145.** `reversed` reads no descriptor callback, so its row is
667    /// `Allocates` where its two neighbours are not.
668    ///
669    /// The contrast is the assertion. `sorted` faults because `compare` may be
670    /// absent; `reversed` has nothing to ask for, and marking it faulting to
671    /// match the barrier beside it would put a dead check after every call.
672    #[test]
673    fn reversal_cannot_fault_where_ordering_can() {
674        assert_eq!(RuntimeSymbol::VecReversed.sig().effect, Effect::Allocates);
675        assert_eq!(
676            RuntimeSymbol::VecSorted.sig().effect,
677            Effect::AllocatesAndFaults,
678            "the neighbour this is contrasted with still orders through `compare`"
679        );
680    }
681
682    /// **ADR-149.** A grouping declares that it faults, where the barrier it
683    /// most resembles does not.
684    ///
685    /// `reversed` and a grouping read the same nothing of their elements, so the
686    /// obvious tidying is to give them the same effect. They must not have it:
687    /// a grouping refuses a size of zero or less, and a row marked `Allocates`
688    /// would emit no `CheckFault` — the fault would be set into a context
689    /// nothing reads, and `chunks(0)` would answer a Unit sentinel typed as a
690    /// `Vec[Vec[T]]` (ADR-088).
691    #[test]
692    fn a_grouping_declares_the_fault_a_reversal_has_not() {
693        for sym in [RuntimeSymbol::VecChunks, RuntimeSymbol::VecWindows] {
694            assert_eq!(sym.sig().effect, Effect::AllocatesAndFaults, "{sym}");
695            assert_eq!(
696                sym.sig().params.len(),
697                3,
698                "{sym} takes the context, the receiver and the size"
699            );
700        }
701        assert_eq!(
702            RuntimeSymbol::VecReversed.sig().effect,
703            Effect::Allocates,
704            "the neighbour this is contrasted with still has nothing to refuse"
705        );
706    }
707
708    /// **ADR-146.** A sized constructor declares that it faults, and its
709    /// nullary neighbour is the contrast.
710    ///
711    /// This row is what makes a negative size *observable*: MIR emits a
712    /// `CheckFault` after an allocation only when a wrapper it reaches declares
713    /// a fault, so a `Vec(n, fill)` marked `Allocates` would set `InvalidSize`
714    /// into a context nothing ever reads and hand the program a Unit sentinel
715    /// typed as a `Vec` (ADR-088).
716    ///
717    /// The arity is asserted too, because the descriptor slot and the fill are
718    /// what distinguish these from the nullary wrappers: a row that grew an
719    /// extent without growing its wrapper would pass garbage in an unfilled
720    /// slot.
721    #[test]
722    fn a_sized_constructor_declares_that_it_faults() {
723        for sym in [RuntimeSymbol::VecFilled, RuntimeSymbol::GridFilled] {
724            assert_eq!(
725                sym.sig().effect,
726                Effect::AllocatesAndFaults,
727                "`{}` refuses a negative or oversized extent, and only a \
728                 declared fault gets a `CheckFault` to observe it",
729                sym.name()
730            );
731        }
732        assert_eq!(
733            RuntimeSymbol::VecNew.sig().effect,
734            Effect::Allocates,
735            "the empty form has no size to refuse, and marking it faulting \
736             would put a dead check after every `Vec()`"
737        );
738        // (ctx, descriptor, count, fill) and (ctx, descriptor, w, h, fill).
739        assert_eq!(
740            RuntimeSymbol::VecFilled.sig().params,
741            &[AbiKind::Ctx, AbiKind::Ptr, AbiKind::Gc, AbiKind::Gc]
742        );
743        assert_eq!(
744            RuntimeSymbol::GridFilled.sig().params,
745            &[
746                AbiKind::Ctx,
747                AbiKind::Ptr,
748                AbiKind::Gc,
749                AbiKind::Gc,
750                AbiKind::Gc
751            ]
752        );
753    }
754
755    /// **ADR-111.** `praxis_alloc_text` trusts its bytes, and the row is where
756    /// that is said.
757    ///
758    /// The UTF-8 requirement is the caller's precondition, not a runtime
759    /// judgement: the compiler's bytes come from a Rust `String` unbroken from
760    /// `Lit::Text` through `Generation::alloc_str`, and the one runtime caller
761    /// that holds raw host bytes (`praxis_get_input`) validates them itself. A
762    /// violation panics into `abi_guard!` and aborts; it never sets a fault.
763    ///
764    /// Written as an assertion rather than left to the manifest because the row
765    /// is read by three things at once and only this one is visible: it decides
766    /// whether `Inst::Alloc { AllocKind::Text }` is followed by a `CheckFault`
767    /// (ADR-088), whether a `Text` literal in a loop is hoisted into the
768    /// preheader (ADR-108 §3), and whether `panic_fault_is_observable` lets the
769    /// wrapper's panic path abort. An edit marking it faulting would silently
770    /// add 41 corpus checks back, un-hoist every `Text` literal, and make the
771    /// abort a fault — this makes it a failing test instead.
772    #[test]
773    fn alloc_text_trusts_its_bytes_and_the_row_says_so() {
774        assert_eq!(
775            RuntimeSymbol::AllocText.sig().effect,
776            Effect::Allocates,
777            "`praxis_alloc_text`'s UTF-8 requirement is its caller's precondition \
778             (ADR-111); declaring it faulting puts a check back after every text \
779             literal and takes `Text` back out of the ADR-108 hoist"
780        );
781        // And the wrapper that owns the fault declares it, so the requirement
782        // is enforced somewhere rather than nowhere.
783        assert!(
784            RuntimeSymbol::GetInput.faults(),
785            "`praxis_get_input` holds raw host bytes and raises `InvalidText` \
786             itself, so the fault still lands at the `read`"
787        );
788    }
789
790    /// Spot-check the rows the compiler is most sensitive to: the two that take
791    /// a narrow `u32`, where passing an `i64` is the mismatch this manifest
792    /// exists to prevent, and the arithmetic wrappers whose fault-and-allocate
793    /// pair drives both the safepoint and the fault check.
794    #[test]
795    fn narrow_and_faulting_rows_are_recorded_exactly() {
796        assert_eq!(
797            RuntimeSymbol::RecordField.sig().params,
798            &[AbiKind::Ctx, AbiKind::Gc, AbiKind::RawU32]
799        );
800        assert_eq!(
801            RuntimeSymbol::RecordSetField.sig().params,
802            &[AbiKind::Ctx, AbiKind::Gc, AbiKind::RawU32, AbiKind::Gc]
803        );
804
805        for sym in [
806            RuntimeSymbol::IntAdd,
807            RuntimeSymbol::IntSub,
808            RuntimeSymbol::IntMul,
809            RuntimeSymbol::IntDiv,
810            RuntimeSymbol::IntRem,
811            RuntimeSymbol::IntNeg,
812        ] {
813            assert_eq!(sym.sig().effect, Effect::AllocatesAndFaults, "{sym}");
814        }
815        // Comparisons hand back an immortal Bool: no collection can happen
816        // inside them, so they are not safepoints.
817        for sym in [
818            RuntimeSymbol::IntEq,
819            RuntimeSymbol::IntLt,
820            RuntimeSymbol::IntGe,
821        ] {
822            assert_eq!(sym.sig().effect, Effect::Pure, "{sym}");
823        }
824    }
825}