Skip to main content

praxis_stdlib/
builtins.rs

1//! The built-in method catalog (§16.2): one structured table of every built-in
2//! method.
3//!
4//! This is the **single source of truth** the type checker, HIR lowering, and
5//! code generator consume (rule 20.3). [`builtin_catalog`] returns a finalized,
6//! duplicate-free [`MethodCatalog`]; the builder rejects any duplicate
7//! `(receiver, name, arity)` triple so an accidental overload is impossible
8//! ("make illegal states unrepresentable").
9
10use crate::abi;
11use crate::catalog::MethodCatalog;
12use crate::type_pattern::{CollectionCtor, ScalarType};
13use crate::{MethodEntry, MethodLowering, Purity, TypePattern};
14
15/// The `Vec[T]` receiver pattern, used by every Vec method entry.
16fn vec_of_t() -> TypePattern {
17    TypePattern::Collection {
18        ctor: CollectionCtor::Vec,
19        args: vec![TypePattern::var("T")],
20    }
21}
22
23/// Build the finalized built-in method catalog.
24///
25/// # Panics
26/// Panics if two entries share a `(receiver, name, arity)` triple — that is a
27/// build-time catalog bug, never a user-facing condition.
28#[must_use]
29pub fn builtin_catalog() -> MethodCatalog {
30    MethodCatalog::build()
31        .entry(vec_push())
32        .entry(vec_len())
33        .entry(vec_get())
34        .entry(vec_is_empty())
35        .entry(vec_to_text())
36        .entry(deque_push_front())
37        .entry(deque_push_back())
38        .entry(deque_pop_front())
39        .entry(deque_pop_back())
40        .entry(deque_len())
41        .entry(deque_get())
42        .entry(deque_is_empty())
43        .entry(map_insert())
44        .entry(map_get())
45        .entry(map_contains())
46        .entry(map_remove())
47        .entry(map_len())
48        .entry(map_is_empty())
49        .entry(set_insert())
50        .entry(set_remove())
51        .entry(set_contains())
52        .entry(set_len())
53        .entry(set_is_empty())
54        .entry(counter_get())
55        .entry(counter_inc())
56        .entry(counter_len())
57        .entry(counter_is_empty())
58        .entry(max_heap_push())
59        .entry(max_heap_pop())
60        .entry(max_heap_peek())
61        .entry(max_heap_len())
62        .entry(max_heap_is_empty())
63        .entry(min_heap_push())
64        .entry(min_heap_pop())
65        .entry(min_heap_peek())
66        .entry(min_heap_len())
67        .entry(min_heap_is_empty())
68        .entry(bitset_insert())
69        .entry(bitset_remove())
70        .entry(bitset_contains())
71        .entry(bitset_len())
72        .entry(bitset_is_empty())
73        .entry(grid_width())
74        .entry(grid_height())
75        .entry(grid_get())
76        .entry(grid_set())
77        .entry(grid_contains())
78        .entry(grid_neighbors4())
79        .entry(grid_neighbors8())
80        .entry(grid_around4())
81        .entry(grid_around8())
82        .entry(grid_count4())
83        .entry(grid_count8())
84        .entry(grid_count4_where())
85        .entry(grid_count8_where())
86        .entry(grid_positions())
87        .entry(grid_cells())
88        .entry(grid_row())
89        .entry(grid_column())
90        .entry(grid_find())
91        .entry(grid_find_all())
92        .entry(grid_transpose())
93        .entry(grid_rotate_left())
94        .entry(grid_rotate_right())
95        // Pipeline combinators (§6.3, ADR-127). Intrinsics the compiler fuses
96        // into a single loop over the source. **One row apiece**, on the generic
97        // `Iterable` receiver: `capability::iter_item` already answers "what can
98        // I iterate and what does it yield" for eleven collections plus `Text`,
99        // and the pipeline reads that rather than keeping a second, smaller
100        // answer of its own.
101        .entry(seq_map())
102        .entry(seq_filter())
103        .entry(seq_fold())
104        .entry(seq_sum())
105        .entry(seq_count())
106        .entry(seq_count_if())
107        // The barrier combinators (§6.3). Runtime symbols rather than
108        // intrinsics — see the block comment above their definitions.
109        .entry(seq_sorted())
110        .entry(seq_sorted_by_key())
111        .entry(seq_unique())
112        .entry(seq_reversed())
113        .entry(seq_frequencies())
114        .entry(seq_join())
115        // The two groupings (ADR-149). Barriers too, and the only rows whose
116        // result nests a collection inside a collection.
117        .entry(seq_chunks())
118        .entry(seq_windows())
119        // The remaining non-barrier combinators. Each is an intrinsic fused by
120        // the MIR pipeline recognizer.
121        .entry(seq_take())
122        .entry(seq_skip())
123        .entry(seq_take_while())
124        .entry(seq_enumerate())
125        .entry(seq_zip())
126        .entry(seq_flat_map())
127        .entry(seq_filter_map())
128        .entry(seq_product())
129        .entry(seq_min())
130        .entry(seq_max())
131        .entry(seq_min_by())
132        .entry(seq_max_by())
133        .entry(seq_any())
134        .entry(seq_all())
135        .entry(seq_find())
136        .entry(seq_position())
137        .entry(seq_reduce())
138        // The conversions (ADR-127 decision 4). Fused sinks, one per collection
139        // with a constructor — a pipeline's currency is `Vec`, and a program
140        // that wants a collection back says which one.
141        .entry(seq_to_vec())
142        .entry(seq_to_set())
143        .entry(seq_to_map())
144        .entry(seq_to_counter())
145        .entry(seq_to_deque())
146        .entry(seq_to_min_heap())
147        .entry(seq_to_max_heap())
148        .entry(seq_to_bitset())
149        .entry(text_len())
150        .entry(text_int())
151        .entry(text_float())
152        .entry(text_is_empty())
153        .entry(text_get())
154        // Float methods (§4.12). Pure unary math, predicates, conversions, and
155        // binary min/max — all lower to `praxis_float_*` runtime wrappers.
156        .entry(float_abs())
157        .entry(float_sqrt())
158        .entry(float_floor())
159        .entry(float_ceil())
160        .entry(float_round())
161        .entry(float_sign())
162        .entry(float_to_int())
163        .entry(float_to_text())
164        .entry(float_is_nan())
165        .entry(float_is_infinite())
166        .entry(float_min())
167        .entry(float_max())
168        // The explicit Int→Float widening method (§4.12).
169        .entry(int_to_float())
170        // The Char/Int conversion pair (ADR-086), written as a pair for the
171        // reason §4.12 writes Float.to_int/Int.to_float as one.
172        .entry(char_to_int())
173        .entry(int_to_char())
174        // The other two thirds of the `to_text` family (ADR-143). `Float`'s row
175        // is above; all three share one renderer with `out`.
176        .entry(int_to_text())
177        .entry(char_to_text())
178        .entry(int_wrapping_add())
179        .entry(int_saturating_add())
180        .entry(int_checked_add())
181        .entry(int_wrapping_sub())
182        .entry(int_saturating_sub())
183        .entry(int_checked_sub())
184        .entry(int_wrapping_mul())
185        .entry(int_saturating_mul())
186        .entry(int_checked_mul())
187        // Subscripts (§4.7/§6.2/§6.4). Six collections read; five of those six
188        // also store — every one but the immutable `Text`. See the block comment
189        // above `vec_index` for why these are catalog rows.
190        .entry(vec_index())
191        .entry(vec_index_set())
192        .entry(deque_index())
193        .entry(deque_index_set())
194        .entry(text_index())
195        .entry(map_index())
196        .entry(map_index_set())
197        .entry(counter_index())
198        .entry(counter_index_set())
199        .entry(grid_index())
200        .entry(grid_index_set())
201        // …and the two updating stores §6.2 writes. Map only: they are the two
202        // wrappers that exist, and an absent entry accepting the first value is
203        // a semantics no read-modify-write over the rows above can express,
204        // because a subscript read of an absent key faults (§4.7).
205        .entry(map_index_min())
206        .entry(map_index_max())
207        // Keyed enumeration: §3.3's `counts.values()`, plus the `Map` siblings.
208        .entry(counter_keys())
209        .entry(counter_values())
210        .entry(map_keys())
211        .entry(map_values())
212        .finish()
213        .expect("built-in catalog must be duplicate-free")
214}
215
216// --- Keyed enumeration -------------------------------------------------------
217//
218// Each answers a `Vec`, so every §6.3 pipeline combinator applies to the result.
219// The order is fixed and deterministic (by the key's rendered form), so `keys()`
220// and `values()` are index-aligned and a program's *answer* cannot depend on a
221// `HashMap`'s per-process seed.
222
223fn counter_keys() -> MethodEntry {
224    MethodEntry {
225        receiver: counter_of_t(),
226        name: "keys",
227        params: vec![],
228        result: TypePattern::Collection {
229            ctor: CollectionCtor::Vec,
230            args: vec![TypePattern::var("T")],
231        },
232        purity: Purity::Pure,
233        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CounterKeys),
234        doc: "Every key, as a `Vec[T]`, ordered with `values()`.",
235    }
236}
237
238fn counter_values() -> MethodEntry {
239    MethodEntry {
240        receiver: counter_of_t(),
241        name: "values",
242        params: vec![],
243        result: TypePattern::Collection {
244            ctor: CollectionCtor::Vec,
245            args: vec![TypePattern::Scalar(ScalarType::Int)],
246        },
247        purity: Purity::Pure,
248        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CounterValues),
249        doc: "Every count, as a `Vec[Int]`, ordered with `keys()`.",
250    }
251}
252
253fn map_keys() -> MethodEntry {
254    MethodEntry {
255        receiver: map_of_k_v(),
256        name: "keys",
257        params: vec![],
258        result: TypePattern::Collection {
259            ctor: CollectionCtor::Vec,
260            args: vec![TypePattern::var("K")],
261        },
262        purity: Purity::Pure,
263        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapKeys),
264        doc: "Every key, as a `Vec[K]`, ordered with `values()`.",
265    }
266}
267
268fn map_values() -> MethodEntry {
269    MethodEntry {
270        receiver: map_of_k_v(),
271        name: "values",
272        params: vec![],
273        result: TypePattern::Collection {
274            ctor: CollectionCtor::Vec,
275            args: vec![TypePattern::var("V")],
276        },
277        purity: Purity::Pure,
278        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapValues),
279        doc: "Every value, as a `Vec[V]`, ordered with `keys()`.",
280    }
281}
282
283// --- Subscript rows (§4.7/§6.2/§6.4) -----------------------------------------
284//
285// `m[key]`, `counts[key] += 1` and `grid[x, y]` dispatch through the catalog on
286// the receiver's shape and the index count, which is what a method call already
287// does. Their names — `[]`, `[]=` — are not identifiers, so no program can spell
288// them; the subscript grammar is their only caller.
289//
290// Which collections index is a language decision and this table is where it is
291// recorded. Six read: `Vec`, `Deque`, `Text`, `Map`, `Counter`, `Grid`. **Five
292// store** — every reader but `Text`, which is immutable (§4.3), so `t[0] = c` is
293// still the report `not_index_assignable` gives.
294//
295// The `Vec` and `Deque` stores go through `praxis_vec_set`/`praxis_deque_set`,
296// and they **replace** and never append: `v[v.len()] = x` is `IndexOutOfBounds`
297// rather than a push, so an off-by-one is reported instead of growing the
298// vector (ADR-064).
299//
300// The read rows repeat their `get` sibling's symbol on purpose — except `Map`,
301// whose two answers differ by design: `.get` returns Unit for an absent key and
302// `map[key]` **faults** (§4.7), so it has its own wrapper.
303
304fn vec_index() -> MethodEntry {
305    MethodEntry {
306        receiver: vec_of_t(),
307        name: crate::catalog::INDEX_READ,
308        params: vec![TypePattern::Scalar(ScalarType::Int)],
309        result: TypePattern::var("T"),
310        purity: Purity::Pure,
311        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecGet),
312        doc: "`v[i]` — the element at `i`; faults if out of range.",
313    }
314}
315
316fn vec_index_set() -> MethodEntry {
317    MethodEntry {
318        receiver: vec_of_t(),
319        name: crate::catalog::INDEX_STORE,
320        params: vec![TypePattern::Scalar(ScalarType::Int), TypePattern::var("T")],
321        result: TypePattern::Unit,
322        purity: Purity::Impure,
323        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecSet),
324        doc: "`v[i] = value` — replace the element at `i`; faults if out of range \
325              (it never appends — `push` is the spelling that grows a vector).",
326    }
327}
328
329fn deque_index() -> MethodEntry {
330    MethodEntry {
331        receiver: deque_of_t(),
332        name: crate::catalog::INDEX_READ,
333        params: vec![TypePattern::Scalar(ScalarType::Int)],
334        result: TypePattern::var("T"),
335        purity: Purity::Pure,
336        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::DequeGet),
337        doc: "`d[i]` — the element at `i` (0-based from the front); faults if out of range.",
338    }
339}
340
341fn deque_index_set() -> MethodEntry {
342    MethodEntry {
343        receiver: deque_of_t(),
344        name: crate::catalog::INDEX_STORE,
345        params: vec![TypePattern::Scalar(ScalarType::Int), TypePattern::var("T")],
346        result: TypePattern::Unit,
347        purity: Purity::Impure,
348        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::DequeSet),
349        doc: "`d[i] = value` — replace the element at `i` (0-based from the front); \
350              faults if out of range (it never inserts).",
351    }
352}
353
354fn text_index() -> MethodEntry {
355    MethodEntry {
356        receiver: text_receiver(),
357        name: crate::catalog::INDEX_READ,
358        params: vec![TypePattern::Scalar(ScalarType::Int)],
359        result: TypePattern::Scalar(ScalarType::Char),
360        purity: Purity::Pure,
361        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::TextGet),
362        doc: "`t[i]` — the `Char` at `i`, indexing by Unicode scalar value and not \
363              by byte; faults if out of range (ADR-086).",
364    }
365}
366
367fn map_index() -> MethodEntry {
368    MethodEntry {
369        receiver: map_of_k_v(),
370        name: crate::catalog::INDEX_READ,
371        params: vec![TypePattern::var("K")],
372        result: TypePattern::var("V"),
373        purity: Purity::Pure,
374        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapIndex),
375        doc: "`m[key]` — the value for `key`; **faults** if absent (§4.7; `.get` is the \
376              spelling that answers with absence).",
377    }
378}
379
380fn map_index_set() -> MethodEntry {
381    MethodEntry {
382        receiver: map_of_k_v(),
383        name: crate::catalog::INDEX_STORE,
384        params: vec![TypePattern::var("K"), TypePattern::var("V")],
385        result: TypePattern::Unit,
386        purity: Purity::Impure,
387        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapInsert),
388        doc: "`m[key] = value` — set `key`, replacing any prior value.",
389    }
390}
391
392/// The `Map[K, V]` receiver of `min=`/`max=`, whose value is bound to `Int`.
393///
394/// A **bound** rather than a literal `Int` argument: the bound *pins* an
395/// unresolved value type instead of merely permitting it, so `var d = Map()`
396/// followed by `d[k] min= 1` gives `d` an `Int` value type rather than
397/// reporting. The bound is what the wrapper needs — `praxis_map_update_min`
398/// compares through `int_payload`, so a `Map[Text, Text]` would read its values
399/// as `i64`s.
400fn map_of_k_int_value() -> TypePattern {
401    TypePattern::Collection {
402        ctor: CollectionCtor::Map,
403        args: vec![
404            TypePattern::var("K"),
405            TypePattern::is_scalar("V", ScalarType::Int),
406        ],
407    }
408}
409
410fn map_index_min() -> MethodEntry {
411    MethodEntry {
412        receiver: map_of_k_int_value(),
413        name: crate::catalog::INDEX_STORE_MIN,
414        params: vec![TypePattern::var("K"), TypePattern::var("V")],
415        result: TypePattern::Unit,
416        purity: Purity::Impure,
417        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapUpdateMin),
418        doc: "`d[key] min= candidate` — keep the smaller value; an absent entry \
419              accepts the first value.",
420    }
421}
422
423fn map_index_max() -> MethodEntry {
424    MethodEntry {
425        receiver: map_of_k_int_value(),
426        name: crate::catalog::INDEX_STORE_MAX,
427        params: vec![TypePattern::var("K"), TypePattern::var("V")],
428        result: TypePattern::Unit,
429        purity: Purity::Impure,
430        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapUpdateMax),
431        doc: "`b[key] max= score` — keep the larger value; an absent entry accepts \
432              the first value.",
433    }
434}
435
436fn counter_index() -> MethodEntry {
437    MethodEntry {
438        receiver: counter_of_t(),
439        name: crate::catalog::INDEX_READ,
440        params: vec![TypePattern::var("T")],
441        result: TypePattern::Scalar(ScalarType::Int),
442        purity: Purity::Pure,
443        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CounterGet),
444        doc: "`c[key]` — the count for `key`, or zero if absent; never faults.",
445    }
446}
447
448fn counter_index_set() -> MethodEntry {
449    MethodEntry {
450        receiver: counter_of_t(),
451        name: crate::catalog::INDEX_STORE,
452        params: vec![TypePattern::var("T"), TypePattern::Scalar(ScalarType::Int)],
453        result: TypePattern::Unit,
454        purity: Purity::Impure,
455        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CounterSet),
456        doc: "`c[key] = n` — set the count for `key`.",
457    }
458}
459
460fn grid_index() -> MethodEntry {
461    MethodEntry {
462        receiver: grid_of_t(),
463        name: crate::catalog::INDEX_READ,
464        params: vec![
465            TypePattern::Scalar(ScalarType::Int),
466            TypePattern::Scalar(ScalarType::Int),
467        ],
468        result: TypePattern::var("T"),
469        purity: Purity::Pure,
470        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridGet),
471        doc: "`grid[x, y]` — the cell at (x, y); faults if out of range.",
472    }
473}
474
475fn grid_index_set() -> MethodEntry {
476    MethodEntry {
477        receiver: grid_of_t(),
478        name: crate::catalog::INDEX_STORE,
479        params: vec![
480            TypePattern::Scalar(ScalarType::Int),
481            TypePattern::Scalar(ScalarType::Int),
482            TypePattern::var("T"),
483        ],
484        result: TypePattern::Unit,
485        purity: Purity::Impure,
486        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridSet),
487        doc: "`grid[x, y] = value` — set the cell at (x, y); faults if out of range.",
488    }
489}
490
491// --- Text methods --------------------------------------------------------
492
493fn text_receiver() -> TypePattern {
494    TypePattern::Scalar(ScalarType::Text)
495}
496
497fn text_len() -> MethodEntry {
498    MethodEntry {
499        receiver: text_receiver(),
500        name: "len",
501        params: vec![],
502        result: TypePattern::Scalar(ScalarType::Int),
503        purity: Purity::Pure,
504        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::TextLen),
505        doc: "Number of Unicode scalar values (chars) in the text.",
506    }
507}
508
509/// `Text.int() -> Option[Int]` — the one text-to-number conversion (ADR-136).
510///
511/// It is what `Y001`'s help on `var count: Int = raw` points at ("this is
512/// `Text`; `.int()` answers `Option[Int]`, so take it apart with `match` (or
513/// use `read lines(int)`)").
514///
515/// `Option[Int]` rather than `Int`, for §4.7's reason and `Map.get`'s: a text
516/// that is not a number is *absence*, not a fault. Input is routinely not what a
517/// program hoped, and a panicking conversion would leave `"abc".int()` a crash
518/// with no way to ask first.
519fn text_int() -> MethodEntry {
520    MethodEntry {
521        receiver: text_receiver(),
522        name: "int",
523        params: vec![],
524        result: TypePattern::Option(Box::new(TypePattern::Scalar(ScalarType::Int))),
525        purity: Purity::Pure,
526        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::TextInt),
527        doc: "The Int this text spells as `Some(n)`, or `None` if it spells none.",
528    }
529}
530
531/// `Text.float() -> Option[Float]` — [`text_int`]'s twin (ADR-136).
532///
533/// Same shape and the same reason: a text that is not a number is *absence*, not
534/// a fault. The accepted set is §7.4's `float` atomic over the whole trimmed
535/// text, so `t.float()` and `parse(t, float)` cannot disagree — which means
536/// `"inf"` and `"nan"` are `None`, because neither is a token the input parser
537/// reads.
538fn text_float() -> MethodEntry {
539    MethodEntry {
540        receiver: text_receiver(),
541        name: "float",
542        params: vec![],
543        result: TypePattern::Option(Box::new(TypePattern::Scalar(ScalarType::Float))),
544        purity: Purity::Pure,
545        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::TextFloat),
546        doc: "The Float this text spells as `Some(x)`, or `None` if it spells none.",
547    }
548}
549
550fn text_is_empty() -> MethodEntry {
551    MethodEntry {
552        receiver: text_receiver(),
553        name: "is_empty",
554        params: vec![],
555        result: TypePattern::Scalar(ScalarType::Bool),
556        purity: Purity::Pure,
557        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::TextIsEmpty),
558        doc: "True iff the text has no chars.",
559    }
560}
561
562fn text_get() -> MethodEntry {
563    MethodEntry {
564        receiver: text_receiver(),
565        name: "get",
566        params: vec![TypePattern::Scalar(ScalarType::Int)],
567        result: TypePattern::Scalar(ScalarType::Char),
568        purity: Purity::Pure,
569        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::TextGet),
570        doc: "The `Char` at `index`; faults if out of range. `t[index]` is the \
571              same row and the same answer (ADR-086).",
572    }
573}
574
575// ---- Float methods (§4.12) --------------------------------------------------
576//
577// All Float method entries share a Float receiver pattern. The pure unary math
578// methods (`abs`/`sqrt`/`floor`/`ceil`/`round`/`sign`) and predicates never
579// fault; `to_int` is the sole faulting method (NaN/inf/out-of-range). `min`/
580// `max` take a Float argument. Conversions return Int/Text.
581
582fn float_receiver() -> TypePattern {
583    TypePattern::Scalar(ScalarType::Float)
584}
585
586fn float_abs() -> MethodEntry {
587    MethodEntry {
588        receiver: float_receiver(),
589        name: "abs",
590        params: vec![],
591        result: TypePattern::Scalar(ScalarType::Float),
592        purity: Purity::Pure,
593        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatAbs),
594        doc: "Absolute value.",
595    }
596}
597
598fn float_sqrt() -> MethodEntry {
599    MethodEntry {
600        receiver: float_receiver(),
601        name: "sqrt",
602        params: vec![],
603        result: TypePattern::Scalar(ScalarType::Float),
604        purity: Purity::Pure,
605        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatSqrt),
606        doc: "Square root. Negative inputs yield NaN (IEEE-754).",
607    }
608}
609
610fn float_floor() -> MethodEntry {
611    MethodEntry {
612        receiver: float_receiver(),
613        name: "floor",
614        params: vec![],
615        result: TypePattern::Scalar(ScalarType::Float),
616        purity: Purity::Pure,
617        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatFloor),
618        doc: "Round toward negative infinity.",
619    }
620}
621
622fn float_ceil() -> MethodEntry {
623    MethodEntry {
624        receiver: float_receiver(),
625        name: "ceil",
626        params: vec![],
627        result: TypePattern::Scalar(ScalarType::Float),
628        purity: Purity::Pure,
629        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatCeil),
630        doc: "Round toward positive infinity.",
631    }
632}
633
634fn float_round() -> MethodEntry {
635    MethodEntry {
636        receiver: float_receiver(),
637        name: "round",
638        params: vec![],
639        result: TypePattern::Scalar(ScalarType::Float),
640        purity: Purity::Pure,
641        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatRound),
642        doc: "Round half away from zero.",
643    }
644}
645
646fn float_sign() -> MethodEntry {
647    MethodEntry {
648        receiver: float_receiver(),
649        name: "sign",
650        params: vec![],
651        result: TypePattern::Scalar(ScalarType::Float),
652        purity: Purity::Pure,
653        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatSign),
654        doc: "Sign as -1.0 / 0.0 / 1.0. NaN yields NaN.",
655    }
656}
657
658fn float_to_int() -> MethodEntry {
659    MethodEntry {
660        receiver: float_receiver(),
661        name: "to_int",
662        params: vec![],
663        result: TypePattern::Scalar(ScalarType::Int),
664        purity: Purity::Pure,
665        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatToInt),
666        doc: "Truncate toward zero to an Int. Faults on NaN, ±inf, or out of i64 range.",
667    }
668}
669
670fn float_to_text() -> MethodEntry {
671    MethodEntry {
672        receiver: float_receiver(),
673        name: "to_text",
674        params: vec![],
675        result: TypePattern::Scalar(ScalarType::Text),
676        purity: Purity::Pure,
677        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatToText),
678        doc: "Format as Text (shortest round-trip form; inf/-inf/NaN as literals).",
679    }
680}
681
682fn float_is_nan() -> MethodEntry {
683    MethodEntry {
684        receiver: float_receiver(),
685        name: "is_nan",
686        params: vec![],
687        result: TypePattern::Scalar(ScalarType::Bool),
688        purity: Purity::Pure,
689        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatIsNan),
690        doc: "True iff NaN.",
691    }
692}
693
694fn float_is_infinite() -> MethodEntry {
695    MethodEntry {
696        receiver: float_receiver(),
697        name: "is_infinite",
698        params: vec![],
699        result: TypePattern::Scalar(ScalarType::Bool),
700        purity: Purity::Pure,
701        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatIsInfinite),
702        doc: "True iff ±infinity.",
703    }
704}
705
706fn float_min() -> MethodEntry {
707    MethodEntry {
708        receiver: float_receiver(),
709        name: "min",
710        params: vec![TypePattern::Scalar(ScalarType::Float)],
711        result: TypePattern::Scalar(ScalarType::Float),
712        purity: Purity::Pure,
713        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatMin),
714        doc: "The smaller of two floats. If either is NaN, returns the other.",
715    }
716}
717
718fn float_max() -> MethodEntry {
719    MethodEntry {
720        receiver: float_receiver(),
721        name: "max",
722        params: vec![TypePattern::Scalar(ScalarType::Float)],
723        result: TypePattern::Scalar(ScalarType::Float),
724        purity: Purity::Pure,
725        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::FloatMax),
726        doc: "The larger of two floats. If either is NaN, returns the other.",
727    }
728}
729
730fn int_to_float() -> MethodEntry {
731    MethodEntry {
732        receiver: TypePattern::Scalar(ScalarType::Int),
733        name: "to_float",
734        params: vec![],
735        result: TypePattern::Scalar(ScalarType::Float),
736        purity: Purity::Pure,
737        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntToFloat),
738        doc: "Widen to Float (explicit Int→Float conversion, §4.12).",
739    }
740}
741
742// --- the `to_text` family (ADR-143) -----------------------------------------
743//
744// Three rows — `Int`, `Float`, `Char` — and the family is closed at three.
745//
746// **Each answers the characters `out` writes, by construction.** The wrapper
747// behind each row calls the same `scalars::write_*` function the type
748// descriptor's `format` callback calls, so there is one renderer per scalar with
749// two callers rather than two renderers that have to be kept in agreement. A
750// program that prints a value and a program that builds a `Text` from it
751// disagreeing is the defect this shape makes unrepresentable; §4.12's
752// shortest-round-trip rule for `Float` is the one that would have drifted first.
753//
754// **Deliberately absent, each for its own reason**, in the convention the Char
755// conversion block below uses:
756//
757// - **`Bool.to_text()`.** No design-doc surface asks for one, and the catalog
758//   invents no rows past what one asks for. `if b { "true" } else { "false" }`
759//   says it, and says which spelling the program wanted.
760// - **A universal `T.to_text()`.** That is §8.1 interpolation's question, not
761//   this one: a hole that stringifies *any* value needs a rendering conversion
762//   defined on every type, which is the implicit conversion to `Text` that
763//   ADR-085 decision 2 refused for `+`. It wants its own decision.
764//
765// §8.1's interpolation itself stays specified and unimplemented. These rows make
766// it cheaper rather than redundant — with `Int`, `Float`, `Char` and `Text` all
767// covered, `"a{n}b"` can desugar to `"a" + n.to_text() + "b"` and needs no new
768// runtime path.
769
770fn int_to_text() -> MethodEntry {
771    MethodEntry {
772        receiver: TypePattern::Scalar(ScalarType::Int),
773        name: "to_text",
774        params: vec![],
775        result: TypePattern::Scalar(ScalarType::Text),
776        purity: Purity::Pure,
777        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntToText),
778        doc: "Format as Text — the same digits `out` writes (ADR-143).",
779    }
780}
781
782// --- Char conversions (ADR-086) ----------------------------------------------
783//
784// The `Char`/`Int` pair, written as a pair for the reason §4.12 writes
785// `Float.to_int`/`Int.to_float` as one: a one-way conversion is a one-way door.
786// With `to_int` alone a program could take a `Char` apart and never build one,
787// so `Grid[Char]`, `Vec[Char]` and `Map[Char, _]` would stay write-only from the
788// language's side.
789//
790// `to_int` is required and not a nicety. A text index answers a `Char`, and
791// `capability::supports_numeric` excludes `Char` on purpose ("a `Char` is a
792// scalar value and not an arithmetic one"), so `t[i] - 48`, `t[i] >= 97` and a
793// `Map[Int, _]` keyed on a character are all spelled by inserting `.to_int()`.
794//
795// **Deliberately absent, each for its own reason** — the same convention the
796// `_add` trio's comment below uses, so an omission is recorded where a reader
797// looks for it rather than only in a commit message:
798//
799// - **`is_digit`, `is_alpha`, `to_upper`, `to_lower`.** No design-doc surface
800//   asks for any of them and `to_int()` expresses every one.
801// - **`Text.chars()`.** `for c in text` **is** the spelling (ADR-099): a `Text`
802//   is iterable and yields the same `Char` `t[i]` answers, through the same
803//   `praxis_text_len`/`praxis_text_get` pair. A `chars()` row would be a second
804//   spelling for one question, which is what ADR-077 refused.
805
806fn char_to_int() -> MethodEntry {
807    MethodEntry {
808        receiver: TypePattern::Scalar(ScalarType::Char),
809        name: "to_int",
810        params: vec![],
811        result: TypePattern::Scalar(ScalarType::Int),
812        purity: Purity::Pure,
813        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CharToInt),
814        doc: "The Unicode scalar value, as an `Int`. Never faults (ADR-086).",
815    }
816}
817
818fn char_to_text() -> MethodEntry {
819    MethodEntry {
820        receiver: TypePattern::Scalar(ScalarType::Char),
821        name: "to_text",
822        params: vec![],
823        result: TypePattern::Scalar(ScalarType::Text),
824        purity: Purity::Pure,
825        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CharToText),
826        doc: "The one-character Text holding this scalar — the same character \
827              `out` writes. Never faults (ADR-143).",
828    }
829}
830
831fn int_to_char() -> MethodEntry {
832    MethodEntry {
833        receiver: TypePattern::Scalar(ScalarType::Int),
834        name: "to_char",
835        params: vec![],
836        result: TypePattern::Scalar(ScalarType::Char),
837        purity: Purity::Pure,
838        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntToChar),
839        doc: "The `Char` with this Unicode scalar value; **faults** \
840              (`InvalidChar`) if it is negative, above `0x10FFFF`, or a \
841              surrogate. The narrowing half of the pair, as `Float.to_int` is \
842              (ADR-086).",
843    }
844}
845
846// §4.12's explicit overflow alternatives — the way out of the checked default.
847//
848// **The family is three modes over three operators** — `wrapping_`,
849// `saturating_`, `checked_` × `add`, `sub`, `mul`. §4.12 states that shape and
850// both of its closures (no `_div`/`_rem`, no `_neg`/`_abs`) and is the only
851// place the rule is written; `the_overflow_alternative_family_is_three_modes_over_three_operators`
852// below is what enforces it against this table.
853
854fn int_wrapping_add() -> MethodEntry {
855    MethodEntry {
856        receiver: TypePattern::Scalar(ScalarType::Int),
857        name: "wrapping_add",
858        params: vec![TypePattern::Scalar(ScalarType::Int)],
859        result: TypePattern::Scalar(ScalarType::Int),
860        purity: Purity::Pure,
861        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntWrappingAdd),
862        doc: "Add with two's-complement wraparound instead of a fault.",
863    }
864}
865
866fn int_saturating_add() -> MethodEntry {
867    MethodEntry {
868        receiver: TypePattern::Scalar(ScalarType::Int),
869        name: "saturating_add",
870        params: vec![TypePattern::Scalar(ScalarType::Int)],
871        result: TypePattern::Scalar(ScalarType::Int),
872        purity: Purity::Pure,
873        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntSaturatingAdd),
874        doc: "Add, clamping to Int's ends instead of faulting.",
875    }
876}
877
878fn int_checked_add() -> MethodEntry {
879    MethodEntry {
880        receiver: TypePattern::Scalar(ScalarType::Int),
881        name: "checked_add",
882        params: vec![TypePattern::Scalar(ScalarType::Int)],
883        result: TypePattern::Option(Box::new(TypePattern::Scalar(ScalarType::Int))),
884        purity: Purity::Pure,
885        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntCheckedAdd),
886        doc: "Add, answering None where the checked `+` would fault.",
887    }
888}
889
890fn int_wrapping_sub() -> MethodEntry {
891    MethodEntry {
892        receiver: TypePattern::Scalar(ScalarType::Int),
893        name: "wrapping_sub",
894        params: vec![TypePattern::Scalar(ScalarType::Int)],
895        result: TypePattern::Scalar(ScalarType::Int),
896        purity: Purity::Pure,
897        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntWrappingSub),
898        doc: "Subtract with two's-complement wraparound instead of a fault.",
899    }
900}
901
902fn int_saturating_sub() -> MethodEntry {
903    MethodEntry {
904        receiver: TypePattern::Scalar(ScalarType::Int),
905        name: "saturating_sub",
906        params: vec![TypePattern::Scalar(ScalarType::Int)],
907        result: TypePattern::Scalar(ScalarType::Int),
908        purity: Purity::Pure,
909        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntSaturatingSub),
910        doc: "Subtract, clamping to Int's ends instead of faulting.",
911    }
912}
913
914fn int_checked_sub() -> MethodEntry {
915    MethodEntry {
916        receiver: TypePattern::Scalar(ScalarType::Int),
917        name: "checked_sub",
918        params: vec![TypePattern::Scalar(ScalarType::Int)],
919        result: TypePattern::Option(Box::new(TypePattern::Scalar(ScalarType::Int))),
920        purity: Purity::Pure,
921        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntCheckedSub),
922        doc: "Subtract, answering None where the checked `-` would fault.",
923    }
924}
925
926fn int_wrapping_mul() -> MethodEntry {
927    MethodEntry {
928        receiver: TypePattern::Scalar(ScalarType::Int),
929        name: "wrapping_mul",
930        params: vec![TypePattern::Scalar(ScalarType::Int)],
931        result: TypePattern::Scalar(ScalarType::Int),
932        purity: Purity::Pure,
933        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntWrappingMul),
934        doc: "Multiply with two's-complement wraparound instead of a fault. The \
935              one row here a program could not write for itself: every arithmetic \
936              operator is checked and the language has no bitwise operators.",
937    }
938}
939
940fn int_saturating_mul() -> MethodEntry {
941    MethodEntry {
942        receiver: TypePattern::Scalar(ScalarType::Int),
943        name: "saturating_mul",
944        params: vec![TypePattern::Scalar(ScalarType::Int)],
945        result: TypePattern::Scalar(ScalarType::Int),
946        purity: Purity::Pure,
947        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntSaturatingMul),
948        doc: "Multiply, clamping to Int's ends instead of faulting.",
949    }
950}
951
952fn int_checked_mul() -> MethodEntry {
953    MethodEntry {
954        receiver: TypePattern::Scalar(ScalarType::Int),
955        name: "checked_mul",
956        params: vec![TypePattern::Scalar(ScalarType::Int)],
957        result: TypePattern::Option(Box::new(TypePattern::Scalar(ScalarType::Int))),
958        purity: Purity::Pure,
959        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::IntCheckedMul),
960        doc: "Multiply, answering None where the checked `*` would fault.",
961    }
962}
963
964fn vec_push() -> MethodEntry {
965    MethodEntry {
966        receiver: vec_of_t(),
967        name: "push",
968        params: vec![TypePattern::var("T")],
969        result: TypePattern::Unit,
970        purity: Purity::Impure,
971        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecPush),
972        doc: "Append a value to the end; returns Unit.",
973    }
974}
975
976fn vec_len() -> MethodEntry {
977    MethodEntry {
978        receiver: vec_of_t(),
979        name: "len",
980        params: vec![],
981        result: TypePattern::Scalar(ScalarType::Int),
982        purity: Purity::Pure,
983        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecLen),
984        doc: "Number of elements in the vector.",
985    }
986}
987
988fn vec_get() -> MethodEntry {
989    MethodEntry {
990        receiver: vec_of_t(),
991        name: "get",
992        params: vec![TypePattern::Scalar(ScalarType::Int)],
993        result: TypePattern::var("T"),
994        purity: Purity::Pure,
995        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecGet),
996        doc: "The element at `index`; faults `IndexOutOfBounds` if out of range.",
997    }
998}
999
1000fn vec_is_empty() -> MethodEntry {
1001    MethodEntry {
1002        receiver: vec_of_t(),
1003        name: "is_empty",
1004        params: vec![],
1005        result: TypePattern::Scalar(ScalarType::Bool),
1006        purity: Purity::Pure,
1007        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecIsEmpty),
1008        doc: "True iff the vector has no elements.",
1009    }
1010}
1011
1012/// The `Vec[Char]` receiver pattern of `to_text` (ADR-144).
1013///
1014/// The element is a **bounded variable** rather than a literal `Char`, for
1015/// `map_of_k_int_value`'s reason: the bound pins an unresolved element type
1016/// instead of merely permitting it, so `var v = Vec()` followed by `v.to_text()`
1017/// gives `v` a `Char` element type, and `[1, 2].to_text()` reports `expected
1018/// Char, found Int` at the method name rather than "no method `to_text`".
1019fn vec_of_char() -> TypePattern {
1020    TypePattern::Collection {
1021        ctor: CollectionCtor::Vec,
1022        args: vec![TypePattern::is_scalar("T", ScalarType::Char)],
1023    }
1024}
1025
1026/// `chars.to_text()` — a sequence of `Char`s as one `Text` (ADR-144), which is
1027/// what renders `g.row(y)` back as the line it was read from.
1028///
1029/// **On `Vec[Char]` and not on the generic `Iterable` receiver**, which is the
1030/// one thing about this row a reader cannot re-derive. `Text` is one of the ten
1031/// pipeline receivers, so an `Iterable.to_text/0` row would sit at `(name,
1032/// arity)` beside every scalar `to_text` the catalog has and beside any future
1033/// `Text.to_text` — and `MethodCatalogBuilder::finish` answers
1034/// `AmbiguousWithIterable` to exactly that. A concrete `Vec` receiver is safe
1035/// against all of them, and a `Set[Char]` or a `Grid[Char]` row would be
1036/// answering a different question anyway: a sequence of characters becomes a
1037/// line because it has an *order*.
1038fn vec_to_text() -> MethodEntry {
1039    MethodEntry {
1040        receiver: vec_of_char(),
1041        name: "to_text",
1042        params: vec![],
1043        result: TypePattern::Scalar(ScalarType::Text),
1044        purity: Purity::Pure,
1045        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecToText),
1046        doc: "These Chars as one Text, with nothing between them (ADR-144).",
1047    }
1048}
1049
1050// --- Deque methods (§6.1) ------------------------------------------------
1051
1052/// The `Deque[T]` receiver pattern, used by every Deque method entry.
1053fn deque_of_t() -> TypePattern {
1054    TypePattern::Collection {
1055        ctor: CollectionCtor::Deque,
1056        args: vec![TypePattern::var("T")],
1057    }
1058}
1059
1060fn deque_push_front() -> MethodEntry {
1061    MethodEntry {
1062        receiver: deque_of_t(),
1063        name: "push_front",
1064        params: vec![TypePattern::var("T")],
1065        result: TypePattern::Unit,
1066        purity: Purity::Impure,
1067        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::DequePushFront),
1068        doc: "Prepend a value to the front; returns Unit.",
1069    }
1070}
1071
1072fn deque_push_back() -> MethodEntry {
1073    MethodEntry {
1074        receiver: deque_of_t(),
1075        name: "push_back",
1076        params: vec![TypePattern::var("T")],
1077        result: TypePattern::Unit,
1078        purity: Purity::Impure,
1079        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::DequePushBack),
1080        doc: "Append a value to the back; returns Unit.",
1081    }
1082}
1083
1084fn deque_pop_front() -> MethodEntry {
1085    MethodEntry {
1086        receiver: deque_of_t(),
1087        name: "pop_front",
1088        params: vec![],
1089        result: TypePattern::var("T"),
1090        purity: Purity::Impure,
1091        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::DequePopFront),
1092        doc: "Remove and return the front element; faults if empty.",
1093    }
1094}
1095
1096fn deque_pop_back() -> MethodEntry {
1097    MethodEntry {
1098        receiver: deque_of_t(),
1099        name: "pop_back",
1100        params: vec![],
1101        result: TypePattern::var("T"),
1102        purity: Purity::Impure,
1103        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::DequePopBack),
1104        doc: "Remove and return the back element; faults if empty.",
1105    }
1106}
1107
1108fn deque_len() -> MethodEntry {
1109    MethodEntry {
1110        receiver: deque_of_t(),
1111        name: "len",
1112        params: vec![],
1113        result: TypePattern::Scalar(ScalarType::Int),
1114        purity: Purity::Pure,
1115        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::DequeLen),
1116        doc: "Number of elements in the deque.",
1117    }
1118}
1119
1120fn deque_get() -> MethodEntry {
1121    MethodEntry {
1122        receiver: deque_of_t(),
1123        name: "get",
1124        params: vec![TypePattern::Scalar(ScalarType::Int)],
1125        result: TypePattern::var("T"),
1126        purity: Purity::Pure,
1127        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::DequeGet),
1128        doc: "The element at `index` (0-based from the front); faults if out of range.",
1129    }
1130}
1131
1132fn deque_is_empty() -> MethodEntry {
1133    MethodEntry {
1134        receiver: deque_of_t(),
1135        name: "is_empty",
1136        params: vec![],
1137        result: TypePattern::Scalar(ScalarType::Bool),
1138        purity: Purity::Pure,
1139        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::DequeIsEmpty),
1140        doc: "True iff the deque has no elements.",
1141    }
1142}
1143
1144// --- Map / Set / Counter methods (§6.1, §11.3) ---------------------------
1145
1146/// The `Map[K, V]` receiver pattern: two type args (key, value).
1147fn map_of_k_v() -> TypePattern {
1148    TypePattern::Collection {
1149        ctor: CollectionCtor::Map,
1150        args: vec![TypePattern::var("K"), TypePattern::var("V")],
1151    }
1152}
1153
1154/// The `Set[T]` receiver pattern.
1155fn set_of_t() -> TypePattern {
1156    TypePattern::Collection {
1157        ctor: CollectionCtor::Set,
1158        args: vec![TypePattern::var("T")],
1159    }
1160}
1161
1162/// The `Counter[T]` receiver pattern (key type only; values are Int).
1163fn counter_of_t() -> TypePattern {
1164    TypePattern::Collection {
1165        ctor: CollectionCtor::Counter,
1166        args: vec![TypePattern::var("T")],
1167    }
1168}
1169
1170fn map_insert() -> MethodEntry {
1171    MethodEntry {
1172        receiver: map_of_k_v(),
1173        name: "insert",
1174        params: vec![TypePattern::var("K"), TypePattern::var("V")],
1175        result: TypePattern::Unit,
1176        purity: Purity::Impure,
1177        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapInsert),
1178        doc: "Set `key` to `value`, replacing any prior value; returns Unit.",
1179    }
1180}
1181
1182fn map_get() -> MethodEntry {
1183    MethodEntry {
1184        receiver: map_of_k_v(),
1185        name: "get",
1186        params: vec![TypePattern::var("K")],
1187        // §5.7 writes this signature literally: `Map[K,V].get(K) -> Option[V]`.
1188        // §4.7: absence is `Option`, and `map[key]` is the assertion-like half
1189        // that faults.
1190        result: TypePattern::Option(Box::new(TypePattern::var("V"))),
1191        purity: Purity::Pure,
1192        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapGet),
1193        doc: "The value for `key` as `Some(value)`, or `None` if absent.",
1194    }
1195}
1196
1197fn map_contains() -> MethodEntry {
1198    MethodEntry {
1199        receiver: map_of_k_v(),
1200        name: "contains",
1201        params: vec![TypePattern::var("K")],
1202        result: TypePattern::Scalar(ScalarType::Bool),
1203        purity: Purity::Pure,
1204        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapContains),
1205        doc: "True iff `key` is present in the map.",
1206    }
1207}
1208
1209fn map_remove() -> MethodEntry {
1210    MethodEntry {
1211        receiver: map_of_k_v(),
1212        name: "remove",
1213        params: vec![TypePattern::var("K")],
1214        result: TypePattern::Unit,
1215        purity: Purity::Impure,
1216        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapRemove),
1217        doc: "Remove `key` if present; returns Unit.",
1218    }
1219}
1220
1221fn map_len() -> MethodEntry {
1222    MethodEntry {
1223        receiver: map_of_k_v(),
1224        name: "len",
1225        params: vec![],
1226        result: TypePattern::Scalar(ScalarType::Int),
1227        purity: Purity::Pure,
1228        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapLen),
1229        doc: "Number of entries in the map.",
1230    }
1231}
1232
1233fn map_is_empty() -> MethodEntry {
1234    MethodEntry {
1235        receiver: map_of_k_v(),
1236        name: "is_empty",
1237        params: vec![],
1238        result: TypePattern::Scalar(ScalarType::Bool),
1239        purity: Purity::Pure,
1240        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MapIsEmpty),
1241        doc: "True iff the map has no entries.",
1242    }
1243}
1244
1245fn set_insert() -> MethodEntry {
1246    MethodEntry {
1247        receiver: set_of_t(),
1248        name: "insert",
1249        params: vec![TypePattern::var("T")],
1250        result: TypePattern::Unit,
1251        purity: Purity::Impure,
1252        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::SetInsert),
1253        doc: "Add `value` to the set; returns Unit.",
1254    }
1255}
1256
1257fn set_remove() -> MethodEntry {
1258    MethodEntry {
1259        receiver: set_of_t(),
1260        name: "remove",
1261        params: vec![TypePattern::var("T")],
1262        result: TypePattern::Unit,
1263        purity: Purity::Impure,
1264        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::SetRemove),
1265        doc: "Remove `value` if present; returns Unit.",
1266    }
1267}
1268
1269fn set_contains() -> MethodEntry {
1270    MethodEntry {
1271        receiver: set_of_t(),
1272        name: "contains",
1273        params: vec![TypePattern::var("T")],
1274        result: TypePattern::Scalar(ScalarType::Bool),
1275        purity: Purity::Pure,
1276        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::SetContains),
1277        doc: "True iff `value` is in the set.",
1278    }
1279}
1280
1281fn set_len() -> MethodEntry {
1282    MethodEntry {
1283        receiver: set_of_t(),
1284        name: "len",
1285        params: vec![],
1286        result: TypePattern::Scalar(ScalarType::Int),
1287        purity: Purity::Pure,
1288        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::SetLen),
1289        doc: "Number of elements in the set.",
1290    }
1291}
1292
1293fn set_is_empty() -> MethodEntry {
1294    MethodEntry {
1295        receiver: set_of_t(),
1296        name: "is_empty",
1297        params: vec![],
1298        result: TypePattern::Scalar(ScalarType::Bool),
1299        purity: Purity::Pure,
1300        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::SetIsEmpty),
1301        doc: "True iff the set has no elements.",
1302    }
1303}
1304
1305fn counter_get() -> MethodEntry {
1306    MethodEntry {
1307        receiver: counter_of_t(),
1308        name: "get",
1309        params: vec![TypePattern::var("T")],
1310        result: TypePattern::Scalar(ScalarType::Int),
1311        purity: Purity::Pure,
1312        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CounterGet),
1313        doc: "The count for `key`, or zero if absent (never faults).",
1314    }
1315}
1316
1317fn counter_inc() -> MethodEntry {
1318    MethodEntry {
1319        receiver: counter_of_t(),
1320        name: "inc",
1321        params: vec![TypePattern::var("T")],
1322        result: TypePattern::Unit,
1323        purity: Purity::Impure,
1324        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CounterInc),
1325        doc: "Increment the count for `key` by one; returns Unit.",
1326    }
1327}
1328
1329fn counter_len() -> MethodEntry {
1330    MethodEntry {
1331        receiver: counter_of_t(),
1332        name: "len",
1333        params: vec![],
1334        result: TypePattern::Scalar(ScalarType::Int),
1335        purity: Purity::Pure,
1336        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CounterLen),
1337        doc: "Number of distinct keys in the counter.",
1338    }
1339}
1340
1341fn counter_is_empty() -> MethodEntry {
1342    MethodEntry {
1343        receiver: counter_of_t(),
1344        name: "is_empty",
1345        params: vec![],
1346        result: TypePattern::Scalar(ScalarType::Bool),
1347        purity: Purity::Pure,
1348        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::CounterIsEmpty),
1349        doc: "True iff the counter has no keys.",
1350    }
1351}
1352
1353// --- MinHeap[T] / MaxHeap[T] methods (§6.1) -----------------------------
1354
1355fn min_heap_of_t() -> TypePattern {
1356    TypePattern::Collection {
1357        ctor: CollectionCtor::MinHeap,
1358        args: vec![TypePattern::var("T")],
1359    }
1360}
1361
1362fn max_heap_of_t() -> TypePattern {
1363    TypePattern::Collection {
1364        ctor: CollectionCtor::MaxHeap,
1365        args: vec![TypePattern::var("T")],
1366    }
1367}
1368
1369fn max_heap_push() -> MethodEntry {
1370    MethodEntry {
1371        receiver: max_heap_of_t(),
1372        name: "push",
1373        params: vec![TypePattern::var("T")],
1374        result: TypePattern::Unit,
1375        purity: Purity::Impure,
1376        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MaxHeapPush),
1377        doc: "Push a value onto the max-heap; returns Unit.",
1378    }
1379}
1380
1381fn max_heap_pop() -> MethodEntry {
1382    MethodEntry {
1383        receiver: max_heap_of_t(),
1384        name: "pop",
1385        params: vec![],
1386        result: TypePattern::var("T"),
1387        purity: Purity::Impure,
1388        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MaxHeapPop),
1389        doc: "Remove and return the largest element; faults if empty.",
1390    }
1391}
1392
1393fn max_heap_peek() -> MethodEntry {
1394    MethodEntry {
1395        receiver: max_heap_of_t(),
1396        name: "peek",
1397        params: vec![],
1398        result: TypePattern::var("T"),
1399        purity: Purity::Pure,
1400        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MaxHeapPeek),
1401        doc: "The largest element without removing it; faults if empty.",
1402    }
1403}
1404
1405fn max_heap_len() -> MethodEntry {
1406    MethodEntry {
1407        receiver: max_heap_of_t(),
1408        name: "len",
1409        params: vec![],
1410        result: TypePattern::Scalar(ScalarType::Int),
1411        purity: Purity::Pure,
1412        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MaxHeapLen),
1413        doc: "Number of elements in the max-heap.",
1414    }
1415}
1416
1417fn max_heap_is_empty() -> MethodEntry {
1418    MethodEntry {
1419        receiver: max_heap_of_t(),
1420        name: "is_empty",
1421        params: vec![],
1422        result: TypePattern::Scalar(ScalarType::Bool),
1423        purity: Purity::Pure,
1424        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MaxHeapIsEmpty),
1425        doc: "True iff the max-heap has no elements.",
1426    }
1427}
1428
1429fn min_heap_push() -> MethodEntry {
1430    MethodEntry {
1431        receiver: min_heap_of_t(),
1432        name: "push",
1433        params: vec![TypePattern::var("T")],
1434        result: TypePattern::Unit,
1435        purity: Purity::Impure,
1436        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MinHeapPush),
1437        doc: "Push a value onto the min-heap; returns Unit.",
1438    }
1439}
1440
1441fn min_heap_pop() -> MethodEntry {
1442    MethodEntry {
1443        receiver: min_heap_of_t(),
1444        name: "pop",
1445        params: vec![],
1446        result: TypePattern::var("T"),
1447        purity: Purity::Impure,
1448        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MinHeapPop),
1449        doc: "Remove and return the smallest element; faults if empty.",
1450    }
1451}
1452
1453fn min_heap_peek() -> MethodEntry {
1454    MethodEntry {
1455        receiver: min_heap_of_t(),
1456        name: "peek",
1457        params: vec![],
1458        result: TypePattern::var("T"),
1459        purity: Purity::Pure,
1460        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MinHeapPeek),
1461        doc: "The smallest element without removing it; faults if empty.",
1462    }
1463}
1464
1465fn min_heap_len() -> MethodEntry {
1466    MethodEntry {
1467        receiver: min_heap_of_t(),
1468        name: "len",
1469        params: vec![],
1470        result: TypePattern::Scalar(ScalarType::Int),
1471        purity: Purity::Pure,
1472        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MinHeapLen),
1473        doc: "Number of elements in the min-heap.",
1474    }
1475}
1476
1477fn min_heap_is_empty() -> MethodEntry {
1478    MethodEntry {
1479        receiver: min_heap_of_t(),
1480        name: "is_empty",
1481        params: vec![],
1482        result: TypePattern::Scalar(ScalarType::Bool),
1483        purity: Purity::Pure,
1484        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::MinHeapIsEmpty),
1485        doc: "True iff the min-heap has no elements.",
1486    }
1487}
1488
1489// --- BitSet methods (§6.1) ----------------------------------------------
1490
1491/// The `BitSet` receiver pattern (nullary — no type args).
1492fn bitset_receiver() -> TypePattern {
1493    TypePattern::Collection {
1494        ctor: CollectionCtor::BitSet,
1495        args: vec![],
1496    }
1497}
1498
1499fn bitset_insert() -> MethodEntry {
1500    MethodEntry {
1501        receiver: bitset_receiver(),
1502        name: "insert",
1503        params: vec![TypePattern::Scalar(ScalarType::Int)],
1504        result: TypePattern::Unit,
1505        purity: Purity::Impure,
1506        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::BitsetInsert),
1507        doc: "Set the bit for a non-negative integer; returns Unit.",
1508    }
1509}
1510
1511fn bitset_remove() -> MethodEntry {
1512    MethodEntry {
1513        receiver: bitset_receiver(),
1514        name: "remove",
1515        params: vec![TypePattern::Scalar(ScalarType::Int)],
1516        result: TypePattern::Unit,
1517        purity: Purity::Impure,
1518        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::BitsetRemove),
1519        doc: "Clear the bit for an integer; returns Unit.",
1520    }
1521}
1522
1523fn bitset_contains() -> MethodEntry {
1524    MethodEntry {
1525        receiver: bitset_receiver(),
1526        name: "contains",
1527        params: vec![TypePattern::Scalar(ScalarType::Int)],
1528        result: TypePattern::Scalar(ScalarType::Bool),
1529        purity: Purity::Pure,
1530        // The one `ScalarPrimitive` row in the catalog (ADR-118 decision 6):
1531        // `bs.contains(x)` lowers to `Inst::BitsetContains`, whose result is a
1532        // `Scalar(Bool)` and whose out-of-line form is this wrapper.
1533        lowering: MethodLowering::ScalarPrimitive(abi::RuntimeSymbol::BitsetContains),
1534        doc: "True iff the bit for the integer is set.",
1535    }
1536}
1537
1538fn bitset_len() -> MethodEntry {
1539    MethodEntry {
1540        receiver: bitset_receiver(),
1541        name: "len",
1542        params: vec![],
1543        result: TypePattern::Scalar(ScalarType::Int),
1544        purity: Purity::Pure,
1545        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::BitsetLen),
1546        doc: "Number of set bits (popcount).",
1547    }
1548}
1549
1550fn bitset_is_empty() -> MethodEntry {
1551    MethodEntry {
1552        receiver: bitset_receiver(),
1553        name: "is_empty",
1554        params: vec![],
1555        result: TypePattern::Scalar(ScalarType::Bool),
1556        purity: Purity::Pure,
1557        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::BitsetIsEmpty),
1558        doc: "True iff no bits are set.",
1559    }
1560}
1561
1562// --- Grid[T] methods (§6.4) ---------------------------------------------
1563
1564/// The `Grid[T]` receiver pattern.
1565fn grid_of_t() -> TypePattern {
1566    TypePattern::Collection {
1567        ctor: CollectionCtor::Grid,
1568        args: vec![TypePattern::var("T")],
1569    }
1570}
1571
1572/// A `(x, y)` point: the `(Int, Int)` tuple shape returned by grid methods.
1573fn point_pattern() -> TypePattern {
1574    TypePattern::Tuple(vec![
1575        TypePattern::Scalar(ScalarType::Int),
1576        TypePattern::Scalar(ScalarType::Int),
1577    ])
1578}
1579
1580fn grid_width() -> MethodEntry {
1581    MethodEntry {
1582        receiver: grid_of_t(),
1583        name: "width",
1584        params: vec![],
1585        result: TypePattern::Scalar(ScalarType::Int),
1586        purity: Purity::Pure,
1587        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridWidth),
1588        doc: "The number of columns.",
1589    }
1590}
1591
1592fn grid_height() -> MethodEntry {
1593    MethodEntry {
1594        receiver: grid_of_t(),
1595        name: "height",
1596        params: vec![],
1597        result: TypePattern::Scalar(ScalarType::Int),
1598        purity: Purity::Pure,
1599        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridHeight),
1600        doc: "The number of rows.",
1601    }
1602}
1603
1604fn grid_get() -> MethodEntry {
1605    MethodEntry {
1606        receiver: grid_of_t(),
1607        name: "get",
1608        params: vec![
1609            TypePattern::Scalar(ScalarType::Int),
1610            TypePattern::Scalar(ScalarType::Int),
1611        ],
1612        result: TypePattern::var("T"),
1613        purity: Purity::Pure,
1614        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridGet),
1615        doc: "The cell at (x, y); faults if out of range.",
1616    }
1617}
1618
1619fn grid_set() -> MethodEntry {
1620    MethodEntry {
1621        receiver: grid_of_t(),
1622        name: "set",
1623        params: vec![
1624            TypePattern::Scalar(ScalarType::Int),
1625            TypePattern::Scalar(ScalarType::Int),
1626            TypePattern::var("T"),
1627        ],
1628        result: TypePattern::Unit,
1629        purity: Purity::Impure,
1630        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridSet),
1631        doc: "Set the cell at (x, y); faults if out of range.",
1632    }
1633}
1634
1635fn grid_contains() -> MethodEntry {
1636    MethodEntry {
1637        receiver: grid_of_t(),
1638        name: "contains",
1639        params: vec![
1640            TypePattern::Scalar(ScalarType::Int),
1641            TypePattern::Scalar(ScalarType::Int),
1642        ],
1643        result: TypePattern::Scalar(ScalarType::Bool),
1644        purity: Purity::Pure,
1645        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridContains),
1646        doc: "True iff (x, y) is within the grid.",
1647    }
1648}
1649
1650fn grid_neighbors4() -> MethodEntry {
1651    MethodEntry {
1652        receiver: grid_of_t(),
1653        name: "neighbors4",
1654        params: vec![point_pattern()],
1655        result: TypePattern::Collection {
1656            ctor: CollectionCtor::Vec,
1657            args: vec![point_pattern()],
1658        },
1659        purity: Purity::Pure,
1660        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridNeighbors4),
1661        doc: "The 4 orthogonal in-bounds neighbors of a point, as a Vec of (x, y).",
1662    }
1663}
1664
1665fn grid_neighbors8() -> MethodEntry {
1666    MethodEntry {
1667        receiver: grid_of_t(),
1668        name: "neighbors8",
1669        params: vec![point_pattern()],
1670        result: TypePattern::Collection {
1671            ctor: CollectionCtor::Vec,
1672            args: vec![point_pattern()],
1673        },
1674        purity: Purity::Pure,
1675        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridNeighbors8),
1676        doc: "The 8 in-bounds neighbors of a point, as a Vec of (x, y).",
1677    }
1678}
1679
1680/// `Option[(Int, Int)]` — one direction of a neighbourhood record. `None` is a
1681/// direction that leaves the grid, which is exactly what a clipped `Vec`
1682/// cannot say.
1683fn maybe_point() -> TypePattern {
1684    TypePattern::Option(Box::new(point_pattern()))
1685}
1686
1687/// `Around4 { up, left, right, down }` — the plus, read off the page with the
1688/// centre skipped (§6.4).
1689///
1690/// **The field order written here is the runtime's layout order.** A field read
1691/// compiles to a slot index taken from this list, and
1692/// `praxis_runtime::records::AROUND4_DIRECTIONS` is what the value is assembled
1693/// in; the record is nominal so ADR-152's canonicalization leaves both alone,
1694/// which means nothing derives one order from the other and the two are simply
1695/// required to agree. `around_schemas_match_the_catalog` is what holds them
1696/// together — a disagreement reads the wrong direction and says nothing.
1697fn around4_pattern() -> TypePattern {
1698    TypePattern::Record {
1699        name: "Around4",
1700        fields: vec![
1701            ("up", maybe_point()),
1702            ("left", maybe_point()),
1703            ("right", maybe_point()),
1704            ("down", maybe_point()),
1705        ],
1706    }
1707}
1708
1709/// `Around8` — the eight cells of a 3×3 block in reading order, centre skipped.
1710/// See [`around4_pattern`] on why the order is load-bearing.
1711fn around8_pattern() -> TypePattern {
1712    TypePattern::Record {
1713        name: "Around8",
1714        fields: vec![
1715            ("up_left", maybe_point()),
1716            ("up", maybe_point()),
1717            ("up_right", maybe_point()),
1718            ("left", maybe_point()),
1719            ("right", maybe_point()),
1720            ("down_left", maybe_point()),
1721            ("down", maybe_point()),
1722            ("down_right", maybe_point()),
1723        ],
1724    }
1725}
1726
1727/// `around4(p)` — the four orthogonal neighbours by name (§6.4).
1728///
1729/// **Not a replacement for `neighbors4`.** That row answers a `Vec` clipped to
1730/// what is in bounds, which is the shape a graph walk takes —
1731/// `bfs(start, |p| g.neighbors4(p))` is ADR-060's own spelling and the walk's
1732/// neighbours closure is typed `(T) -> Vec[T]`. What it cannot express is
1733/// *which* direction each neighbour was, and off the edge of the grid, that
1734/// there was a direction at all. Every field here is an `Option`, so both
1735/// survive.
1736fn grid_around4() -> MethodEntry {
1737    MethodEntry {
1738        receiver: grid_of_t(),
1739        name: "around4",
1740        params: vec![point_pattern()],
1741        result: around4_pattern(),
1742        purity: Purity::Pure,
1743        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridAround4),
1744        doc: "The 4 orthogonal neighbors by name: { up, left, right, down }, \
1745              each Some((x, y)) or None off the grid.",
1746    }
1747}
1748
1749/// `around8(p)` — all eight neighbours by name. See [`grid_around4`].
1750fn grid_around8() -> MethodEntry {
1751    MethodEntry {
1752        receiver: grid_of_t(),
1753        name: "around8",
1754        params: vec![point_pattern()],
1755        result: around8_pattern(),
1756        purity: Purity::Pure,
1757        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridAround8),
1758        doc: "The 8 neighbors by name, in reading order: { up_left, up, up_right, \
1759              left, right, down_left, down, down_right }, each Some((x, y)) or None.",
1760    }
1761}
1762
1763/// `count4(p, v)` — the orthogonal in-bounds neighbours holding `v`.
1764///
1765/// The receiver's `T` is unbounded, exactly as `find(T)` and `find_all(T)` are:
1766/// equality goes through the value's own descriptor callback at run time, and a
1767/// type without one answers "not equal" rather than being refused here. A
1768/// `CapKind` bound would refuse `Grid[T]` receivers that never call this.
1769fn grid_count4() -> MethodEntry {
1770    MethodEntry {
1771        receiver: grid_of_t(),
1772        name: "count4",
1773        params: vec![point_pattern(), TypePattern::var("T")],
1774        result: TypePattern::Scalar(ScalarType::Int),
1775        purity: Purity::Pure,
1776        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridCount4),
1777        doc: "How many of the 4 orthogonal neighbors hold `value`. \
1778              A neighbor off the grid has no cell and is not counted.",
1779    }
1780}
1781
1782/// `count8(p, v)` — the eight in-bounds neighbours holding `v`. See
1783/// [`grid_count4`].
1784fn grid_count8() -> MethodEntry {
1785    MethodEntry {
1786        receiver: grid_of_t(),
1787        name: "count8",
1788        params: vec![point_pattern(), TypePattern::var("T")],
1789        result: TypePattern::Scalar(ScalarType::Int),
1790        purity: Purity::Pure,
1791        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridCount8),
1792        doc: "How many of the 8 neighbors hold `value`. \
1793              A neighbor off the grid has no cell and is not counted.",
1794    }
1795}
1796
1797/// `count4_where(p, f)` — the orthogonal in-bounds neighbours whose cell `f`
1798/// accepts.
1799///
1800/// The predicate is never called for a direction that leaves the grid: there is
1801/// no cell to hand it, and inventing one would mean choosing a value the cell
1802/// type may not have.
1803fn grid_count4_where() -> MethodEntry {
1804    MethodEntry {
1805        receiver: grid_of_t(),
1806        name: "count4_where",
1807        params: vec![
1808            point_pattern(),
1809            TypePattern::Function {
1810                params: vec![TypePattern::var("T")],
1811                result: Box::new(TypePattern::Scalar(ScalarType::Bool)),
1812            },
1813        ],
1814        result: TypePattern::Scalar(ScalarType::Int),
1815        purity: Purity::Pure,
1816        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridCount4Where),
1817        doc: "How many of the 4 orthogonal neighbors hold a cell the closure accepts. \
1818              A neighbor off the grid has no cell, so the closure never sees one.",
1819    }
1820}
1821
1822/// `count8_where(p, f)` — the eight in-bounds neighbours whose cell `f`
1823/// accepts. See [`grid_count4_where`].
1824fn grid_count8_where() -> MethodEntry {
1825    MethodEntry {
1826        receiver: grid_of_t(),
1827        name: "count8_where",
1828        params: vec![
1829            point_pattern(),
1830            TypePattern::Function {
1831                params: vec![TypePattern::var("T")],
1832                result: Box::new(TypePattern::Scalar(ScalarType::Bool)),
1833            },
1834        ],
1835        result: TypePattern::Scalar(ScalarType::Int),
1836        purity: Purity::Pure,
1837        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridCount8Where),
1838        doc: "How many of the 8 neighbors hold a cell the closure accepts. \
1839              A neighbor off the grid has no cell, so the closure never sees one.",
1840    }
1841}
1842
1843fn grid_positions() -> MethodEntry {
1844    MethodEntry {
1845        receiver: grid_of_t(),
1846        name: "positions",
1847        params: vec![],
1848        result: TypePattern::Collection {
1849            ctor: CollectionCtor::Vec,
1850            args: vec![point_pattern()],
1851        },
1852        purity: Purity::Pure,
1853        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridPositions),
1854        doc: "All (x, y) positions in row-major order, as a Vec.",
1855    }
1856}
1857
1858fn grid_cells() -> MethodEntry {
1859    MethodEntry {
1860        receiver: grid_of_t(),
1861        name: "cells",
1862        params: vec![],
1863        result: TypePattern::Collection {
1864            ctor: CollectionCtor::Vec,
1865            args: vec![TypePattern::var("T")],
1866        },
1867        purity: Purity::Pure,
1868        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridCells),
1869        doc: "All cells in row-major order, as a Vec.",
1870    }
1871}
1872
1873fn grid_row() -> MethodEntry {
1874    MethodEntry {
1875        receiver: grid_of_t(),
1876        name: "row",
1877        params: vec![TypePattern::Scalar(ScalarType::Int)],
1878        result: TypePattern::Collection {
1879            ctor: CollectionCtor::Vec,
1880            args: vec![TypePattern::var("T")],
1881        },
1882        purity: Purity::Pure,
1883        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridRow),
1884        doc: "Row `y` as a Vec; faults if out of range.",
1885    }
1886}
1887
1888fn grid_column() -> MethodEntry {
1889    MethodEntry {
1890        receiver: grid_of_t(),
1891        name: "column",
1892        params: vec![TypePattern::Scalar(ScalarType::Int)],
1893        result: TypePattern::Collection {
1894            ctor: CollectionCtor::Vec,
1895            args: vec![TypePattern::var("T")],
1896        },
1897        purity: Purity::Pure,
1898        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridColumn),
1899        doc: "Column `x` as a Vec; faults if out of range.",
1900    }
1901}
1902
1903fn grid_find() -> MethodEntry {
1904    MethodEntry {
1905        receiver: grid_of_t(),
1906        name: "find",
1907        params: vec![TypePattern::var("T")],
1908        // Absence is `Option`, not the Unit sentinel under a `(Int, Int)`
1909        // static type (§4.7). `find_all` needs no such thing — a `Vec` already
1910        // encodes "nothing matched" as emptiness.
1911        result: TypePattern::Option(Box::new(point_pattern())),
1912        purity: Purity::Pure,
1913        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridFind),
1914        doc: "The first (x, y) whose cell equals `value` as `Some((x, y))`, or `None`.",
1915    }
1916}
1917
1918fn grid_find_all() -> MethodEntry {
1919    MethodEntry {
1920        receiver: grid_of_t(),
1921        name: "find_all",
1922        params: vec![TypePattern::var("T")],
1923        result: TypePattern::Collection {
1924            ctor: CollectionCtor::Vec,
1925            args: vec![point_pattern()],
1926        },
1927        purity: Purity::Pure,
1928        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridFindAll),
1929        doc: "All (x, y) positions whose cell equals `value`, as a Vec.",
1930    }
1931}
1932
1933fn grid_transpose() -> MethodEntry {
1934    MethodEntry {
1935        receiver: grid_of_t(),
1936        name: "transpose",
1937        params: vec![],
1938        result: grid_of_t(),
1939        purity: Purity::Pure,
1940        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridTranspose),
1941        doc: "A transposed copy (rows ↔ columns).",
1942    }
1943}
1944
1945fn grid_rotate_left() -> MethodEntry {
1946    MethodEntry {
1947        receiver: grid_of_t(),
1948        name: "rotate_left",
1949        params: vec![],
1950        result: grid_of_t(),
1951        purity: Purity::Pure,
1952        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridRotateLeft),
1953        doc: "A copy rotated 90° counter-clockwise.",
1954    }
1955}
1956
1957fn grid_rotate_right() -> MethodEntry {
1958    MethodEntry {
1959        receiver: grid_of_t(),
1960        name: "rotate_right",
1961        params: vec![],
1962        result: grid_of_t(),
1963        purity: Purity::Pure,
1964        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::GridRotateRight),
1965        doc: "A copy rotated 90° clockwise.",
1966    }
1967}
1968
1969// --- Pipeline combinators (§6.3, ADR-127) ---------------------------------
1970// The functional-sequence pipeline: intrinsics the compiler fuses into a single
1971// loop over the source. Sinks (sum/count/fold) terminate, and a chain that ends
1972// without one materializes anyway (ADR-126).
1973//
1974// **A pipeline's receiver is anything a `for` loop can walk**, and it yields
1975// what the `for` loop's variable would bind (ADR-127 decision 1). One row per
1976// combinator, on `TypePattern::Iterable`, whose ten accepted receivers are
1977// `PIPELINE_RECEIVERS` plus `Text`.
1978//
1979// The receiver generalizes; **two parameters deliberately do not.** `zip`'s
1980// argument is a `Vec[U]` and `flat_map`'s closure answers one, because the fused
1981// loop indexes each of them with `praxis_vec_len`/`praxis_vec_get` directly and
1982// neither has an `IterPlan` in scope. `m.zip(s)` on a `Set` is a unification
1983// failure at the argument and the spelling is `m.zip(s.to_vec())`;
1984// `MethodCatalogBuilder::finish` refuses a row that generalizes either.
1985
1986/// The generic pipeline receiver at an unconstrained item — `map`, `filter`,
1987/// `count` and the rest of the twenty-three fused rows.
1988fn iterable_of_t() -> TypePattern {
1989    TypePattern::iterable(TypePattern::var("T"))
1990}
1991
1992/// `(T) -> U` — the shape of `map`'s closure argument.
1993fn t_to_u() -> TypePattern {
1994    TypePattern::Function {
1995        params: vec![TypePattern::var("T")],
1996        result: Box::new(TypePattern::var("U")),
1997    }
1998}
1999
2000/// `(T) -> Bool` — the shape of `filter`'s predicate.
2001fn t_to_bool() -> TypePattern {
2002    TypePattern::Function {
2003        params: vec![TypePattern::var("T")],
2004        result: Box::new(TypePattern::Scalar(ScalarType::Bool)),
2005    }
2006}
2007
2008/// `(T) -> Option[U]` — the shape of `filter_map`'s closure argument.
2009///
2010/// `Option[U]` and not `map`'s `(T) -> U`: with an unconstrained `U` there is
2011/// nothing at runtime that says "this element mapped to nothing", so no
2012/// filtering would be possible. `Option` (ADR-076) makes the distinction
2013/// representable — absence is a variant, so the drop test is a tag compare.
2014fn t_to_option_u() -> TypePattern {
2015    TypePattern::Function {
2016        params: vec![TypePattern::var("T")],
2017        result: Box::new(TypePattern::Option(Box::new(TypePattern::var("U")))),
2018    }
2019}
2020
2021/// `(Acc, T) -> Acc` — the shape of `fold`'s combining closure.
2022fn acc_t_to_acc() -> TypePattern {
2023    TypePattern::Function {
2024        params: vec![TypePattern::var("Acc"), TypePattern::var("T")],
2025        result: Box::new(TypePattern::var("Acc")),
2026    }
2027}
2028
2029/// `(T, T) -> T` — the shape of `reduce`'s combining closure.
2030///
2031/// **`reduce` is `fold` without the seed**, so its accumulator *is* the element
2032/// type; there is no second variable for it to be. Sharing [`acc_t_to_acc`]
2033/// with `fold` would leave the closure's first parameter untied to the element,
2034/// so `["ab", "c"].reduce(|a, b| a.len())` would type-check with `a` unpinned —
2035/// the closure answering `Int` while `reduce` answers `Text`, a disagreement
2036/// MIR's pipeline recognizer then asserts on.
2037fn t_t_to_t() -> TypePattern {
2038    TypePattern::Function {
2039        params: vec![TypePattern::var("T"), TypePattern::var("T")],
2040        result: Box::new(TypePattern::var("T")),
2041    }
2042}
2043
2044fn seq_map() -> MethodEntry {
2045    MethodEntry {
2046        receiver: iterable_of_t(),
2047        name: "map",
2048        params: vec![t_to_u()],
2049        result: vec_of_u(),
2050        purity: Purity::Pure,
2051        lowering: MethodLowering::Intrinsic("seq_map"),
2052        doc: "Apply a function to each element, collecting into a Vec.",
2053    }
2054}
2055
2056/// `Vec[U]` — the result of a `map` (a fresh element variable U). The pipeline
2057/// is eager (ADR-028 decision 2): a stage materializes a `Vec`.
2058fn vec_of_u() -> TypePattern {
2059    TypePattern::Collection {
2060        ctor: CollectionCtor::Vec,
2061        args: vec![TypePattern::var("U")],
2062    }
2063}
2064
2065fn seq_filter() -> MethodEntry {
2066    MethodEntry {
2067        receiver: iterable_of_t(),
2068        name: "filter",
2069        params: vec![t_to_bool()],
2070        result: vec_of_t(),
2071        purity: Purity::Pure,
2072        lowering: MethodLowering::Intrinsic("seq_filter"),
2073        doc: "Keep elements satisfying a predicate, collecting into a Vec.",
2074    }
2075}
2076
2077fn seq_fold() -> MethodEntry {
2078    MethodEntry {
2079        receiver: iterable_of_t(),
2080        name: "fold",
2081        params: vec![TypePattern::var("Acc"), acc_t_to_acc()],
2082        result: TypePattern::var("Acc"),
2083        purity: Purity::Pure,
2084        lowering: MethodLowering::Intrinsic("seq_fold"),
2085        doc: "Reduce elements left-to-right with an accumulator and combining closure.",
2086    }
2087}
2088
2089fn seq_sum() -> MethodEntry {
2090    MethodEntry {
2091        receiver: iterable_of_int_elem(),
2092        name: "sum",
2093        params: vec![],
2094        result: TypePattern::Scalar(ScalarType::Int),
2095        purity: Purity::Pure,
2096        lowering: MethodLowering::Intrinsic("seq_sum"),
2097        doc: "Sum the (Int) elements.",
2098    }
2099}
2100
2101fn seq_count() -> MethodEntry {
2102    MethodEntry {
2103        receiver: iterable_of_t(),
2104        name: "count",
2105        params: vec![],
2106        result: TypePattern::Scalar(ScalarType::Int),
2107        purity: Purity::Pure,
2108        lowering: MethodLowering::Intrinsic("seq_count"),
2109        doc: "Number of elements.",
2110    }
2111}
2112
2113/// `v.count(pred)` — §6.3's `count` with a predicate, which is what §3.3 writes.
2114///
2115/// A second *arity* of one name, which the catalog's `(receiver, name, arity)`
2116/// key allows: `count()` is the element count and `count(pred)` the
2117/// matching-element count.
2118fn seq_count_if() -> MethodEntry {
2119    MethodEntry {
2120        receiver: iterable_of_t(),
2121        name: "count",
2122        params: vec![t_to_bool()],
2123        result: TypePattern::Scalar(ScalarType::Int),
2124        purity: Purity::Pure,
2125        lowering: MethodLowering::Intrinsic("seq_count"),
2126        doc: "Number of elements satisfying the predicate.",
2127    }
2128}
2129
2130// **There is no `collect` row, and ADR-126 is why.** A chain that ends on a
2131// streaming stage already materializes: `recognize_pipeline` appends the
2132// `Collect` sink itself, so `v.map(f)` *is* a `Vec[U]`. This pipeline is eager
2133// (ADR-028 decision 2), so a `collect` row would name a step the compiler takes
2134// whether or not it is written.
2135
2136// --- the barrier combinators (§6.3) ---------------------------------------
2137//
2138// A barrier needs the whole sequence before it can answer anything, so it
2139// cannot be fused into the loop feeding it. That makes it the opposite kind of
2140// row from everything above: a `RuntimeSymbol`, not an `Intrinsic`. That is the
2141// MIR fuser's guardrail and not a style preference — an `Intrinsic` with no
2142// `classify_link`/`classify_sink` arm is a row with no lowering at all, which
2143// `intrinsics_are_all_recognized_so_there_is_no_second_lowering` refuses.
2144//
2145// `recognize_pipeline` already ends a fused chain at an unclassified
2146// `MethodCall` and starts a fresh one from its result, which is exactly what a
2147// barrier means: `pairs.map(f).sorted()` fuses the map into a collect, calls the
2148// wrapper, and `sorted(…).zip(…)` starts again.
2149//
2150// **The receiver is `Iterable` like everything else (ADR-127 decision 3), and
2151// the lowering materializes it first.** A wrapper needs a real `VecPayload`, so
2152// `build::emit_iter_vec` puts the plan's snapshot — or, for a receiver with no
2153// snapshot symbol, a materializing walk — in front of the call. A `Vec[T]`
2154// receiver is the rejected alternative: it makes `set.map(f).sorted()` legal
2155// and `set.sorted()` a `Y110`, which is a rule nobody can hold in their head.
2156//
2157// **`reversed` is a barrier for the definition's own reason** (ADR-145): it
2158// cannot answer its first element until it has seen the last one. A
2159// `classify_link` arm would be classified on name and arity alone and applied
2160// wherever the name appears in a chain, and `v.filter(p).reversed()` does not
2161// know the filtered length up front — so a fused reverse would be unsound
2162// anywhere but immediately adjacent to the source. Walking `emit_iter_item` at
2163// `len - 1 - idx` for that source-adjacent case is a real optimization and a
2164// deliberate non-goal here: a row has exactly one `MethodLowering`, and the
2165// general case has to keep working.
2166//
2167// **`join` is one row, on the generic receiver, with its item bounded to
2168// `Text`** (ADR-144). The bound is on the item and not on a second row because
2169// two `Iterable` rows differing only in their item bound are not duplicates —
2170// `finish` accepts them, and dispatch would then resolve by insertion order,
2171// which is the precedence rule ADR-127 decision 6 refuses. A concrete
2172// `Vec[Char].join/1` beside the generic row is refused outright as
2173// `AmbiguousWithIterable`. So the sequence-of-`Char` case is a differently-named
2174// row — `Vec[Char].to_text()`, defined beside `vec_is_empty` — and a
2175// sequence-of-`Int` joins by rendering first: `ns.map(|n| n.to_text()).join(",")`.
2176//
2177// **`chunks` and `windows` answer `Vec[Vec[T]]`** (ADR-149). Their wrappers
2178// label the *outer* `Vec` with `collections::VEC` while the inner ones keep the
2179// element descriptor: `outer.push(inner)` builds a `Vec[Vec[T]]` and
2180// `adopt_or_reject` labels it `VEC`, so a wrapper answering anything else would
2181// disagree with `push`. Naming a label its receiver cannot supply is what
2182// `praxis_grid_positions` and three siblings do with `&tuples::TUPLE`.
2183//
2184// They are barriers for `reversed`'s reason and not for a new one: a grouping
2185// is a fact about positions in the whole sequence, so neither can answer its
2186// first group from one element. And they are the two rows in the catalog that
2187// fault on an *argument* — see [`seq_chunks`].
2188
2189/// `sorted` — a new `Vec` in ascending order (§6.3).
2190///
2191/// The `Ord` bound is the row's own, and it has to be: the wrapper orders
2192/// through the element descriptor's `compare` callback, and
2193/// `require_collection_invariants` — which is where the language's other
2194/// ordering rule lives — is applied to the receiver *type*, where it would be
2195/// wrong. A `Vec` of unorderable things is a perfectly good `Vec` right up until
2196/// someone sorts it.
2197fn seq_sorted() -> MethodEntry {
2198    MethodEntry {
2199        receiver: TypePattern::iterable(TypePattern::of_kind("T", crate::CapKind::Ord)),
2200        name: "sorted",
2201        params: vec![],
2202        result: vec_of_t(),
2203        purity: Purity::Pure,
2204        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecSorted),
2205        doc: "A new Vec holding these elements in ascending order.",
2206    }
2207}
2208
2209/// `unique` — a new `Vec` with later duplicates dropped, in first-occurrence
2210/// order (§6.3).
2211///
2212/// `HashStable` and not `Hash`: sameness is decided by the descriptor's `hash`
2213/// and `equals`, so an element that can change after it has been seen would not
2214/// be recognized the second time (D4).
2215fn seq_unique() -> MethodEntry {
2216    MethodEntry {
2217        receiver: TypePattern::iterable(TypePattern::of_kind("T", crate::CapKind::HashStable)),
2218        name: "unique",
2219        params: vec![],
2220        result: vec_of_t(),
2221        purity: Purity::Pure,
2222        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecUnique),
2223        doc: "A new Vec with duplicate elements removed, keeping first occurrences.",
2224    }
2225}
2226
2227/// `reversed` — a new `Vec` holding these elements back to front (ADR-145).
2228///
2229/// **No capability bound at all**, and that is the row's own claim rather than
2230/// an omission: reversal reads no descriptor callback, so where `sorted` needs
2231/// `Ord` and `unique` needs `HashStable`, a `Vec` of closures reverses. The
2232/// wrapper cannot fail either, which is why its manifest row is `Allocates`.
2233///
2234/// It answers `Vec[T]` on every one of the ten receivers — including a `Range`,
2235/// so `for y in (0..n).reversed()` is the countdown. That does *not* reopen
2236/// ADR-059 decision 3: no descending `Range` value exists, `RangeVal::new` still
2237/// clamps, and a pipeline's currency is `Vec` (ADR-127 decision 6).
2238fn seq_reversed() -> MethodEntry {
2239    MethodEntry {
2240        receiver: iterable_of_t(),
2241        name: "reversed",
2242        params: vec![],
2243        result: vec_of_t(),
2244        purity: Purity::Pure,
2245        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecReversed),
2246        doc: "A new Vec holding these elements in reverse order.",
2247    }
2248}
2249
2250/// `Vec[Vec[T]]` — what `chunks` and `windows` answer (ADR-149).
2251///
2252/// The one result pattern in the catalog that nests a collection inside a
2253/// collection, and it is written once for both rows so the two cannot come to
2254/// disagree about their shape. `pattern_to_type` recurses, so nothing else is
2255/// needed to instantiate it.
2256fn vec_of_vec_of_t() -> TypePattern {
2257    TypePattern::Collection {
2258        ctor: CollectionCtor::Vec,
2259        args: vec![vec_of_t()],
2260    }
2261}
2262
2263/// `chunks(n)` — these elements in consecutive runs of `n`, the last short if
2264/// the length does not divide (ADR-149).
2265///
2266/// **No capability bound, for `reversed`'s reason**: a grouping reads no
2267/// descriptor callback — not `compare`, not `equals`, not `hash` — so a `Vec` of
2268/// closures chunks. What it *does* have that `reversed` has not is a fault, and
2269/// the fault is on the argument rather than on an element: `n <= 0` is refused
2270/// with `InvalidSize` before the receiver is walked. A run of zero elements is
2271/// not a short run, it is not a run — chunking any non-empty sequence into them
2272/// has no finite answer — and a negative one names nothing at all. That is the
2273/// row's whole faulting surface, which is why its manifest row is
2274/// `AllocatesAndFaults` and `reversed`'s is `Allocates`.
2275///
2276/// A *short last chunk* is not that fault and must not be confused with it:
2277/// `[1, 2, 3].chunks(2)` is `[[1, 2], [3]]`, because the question "which
2278/// consecutive runs of two are there" has an answer for a sequence of three and
2279/// the trailing element is part of it. `windows` is where a group that does not
2280/// fit is dropped instead, and the two differ there because they are asking
2281/// different questions.
2282fn seq_chunks() -> MethodEntry {
2283    MethodEntry {
2284        receiver: iterable_of_t(),
2285        name: "chunks",
2286        params: vec![TypePattern::Scalar(ScalarType::Int)],
2287        result: vec_of_vec_of_t(),
2288        purity: Purity::Pure,
2289        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecChunks),
2290        doc: "Consecutive non-overlapping runs of n; the last may be shorter. \
2291              Faults if n is not positive.",
2292    }
2293}
2294
2295/// `windows(n)` — every consecutive run of exactly `n`, each starting one element
2296/// after the last (ADR-149).
2297///
2298/// Plural, like `chunks` beside it and like every other row that answers many
2299/// things — `frequencies`, `positions`, `cells`, `keys`, `items`. It is Rust's
2300/// spelling too, and it is what §6.3 and ADR-029 have called this row since
2301/// before it existed.
2302///
2303/// **A window that does not fit is dropped, and that is not the `chunks` fault
2304/// arriving late.** `[1, 2].windows(5)` is `[]`: "which runs of five are there"
2305/// is a perfectly good question about a sequence of two, and its answer is
2306/// none. What has no answer is a run of `n <= 0`, which is the same
2307/// `InvalidSize` [`seq_chunks`] raises and for the same reason.
2308fn seq_windows() -> MethodEntry {
2309    MethodEntry {
2310        receiver: iterable_of_t(),
2311        name: "windows",
2312        params: vec![TypePattern::Scalar(ScalarType::Int)],
2313        result: vec_of_vec_of_t(),
2314        purity: Purity::Pure,
2315        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecWindows),
2316        doc: "Every consecutive run of exactly n, sliding by one. Empty if n \
2317              exceeds the length; faults if n is not positive.",
2318    }
2319}
2320
2321/// An iterable of `Text`s, spelled as a bounded variable for
2322/// [`iterable_of_int_elem`]'s reason: the row still *matches* a receiver whose
2323/// item is a `Char` and rejects it with `expected Text, found Char` at the
2324/// method name, rather than "no method `join` on this type".
2325fn iterable_of_text_elem() -> TypePattern {
2326    TypePattern::iterable(TypePattern::is_scalar("T", ScalarType::Text))
2327}
2328
2329/// `join` — these `Text` elements concatenated with a separator between them
2330/// (ADR-144).
2331///
2332/// The separator is a required argument and not an optional one, because the
2333/// catalog has no optional arguments: a row is `(receiver, name, arity)`, and
2334/// `join()` beside `join(Text)` would be two rows for one question. `join("")`
2335/// is the no-separator spelling and says so where it is written.
2336///
2337/// **It renders nothing.** A `Vec[Int]` is a type error at the item, not a
2338/// sequence quietly stringified — which is what keeps `join` from being a back
2339/// door around ADR-143's decision about which types have a `to_text`. The
2340/// spelling is `ns.map(|n| n.to_text()).join(", ")`, and it says that it renders.
2341fn seq_join() -> MethodEntry {
2342    MethodEntry {
2343        receiver: iterable_of_text_elem(),
2344        name: "join",
2345        params: vec![TypePattern::Scalar(ScalarType::Text)],
2346        result: TypePattern::Scalar(ScalarType::Text),
2347        purity: Purity::Pure,
2348        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecJoin),
2349        doc: "These Text items concatenated with `sep` between them.",
2350    }
2351}
2352
2353/// `frequencies` — a `Counter[T]` of how often each element occurs (§6.3, §6.2).
2354///
2355/// The **first** catalog row whose result is a keyed collection, and the reason
2356/// `Bound::Kind` exists. `require_collection_invariants` asks the key rule of a
2357/// method's receiver only; here the receiver is an ordinary `Vec` that is
2358/// allowed to hold anything, and it is the *result* that has keys. So the bound
2359/// is written on the row, where `MethodCatalogBuilder::finish` will refuse it if
2360/// it ever contradicts another.
2361fn seq_frequencies() -> MethodEntry {
2362    MethodEntry {
2363        receiver: TypePattern::iterable(TypePattern::of_kind("T", crate::CapKind::HashStable)),
2364        name: "frequencies",
2365        params: vec![],
2366        result: counter_of_t(),
2367        purity: Purity::Pure,
2368        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecFrequencies),
2369        doc: "A Counter holding how many times each element occurs.",
2370    }
2371}
2372
2373// --- the remaining non-barrier combinators (§6.3) -------------------------
2374// Each is an intrinsic lowered by the MIR fuser (`recognize_pipeline` +
2375// `lower_pipeline`) into a single fused loop, on the generic `Iterable`
2376// receiver like every other combinator (ADR-127 decision 1).
2377
2378/// `(T, T) -> Bool` — the shape of `min_by`/`max_by`'s comparator ("less-than").
2379fn t_t_to_bool() -> TypePattern {
2380    TypePattern::Function {
2381        params: vec![TypePattern::var("T"), TypePattern::var("T")],
2382        result: Box::new(TypePattern::Scalar(ScalarType::Bool)),
2383    }
2384}
2385
2386/// `(T) -> Vec<U>` — the shape of `flat_map`'s closure.
2387fn t_to_vec_u() -> TypePattern {
2388    TypePattern::Function {
2389        params: vec![TypePattern::var("T")],
2390        result: Box::new(vec_of_u()),
2391    }
2392}
2393
2394// Streaming stages ---------------------------------------------------------
2395
2396fn seq_take() -> MethodEntry {
2397    MethodEntry {
2398        receiver: iterable_of_t(),
2399        name: "take",
2400        params: vec![TypePattern::Scalar(ScalarType::Int)],
2401        result: vec_of_t(),
2402        purity: Purity::Pure,
2403        lowering: MethodLowering::Intrinsic("seq_take"),
2404        doc: "Keep at most the first n elements.",
2405    }
2406}
2407
2408fn seq_skip() -> MethodEntry {
2409    MethodEntry {
2410        receiver: iterable_of_t(),
2411        name: "skip",
2412        params: vec![TypePattern::Scalar(ScalarType::Int)],
2413        result: vec_of_t(),
2414        purity: Purity::Pure,
2415        lowering: MethodLowering::Intrinsic("seq_skip"),
2416        doc: "Drop the first n elements.",
2417    }
2418}
2419
2420fn seq_take_while() -> MethodEntry {
2421    MethodEntry {
2422        receiver: iterable_of_t(),
2423        name: "take_while",
2424        params: vec![t_to_bool()],
2425        result: vec_of_t(),
2426        purity: Purity::Pure,
2427        lowering: MethodLowering::Intrinsic("seq_take_while"),
2428        doc: "Keep elements until the predicate is false.",
2429    }
2430}
2431
2432/// `Vec[(Int, T)]` — the index/element pairs `enumerate`'s fused loop builds.
2433fn vec_of_index_and_t() -> TypePattern {
2434    TypePattern::Collection {
2435        ctor: CollectionCtor::Vec,
2436        args: vec![TypePattern::Tuple(vec![
2437            TypePattern::Scalar(ScalarType::Int),
2438            TypePattern::var("T"),
2439        ])],
2440    }
2441}
2442
2443/// `Vec[(T, U)]` — what `zip` yields, pairing the receiver's element with the
2444/// argument sequence's. The two element types are independent.
2445fn vec_of_t_and_u() -> TypePattern {
2446    TypePattern::Collection {
2447        ctor: CollectionCtor::Vec,
2448        args: vec![TypePattern::Tuple(vec![
2449            TypePattern::var("T"),
2450            TypePattern::var("U"),
2451        ])],
2452    }
2453}
2454
2455fn seq_enumerate() -> MethodEntry {
2456    MethodEntry {
2457        receiver: iterable_of_t(),
2458        name: "enumerate",
2459        params: vec![],
2460        result: vec_of_index_and_t(),
2461        purity: Purity::Pure,
2462        lowering: MethodLowering::Intrinsic("seq_enumerate"),
2463        doc: "Pair each element with its index.",
2464    }
2465}
2466
2467fn seq_zip() -> MethodEntry {
2468    MethodEntry {
2469        receiver: iterable_of_t(),
2470        name: "zip",
2471        params: vec![vec_of_u()],
2472        result: vec_of_t_and_u(),
2473        purity: Purity::Pure,
2474        lowering: MethodLowering::Intrinsic("seq_zip"),
2475        doc: "Pair elements with another sequence, stopping at the shorter length.",
2476    }
2477}
2478
2479fn seq_flat_map() -> MethodEntry {
2480    MethodEntry {
2481        receiver: iterable_of_t(),
2482        name: "flat_map",
2483        params: vec![t_to_vec_u()],
2484        result: vec_of_u(),
2485        purity: Purity::Pure,
2486        lowering: MethodLowering::Intrinsic("seq_flat_map"),
2487        doc: "Map each element to a Vec and concatenate the results.",
2488    }
2489}
2490
2491fn seq_filter_map() -> MethodEntry {
2492    MethodEntry {
2493        receiver: iterable_of_t(),
2494        name: "filter_map",
2495        params: vec![t_to_option_u()],
2496        result: vec_of_u(),
2497        purity: Purity::Pure,
2498        lowering: MethodLowering::Intrinsic("seq_filter_map"),
2499        doc: "Map each element to an Option and keep the Some payloads.",
2500    }
2501}
2502
2503// Aggregating sinks (scalar result) ---------------------------------------
2504//
2505// `sum`, `product`, `min` and `max` are **Int** operations. Each one lowers to
2506// an `ExtractScalar` at `ScalarKind::Int` followed by an `IntBinOp` or an
2507// `IntCmp`, and the row's own result says `Int`. The element bound therefore has
2508// to be `Int` and not `Numeric`: a `Numeric` bound would bless `Float`, and
2509// `Vec[Float].sum()` would reinterpret each float's bits as an integer and
2510// return nonsense. `Bool` is excluded for the same reason.
2511//
2512// The bound is discharged by unification, so an element type that is *not yet
2513// known* is pinned to `Int` rather than merely allowed: `v.map(f).sum()` pins the
2514// closure's result.
2515
2516/// An iterable of `Int`s, spelled as a bounded variable so the entry still
2517/// *matches* a receiver whose item is `Bool` or `Float` and rejects it with
2518/// `expected Int, found …` instead of "no method `sum` on this type".
2519///
2520/// Written once for all ten receivers rather than once per receiver, which is
2521/// what keeps the bound from drifting between them (ADR-127 decision 1).
2522fn iterable_of_int_elem() -> TypePattern {
2523    TypePattern::iterable(TypePattern::is_scalar("T", ScalarType::Int))
2524}
2525
2526fn seq_product() -> MethodEntry {
2527    MethodEntry {
2528        receiver: iterable_of_int_elem(),
2529        name: "product",
2530        params: vec![],
2531        result: TypePattern::Scalar(ScalarType::Int),
2532        purity: Purity::Pure,
2533        lowering: MethodLowering::Intrinsic("seq_product"),
2534        doc: "Multiply the (Int) elements.",
2535    }
2536}
2537
2538fn seq_min() -> MethodEntry {
2539    MethodEntry {
2540        receiver: iterable_of_int_elem(),
2541        name: "min",
2542        params: vec![],
2543        result: TypePattern::Scalar(ScalarType::Int),
2544        purity: Purity::Pure,
2545        lowering: MethodLowering::Intrinsic("seq_min"),
2546        doc: "Smallest (Int) element. Faults on an empty sequence (D1).",
2547    }
2548}
2549
2550fn seq_max() -> MethodEntry {
2551    MethodEntry {
2552        receiver: iterable_of_int_elem(),
2553        name: "max",
2554        params: vec![],
2555        result: TypePattern::Scalar(ScalarType::Int),
2556        purity: Purity::Pure,
2557        lowering: MethodLowering::Intrinsic("seq_max"),
2558        doc: "Largest (Int) element. Faults on an empty sequence (D1).",
2559    }
2560}
2561
2562fn seq_min_by() -> MethodEntry {
2563    MethodEntry {
2564        receiver: iterable_of_t(),
2565        name: "min_by",
2566        params: vec![t_t_to_bool()],
2567        result: TypePattern::var("T"),
2568        purity: Purity::Pure,
2569        lowering: MethodLowering::Intrinsic("seq_min_by"),
2570        doc: "Smallest element per a (T,T)->Bool \"less-than\" comparator.",
2571    }
2572}
2573
2574fn seq_max_by() -> MethodEntry {
2575    MethodEntry {
2576        receiver: iterable_of_t(),
2577        name: "max_by",
2578        params: vec![t_t_to_bool()],
2579        result: TypePattern::var("T"),
2580        purity: Purity::Pure,
2581        lowering: MethodLowering::Intrinsic("seq_max_by"),
2582        doc: "Largest element per a (T,T)->Bool \"less-than\" comparator.",
2583    }
2584}
2585
2586fn seq_any() -> MethodEntry {
2587    MethodEntry {
2588        receiver: iterable_of_t(),
2589        name: "any",
2590        params: vec![t_to_bool()],
2591        result: TypePattern::Scalar(ScalarType::Bool),
2592        purity: Purity::Pure,
2593        lowering: MethodLowering::Intrinsic("seq_any"),
2594        doc: "True if any element satisfies the predicate (short-circuits).",
2595    }
2596}
2597
2598fn seq_all() -> MethodEntry {
2599    MethodEntry {
2600        receiver: iterable_of_t(),
2601        name: "all",
2602        params: vec![t_to_bool()],
2603        result: TypePattern::Scalar(ScalarType::Bool),
2604        purity: Purity::Pure,
2605        lowering: MethodLowering::Intrinsic("seq_all"),
2606        doc: "True if all elements satisfy the predicate (short-circuits).",
2607    }
2608}
2609
2610fn seq_find() -> MethodEntry {
2611    MethodEntry {
2612        receiver: iterable_of_t(),
2613        name: "find",
2614        params: vec![t_to_bool()],
2615        result: TypePattern::Option(Box::new(TypePattern::var("T"))),
2616        purity: Purity::Pure,
2617        lowering: MethodLowering::Intrinsic("seq_find"),
2618        doc: "The first matching element, or None.",
2619    }
2620}
2621
2622fn seq_position() -> MethodEntry {
2623    MethodEntry {
2624        receiver: iterable_of_t(),
2625        name: "position",
2626        params: vec![t_to_bool()],
2627        result: TypePattern::Option(Box::new(TypePattern::Scalar(ScalarType::Int))),
2628        purity: Purity::Pure,
2629        lowering: MethodLowering::Intrinsic("seq_position"),
2630        doc: "The index of the first matching element, or None.",
2631    }
2632}
2633
2634fn seq_reduce() -> MethodEntry {
2635    MethodEntry {
2636        receiver: iterable_of_t(),
2637        name: "reduce",
2638        params: vec![t_t_to_t()],
2639        result: TypePattern::var("T"),
2640        purity: Purity::Pure,
2641        lowering: MethodLowering::Intrinsic("seq_reduce"),
2642        doc: "Reduce left-to-right, seeded with the first element.",
2643    }
2644}
2645
2646/// `sorted_by_key(f)` — a new `Vec` ordered by the key `f` extracts (ADR-127
2647/// decision 5).
2648///
2649/// **A keyed collection cannot order its own items.** ADR-045 decided that no
2650/// composite is orderable, because MIR had one integer compare and `(1, 2) < (1,
2651/// 3)` would have compared two schema pointers — so the moment a pipeline's item
2652/// is a pair, which is the moment its source is a `Map` or a `Counter`, `sorted`
2653/// is unavailable and "the five most common values" has no spelling.
2654///
2655/// So the `Ord` bound is on the **extracted key** rather than on the element,
2656/// and the composite-ordering question ADR-045 deferred stays deferred.
2657///
2658/// A barrier like `sorted`, and for the same reason: it needs the whole sequence
2659/// before it can answer its first element. Its receiver is `Iterable`, so
2660/// `build::emit_iter_vec` materializes it in front of the wrapper.
2661fn seq_sorted_by_key() -> MethodEntry {
2662    MethodEntry {
2663        receiver: iterable_of_t(),
2664        name: "sorted_by_key",
2665        params: vec![TypePattern::Function {
2666            params: vec![TypePattern::var("T")],
2667            result: Box::new(TypePattern::of_kind("K", crate::CapKind::Ord)),
2668        }],
2669        result: vec_of_t(),
2670        purity: Purity::Pure,
2671        lowering: MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecSortedByKey),
2672        doc: "A new Vec ordered by the key the closure extracts.",
2673    }
2674}
2675
2676// --- the conversions (ADR-127 decision 4) ---------------------------------
2677//
2678// **A pipeline's currency is `Vec`**: every streaming stage answers one,
2679// whatever the receiver was, and a program that wants a collection back says
2680// which one. `set.filter(p)` is a `Vec[T]`; `set.filter(p).to_set()` is a
2681// `Set[T]`. One sentence answers "what does `filter` return" for all ten
2682// receivers, and it is answerable without knowing which receiver you are on —
2683// which is what decision 6 declined `map_values` and the shape-preserving family
2684// to keep.
2685//
2686// Each is a **fused sink**, not a barrier: `Sink::CollectInto`'s accumulator is
2687// the target collection, so `v.map(f).to_set()` is one loop with no intermediate
2688// `Vec`. The per-element step is one wrapper that already exists.
2689//
2690// **The set is closed at "every collection with a constructor", and that is the
2691// point.** The ask named `Set`/`Map`/`Counter`; the last four are here because
2692// leaving them out is what creates an asymmetry decision 6 would then have to
2693// defend — `deque.filter(p)` answering a `Vec` with no way back to a `Deque`,
2694// and no way to build a heap from a sequence without a `while` loop. `Grid` is
2695// the one collection with no row: a grid needs a width, and a flat item sequence
2696// does not carry one.
2697
2698/// `to_vec()` — the item sequence as a `Vec`.
2699///
2700/// **This is not `collect` coming back.** ADR-126 deleted `collect` because it
2701/// "named a step the compiler takes anyway" — a chain ending on a stage already
2702/// materializes. For nine of `to_vec`'s ten receivers it names a step the
2703/// compiler does **not** take: `s.to_vec()` is the only way to get a `Vec[T]` out
2704/// of a `Set`, and `m.to_vec()` the only way to get `Vec[(K, V)]` out of a `Map`,
2705/// where `keys()` and `values()` answer two aligned halves and nothing joins
2706/// them.
2707///
2708/// On a `Vec` receiver it degenerates to the identity, and it answers **the same
2709/// reference**, not a copy. That is the second half of ADR-126 decision 2 kept:
2710/// that decision declined to leave a shallow copy behind "under a name that does
2711/// not mention it", and this name does not mention one either.
2712fn seq_to_vec() -> MethodEntry {
2713    MethodEntry {
2714        receiver: iterable_of_t(),
2715        name: "to_vec",
2716        params: vec![],
2717        result: vec_of_t(),
2718        purity: Purity::Pure,
2719        lowering: MethodLowering::Intrinsic("seq_to_vec"),
2720        doc: "The items as a Vec. On a Vec receiver this is the receiver itself.",
2721    }
2722}
2723
2724/// `to_set()` — a `Set[T]`, duplicates dropped and no order kept.
2725///
2726/// `HashStable` for the reason every key rule is: an element that can change
2727/// after it is stored moves its own bucket without moving the entry, and cannot
2728/// be found again (D4). The bound is the row's own because
2729/// `require_collection_invariants` asks about the *receiver*, and here it is the
2730/// **result** that has the keys — the same shape `frequencies` established.
2731///
2732/// Not the same question as `unique()`, and the reason is worth writing next to
2733/// both: `unique` answers a `Vec` in first-occurrence order, and a `Set` has no
2734/// order to preserve.
2735fn seq_to_set() -> MethodEntry {
2736    MethodEntry {
2737        receiver: TypePattern::iterable(TypePattern::of_kind("T", crate::CapKind::HashStable)),
2738        name: "to_set",
2739        params: vec![],
2740        result: set_of_t(),
2741        purity: Purity::Pure,
2742        lowering: MethodLowering::Intrinsic("seq_to_set"),
2743        doc: "A Set holding these items, duplicates dropped.",
2744    }
2745}
2746
2747/// `to_map()` — a `Map[K, V]`, on a pipeline whose item is a pair.
2748///
2749/// **The receiver pattern says "a pair" rather than prose saying it**, so
2750/// `[1, 2].to_map()` is a unification failure at the method name — "expected
2751/// `(K, V)`, found `Int`" — and not a row that resolves and then faults. That is
2752/// the whole of why `TypePattern::Iterable` carries an item pattern at all.
2753///
2754/// Duplicate keys resolve last-wins, which is `insert`'s existing rule.
2755fn seq_to_map() -> MethodEntry {
2756    MethodEntry {
2757        receiver: TypePattern::iterable(TypePattern::Tuple(vec![
2758            TypePattern::of_kind("K", crate::CapKind::HashStable),
2759            TypePattern::var("V"),
2760        ])),
2761        name: "to_map",
2762        params: vec![],
2763        result: map_of_k_v(),
2764        purity: Purity::Pure,
2765        lowering: MethodLowering::Intrinsic("seq_to_map"),
2766        doc: "A Map built from (key, value) pairs. Duplicate keys: last wins.",
2767    }
2768}
2769
2770/// `to_counter()` — a `Counter[T]` from `(T, Int)` pairs, taking each pair's
2771/// count.
2772///
2773/// `frequencies()` is the other direction and neither expresses the other:
2774/// `v.frequencies()` *counts* occurrences of each element, `pairs.to_counter()`
2775/// *assigns* the count each pair carries. They are the two directions of one type
2776/// change, so both stay.
2777fn seq_to_counter() -> MethodEntry {
2778    MethodEntry {
2779        receiver: TypePattern::iterable(TypePattern::Tuple(vec![
2780            TypePattern::of_kind("T", crate::CapKind::HashStable),
2781            TypePattern::Scalar(ScalarType::Int),
2782        ])),
2783        name: "to_counter",
2784        params: vec![],
2785        result: counter_of_t(),
2786        purity: Purity::Pure,
2787        lowering: MethodLowering::Intrinsic("seq_to_counter"),
2788        doc: "A Counter built from (key, count) pairs. Duplicate keys: last wins.",
2789    }
2790}
2791
2792fn seq_to_deque() -> MethodEntry {
2793    MethodEntry {
2794        receiver: iterable_of_t(),
2795        name: "to_deque",
2796        params: vec![],
2797        result: deque_of_t(),
2798        purity: Purity::Pure,
2799        lowering: MethodLowering::Intrinsic("seq_to_deque"),
2800        doc: "A Deque holding these items, in order.",
2801    }
2802}
2803
2804/// `to_min_heap()` — and [`seq_to_max_heap`], its dual.
2805///
2806/// The `Ord` bound is on the row for `to_set`'s reason: the heap being built is
2807/// the *result*, and a heap orders its elements as it pushes them.
2808fn seq_to_min_heap() -> MethodEntry {
2809    MethodEntry {
2810        receiver: TypePattern::iterable(TypePattern::of_kind("T", crate::CapKind::Ord)),
2811        name: "to_min_heap",
2812        params: vec![],
2813        result: min_heap_of_t(),
2814        purity: Purity::Pure,
2815        lowering: MethodLowering::Intrinsic("seq_to_min_heap"),
2816        doc: "A MinHeap holding these items.",
2817    }
2818}
2819
2820fn seq_to_max_heap() -> MethodEntry {
2821    MethodEntry {
2822        receiver: TypePattern::iterable(TypePattern::of_kind("T", crate::CapKind::Ord)),
2823        name: "to_max_heap",
2824        params: vec![],
2825        result: max_heap_of_t(),
2826        purity: Purity::Pure,
2827        lowering: MethodLowering::Intrinsic("seq_to_max_heap"),
2828        doc: "A MaxHeap holding these items.",
2829    }
2830}
2831
2832/// `to_bitset()` — a `BitSet` of `Int` members.
2833///
2834/// The receiver says `Int` the way `to_map` says "a pair", so a non-`Int` item is
2835/// a type error at the method name. `praxis_bitset_insert` still faults on a
2836/// negative or oversized member, which is a *value* question no type can answer.
2837fn seq_to_bitset() -> MethodEntry {
2838    MethodEntry {
2839        receiver: iterable_of_int_elem(),
2840        name: "to_bitset",
2841        params: vec![],
2842        result: bitset_receiver(),
2843        purity: Purity::Pure,
2844        lowering: MethodLowering::Intrinsic("seq_to_bitset"),
2845        doc: "A BitSet holding these (Int) items. Faults on a negative or oversized member.",
2846    }
2847}
2848
2849#[cfg(test)]
2850mod tests {
2851    use super::*;
2852
2853    /// **Every catalog row describes itself.** The `doc` field is what hover,
2854    /// completion and signature help put in front of a reader, so a row added
2855    /// with an empty or placeholder one ships a method the editor can name and
2856    /// cannot explain — and nothing else in the build would notice, because the
2857    /// field is a `&'static str` that `""` satisfies.
2858    ///
2859    /// The sentence rule is the half that catches a placeholder: `"TODO"` and
2860    /// `"len"` are both long enough to pass a length floor alone.
2861    #[test]
2862    fn every_catalog_row_documents_itself() {
2863        for e in builtin_catalog().entries() {
2864            let what = format!("{}.{}/{}", e.receiver, e.name, e.arity());
2865            assert!(e.doc.len() > 8, "{what} has no real documentation");
2866            assert!(
2867                e.doc.ends_with('.') || e.doc.ends_with(')'),
2868                "{what}'s doc is not a sentence: {:?}",
2869                e.doc
2870            );
2871            // A backtick opens a sentence too: the subscript rows lead with the
2872            // syntax they are about (`` `v[i]` — the element at `i` ``), which
2873            // is the clearest thing they could say and not a placeholder.
2874            assert!(
2875                e.doc
2876                    .chars()
2877                    .next()
2878                    .is_some_and(|c| c.is_uppercase() || c == '`'),
2879                "{what}'s doc does not open a sentence: {:?}",
2880                e.doc
2881            );
2882        }
2883    }
2884
2885    #[test]
2886    fn builtin_catalog_has_vec_methods() {
2887        let cat = builtin_catalog();
2888        assert!(cat.len() >= 4);
2889        let vec_pat = vec_of_t();
2890        let push_hits: Vec<_> = cat.by_receiver_and_name(&vec_pat, "push").collect();
2891        assert_eq!(push_hits.len(), 1);
2892        assert_eq!(
2893            push_hits[0].lowering,
2894            MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecPush)
2895        );
2896    }
2897
2898    #[test]
2899    fn builtin_catalog_get_can_fault() {
2900        let cat = builtin_catalog();
2901        let vec_pat = vec_of_t();
2902        let get = cat
2903            .by_receiver_and_name(&vec_pat, "get")
2904            .next()
2905            .expect("vec.get exists");
2906        assert!(get.can_fault());
2907        // Derived from the manifest, not restated on the row:
2908        // `praxis_bitset_insert` raises `InvalidSize` for a member outside
2909        // `BitIndex`'s range, so `bitset.insert` can fault.
2910        let bitset_pat = bitset_receiver();
2911        let insert = cat
2912            .by_receiver_and_name(&bitset_pat, "insert")
2913            .next()
2914            .expect("bitset.insert exists");
2915        assert!(insert.can_fault());
2916    }
2917
2918    /// A keyed collection can be enumerated, `count` has two arities, and every
2919    /// enumeration answers a `Vec` so §6.3 applies to it — which is what makes
2920    /// §3.3's `counts.values().count(|n| n >= 2)` spellable.
2921    ///
2922    /// The second arity is not a language decision: the catalog's key is
2923    /// `(receiver, name, arity)`, and `count` is the row that uses it.
2924    #[test]
2925    fn a_keyed_collection_enumerates_and_count_has_two_arities() {
2926        let cat = builtin_catalog();
2927        let map_pat = map_of_k_v();
2928        let counter_pat = counter_of_t();
2929
2930        // Both collections enumerate both ways, and each answers a `Vec`.
2931        for (pat, what) in [(map_pat.clone(), "Map"), (counter_pat.clone(), "Counter")] {
2932            for name in ["keys", "values"] {
2933                let hits: Vec<_> = cat.by_receiver_and_name(&pat, name).collect();
2934                assert_eq!(hits.len(), 1, "{what}.{name}()");
2935                assert_eq!(hits[0].arity(), 0, "{what}.{name}() takes no arguments");
2936                assert!(
2937                    matches!(
2938                        hits[0].result,
2939                        TypePattern::Collection {
2940                            ctor: CollectionCtor::Vec,
2941                            ..
2942                        }
2943                    ),
2944                    "{what}.{name}() answers a Vec so every §6.3 combinator applies"
2945                );
2946            }
2947        }
2948
2949        // A `Counter`'s values are its counts, whatever its key type is (§6.2),
2950        // where a `Map`'s are its value type. The two are not the same row written
2951        // twice.
2952        let counter_values = cat
2953            .by_receiver_and_name(&counter_pat, "values")
2954            .next()
2955            .expect("Counter.values");
2956        assert_eq!(
2957            counter_values.result,
2958            TypePattern::Collection {
2959                ctor: CollectionCtor::Vec,
2960                args: vec![TypePattern::Scalar(ScalarType::Int)]
2961            }
2962        );
2963        let map_values = cat
2964            .by_receiver_and_name(&map_pat, "values")
2965            .next()
2966            .expect("Map.values");
2967        assert_eq!(
2968            map_values.result,
2969            TypePattern::Collection {
2970                ctor: CollectionCtor::Vec,
2971                args: vec![TypePattern::var("V")]
2972            }
2973        );
2974        // …and `keys()` is the *key* type, which is what makes `m[ks[i]]` legal.
2975        let map_keys = cat
2976            .by_receiver_and_name(&map_pat, "keys")
2977            .next()
2978            .expect("Map.keys");
2979        assert_eq!(
2980            map_keys.result,
2981            TypePattern::Collection {
2982                ctor: CollectionCtor::Vec,
2983                args: vec![TypePattern::var("K")]
2984            }
2985        );
2986
2987        // `count` at two arities — one pair of rows, on the generic receiver
2988        // every pipeline starts from (ADR-127).
2989        let arities: Vec<usize> = cat
2990            .by_receiver_and_name(&iterable_of_t(), "count")
2991            .map(|e| e.arity())
2992            .collect();
2993        assert_eq!(arities.len(), 2, "count has two rows");
2994        assert!(arities.contains(&0) && arities.contains(&1));
2995    }
2996
2997    /// **ADR-127 decision 1.** Every §6.3 combinator is **one** row, on the
2998    /// generic pipeline receiver.
2999    ///
3000    /// The assertion is two-sided: each name is on `Iterable` and on nothing
3001    /// else, and no `Seq`-receiver row exists anywhere in the table — nothing
3002    /// produces or consumes a `Seq`, so a row on one would be unreachable.
3003    #[test]
3004    fn every_pipeline_combinator_is_one_row_on_the_generic_receiver() {
3005        let cat = builtin_catalog();
3006        // The twenty-three fused stages and sinks, six of the eight barriers
3007        // (`chunks` and `windows` are not listed), and the eight conversions.
3008        // `count` is here once and checked at both arities by
3009        // `a_keyed_collection_enumerates_and_count_has_two_arities`.
3010        //
3011        // `to_text` is deliberately *not* in this list: it has no generic row,
3012        // and the reason is written on `vec_to_text` (ADR-144).
3013        let combinators = [
3014            "map",
3015            "filter",
3016            "filter_map",
3017            "flat_map",
3018            "take",
3019            "skip",
3020            "take_while",
3021            "enumerate",
3022            "zip",
3023            "fold",
3024            "reduce",
3025            "sum",
3026            "product",
3027            "count",
3028            "any",
3029            "all",
3030            "find",
3031            "position",
3032            "min",
3033            "max",
3034            "min_by",
3035            "max_by",
3036            "sorted",
3037            "sorted_by_key",
3038            "unique",
3039            "reversed",
3040            "frequencies",
3041            "join",
3042            "to_vec",
3043            "to_set",
3044            "to_map",
3045            "to_counter",
3046            "to_deque",
3047            "to_min_heap",
3048            "to_max_heap",
3049            "to_bitset",
3050        ];
3051        for name in combinators {
3052            let rows: Vec<_> = cat.entries().iter().filter(|e| e.name == name).collect();
3053            assert!(!rows.is_empty(), "`{name}` has no row at all");
3054            // One row per *arity*: `count()` is the element count and
3055            // `count(pred)` the matching-element count, which the catalog's key
3056            // allows. Two at one arity would be a duplicated surface.
3057            let mut generic: Vec<usize> = rows
3058                .iter()
3059                .filter(|e| matches!(e.receiver, TypePattern::Iterable { .. }))
3060                .map(|e| e.arity())
3061                .collect();
3062            assert!(!generic.is_empty(), "`{name}` has no generic row");
3063            let seen = generic.len();
3064            generic.sort_unstable();
3065            generic.dedup();
3066            assert_eq!(
3067                generic.len(),
3068                seen,
3069                "`{name}` has two generic rows at one arity — one receiver \
3070                 getting a feature ten should have"
3071            );
3072            // A row that shares the *name* must be on a receiver the generic one
3073            // does not accept, which the builder's collision check enforces and
3074            // this restates from the reader's side. `Grid[T].find(value)` is the
3075            // live example: §6.4's "where is this cell" is a different question
3076            // from §6.3's "which element matches", and they can coexist because a
3077            // `Grid` is not one of the ten.
3078            for row in rows {
3079                assert!(
3080                    matches!(row.receiver, TypePattern::Iterable { .. })
3081                        || !crate::is_pipeline_receiver(&row.receiver),
3082                    "`{name}` also has a row on {}, which the generic row \
3083                     accepts — both would match, and which one a call resolves \
3084                     to would be insertion order",
3085                    row.receiver
3086                );
3087            }
3088        }
3089
3090        // `Grid[T].map` is the row §6.4 still owes, and the *absence* of a
3091        // generic row claiming the name is what leaves room for it. The builder's
3092        // collision check is the other half; this is the half that says the
3093        // exclusion was deliberate.
3094        assert!(
3095            !crate::PIPELINE_RECEIVERS.contains(&CollectionCtor::Grid),
3096            "a generic `map` would claim §6.4's name and answer a `Vec`"
3097        );
3098
3099        // Nothing is registered on a `Seq`, which has no values.
3100        for e in cat.entries() {
3101            assert!(
3102                !matches!(
3103                    e.receiver,
3104                    TypePattern::Collection {
3105                        ctor: CollectionCtor::Seq,
3106                        ..
3107                    }
3108                ),
3109                "`{}` is still on a `Seq`, which has no values",
3110                e.name
3111            );
3112        }
3113    }
3114
3115    /// **ADR-127 decision 4.** There is a conversion for every collection with a
3116    /// constructor, and exactly one collection with none.
3117    ///
3118    /// The set is closed at that boundary on purpose: the ask named
3119    /// `Set`/`Map`/`Counter`, and leaving the other four out is what would create
3120    /// the asymmetry decision 6 then has to defend — `deque.filter(p)` answering
3121    /// a `Vec` with no way back to a `Deque`. `Grid` is the exception because a
3122    /// grid needs a width and a flat item sequence does not carry one.
3123    #[test]
3124    fn a_conversion_exists_for_every_collection_that_can_be_constructed() {
3125        let cat = builtin_catalog();
3126        for (name, ctor) in [
3127            ("to_vec", CollectionCtor::Vec),
3128            ("to_set", CollectionCtor::Set),
3129            ("to_map", CollectionCtor::Map),
3130            ("to_counter", CollectionCtor::Counter),
3131            ("to_deque", CollectionCtor::Deque),
3132            ("to_min_heap", CollectionCtor::MinHeap),
3133            ("to_max_heap", CollectionCtor::MaxHeap),
3134            ("to_bitset", CollectionCtor::BitSet),
3135        ] {
3136            let row = cat
3137                .entries()
3138                .iter()
3139                .find(|e| e.name == name)
3140                .unwrap_or_else(|| panic!("`{name}` has no row"));
3141            assert_eq!(row.arity(), 0, "`{name}` takes no arguments");
3142            let built = match &row.result {
3143                TypePattern::Collection { ctor, .. } => *ctor,
3144                other => panic!("`{name}` answers {other}, not a collection"),
3145            };
3146            assert_eq!(built, ctor, "`{name}` builds the collection it names");
3147        }
3148        assert!(
3149            cat.entries().iter().all(|e| e.name != "to_grid"),
3150            "a grid needs a width, and an item sequence does not carry one"
3151        );
3152
3153        // **The pair shape is in the receiver, not in prose.** That is what makes
3154        // `[1, 2].to_map()` "expected `(K, V)`, found `Int`" at the method name
3155        // rather than a row that resolves and then faults at runtime.
3156        for name in ["to_map", "to_counter"] {
3157            let row = cat.entries().iter().find(|e| e.name == name).unwrap();
3158            let TypePattern::Iterable { item } = &row.receiver else {
3159                panic!("`{name}` is not on the generic receiver")
3160            };
3161            assert!(
3162                matches!(**item, TypePattern::Tuple(ref els) if els.len() == 2),
3163                "`{name}` accepts a `Map` or a `Counter` by saying its item is a pair"
3164            );
3165        }
3166    }
3167
3168    /// The subscript rows are the closed set the language documents, and their
3169    /// names cannot be written in source.
3170    ///
3171    /// Two properties in one test because they are both about the same decision.
3172    /// Which collections index is a *language* answer (§4.7/§6.2/§6.4), so a row
3173    /// added or dropped by accident should fail here rather than surface as a
3174    /// program that mysteriously compiles. And a row whose name is an identifier
3175    /// would be callable as `m.foo(k)`, which no design section describes.
3176    #[test]
3177    fn the_subscript_rows_are_a_closed_set_no_program_can_name() {
3178        let cat = builtin_catalog();
3179        let of = |ctor: CollectionCtor, args: usize| TypePattern::Collection {
3180            ctor,
3181            args: (0..args).map(|_| TypePattern::var("T")).collect(),
3182        };
3183        let map_pat = map_of_k_v();
3184
3185        // Six read. `Text` is a scalar receiver, so it is spelled differently.
3186        for (pat, indices, what) in [
3187            (of(CollectionCtor::Vec, 1), 1, "Vec"),
3188            (of(CollectionCtor::Deque, 1), 1, "Deque"),
3189            (map_pat.clone(), 1, "Map"),
3190            (of(CollectionCtor::Counter, 1), 1, "Counter"),
3191            (of(CollectionCtor::Grid, 1), 2, "Grid"),
3192            (text_receiver(), 1, "Text"),
3193        ] {
3194            let hits: Vec<_> = cat
3195                .by_receiver_and_name(&pat, crate::catalog::INDEX_READ)
3196                .collect();
3197            assert_eq!(hits.len(), 1, "{what} reads through exactly one row");
3198            assert_eq!(hits[0].arity(), indices, "{what} indexes at {indices}");
3199        }
3200
3201        // Five store — every reader but `Text`. The asymmetry the `Y020` message
3202        // has to describe is now `Text`'s alone: it reads a `Char` out and is
3203        // immutable (§4.3), so there is nothing to write back through.
3204        for (pat, args, what) in [
3205            (of(CollectionCtor::Vec, 1), 2, "Vec"),
3206            (of(CollectionCtor::Deque, 1), 2, "Deque"),
3207            (map_pat.clone(), 2, "Map"),
3208            (of(CollectionCtor::Counter, 1), 2, "Counter"),
3209            (of(CollectionCtor::Grid, 1), 3, "Grid"),
3210        ] {
3211            let hits: Vec<_> = cat
3212                .by_receiver_and_name(&pat, crate::catalog::INDEX_STORE)
3213                .collect();
3214            assert_eq!(hits.len(), 1, "{what} stores through exactly one row");
3215            assert_eq!(
3216                hits[0].arity(),
3217                args,
3218                "{what}'s store takes its indices and then the value"
3219            );
3220        }
3221
3222        // Nothing else has either row.
3223        for (pat, what) in [
3224            (of(CollectionCtor::Set, 1), "Set"),
3225            (of(CollectionCtor::MinHeap, 1), "MinHeap"),
3226            (of(CollectionCtor::MaxHeap, 1), "MaxHeap"),
3227            (of(CollectionCtor::BitSet, 0), "BitSet"),
3228        ] {
3229            for name in [crate::catalog::INDEX_READ, crate::catalog::INDEX_STORE] {
3230                assert_eq!(
3231                    cat.by_receiver_and_name(&pat, name).count(),
3232                    0,
3233                    "{what} has no `{name}`"
3234                );
3235            }
3236        }
3237        assert_eq!(
3238            cat.by_receiver_and_name(&text_receiver(), crate::catalog::INDEX_STORE)
3239                .count(),
3240            0,
3241            "a `Text` is immutable: it reads through a subscript and has \
3242             no element store"
3243        );
3244
3245        // A `Vec`/`Deque` store is a **replacement**, and the wrapper it names is
3246        // the assertion of that: `praxis_vec_set` faults on an index the vector
3247        // does not hold, where `praxis_vec_push` would have grown it. A row that
3248        // pointed at the push would spell `v[i] = x` and mean `v.push(x)`.
3249        for (pat, push, what) in [
3250            (of(CollectionCtor::Vec, 1), "push", "Vec"),
3251            (of(CollectionCtor::Deque, 1), "push_back", "Deque"),
3252        ] {
3253            let store = cat
3254                .by_receiver_and_name(&pat, crate::catalog::INDEX_STORE)
3255                .next()
3256                .unwrap_or_else(|| panic!("{what}'s store"));
3257            let appender = cat
3258                .by_receiver_and_name(&pat, push)
3259                .next()
3260                .unwrap_or_else(|| panic!("{what}.{push}"));
3261            assert_ne!(
3262                store.lowering, appender.lowering,
3263                "{what}'s store replaces; `{push}` appends"
3264            );
3265            assert!(
3266                store.can_fault(),
3267                "{what}'s store reports an index it does not hold"
3268            );
3269        }
3270
3271        // A `Map`'s two reads are two *different* wrappers: §4.7 gives `.get` and
3272        // `map[key]` different answers about an absent key, so pointing both rows
3273        // at one wrapper would take the choice away from the user.
3274        let get = cat
3275            .by_receiver_and_name(&map_pat, "get")
3276            .next()
3277            .expect("Map.get");
3278        let index = cat
3279            .by_receiver_and_name(&map_pat, crate::catalog::INDEX_READ)
3280            .next()
3281            .expect("Map's subscript");
3282        assert_ne!(get.lowering, index.lowering);
3283        assert!(
3284            index.can_fault() && !get.can_fault(),
3285            "indexing faults where `.get` answers"
3286        );
3287
3288        // The two **updating** stores: `Map` only, at the same arity as its
3289        // plain store, and pointing at wrappers of their own — a row that
3290        // reused `MapInsert` would spell `min=` and mean `=`.
3291        let map_int_value = map_of_k_int_value();
3292        let plain_store = cat
3293            .by_receiver_and_name(&map_pat, crate::catalog::INDEX_STORE)
3294            .next()
3295            .expect("Map's store")
3296            .lowering
3297            .clone();
3298        for (name, what) in [
3299            (crate::catalog::INDEX_STORE_MIN, "min="),
3300            (crate::catalog::INDEX_STORE_MAX, "max="),
3301        ] {
3302            let hits: Vec<_> = cat.by_receiver_and_name(&map_int_value, name).collect();
3303            assert_eq!(hits.len(), 1, "`{what}` is one row on a Map");
3304            assert_eq!(hits[0].arity(), 2, "`{what}` takes its key and its value");
3305            assert_ne!(
3306                hits[0].lowering, plain_store,
3307                "`{what}` must not lower to the plain store"
3308            );
3309            // …and no other receiver has one, including the collections that do
3310            // have a plain store.
3311            for (pat, other) in [
3312                (of(CollectionCtor::Counter, 1), "Counter"),
3313                (of(CollectionCtor::Grid, 1), "Grid"),
3314                (of(CollectionCtor::Vec, 1), "Vec"),
3315                (of(CollectionCtor::Set, 1), "Set"),
3316            ] {
3317                assert_eq!(
3318                    cat.by_receiver_and_name(&pat, name).count(),
3319                    0,
3320                    "{other} has no `{what}`"
3321                );
3322            }
3323        }
3324        // The two are different rows from each other, or one of them computes
3325        // the other's answer.
3326        assert_ne!(
3327            cat.by_receiver_and_name(&map_int_value, crate::catalog::INDEX_STORE_MIN)
3328                .next()
3329                .expect("min=")
3330                .lowering,
3331            cat.by_receiver_and_name(&map_int_value, crate::catalog::INDEX_STORE_MAX)
3332                .next()
3333                .expect("max=")
3334                .lowering,
3335        );
3336
3337        // No subscript name is an identifier, so the subscript grammar is their
3338        // only caller: the parser accepts only an `Ident` after `.`.
3339        for name in [
3340            crate::catalog::INDEX_READ,
3341            crate::catalog::INDEX_STORE,
3342            crate::catalog::INDEX_STORE_MIN,
3343            crate::catalog::INDEX_STORE_MAX,
3344        ] {
3345            assert!(
3346                !name
3347                    .chars()
3348                    .next()
3349                    .is_some_and(|c| c.is_alphabetic() || c == '_'),
3350                "`{name}` must not be spellable as a method name"
3351            );
3352        }
3353    }
3354
3355    /// Closed-catalog check: every §6.1 collection has at least the
3356    /// `len`/`is_empty` pair, plus its type-specific methods. This guards against
3357    /// an accidental catalog gap where a collection ships without its methods.
3358    #[test]
3359    fn catalog_covers_every_collection_kind() {
3360        let cat = builtin_catalog();
3361        // Each collection must have a `len` and `is_empty` method (or the
3362        // type-specific equivalent — heaps have len/is_empty; bitset has them too).
3363        for (ctor, name) in [
3364            (CollectionCtor::Vec, "Vec"),
3365            (CollectionCtor::Deque, "Deque"),
3366            (CollectionCtor::Set, "Set"),
3367            (CollectionCtor::Counter, "Counter"),
3368            (CollectionCtor::MinHeap, "MinHeap"),
3369            (CollectionCtor::MaxHeap, "MaxHeap"),
3370            (CollectionCtor::BitSet, "BitSet"),
3371        ] {
3372            let args: Vec<TypePattern> = match ctor.arity() {
3373                0 => Vec::new(),
3374                n => (0..n).map(|_| TypePattern::var("T")).collect(),
3375            };
3376            let pat = TypePattern::Collection { ctor, args };
3377            let len = cat.by_receiver_and_name(&pat, "len").count();
3378            let is_empty = cat.by_receiver_and_name(&pat, "is_empty").count();
3379            assert!(len >= 1, "{name} missing len method");
3380            assert!(is_empty >= 1, "{name} missing is_empty method");
3381        }
3382        // Map has two type args with distinct var names (K, V), so the loop's
3383        // `T`-repeated shape above does not describe it.
3384        let map_pat = map_of_k_v();
3385        assert!(cat.by_receiver_and_name(&map_pat, "len").count() >= 1);
3386        assert!(cat.by_receiver_and_name(&map_pat, "is_empty").count() >= 1);
3387        // Grid has width/height (its dimension methods).
3388        let grid_pat = grid_of_t();
3389        assert!(cat.by_receiver_and_name(&grid_pat, "width").count() >= 1);
3390        assert!(cat.by_receiver_and_name(&grid_pat, "neighbors4").count() >= 1);
3391    }
3392
3393    /// **The neighbourhood record's field order is written down twice**, and
3394    /// the runtime holds the other copy.
3395    ///
3396    /// A field read compiles to a slot index taken from *this* list, so what
3397    /// this pins is the sequence itself: row order, spelling and arity.
3398    /// `praxis_runtime::records::tests::around_schemas_match_the_catalog` is
3399    /// the far end of the same assertion — it can see both lists at once, and
3400    /// this one is what tells an author editing the catalog that the order is
3401    /// not theirs alone to change.
3402    ///
3403    /// Every field is an `Option`: `None` is a direction that leaves the grid,
3404    /// which is the one thing `neighbors4`'s clipped `Vec` cannot say.
3405    #[test]
3406    fn the_neighbourhood_records_field_order_is_reading_order() {
3407        let cat = builtin_catalog();
3408        let grid_pat = grid_of_t();
3409        for (method, expected) in [
3410            ("around4", &["up", "left", "right", "down"][..]),
3411            (
3412                "around8",
3413                &[
3414                    "up_left",
3415                    "up",
3416                    "up_right",
3417                    "left",
3418                    "right",
3419                    "down_left",
3420                    "down",
3421                    "down_right",
3422                ][..],
3423            ),
3424        ] {
3425            let rows: Vec<_> = cat.by_receiver_and_name(&grid_pat, method).collect();
3426            assert_eq!(rows.len(), 1, "`Grid[T].{method}` is one row");
3427            let TypePattern::Record { name, fields } = &rows[0].result else {
3428                panic!("`Grid[T].{method}` answers a nominal record");
3429            };
3430            assert_eq!(
3431                *name,
3432                if method == "around4" {
3433                    "Around4"
3434                } else {
3435                    "Around8"
3436                }
3437            );
3438            let names: Vec<&str> = fields.iter().map(|(n, _)| *n).collect();
3439            assert_eq!(names, expected, "`{name}`'s fields are its slot order");
3440            for (fname, fpat) in fields {
3441                assert_eq!(
3442                    *fpat,
3443                    TypePattern::Option(Box::new(point_pattern())),
3444                    "`{name}.{fname}` is an Option[(Int, Int)]"
3445                );
3446            }
3447            // The display is the name alone — a nominal record *is* its name,
3448            // and that is what hover and completion read.
3449            assert_eq!(rows[0].result.to_string(), *name);
3450        }
3451    }
3452
3453    /// **`neighbors4`/`neighbors8` are unchanged**, and that is a decision
3454    /// rather than an omission.
3455    ///
3456    /// The graph walks take a neighbours closure typed `(T) -> Vec[T]` —
3457    /// `bfs(start, |p| g.neighbors4(p))` is ADR-060's own spelling, and
3458    /// `docs/book/examples/grid-graphs/neighbours-must-be-a-vec.px` gates the
3459    /// refusal when it is not a `Vec`. Retyping these rows to the record would
3460    /// break every walk in the book; the record is an addition beside them.
3461    #[test]
3462    fn the_clipped_vec_rows_still_answer_a_vec() {
3463        let cat = builtin_catalog();
3464        let grid_pat = grid_of_t();
3465        let vec_of_points = TypePattern::Collection {
3466            ctor: CollectionCtor::Vec,
3467            args: vec![point_pattern()],
3468        };
3469        for method in ["neighbors4", "neighbors8"] {
3470            let rows: Vec<_> = cat.by_receiver_and_name(&grid_pat, method).collect();
3471            assert_eq!(rows.len(), 1);
3472            assert_eq!(
3473                rows[0].result, vec_of_points,
3474                "`{method}` answers the clipped Vec a graph walk consumes"
3475            );
3476        }
3477    }
3478
3479    /// The four counts: two compare a value, two run a closure, all four answer
3480    /// an `Int`, and only the closure pair can fault.
3481    ///
3482    /// The effect is the assertion. `MethodEntry::can_fault` reads the manifest,
3483    /// and a `_where` row marked `Allocates` would emit no `CheckFault` — a
3484    /// predicate that divided by zero would set a fault nothing reads and hand
3485    /// the program a Unit sentinel typed as an `Int` (ADR-088). The
3486    /// value-comparing pair is the contrast: it calls nothing, so a declared
3487    /// fault there would be a dead check after every call.
3488    #[test]
3489    fn only_the_counts_that_run_a_closure_can_fault() {
3490        let cat = builtin_catalog();
3491        let grid_pat = grid_of_t();
3492        let predicate = TypePattern::Function {
3493            params: vec![TypePattern::var("T")],
3494            result: Box::new(TypePattern::Scalar(ScalarType::Bool)),
3495        };
3496        for (method, second, can_fault) in [
3497            ("count4", TypePattern::var("T"), false),
3498            ("count8", TypePattern::var("T"), false),
3499            ("count4_where", predicate.clone(), true),
3500            ("count8_where", predicate, true),
3501        ] {
3502            let rows: Vec<_> = cat.by_receiver_and_name(&grid_pat, method).collect();
3503            assert_eq!(rows.len(), 1, "`Grid[T].{method}` is one row");
3504            let row = rows[0];
3505            assert_eq!(
3506                row.params,
3507                vec![point_pattern(), second],
3508                "`{method}` takes the position and then what to count"
3509            );
3510            assert_eq!(row.result, TypePattern::Scalar(ScalarType::Int));
3511            assert_eq!(
3512                row.can_fault(),
3513                can_fault,
3514                "`{method}`: only a row that calls back into the program \
3515                 needs a fault check after it"
3516            );
3517        }
3518    }
3519
3520    /// **A standing invariant.** Every catalog row that lowers to a runtime
3521    /// wrapper declares a `Unit` result if and only if that wrapper's manifest
3522    /// return is `AbiRet::GcUnit`.
3523    ///
3524    /// A value whose *static* type is `V` and whose *runtime descriptor* is
3525    /// `Unit` is the defect this rules out. The **absence of a third `AbiRet`
3526    /// arm** is the load-bearing part rather than this test: "may be Unit, may
3527    /// be a value" cannot be spelled, so an author reaching for a Unit sentinel
3528    /// has to write either `GcUnit` — which this test refuses beside a `V`
3529    /// result — or `Gc`, which is then a claim the wrapper must honour and which
3530    /// `absent_map_get_does_not_return_an_untyped_unit_sentinel` checks in the
3531    /// runtime.
3532    ///
3533    /// **What this does not prove**, stated plainly: the manifest row is
3534    /// hand-asserted, at exactly the trust level `Effect` already is, so what
3535    /// this catches is a *catalog row disagreeing with its manifest row*, not a
3536    /// manifest row that lies about its wrapper. Setting `map_get`'s result to
3537    /// `TypePattern::var("V")` and leaving `MapGet` at `-> Gc` passes here, and
3538    /// is caught in `praxis-runtime` instead.
3539    ///
3540    /// The sweep runs over *every* such row, faulting ones included. A faulting
3541    /// wrapper's Unit is the ABI's universal unwind answer, which is a different
3542    /// thing from its declared result — so the biconditional holds there too,
3543    /// and restricting the sweep would only make it weaker.
3544    #[test]
3545    fn a_non_faulting_row_with_a_value_result_cannot_answer_the_unit_sentinel() {
3546        let cat = builtin_catalog();
3547        let mut checked = 0;
3548        for entry in cat.entries() {
3549            // An intrinsic has no wrapper: it expands to MIR instructions whose
3550            // effects are their own (`MethodEntry::can_fault` says the same).
3551            let MethodLowering::RuntimeSymbol(sym) = entry.lowering else {
3552                continue;
3553            };
3554            checked += 1;
3555            let ret = sym.sig().ret;
3556            let result_is_unit = entry.result == TypePattern::Unit;
3557            match (ret, result_is_unit) {
3558                (abi::AbiRet::GcUnit, true) | (abi::AbiRet::Gc, false) => {}
3559                _ => panic!(
3560                    "{}.{} declares `{}` and lowers to `{}`, whose manifest return is \
3561                     {ret:?}. A wrapper answers either a value (`AbiRet::Gc`, \
3562                     non-`Unit` result) or nothing (`AbiRet::GcUnit`, `Unit` \
3563                     result); an answer that is sometimes absent is spelled \
3564                     `Option[T]`, never a Unit sentinel under a value type.",
3565                    entry.receiver,
3566                    entry.name,
3567                    entry.result,
3568                    sym.name(),
3569                ),
3570            }
3571        }
3572        // A guard against the sweep silently covering nothing — every row being
3573        // faulting, or `entries()` being empty, would otherwise pass.
3574        assert!(
3575            checked >= 40,
3576            "expected the sweep to reach most of the catalog, it reached {checked} rows"
3577        );
3578    }
3579
3580    /// The sweep above skips [`MethodLowering::ScalarPrimitive`] rows, because
3581    /// its biconditional is about wrappers that answer a `GcRef` and those
3582    /// answer the scalar channel. This is their half of it (ADR-118 decision 6).
3583    ///
3584    /// **Skipping a row is how a sweep quietly stops covering anything**, so
3585    /// the arm that made them skippable owes this test the day it is added.
3586    /// What it rules out is the state that would matter: a row marked
3587    /// `ScalarPrimitive` while its wrapper still returns a `GcRef`, which MIR
3588    /// would take at its word and put a raw integer into a rootable slot. The
3589    /// complementary refusal is in `praxis-mir`'s `lower_scalar_primitive`,
3590    /// whose fallthrough is an ICE naming the symbol: a `-> RawI64` wrapper
3591    /// reachable from a method call with no instruction to produce it is a
3592    /// compiler bug, not a program's.
3593    #[test]
3594    fn a_scalar_primitive_row_answers_the_scalar_channel_and_a_scalar_type() {
3595        let cat = builtin_catalog();
3596        let mut rows = 0;
3597        for entry in cat.entries() {
3598            let MethodLowering::ScalarPrimitive(sym) = entry.lowering else {
3599                continue;
3600            };
3601            rows += 1;
3602            assert_eq!(
3603                sym.sig().ret,
3604                abi::AbiRet::RawI64,
3605                "{}.{} lowers as a scalar primitive, so `{}` must answer the \
3606                 scalar channel; a `-> Gc` row here is a box the caller would \
3607                 have to unwrap again, which is the whole thing this arm exists \
3608                 to remove",
3609                entry.receiver,
3610                entry.name,
3611                sym.name(),
3612            );
3613            assert!(
3614                matches!(entry.result, TypePattern::Scalar(_)),
3615                "{}.{} answers `{}`, which the scalar channel cannot carry",
3616                entry.receiver,
3617                entry.name,
3618                entry.result,
3619            );
3620            // A scalar primitive is not a safepoint in MIR, so a row that
3621            // allocates would be one the collector is never shown a frame at.
3622            assert!(
3623                !entry.allocates(),
3624                "{}.{} allocates, so it cannot be a non-safepoint instruction",
3625                entry.receiver,
3626                entry.name,
3627            );
3628        }
3629        assert_eq!(
3630            rows, 1,
3631            "`BitSet.contains` is the only scalar-primitive row today; a second \
3632             one wants an arm in `praxis-mir`'s `lower_scalar_primitive` before \
3633             this number moves"
3634        );
3635    }
3636
3637    /// `Map.get` and `Grid.find` answer an `Option`, spelled out by name so an
3638    /// edit that puts either back to a bare value type fails here as well as
3639    /// through the sweep above.
3640    #[test]
3641    fn map_get_and_grid_find_answer_an_option() {
3642        let cat = builtin_catalog();
3643        let map_pat = map_of_k_v();
3644        let get = cat
3645            .by_receiver_and_name(&map_pat, "get")
3646            .next()
3647            .expect("map.get exists");
3648        assert_eq!(
3649            get.result,
3650            TypePattern::Option(Box::new(TypePattern::var("V"))),
3651            "§5.7 writes `Map[K,V].get(K) -> Option[V]`"
3652        );
3653
3654        let grid_pat = grid_of_t();
3655        let find = cat
3656            .by_receiver_and_name(&grid_pat, "find")
3657            .next()
3658            .expect("grid.find exists");
3659        assert_eq!(find.result, TypePattern::Option(Box::new(point_pattern())));
3660
3661        // …and `Counter.get` keeps its zero default, which is not absence at
3662        // all: §6.2 says a counter's absent values *read as zero*.
3663        let counter_pat = counter_of_t();
3664        let counter_get = cat
3665            .by_receiver_and_name(&counter_pat, "get")
3666            .next()
3667            .expect("counter.get exists");
3668        assert_eq!(
3669            counter_get.result,
3670            TypePattern::Scalar(ScalarType::Int),
3671            "§6.2: a Counter's absent values read as zero, deliberately"
3672        );
3673    }
3674
3675    /// **ADR-136, the catalog half.** `Text.int()` and `Text.float()` exist and
3676    /// each answers an `Option`.
3677    ///
3678    /// `Y001`'s help on the most common mistake in a puzzle program points at
3679    /// `.int()`, so the row has to be here. Pure data, so this goes red on the
3680    /// catalog edit alone.
3681    ///
3682    /// The two are asserted together because the pair is the decision: a
3683    /// language with a text-to-`Int` conversion and no text-to-`Float` one is a
3684    /// language where "read a number out of text" has a different answer per
3685    /// type, which is the asymmetry `parse(t, int)`/`parse(t, float)` does not
3686    /// have.
3687    #[test]
3688    fn text_int_and_float_answer_options() {
3689        let cat = builtin_catalog();
3690        for (name, scalar) in [("int", ScalarType::Int), ("float", ScalarType::Float)] {
3691            let entry = cat
3692                .by_receiver_and_name(&TypePattern::Scalar(ScalarType::Text), name)
3693                .next()
3694                .unwrap_or_else(|| panic!("`Text.{name}()` exists"));
3695            assert_eq!(
3696                entry.result,
3697                TypePattern::Option(Box::new(TypePattern::Scalar(scalar))),
3698                "a text that is not a number is absence, not a fault (\u{00a7}4.7)"
3699            );
3700            assert!(entry.params.is_empty(), "`{name}` takes no arguments");
3701        }
3702    }
3703
3704    /// **ADR-086, the catalog half.** `t[i]` and `t.get(i)` answer a `Char`.
3705    ///
3706    /// This is pure data, so it goes red on the catalog edit alone and stays red
3707    /// whatever the runtime does — which is what makes it the *catalog's* gate.
3708    /// Its runtime twin is `text_get_answers_a_char_object` in
3709    /// `praxis-runtime`'s `abi.rs`, and neither can see the other's half.
3710    #[test]
3711    fn the_two_text_reads_answer_a_char() {
3712        let cat = builtin_catalog();
3713        let text = text_receiver();
3714
3715        for name in [crate::catalog::INDEX_READ, "get"] {
3716            let row = cat
3717                .by_receiver_and_name(&text, name)
3718                .next()
3719                .unwrap_or_else(|| panic!("Text.{name} exists"));
3720            assert_eq!(
3721                row.result,
3722                TypePattern::Scalar(ScalarType::Char),
3723                "ADR-086: `Text.{name}` answers a Char, not the char's scalar value"
3724            );
3725            // The two spellings are one answer, so they are one wrapper — unlike
3726            // `Map`, whose two reads are two wrappers on purpose (§4.7).
3727            assert_eq!(
3728                row.lowering,
3729                MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::TextGet),
3730                "`Text.{name}` lowers through the one text read"
3731            );
3732        }
3733    }
3734
3735    /// **ADR-086, the conversion pair.** A one-way conversion would make
3736    /// `Grid[Char]`, `Vec[Char]` and `Map[Char, _]` write-only from the
3737    /// language's side, so the pair is asserted as a pair — the same shape
3738    /// §4.12 gives `Float.to_int`/`Int.to_float`.
3739    #[test]
3740    fn char_and_int_convert_both_ways() {
3741        let cat = builtin_catalog();
3742
3743        let to_int = cat
3744            .by_receiver_and_name(&TypePattern::Scalar(ScalarType::Char), "to_int")
3745            .next()
3746            .expect("Char.to_int exists");
3747        assert_eq!(to_int.result, TypePattern::Scalar(ScalarType::Int));
3748        assert_eq!(to_int.purity, Purity::Pure);
3749
3750        let to_char = cat
3751            .by_receiver_and_name(&TypePattern::Scalar(ScalarType::Int), "to_char")
3752            .next()
3753            .expect("Int.to_char exists");
3754        assert_eq!(to_char.result, TypePattern::Scalar(ScalarType::Char));
3755
3756        // The narrowing direction is the one that can fail — `Int.to_char` is to
3757        // `Char.to_int` what `Float.to_int` is to `Int.to_float`. The manifest is
3758        // where that is enforced, so read it there rather than restating it.
3759        assert!(
3760            to_char.can_fault(),
3761            "not every Int is a Unicode scalar value, so the narrowing faults"
3762        );
3763        assert!(
3764            !to_int.can_fault(),
3765            "every Unicode scalar value fits an Int, so the widening cannot"
3766        );
3767    }
3768
3769    /// **ADR-143.** The `to_text` family is `Int`, `Float` and `Char`, and it is
3770    /// closed at three.
3771    ///
3772    /// The closure is the half worth asserting: a reader arriving with a
3773    /// `Bool.to_text()` or a universal one should meet a failing test rather
3774    /// than an empty space. Adding either is a decision, not a completion.
3775    #[test]
3776    fn the_to_text_family_is_int_float_and_char() {
3777        let cat = builtin_catalog();
3778        for scalar in [ScalarType::Int, ScalarType::Float, ScalarType::Char] {
3779            let receiver = TypePattern::Scalar(scalar);
3780            let row = cat
3781                .by_receiver_and_name(&receiver, "to_text")
3782                .next()
3783                .unwrap_or_else(|| panic!("{scalar:?}.to_text exists"));
3784            assert_eq!(row.result, TypePattern::Scalar(ScalarType::Text));
3785            assert_eq!(row.purity, Purity::Pure);
3786            assert!(row.allocates(), "{scalar:?}.to_text answers a fresh Text");
3787            // Each renders a payload that was validated at construction, so
3788            // there is nothing left to fault on — and a faulting row would put a
3789            // `CheckFault` after every call site that can never fire.
3790            assert!(!row.can_fault(), "{scalar:?}.to_text cannot fail");
3791        }
3792        for scalar in [ScalarType::Bool, ScalarType::Byte, ScalarType::Text] {
3793            let receiver = TypePattern::Scalar(scalar);
3794            assert_eq!(
3795                cat.by_receiver_and_name(&receiver, "to_text").count(),
3796                0,
3797                "the `to_text` family is three scalars; {scalar:?} is not one of \
3798                 them, and a universal `to_text` is §8.1 interpolation's question"
3799            );
3800        }
3801    }
3802
3803    /// **ADR-144.** `join` is one row on the generic receiver bounded to `Text`
3804    /// items; the sequence-of-`Char` case is `Vec[Char].to_text()` under a
3805    /// different name.
3806    ///
3807    /// The two negatives are the point. A concrete `Vec[Char].join/1` beside the
3808    /// generic row is refused by `finish` as `AmbiguousWithIterable`, and a
3809    /// second `Iterable` row differing only in its item bound would resolve by
3810    /// insertion order — so the surface has to be one `join` plus a different
3811    /// name.
3812    #[test]
3813    fn join_is_one_row_and_a_sequence_of_chars_has_its_own_name() {
3814        let cat = builtin_catalog();
3815
3816        let join: Vec<_> = cat.entries().iter().filter(|e| e.name == "join").collect();
3817        assert_eq!(join.len(), 1, "`join` is one row");
3818        assert_eq!(join[0].receiver, iterable_of_text_elem());
3819        assert_eq!(join[0].params, vec![TypePattern::Scalar(ScalarType::Text)]);
3820        assert_eq!(join[0].result, TypePattern::Scalar(ScalarType::Text));
3821
3822        // No `to_text` row generalizes. It cannot: `Text` is one of the ten
3823        // pipeline receivers, so an `Iterable.to_text/0` would shadow every
3824        // scalar row of ADR-143's family at once.
3825        assert!(
3826            !cat.entries()
3827                .iter()
3828                .any(|e| e.name == "to_text" && matches!(e.receiver, TypePattern::Iterable { .. })),
3829            "an `Iterable.to_text` would collide with the scalar `to_text` rows"
3830        );
3831        let chars_pat = vec_of_char();
3832        let chars = cat
3833            .by_receiver_and_name(&chars_pat, "to_text")
3834            .next()
3835            .expect("Vec[Char].to_text exists");
3836        assert_eq!(chars.result, TypePattern::Scalar(ScalarType::Text));
3837        assert!(chars.params.is_empty());
3838    }
3839
3840    /// **ADR-145.** `reversed` carries no capability bound, and that is the
3841    /// row's claim rather than an oversight.
3842    ///
3843    /// `sorted` needs `Ord` because it calls `compare`, `unique` needs
3844    /// `HashStable` because it calls `hash` and `equals`. Reversal calls
3845    /// nothing, so a `Vec` of closures reverses — and a later edit that "tidies
3846    /// up" by giving it a bound to match its neighbours would take that away
3847    /// with no wrapper behaviour to justify it.
3848    #[test]
3849    fn reversed_is_a_barrier_with_no_bound_on_its_element() {
3850        let cat = builtin_catalog();
3851        let receiver = iterable_of_t();
3852        let row = cat
3853            .by_receiver_and_name(&receiver, "reversed")
3854            .next()
3855            .expect("Iterable.reversed exists");
3856        assert_eq!(row.result, vec_of_t());
3857        assert_eq!(row.purity, Purity::Pure);
3858        assert!(row.bounds().is_empty(), "reversal reads no callback");
3859        assert!(
3860            !row.can_fault(),
3861            "there is no element `reversed` can be handed that it cannot reverse"
3862        );
3863        assert!(
3864            matches!(
3865                row.lowering,
3866                MethodLowering::RuntimeSymbol(abi::RuntimeSymbol::VecReversed)
3867            ),
3868            "a barrier is a runtime call, not a fused stage: reversal cannot \
3869             answer its first element until it has seen the last"
3870        );
3871    }
3872
3873    /// **ADR-149.** The two groupings are barriers that answer `Vec[Vec[T]]`,
3874    /// carry no capability bound, and fault.
3875    ///
3876    /// Three claims, each of which a later edit could undo for a plausible
3877    /// reason, so each is asserted rather than described:
3878    ///
3879    /// * **The nesting.** A row declaring `Vec[T]` would flatten the answer and
3880    ///   nothing in the row's own text would look wrong.
3881    /// * **The absent bound.** `sorted` and `unique` sit beside these with one
3882    ///   each, and a tidying edit that gave these one to match would take away
3883    ///   the `Vec` of closures that groups today — with no wrapper behaviour to
3884    ///   justify it, because a grouping calls no descriptor callback.
3885    /// * **The fault.** It is the one place these differ from `reversed`, and it
3886    ///   is what makes `chunks(0)` observable at all: MIR emits a `CheckFault`
3887    ///   after a call only when the wrapper declares one, so an `Allocates` row
3888    ///   here would set `InvalidSize` into a context nothing reads and hand the
3889    ///   program a Unit sentinel typed as a `Vec[Vec[T]]` (ADR-088).
3890    #[test]
3891    fn a_grouping_answers_a_nested_vec_with_no_bound_and_can_fault() {
3892        let cat = builtin_catalog();
3893        let receiver = iterable_of_t();
3894        for (name, symbol) in [
3895            ("chunks", abi::RuntimeSymbol::VecChunks),
3896            ("windows", abi::RuntimeSymbol::VecWindows),
3897        ] {
3898            let row = cat
3899                .by_receiver_and_name(&receiver, name)
3900                .next()
3901                .unwrap_or_else(|| panic!("Iterable.{name} exists"));
3902            assert_eq!(
3903                row.result,
3904                vec_of_vec_of_t(),
3905                "`{name}` groups without flattening"
3906            );
3907            assert_eq!(row.params, vec![TypePattern::Scalar(ScalarType::Int)]);
3908            assert_eq!(row.purity, Purity::Pure);
3909            assert!(
3910                row.bounds().is_empty(),
3911                "a grouping reads no descriptor callback ({name})"
3912            );
3913            assert!(
3914                row.can_fault(),
3915                "`{name}(0)` names no run, and the program has to be able to see that"
3916            );
3917            assert!(
3918                matches!(row.lowering, MethodLowering::RuntimeSymbol(s) if s == symbol),
3919                "a grouping is a barrier: it cannot answer its first group from \
3920                 one element ({name})"
3921            );
3922        }
3923    }
3924
3925    /// §4.12's overflow alternatives are three modes over three operators, and
3926    /// the table says exactly that — no more and no fewer.
3927    ///
3928    /// This is the enforcement of a rule §4.12 states and nothing else should
3929    /// restate. It asserts the closures as well as the members, because the
3930    /// closures are the half a reader is most likely to undo by adding the
3931    /// "obviously missing" `checked_div` — which would contradict §4.12's own
3932    /// next sentence, "Division by zero always faults".
3933    ///
3934    /// It pins the *shape* of the family, not the behaviour of any row: the
3935    /// gates for the behaviour are in `jit.rs`, at the boundaries where the
3936    /// ordinary operator faults.
3937    #[test]
3938    fn the_overflow_alternative_family_is_three_modes_over_three_operators() {
3939        let cat = builtin_catalog();
3940        let int = TypePattern::Scalar(ScalarType::Int);
3941
3942        for mode in ["wrapping", "saturating", "checked"] {
3943            for op in ["add", "sub", "mul"] {
3944                let name = format!("{mode}_{op}");
3945                let row = cat
3946                    .by_receiver_and_name(&int, &name)
3947                    .next()
3948                    .unwrap_or_else(|| panic!("§4.12's family includes `Int.{name}`"));
3949                assert_eq!(row.params, vec![TypePattern::Scalar(ScalarType::Int)]);
3950                // None of the nine may fault: that is what an *alternative* to a
3951                // faulting operator means, and ADR-088's verifier rule turns it
3952                // into "no `CheckFault` follows the call".
3953                assert!(!row.can_fault(), "`{name}` is an alternative to faulting");
3954
3955                let want = if mode == "checked" {
3956                    TypePattern::Option(Box::new(TypePattern::Scalar(ScalarType::Int)))
3957                } else {
3958                    TypePattern::Scalar(ScalarType::Int)
3959                };
3960                assert_eq!(row.result, want, "`{name}`'s result");
3961            }
3962        }
3963
3964        // The two closures §4.12 draws, asserted as absences.
3965        for absent in [
3966            "wrapping_div",
3967            "saturating_div",
3968            "checked_div",
3969            "wrapping_rem",
3970            "checked_rem",
3971            "wrapping_neg",
3972            "checked_neg",
3973            "saturating_abs",
3974        ] {
3975            assert!(
3976                cat.by_receiver_and_name(&int, absent).next().is_none(),
3977                "§4.12 closes the family before `{absent}`: division's escape \
3978                 hatch is closed by \"Division by zero always faults\", and \
3979                 `_neg`/`_abs` are spelled with `0.wrapping_sub(x)`"
3980            );
3981        }
3982    }
3983}