Skip to main content

praxis_stdlib/
catalog.rs

1//! The method catalog (§16.2): one structured table of built-in methods,
2//! consumed by every part of the compiler and the LSP.
3//!
4//! The catalog is built with [`MethodCatalogBuilder`] and finalized with
5//! [`MethodCatalogBuilder::finish`], which **rejects duplicate entries** — the
6//! `(receiver, name, parameter-count)` triple must be unique. That makes a
7//! duplicate overload unrepresentable: the builder errors rather than silently
8//! shadowing an earlier entry.
9
10use std::fmt;
11
12use crate::type_pattern::{Bound, TypePattern};
13
14/// The catalog name of the subscript **read** `m[key]`.
15///
16/// A subscript is dispatched on the receiver's shape and its arity exactly as a
17/// method is — `grid[x, y]` is `Grid[T]` at arity two, `m[key]` is `Map[K, V]` at
18/// arity one — so it is a catalog row rather than a second dispatch table. The
19/// spelling is not an identifier, which is what keeps it out of source: the
20/// parser only accepts an `Ident` after `.`, so `m.[](k)` cannot be written, and
21/// nothing in the language can name these rows except the subscript grammar.
22pub const INDEX_READ: &str = "[]";
23
24/// The catalog name of the subscript **store** `m[key] = value`. The value is
25/// the last parameter, after the indices.
26pub const INDEX_STORE: &str = "[]=";
27
28/// The catalog name of `distance[key] min= candidate` (§6.2) — and
29/// [`INDEX_STORE_MAX`] its `max=` dual.
30///
31/// Their own rows rather than a read-modify-write over [`INDEX_READ`] and
32/// [`INDEX_STORE`], because §6.2 gives them a semantics no read can express: "an
33/// absent entry accepts the first value", where a subscript *read* of an absent
34/// key faults (§4.7).
35pub const INDEX_STORE_MIN: &str = "[]min=";
36
37/// The catalog name of `best[key] max= score` (§6.2). See [`INDEX_STORE_MIN`].
38pub const INDEX_STORE_MAX: &str = "[]max=";
39
40/// Whether a method is pure or has side effects.
41#[derive(Clone, Copy, PartialEq, Eq, Debug)]
42pub enum Purity {
43    /// No allocation, no I/O, no mutation of receiver state visible to the
44    /// caller.
45    Pure,
46    /// May mutate the receiver, allocate, or perform I/O.
47    Impure,
48}
49
50/// How a catalog entry lowers to actual code.
51#[derive(Clone, PartialEq, Eq, Debug)]
52pub enum MethodLowering {
53    /// Lowers to a call into the runtime wrapper named by the ABI manifest,
54    /// e.g. [`RuntimeSymbol::VecPush`] (§11.1). Carrying the symbol rather than
55    /// its name means a catalog row cannot name a wrapper that does not exist,
56    /// and the row's allocation/fault behaviour comes from the manifest instead
57    /// of being restated here.
58    RuntimeSymbol(crate::abi::RuntimeSymbol),
59    /// Lowers to a compiler intrinsic (no runtime symbol). Reserved for the
60    /// sequence pipeline and a handful of primitives that the compiler folds
61    /// directly.
62    Intrinsic(&'static str),
63    /// Lowers to a **dedicated MIR instruction whose result is a scalar**, with
64    /// this symbol as the out-of-line form the backend's cold arm calls
65    /// (ADR-118 decision 6).
66    ///
67    /// The distinction from [`RuntimeSymbol`](Self::RuntimeSymbol) is not
68    /// cosmetic and it is not "this one is inlined". Two facts follow from it
69    /// that the plain arm cannot express:
70    ///
71    /// * **The answer is not a `GcRef`.** The row's manifest return is
72    ///   `AbiRet::RawI64`, so the wrapper hands back the scalar channel and the
73    ///   builder decides whether the value is ever boxed at all. Every value
74    ///   answer in the plain arm is an `AbiRet::Gc`, which is what
75    ///   `a_non_faulting_row_with_a_value_result_cannot_answer_the_unit_sentinel`
76    ///   checks — and the check is *right* about that arm, which is why this one
77    ///   is a separate variant rather than a loosening of it.
78    /// * **The call site's safepoint status is the instruction's, not
79    ///   `Inst::Call`'s.** `liveness::is_gc_safepoint` matches every
80    ///   `Inst::Call` regardless of the symbol's [`Effect`](crate::abi::Effect),
81    ///   so a `Pure` primitive lowered as a call spills the whole root set at a
82    ///   point no collection can happen. A row lowered this way gets an
83    ///   instruction MIR can classify honestly.
84    ///
85    /// `BitSet.contains` is the only row here today. `Vec.get`/`Vec[]` want the
86    /// same treatment and cannot have it yet: their answer is a `GcRef` element,
87    /// so they need a `Gc`-dst instruction rather than a scalar one. See
88    /// ADR-118's open questions.
89    ScalarPrimitive(crate::abi::RuntimeSymbol),
90}
91
92/// One row of the method catalog (§16.2 fields).
93#[derive(Clone, Debug)]
94pub struct MethodEntry {
95    /// The receiver shape the method is defined on, e.g. `Vec[T]`.
96    pub receiver: TypePattern,
97    /// The method name, e.g. `push`.
98    pub name: &'static str,
99    /// Parameter type patterns, positional.
100    pub params: Vec<TypePattern>,
101    /// Result type pattern.
102    pub result: TypePattern,
103    /// Whether the method is pure.
104    pub purity: Purity,
105    /// How the method lowers.
106    pub lowering: MethodLowering,
107    /// One-line documentation, surfaced in hover.
108    pub doc: &'static str,
109}
110
111impl MethodEntry {
112    /// The arity (number of explicit parameters, excluding the receiver).
113    pub fn arity(&self) -> usize {
114        self.params.len()
115    }
116
117    /// Whether calling this method may allocate, and so whether its call site
118    /// is a GC safepoint.
119    ///
120    /// Derived from the ABI manifest, not restated per row: a row that carried
121    /// its own answer could disagree with the wrapper it lowers to. An
122    /// intrinsic has no wrapper — the MIR lowering it expands to carries its
123    /// own per-instruction effects.
124    pub fn allocates(&self) -> bool {
125        match self.lowering {
126            MethodLowering::RuntimeSymbol(sym) | MethodLowering::ScalarPrimitive(sym) => {
127                sym.allocates()
128            }
129            MethodLowering::Intrinsic(_) => false,
130        }
131    }
132
133    /// Whether calling this method may raise a runtime fault (§9.1), and so
134    /// whether its call site needs a fault check after it.
135    ///
136    /// Derived, for the same reason as [`MethodEntry::allocates`]: a per-row
137    /// field would be a second statement of the manifest's answer, free to
138    /// drift — a `bitset_insert` row claiming it cannot fault beside a
139    /// `praxis_bitset_insert` that raises `InvalidSize` for a member outside
140    /// `BitIndex`'s range. MIR's own `Inst::can_fault` reads the same manifest
141    /// row, so the check lowering emits and this answer agree by construction.
142    pub fn can_fault(&self) -> bool {
143        match self.lowering {
144            MethodLowering::RuntimeSymbol(sym) | MethodLowering::ScalarPrimitive(sym) => {
145                sym.faults()
146            }
147            MethodLowering::Intrinsic(_) => false,
148        }
149    }
150
151    /// What each of this entry's type variables must be, by name.
152    ///
153    /// A bound is a fact about the *variable*, not about the position it is
154    /// written in, so this sweeps the receiver, the parameters and the result and
155    /// reports each name once. That is why `Vec[T].sum()` can declare its `Int`
156    /// requirement on the receiver's element and have it apply — there is nowhere
157    /// else in the row for it to live.
158    ///
159    /// A name that declares the *same* bound twice is one requirement.
160    /// [`MethodCatalogBuilder::finish`] refuses two *different* ones, so the
161    /// dedup here cannot hide a contradiction.
162    #[must_use]
163    pub fn bounds(&self) -> Vec<(&'static str, Bound)> {
164        let mut all = Vec::new();
165        self.receiver.collect_bounds(&mut all);
166        for p in &self.params {
167            p.collect_bounds(&mut all);
168        }
169        self.result.collect_bounds(&mut all);
170        let mut seen: Vec<(&'static str, Bound)> = Vec::new();
171        for entry in all {
172            if !seen.contains(&entry) {
173                seen.push(entry);
174            }
175        }
176        seen
177    }
178}
179
180/// Errors that can occur while building a [`MethodCatalog`].
181#[derive(Clone, Debug, PartialEq, Eq)]
182pub enum MethodCatalogError {
183    /// Two entries share the same `(receiver, name, arity)` triple. Overloads
184    /// are not permitted; this is a build-time catalog bug.
185    Duplicate {
186        receiver: TypePattern,
187        name: &'static str,
188        arity: usize,
189    },
190    /// One entry declares two *different* bounds for the same type variable. A
191    /// bound is a fact about the variable, so the row is asking for two
192    /// incompatible things and whichever the checker happened to read first
193    /// would win silently.
194    ConflictingBound {
195        method: &'static str,
196        var: &'static str,
197        first: Bound,
198        second: Bound,
199    },
200    /// A concrete-receiver row shares a `(name, arity)` with a generic
201    /// [`TypePattern::Iterable`] row, on a receiver the generic one accepts
202    /// (ADR-127 decision 1).
203    ///
204    /// Both spellings would match at the call site, and the catalog's key is
205    /// `(receiver, name, arity)` — so the two are not duplicates and nothing
206    /// would refuse them; whichever came first in insertion order would win.
207    /// That is a **precedence rule**, and a precedence rule is what makes "which
208    /// does this call resolve to" a question at all (decision 6). The check
209    /// scopes to [`PIPELINE_RECEIVERS`](crate::type_pattern::PIPELINE_RECEIVERS)
210    /// deliberately: a `Grid[T].map/1` beside `Iterable.map/1` is *allowed*,
211    /// because `Grid` is not one of the ten and §6.4 asks for that row by name.
212    AmbiguousWithIterable {
213        receiver: TypePattern,
214        name: &'static str,
215        arity: usize,
216    },
217    /// Two generic [`TypePattern::Iterable`] rows share a `(name, arity)`,
218    /// differing only in what they bound their item to (ADR-144).
219    ///
220    /// This is [`AmbiguousWithIterable`](Self::AmbiguousWithIterable)'s blind
221    /// spot — `join` for a sequence of `Text` beside `join` for a sequence of
222    /// `Char`. The pair is not a `Duplicate`, because the receivers differ; it
223    /// is not a shadowing, because neither row is the concrete one. But
224    /// `praxis_hir::catalog::lookup` matches an `Iterable` receiver on *shape*,
225    /// so both hit and inference takes the first — which is a precedence rule
226    /// by insertion order, exactly what ADR-127 decision 6 refuses. A sequence
227    /// of `Char` gets a differently-named row instead.
228    AmbiguousIterablePair { name: &'static str, arity: usize },
229    /// A row writes [`TypePattern::Iterable`] somewhere other than its receiver
230    /// (ADR-127 decision 1).
231    ///
232    /// **The receiver generalizes; two parameters must not.** `zip`'s argument
233    /// is a `Vec[U]` and `flat_map`'s closure answers one, and the fused loop
234    /// indexes both with `praxis_vec_len`/`praxis_vec_get` directly — neither
235    /// has an `IterPlan` in scope, because neither is the source. Generalizing
236    /// either would put a `SetPayload` under `praxis_vec_get`, which is the
237    /// exact wrong-type read `IterPlan` exists to prevent. The pipeline
238    /// generalizes over what it *walks*, not over every sequence a row mentions.
239    ///
240    /// The rule is also what makes the instantiation path total: an `Iterable`
241    /// names ten types, so `pattern_to_type` has no answer for one, and the
242    /// receiver is the one position that never asks it for an answer.
243    IterableOutsideReceiver { method: &'static str, arity: usize },
244}
245
246impl fmt::Display for MethodCatalogError {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        match self {
249            MethodCatalogError::Duplicate {
250                receiver,
251                name,
252                arity,
253            } => write!(
254                f,
255                "duplicate catalog entry: {receiver}.{name}/{arity} already defined"
256            ),
257            MethodCatalogError::ConflictingBound {
258                method,
259                var,
260                first,
261                second,
262            } => write!(
263                f,
264                "catalog entry `{method}` bounds `{var}` as both {first:?} and {second:?}"
265            ),
266            MethodCatalogError::AmbiguousWithIterable {
267                receiver,
268                name,
269                arity,
270            } => write!(
271                f,
272                "catalog entry {receiver}.{name}/{arity} shadows the generic \
273                 Iterable.{name}/{arity}: both match this receiver, and which \
274                 one a call resolves to would be insertion order"
275            ),
276            MethodCatalogError::AmbiguousIterablePair { name, arity } => write!(
277                f,
278                "two generic Iterable.{name}/{arity} rows differ only in the \
279                 bound on their item: both match every receiver, and which one \
280                 a call resolves to would be insertion order"
281            ),
282            MethodCatalogError::IterableOutsideReceiver { method, arity } => write!(
283                f,
284                "catalog entry `{method}`/{arity} writes an Iterable pattern \
285                 outside its receiver: the pipeline generalizes over what it \
286                 walks, not over every sequence a row mentions"
287            ),
288        }
289    }
290}
291
292impl std::error::Error for MethodCatalogError {}
293
294/// The finalized method catalog: an ordered, duplicate-free list of entries.
295#[derive(Clone, Debug, Default)]
296pub struct MethodCatalog {
297    entries: Vec<MethodEntry>,
298}
299
300impl MethodCatalog {
301    /// Begin a builder. The builder is the only way to add entries, and its
302    /// `finish` step enforces uniqueness.
303    pub fn build() -> MethodCatalogBuilder {
304        MethodCatalogBuilder::default()
305    }
306
307    /// All entries, in insertion order.
308    pub fn entries(&self) -> &[MethodEntry] {
309        &self.entries
310    }
311
312    /// Entries whose `(receiver, name)` match, in insertion order. The caller
313    /// disambiguates by arity at the call site.
314    pub fn by_receiver_and_name<'a>(
315        &'a self,
316        receiver: &'a TypePattern,
317        name: &'a str,
318    ) -> impl Iterator<Item = &'a MethodEntry> + 'a {
319        self.entries
320            .iter()
321            .filter(move |e| &e.receiver == receiver && e.name == name)
322    }
323
324    /// Does **any** receiver in the catalog have a method `name` taking `arity`
325    /// arguments?
326    ///
327    /// The predicate lives here rather than at the call site because its
328    /// justification is a fact about this table: the catalog is the *complete*
329    /// method universe of the language. A record carries no rows (`p.len()` on
330    /// `struct P { len: Int }` is a missing method, not a field read), an enum
331    /// carries none, and there is no user `impl` — so a name this table does not
332    /// hold at that arity can never resolve against **any** receiver, known or
333    /// not yet known.
334    ///
335    /// That is what lets inference refuse `fn f(x) { x.nope() }` before anything
336    /// says what `x` is (ADR-093). The complementary half matters just as much:
337    /// a name the table *does* hold — `sum`, at arity 0 — is left deferred even
338    /// though no receiver is known, because §5.2's `fn total(values) {
339    /// values.sum() }` must still infer. Spelling the predicate as "no row
340    /// matches this receiver" instead would reject that program.
341    ///
342    /// If this language ever grows user-defined methods, this predicate loses
343    /// its justification and ADR-093's Rule B has to go with it.
344    pub fn has_name_at_arity(&self, name: &str, arity: usize) -> bool {
345        self.entries
346            .iter()
347            .any(|e| e.name == name && e.arity() == arity)
348    }
349
350    /// The number of entries.
351    pub fn len(&self) -> usize {
352        self.entries.len()
353    }
354
355    /// True if there are no entries.
356    pub fn is_empty(&self) -> bool {
357        self.entries.is_empty()
358    }
359}
360
361/// Builder for [`MethodCatalog`]. Enforces the duplicate-entry invariant at
362/// `finish`.
363#[derive(Default)]
364pub struct MethodCatalogBuilder {
365    entries: Vec<MethodEntry>,
366}
367
368impl MethodCatalogBuilder {
369    /// Add an entry. Duplicates are detected at [`finish`](Self::finish).
370    pub fn entry(mut self, entry: MethodEntry) -> Self {
371        self.entries.push(entry);
372        self
373    }
374
375    /// Finalize the catalog, returning an error if any two entries share a
376    /// `(receiver, name, arity)` triple, if any single entry bounds one type
377    /// variable two different ways, or if a concrete row shadows a generic
378    /// `Iterable` one on a receiver both accept (ADR-127).
379    pub fn finish(self) -> Result<MethodCatalog, MethodCatalogError> {
380        for (i, a) in self.entries.iter().enumerate() {
381            // A receiver may *be* the generic pattern — that is the whole point
382            // — but nothing, the receiver included, may contain one.
383            let nested_in_receiver = match &a.receiver {
384                TypePattern::Iterable { item } => mentions_iterable(item),
385                other => mentions_iterable(other),
386            };
387            if nested_in_receiver || a.params.iter().chain([&a.result]).any(mentions_iterable) {
388                return Err(MethodCatalogError::IterableOutsideReceiver {
389                    method: a.name,
390                    arity: a.arity(),
391                });
392            }
393            for b in self.entries.iter().skip(i + 1) {
394                if a.receiver == b.receiver && a.name == b.name && a.arity() == b.arity() {
395                    return Err(MethodCatalogError::Duplicate {
396                        receiver: a.receiver.clone(),
397                        name: a.name,
398                        arity: a.arity(),
399                    });
400                }
401                if let Some(concrete) = shadowed_by_iterable(a, b) {
402                    return Err(MethodCatalogError::AmbiguousWithIterable {
403                        receiver: concrete.receiver.clone(),
404                        name: concrete.name,
405                        arity: concrete.arity(),
406                    });
407                }
408                if a.name == b.name
409                    && a.arity() == b.arity()
410                    && matches!(a.receiver, TypePattern::Iterable { .. })
411                    && matches!(b.receiver, TypePattern::Iterable { .. })
412                {
413                    return Err(MethodCatalogError::AmbiguousIterablePair {
414                        name: a.name,
415                        arity: a.arity(),
416                    });
417                }
418            }
419            // `bounds()` dedups equal declarations, so anything left twice under
420            // one name is a contradiction the checker would resolve by accident.
421            let bounds = a.bounds();
422            for (j, (var, first)) in bounds.iter().enumerate() {
423                if let Some((_, second)) = bounds.iter().skip(j + 1).find(|(v, _)| v == var) {
424                    return Err(MethodCatalogError::ConflictingBound {
425                        method: a.name,
426                        var,
427                        first: *first,
428                        second: *second,
429                    });
430                }
431            }
432        }
433        Ok(MethodCatalog {
434            entries: self.entries,
435        })
436    }
437}
438
439/// Whether `pat` writes a [`TypePattern::Iterable`] anywhere inside it, at any
440/// depth. A row's receiver is allowed to *be* one; nothing is allowed to
441/// *contain* one.
442fn mentions_iterable(pat: &TypePattern) -> bool {
443    match pat {
444        TypePattern::Iterable { .. } => true,
445        TypePattern::Collection { args, .. } | TypePattern::Tuple(args) => {
446            args.iter().any(mentions_iterable)
447        }
448        TypePattern::Option(inner) => mentions_iterable(inner),
449        TypePattern::Function { params, result } => {
450            params.iter().any(mentions_iterable) || mentions_iterable(result)
451        }
452        TypePattern::Scalar(_) | TypePattern::Var { .. } | TypePattern::Unit => false,
453    }
454}
455
456/// The concrete row of `(a, b)` that a generic `Iterable` row shadows, if that
457/// is what this pair is (ADR-127 decision 1).
458///
459/// "Shadows" is: one receiver is [`TypePattern::Iterable`], the other is a
460/// receiver that pattern accepts, and the two agree on `(name, arity)`. Order is
461/// not part of the question — the pair is checked once, from whichever side each
462/// row happens to sit on.
463fn shadowed_by_iterable<'e>(a: &'e MethodEntry, b: &'e MethodEntry) -> Option<&'e MethodEntry> {
464    if a.name != b.name || a.arity() != b.arity() {
465        return None;
466    }
467    let concrete = match (&a.receiver, &b.receiver) {
468        (TypePattern::Iterable { .. }, TypePattern::Iterable { .. }) => return None,
469        (TypePattern::Iterable { .. }, _) => b,
470        (_, TypePattern::Iterable { .. }) => a,
471        _ => return None,
472    };
473    crate::type_pattern::is_pipeline_receiver(&concrete.receiver).then_some(concrete)
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use crate::type_pattern::{CollectionCtor, ScalarType};
480
481    fn vec_of_t() -> TypePattern {
482        TypePattern::Collection {
483            ctor: CollectionCtor::Vec,
484            args: vec![TypePattern::var("T")],
485        }
486    }
487
488    fn vec_push() -> MethodEntry {
489        MethodEntry {
490            receiver: vec_of_t(),
491            name: "push",
492            params: vec![TypePattern::var("T")],
493            result: TypePattern::Unit,
494            purity: Purity::Impure,
495            lowering: MethodLowering::RuntimeSymbol(crate::abi::RuntimeSymbol::VecPush),
496            doc: "Append a value to the end of the vector.",
497        }
498    }
499
500    fn vec_len() -> MethodEntry {
501        MethodEntry {
502            receiver: vec_of_t(),
503            name: "len",
504            params: vec![],
505            result: TypePattern::Scalar(ScalarType::Int),
506            purity: Purity::Pure,
507            lowering: MethodLowering::RuntimeSymbol(crate::abi::RuntimeSymbol::VecLen),
508            doc: "Number of elements in the vector.",
509        }
510    }
511
512    #[test]
513    fn finish_accepts_distinct_entries() {
514        let catalog = MethodCatalog::build()
515            .entry(vec_push())
516            .entry(vec_len())
517            .finish()
518            .expect("distinct entries");
519        assert_eq!(catalog.len(), 2);
520        let names: Vec<_> = catalog
521            .by_receiver_and_name(&vec_of_t(), "push")
522            .map(|e| e.name)
523            .collect();
524        assert_eq!(names, vec!["push"]);
525    }
526
527    #[test]
528    fn finish_rejects_duplicate_triple() {
529        // Same receiver, name, and arity as `vec_push` → duplicate.
530        let dup = MethodEntry {
531            doc: "alternate overload that the language does not allow",
532            ..vec_push()
533        };
534        let err = MethodCatalog::build()
535            .entry(vec_push())
536            .entry(dup)
537            .finish()
538            .unwrap_err();
539        match err {
540            MethodCatalogError::Duplicate { name, arity, .. } => {
541                assert_eq!(name, "push");
542                assert_eq!(arity, 1);
543            }
544            other => panic!("expected a duplicate, got {other}"),
545        }
546    }
547
548    /// A bound is a fact about the *variable*, so one entry cannot declare two
549    /// of them for one name: whichever the checker read first would win,
550    /// silently, and the row's other claim would simply not happen.
551    ///
552    /// The same bound written twice is *not* a conflict — an entry that names `T`
553    /// in three positions may restate it — which is the half a "reject
554    /// duplicates" rule would get wrong.
555    #[test]
556    fn finish_rejects_two_bounds_on_one_variable() {
557        let conflicted = MethodEntry {
558            receiver: TypePattern::Collection {
559                ctor: CollectionCtor::Vec,
560                args: vec![TypePattern::is_scalar("T", ScalarType::Int)],
561            },
562            params: vec![TypePattern::is_scalar("T", ScalarType::Text)],
563            ..vec_push()
564        };
565        let err = MethodCatalog::build()
566            .entry(conflicted)
567            .finish()
568            .unwrap_err();
569        match err {
570            MethodCatalogError::ConflictingBound { method, var, .. } => {
571                assert_eq!(method, "push");
572                assert_eq!(var, "T");
573            }
574            other => panic!("expected a conflicting bound, got {other}"),
575        }
576
577        // Restating the *same* bound is one requirement, not a conflict.
578        let restated = MethodEntry {
579            receiver: TypePattern::Collection {
580                ctor: CollectionCtor::Vec,
581                args: vec![TypePattern::is_scalar("T", ScalarType::Int)],
582            },
583            params: vec![TypePattern::is_scalar("T", ScalarType::Int)],
584            ..vec_push()
585        };
586        let bounds = restated.bounds();
587        assert_eq!(bounds, vec![("T", Bound::Is(ScalarType::Int))]);
588        assert!(MethodCatalog::build().entry(restated).finish().is_ok());
589    }
590
591    /// `bounds()` finds a declaration wherever it is written — the whole point of
592    /// keying on the variable rather than the position. `sum` declares its `Int`
593    /// requirement on the receiver's element, and `min_by`-shaped rows would
594    /// declare one inside a closure parameter.
595    #[test]
596    fn bounds_are_found_in_every_position() {
597        let on_receiver = MethodEntry {
598            receiver: TypePattern::Collection {
599                ctor: CollectionCtor::Vec,
600                args: vec![TypePattern::is_scalar("T", ScalarType::Int)],
601            },
602            ..vec_len()
603        };
604        assert_eq!(
605            on_receiver.bounds(),
606            vec![("T", Bound::Is(ScalarType::Int))]
607        );
608
609        // Inside a closure parameter, two levels down.
610        let in_a_closure = MethodEntry {
611            params: vec![TypePattern::Function {
612                params: vec![TypePattern::is_scalar("U", ScalarType::Char)],
613                result: Box::new(TypePattern::Unit),
614            }],
615            ..vec_len()
616        };
617        assert_eq!(
618            in_a_closure.bounds(),
619            vec![("U", Bound::Is(ScalarType::Char))]
620        );
621
622        // In the result, and inside a tuple in it.
623        let in_the_result = MethodEntry {
624            result: TypePattern::Tuple(vec![
625                TypePattern::Scalar(ScalarType::Int),
626                TypePattern::is_scalar("V", ScalarType::Byte),
627            ]),
628            ..vec_len()
629        };
630        assert_eq!(
631            in_the_result.bounds(),
632            vec![("V", Bound::Is(ScalarType::Byte))]
633        );
634
635        // An unbounded variable declares nothing, which is the common case.
636        assert!(vec_push().bounds().is_empty());
637    }
638
639    #[test]
640    fn same_name_different_arity_is_allowed() {
641        // `len` (0 args) and a hypothetical `len` taking a sentinel are two
642        // different triples. The catalog allows them; the *language* may not,
643        // but that is a separate concern from table integrity.
644        let other = MethodEntry {
645            params: vec![TypePattern::Scalar(ScalarType::Int)],
646            ..vec_len()
647        };
648        let catalog = MethodCatalog::build()
649            .entry(vec_len())
650            .entry(other)
651            .finish()
652            .expect("different arity is not a duplicate");
653        assert_eq!(catalog.len(), 2);
654    }
655
656    /// **ADR-127 decision 1.** A concrete row beside a generic `Iterable` one at
657    /// the same `(name, arity)` is not a duplicate — the catalog's key includes
658    /// the receiver — so nothing would refuse it, and both would match the same
659    /// call. That is a precedence rule arriving by accident, and decision 6's
660    /// whole argument against the shape-preserving family is that a precedence
661    /// rule is what makes "which does this resolve to" a question at all.
662    #[test]
663    fn finish_rejects_a_concrete_row_that_shadows_the_generic_one() {
664        let generic = MethodEntry {
665            receiver: TypePattern::iterable(TypePattern::var("T")),
666            name: "map",
667            params: vec![TypePattern::Function {
668                params: vec![TypePattern::var("T")],
669                result: Box::new(TypePattern::var("U")),
670            }],
671            result: TypePattern::Collection {
672                ctor: CollectionCtor::Vec,
673                args: vec![TypePattern::var("U")],
674            },
675            purity: Purity::Pure,
676            lowering: MethodLowering::Intrinsic("seq_map"),
677            doc: "Apply a function to each element.",
678        };
679        // A `Set` is one of the ten, so `Set[T].map/1` and `Iterable.map/1` both
680        // answer `set.map(f)`.
681        let on_a_set = MethodEntry {
682            receiver: TypePattern::Collection {
683                ctor: CollectionCtor::Set,
684                args: vec![TypePattern::var("T")],
685            },
686            ..generic.clone()
687        };
688        let err = MethodCatalog::build()
689            .entry(generic.clone())
690            .entry(on_a_set)
691            .finish()
692            .unwrap_err();
693        match err {
694            MethodCatalogError::AmbiguousWithIterable { name, arity, .. } => {
695                assert_eq!((name, arity), ("map", 1));
696            }
697            other => panic!("expected an Iterable shadow, got {other}"),
698        }
699
700        // Insertion order is not the question: the same pair the other way round
701        // is the same collision.
702        let on_a_set = MethodEntry {
703            receiver: TypePattern::Collection {
704                ctor: CollectionCtor::Set,
705                args: vec![TypePattern::var("T")],
706            },
707            ..generic.clone()
708        };
709        assert!(
710            MethodCatalog::build()
711                .entry(on_a_set)
712                .entry(generic.clone())
713                .finish()
714                .is_err()
715        );
716
717        // **`Grid[T].map/1` is allowed**, and that is the point of scoping the
718        // check to the ten: §6.4 asks for a shape-preserving `grid.map` by name,
719        // and `Grid` is not a receiver the generic row accepts.
720        let on_a_grid = MethodEntry {
721            receiver: TypePattern::Collection {
722                ctor: CollectionCtor::Grid,
723                args: vec![TypePattern::var("T")],
724            },
725            result: TypePattern::Collection {
726                ctor: CollectionCtor::Grid,
727                args: vec![TypePattern::var("U")],
728            },
729            ..generic.clone()
730        };
731        assert!(
732            MethodCatalog::build()
733                .entry(generic.clone())
734                .entry(on_a_grid)
735                .finish()
736                .is_ok()
737        );
738
739        // A different arity is a different question, as it is for a duplicate.
740        let different_arity = MethodEntry {
741            receiver: TypePattern::Collection {
742                ctor: CollectionCtor::Set,
743                args: vec![TypePattern::var("T")],
744            },
745            params: vec![],
746            ..generic.clone()
747        };
748        assert!(
749            MethodCatalog::build()
750                .entry(generic)
751                .entry(different_arity)
752                .finish()
753                .is_ok()
754        );
755    }
756
757    /// **ADR-144.** Two generic rows at one `(name, arity)` are refused, even
758    /// though their receivers are not equal.
759    ///
760    /// The shape this refuses: one row for a sequence of `Text` and one for a
761    /// sequence of `Char`. Neither the `Duplicate` check (the receivers differ)
762    /// nor the shadowing check (neither row is concrete) catches it, and
763    /// `lookup` matches an `Iterable` on shape — so `cs.join("")` would resolve
764    /// to whichever row was registered first and report `expected Text, found
765    /// Char`. A precedence rule nobody wrote is worse than a build failure.
766    #[test]
767    fn finish_rejects_two_generic_rows_at_one_arity() {
768        let of_text = MethodEntry {
769            receiver: TypePattern::iterable(TypePattern::is_scalar("T", ScalarType::Text)),
770            name: "join",
771            params: vec![TypePattern::Scalar(ScalarType::Text)],
772            result: TypePattern::Scalar(ScalarType::Text),
773            purity: Purity::Pure,
774            lowering: MethodLowering::Intrinsic("seq_join"),
775            doc: "These Text items concatenated.",
776        };
777        let of_char = MethodEntry {
778            receiver: TypePattern::iterable(TypePattern::is_scalar("T", ScalarType::Char)),
779            ..of_text.clone()
780        };
781        let err = MethodCatalog::build()
782            .entry(of_text.clone())
783            .entry(of_char)
784            .finish()
785            .unwrap_err();
786        match err {
787            MethodCatalogError::AmbiguousIterablePair { name, arity } => {
788                assert_eq!((name, arity), ("join", 1));
789            }
790            other => panic!("expected an ambiguous generic pair, got {other}"),
791        }
792
793        // A different arity is a different question here too, which is what
794        // keeps `count()` and `count(pred)` — both generic — legal.
795        let nullary = MethodEntry {
796            params: vec![],
797            ..of_text.clone()
798        };
799        assert!(
800            MethodCatalog::build()
801                .entry(of_text)
802                .entry(nullary)
803                .finish()
804                .is_ok()
805        );
806    }
807
808    /// **ADR-127.** The receiver generalizes; a parameter and a result do not.
809    ///
810    /// `zip`'s argument and `flat_map`'s closure result are the two rows this is
811    /// about, and the reason is not symmetry: the fused loop indexes each of them
812    /// with `praxis_vec_len`/`praxis_vec_get` directly — `Step::Zip` walks its
813    /// second source with its own dense counter, a splice walks the inner `Vec` —
814    /// and neither has an `IterPlan` in scope, because neither is the source.
815    /// Generalizing either would put a `SetPayload` under `praxis_vec_get`, which
816    /// is the exact wrong-type read `IterPlan` exists to prevent.
817    ///
818    /// It is also what keeps the instantiation path total: an `Iterable` names
819    /// ten types, so `pattern_to_type` has no single answer for one, and the
820    /// receiver is the only position that never asks it for one.
821    #[test]
822    fn finish_rejects_an_iterable_written_outside_the_receiver() {
823        let iterable = || TypePattern::iterable(TypePattern::var("T"));
824        let base = MethodEntry {
825            receiver: iterable(),
826            name: "zip",
827            params: vec![],
828            result: TypePattern::Unit,
829            purity: Purity::Pure,
830            lowering: MethodLowering::Intrinsic("seq_zip"),
831            doc: "Pair elements with another sequence.",
832        };
833        // Bare in a parameter, nested inside one, in the result, and nested
834        // inside the receiver's own item.
835        for offender in [
836            MethodEntry {
837                params: vec![iterable()],
838                ..base.clone()
839            },
840            MethodEntry {
841                params: vec![TypePattern::Function {
842                    params: vec![TypePattern::var("T")],
843                    result: Box::new(iterable()),
844                }],
845                ..base.clone()
846            },
847            MethodEntry {
848                result: TypePattern::Collection {
849                    ctor: CollectionCtor::Vec,
850                    args: vec![iterable()],
851                },
852                ..base.clone()
853            },
854            MethodEntry {
855                receiver: TypePattern::iterable(iterable()),
856                ..base.clone()
857            },
858        ] {
859            let err = MethodCatalog::build().entry(offender).finish().unwrap_err();
860            assert!(
861                matches!(err, MethodCatalogError::IterableOutsideReceiver { .. }),
862                "expected the parameter rule, got {err}"
863            );
864        }
865        // The receiver itself is the one position that may be one.
866        assert!(MethodCatalog::build().entry(base).finish().is_ok());
867    }
868
869    #[test]
870    fn entry_reports_capabilities() {
871        let e = vec_push();
872        assert_eq!(e.arity(), 1);
873        // `allocates` is the manifest's answer, not a field the row restates.
874        // Both of these are safepoints, and `len` is the interesting one:
875        // "reading a length allocates nothing" is the wrong reading —
876        // `praxis_vec_len` boxes the count into a fresh `Int`, so a collection
877        // can run inside it.
878        assert!(e.allocates());
879        assert!(vec_len().allocates());
880        // `praxis_vec_push` calls `adopt_or_reject`, which ends in
881        // `set_fault(ctx, TYPE_MISMATCH)`, so a row declaring "Allocates, not
882        // AllocatesAndFaults" would be restating the manifest rather than
883        // reading it. `praxis_vec_len` is the contrast that keeps the assertion
884        // meaningful: it really cannot fault.
885        assert!(e.can_fault(), "praxis_vec_push raises TypeMismatch");
886        assert!(!vec_len().can_fault());
887        assert_eq!(e.purity, Purity::Impure);
888    }
889}