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