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