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