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