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::Record { fields, .. } => fields.iter().any(|(_, f)| mentions_iterable(f)),
450        TypePattern::Function { params, result } => {
451            params.iter().any(mentions_iterable) || mentions_iterable(result)
452        }
453        TypePattern::Scalar(_) | TypePattern::Var { .. } | TypePattern::Unit => false,
454    }
455}
456
457/// The concrete row of `(a, b)` that a generic `Iterable` row shadows, if that
458/// is what this pair is (ADR-127 decision 1).
459///
460/// "Shadows" is: one receiver is [`TypePattern::Iterable`], the other is a
461/// receiver that pattern accepts, and the two agree on `(name, arity)`. Order is
462/// not part of the question — the pair is checked once, from whichever side each
463/// row happens to sit on.
464fn shadowed_by_iterable<'e>(a: &'e MethodEntry, b: &'e MethodEntry) -> Option<&'e MethodEntry> {
465    if a.name != b.name || a.arity() != b.arity() {
466        return None;
467    }
468    let concrete = match (&a.receiver, &b.receiver) {
469        (TypePattern::Iterable { .. }, TypePattern::Iterable { .. }) => return None,
470        (TypePattern::Iterable { .. }, _) => b,
471        (_, TypePattern::Iterable { .. }) => a,
472        _ => return None,
473    };
474    crate::type_pattern::is_pipeline_receiver(&concrete.receiver).then_some(concrete)
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480    use crate::type_pattern::{CollectionCtor, ScalarType};
481
482    fn vec_of_t() -> TypePattern {
483        TypePattern::Collection {
484            ctor: CollectionCtor::Vec,
485            args: vec![TypePattern::var("T")],
486        }
487    }
488
489    fn vec_push() -> MethodEntry {
490        MethodEntry {
491            receiver: vec_of_t(),
492            name: "push",
493            params: vec![TypePattern::var("T")],
494            result: TypePattern::Unit,
495            purity: Purity::Impure,
496            lowering: MethodLowering::RuntimeSymbol(crate::abi::RuntimeSymbol::VecPush),
497            doc: "Append a value to the end of the vector.",
498        }
499    }
500
501    fn vec_len() -> MethodEntry {
502        MethodEntry {
503            receiver: vec_of_t(),
504            name: "len",
505            params: vec![],
506            result: TypePattern::Scalar(ScalarType::Int),
507            purity: Purity::Pure,
508            lowering: MethodLowering::RuntimeSymbol(crate::abi::RuntimeSymbol::VecLen),
509            doc: "Number of elements in the vector.",
510        }
511    }
512
513    #[test]
514    fn finish_accepts_distinct_entries() {
515        let catalog = MethodCatalog::build()
516            .entry(vec_push())
517            .entry(vec_len())
518            .finish()
519            .expect("distinct entries");
520        assert_eq!(catalog.len(), 2);
521        let names: Vec<_> = catalog
522            .by_receiver_and_name(&vec_of_t(), "push")
523            .map(|e| e.name)
524            .collect();
525        assert_eq!(names, vec!["push"]);
526    }
527
528    #[test]
529    fn finish_rejects_duplicate_triple() {
530        // Same receiver, name, and arity as `vec_push` → duplicate.
531        let dup = MethodEntry {
532            doc: "alternate overload that the language does not allow",
533            ..vec_push()
534        };
535        let err = MethodCatalog::build()
536            .entry(vec_push())
537            .entry(dup)
538            .finish()
539            .unwrap_err();
540        match err {
541            MethodCatalogError::Duplicate { name, arity, .. } => {
542                assert_eq!(name, "push");
543                assert_eq!(arity, 1);
544            }
545            other => panic!("expected a duplicate, got {other}"),
546        }
547    }
548
549    /// A bound is a fact about the *variable*, so one entry cannot declare two
550    /// of them for one name: whichever the checker read first would win,
551    /// silently, and the row's other claim would simply not happen.
552    ///
553    /// The same bound written twice is *not* a conflict — an entry that names `T`
554    /// in three positions may restate it — which is the half a "reject
555    /// duplicates" rule would get wrong.
556    #[test]
557    fn finish_rejects_two_bounds_on_one_variable() {
558        let conflicted = MethodEntry {
559            receiver: TypePattern::Collection {
560                ctor: CollectionCtor::Vec,
561                args: vec![TypePattern::is_scalar("T", ScalarType::Int)],
562            },
563            params: vec![TypePattern::is_scalar("T", ScalarType::Text)],
564            ..vec_push()
565        };
566        let err = MethodCatalog::build()
567            .entry(conflicted)
568            .finish()
569            .unwrap_err();
570        match err {
571            MethodCatalogError::ConflictingBound { method, var, .. } => {
572                assert_eq!(method, "push");
573                assert_eq!(var, "T");
574            }
575            other => panic!("expected a conflicting bound, got {other}"),
576        }
577
578        // Restating the *same* bound is one requirement, not a conflict.
579        let restated = MethodEntry {
580            receiver: TypePattern::Collection {
581                ctor: CollectionCtor::Vec,
582                args: vec![TypePattern::is_scalar("T", ScalarType::Int)],
583            },
584            params: vec![TypePattern::is_scalar("T", ScalarType::Int)],
585            ..vec_push()
586        };
587        let bounds = restated.bounds();
588        assert_eq!(bounds, vec![("T", Bound::Is(ScalarType::Int))]);
589        assert!(MethodCatalog::build().entry(restated).finish().is_ok());
590    }
591
592    /// `bounds()` finds a declaration wherever it is written — the whole point of
593    /// keying on the variable rather than the position. `sum` declares its `Int`
594    /// requirement on the receiver's element, and `min_by`-shaped rows would
595    /// declare one inside a closure parameter.
596    #[test]
597    fn bounds_are_found_in_every_position() {
598        let on_receiver = MethodEntry {
599            receiver: TypePattern::Collection {
600                ctor: CollectionCtor::Vec,
601                args: vec![TypePattern::is_scalar("T", ScalarType::Int)],
602            },
603            ..vec_len()
604        };
605        assert_eq!(
606            on_receiver.bounds(),
607            vec![("T", Bound::Is(ScalarType::Int))]
608        );
609
610        // Inside a closure parameter, two levels down.
611        let in_a_closure = MethodEntry {
612            params: vec![TypePattern::Function {
613                params: vec![TypePattern::is_scalar("U", ScalarType::Char)],
614                result: Box::new(TypePattern::Unit),
615            }],
616            ..vec_len()
617        };
618        assert_eq!(
619            in_a_closure.bounds(),
620            vec![("U", Bound::Is(ScalarType::Char))]
621        );
622
623        // In the result, and inside a tuple in it.
624        let in_the_result = MethodEntry {
625            result: TypePattern::Tuple(vec![
626                TypePattern::Scalar(ScalarType::Int),
627                TypePattern::is_scalar("V", ScalarType::Byte),
628            ]),
629            ..vec_len()
630        };
631        assert_eq!(
632            in_the_result.bounds(),
633            vec![("V", Bound::Is(ScalarType::Byte))]
634        );
635
636        // An unbounded variable declares nothing, which is the common case.
637        assert!(vec_push().bounds().is_empty());
638    }
639
640    #[test]
641    fn same_name_different_arity_is_allowed() {
642        // `len` (0 args) and a hypothetical `len` taking a sentinel are two
643        // different triples. The catalog allows them; the *language* may not,
644        // but that is a separate concern from table integrity.
645        let other = MethodEntry {
646            params: vec![TypePattern::Scalar(ScalarType::Int)],
647            ..vec_len()
648        };
649        let catalog = MethodCatalog::build()
650            .entry(vec_len())
651            .entry(other)
652            .finish()
653            .expect("different arity is not a duplicate");
654        assert_eq!(catalog.len(), 2);
655    }
656
657    /// **ADR-127 decision 1.** A concrete row beside a generic `Iterable` one at
658    /// the same `(name, arity)` is not a duplicate — the catalog's key includes
659    /// the receiver — so nothing would refuse it, and both would match the same
660    /// call. That is a precedence rule arriving by accident, and decision 6's
661    /// whole argument against the shape-preserving family is that a precedence
662    /// rule is what makes "which does this resolve to" a question at all.
663    #[test]
664    fn finish_rejects_a_concrete_row_that_shadows_the_generic_one() {
665        let generic = MethodEntry {
666            receiver: TypePattern::iterable(TypePattern::var("T")),
667            name: "map",
668            params: vec![TypePattern::Function {
669                params: vec![TypePattern::var("T")],
670                result: Box::new(TypePattern::var("U")),
671            }],
672            result: TypePattern::Collection {
673                ctor: CollectionCtor::Vec,
674                args: vec![TypePattern::var("U")],
675            },
676            purity: Purity::Pure,
677            lowering: MethodLowering::Intrinsic("seq_map"),
678            doc: "Apply a function to each element.",
679        };
680        // A `Set` is one of the ten, so `Set[T].map/1` and `Iterable.map/1` both
681        // answer `set.map(f)`.
682        let on_a_set = MethodEntry {
683            receiver: TypePattern::Collection {
684                ctor: CollectionCtor::Set,
685                args: vec![TypePattern::var("T")],
686            },
687            ..generic.clone()
688        };
689        let err = MethodCatalog::build()
690            .entry(generic.clone())
691            .entry(on_a_set)
692            .finish()
693            .unwrap_err();
694        match err {
695            MethodCatalogError::AmbiguousWithIterable { name, arity, .. } => {
696                assert_eq!((name, arity), ("map", 1));
697            }
698            other => panic!("expected an Iterable shadow, got {other}"),
699        }
700
701        // Insertion order is not the question: the same pair the other way round
702        // is the same collision.
703        let on_a_set = MethodEntry {
704            receiver: TypePattern::Collection {
705                ctor: CollectionCtor::Set,
706                args: vec![TypePattern::var("T")],
707            },
708            ..generic.clone()
709        };
710        assert!(
711            MethodCatalog::build()
712                .entry(on_a_set)
713                .entry(generic.clone())
714                .finish()
715                .is_err()
716        );
717
718        // **`Grid[T].map/1` is allowed**, and that is the point of scoping the
719        // check to the ten: §6.4 asks for a shape-preserving `grid.map` by name,
720        // and `Grid` is not a receiver the generic row accepts.
721        let on_a_grid = MethodEntry {
722            receiver: TypePattern::Collection {
723                ctor: CollectionCtor::Grid,
724                args: vec![TypePattern::var("T")],
725            },
726            result: TypePattern::Collection {
727                ctor: CollectionCtor::Grid,
728                args: vec![TypePattern::var("U")],
729            },
730            ..generic.clone()
731        };
732        assert!(
733            MethodCatalog::build()
734                .entry(generic.clone())
735                .entry(on_a_grid)
736                .finish()
737                .is_ok()
738        );
739
740        // A different arity is a different question, as it is for a duplicate.
741        let different_arity = MethodEntry {
742            receiver: TypePattern::Collection {
743                ctor: CollectionCtor::Set,
744                args: vec![TypePattern::var("T")],
745            },
746            params: vec![],
747            ..generic.clone()
748        };
749        assert!(
750            MethodCatalog::build()
751                .entry(generic)
752                .entry(different_arity)
753                .finish()
754                .is_ok()
755        );
756    }
757
758    /// **ADR-144.** Two generic rows at one `(name, arity)` are refused, even
759    /// though their receivers are not equal.
760    ///
761    /// The shape this refuses: one row for a sequence of `Text` and one for a
762    /// sequence of `Char`. Neither the `Duplicate` check (the receivers differ)
763    /// nor the shadowing check (neither row is concrete) catches it, and
764    /// `lookup` matches an `Iterable` on shape — so `cs.join("")` would resolve
765    /// to whichever row was registered first and report `expected Text, found
766    /// Char`. A precedence rule nobody wrote is worse than a build failure.
767    #[test]
768    fn finish_rejects_two_generic_rows_at_one_arity() {
769        let of_text = MethodEntry {
770            receiver: TypePattern::iterable(TypePattern::is_scalar("T", ScalarType::Text)),
771            name: "join",
772            params: vec![TypePattern::Scalar(ScalarType::Text)],
773            result: TypePattern::Scalar(ScalarType::Text),
774            purity: Purity::Pure,
775            lowering: MethodLowering::Intrinsic("seq_join"),
776            doc: "These Text items concatenated.",
777        };
778        let of_char = MethodEntry {
779            receiver: TypePattern::iterable(TypePattern::is_scalar("T", ScalarType::Char)),
780            ..of_text.clone()
781        };
782        let err = MethodCatalog::build()
783            .entry(of_text.clone())
784            .entry(of_char)
785            .finish()
786            .unwrap_err();
787        match err {
788            MethodCatalogError::AmbiguousIterablePair { name, arity } => {
789                assert_eq!((name, arity), ("join", 1));
790            }
791            other => panic!("expected an ambiguous generic pair, got {other}"),
792        }
793
794        // A different arity is a different question here too, which is what
795        // keeps `count()` and `count(pred)` — both generic — legal.
796        let nullary = MethodEntry {
797            params: vec![],
798            ..of_text.clone()
799        };
800        assert!(
801            MethodCatalog::build()
802                .entry(of_text)
803                .entry(nullary)
804                .finish()
805                .is_ok()
806        );
807    }
808
809    /// **ADR-127.** The receiver generalizes; a parameter and a result do not.
810    ///
811    /// `zip`'s argument and `flat_map`'s closure result are the two rows this is
812    /// about, and the reason is not symmetry: the fused loop indexes each of them
813    /// with `praxis_vec_len`/`praxis_vec_get` directly — `Step::Zip` walks its
814    /// second source with its own dense counter, a splice walks the inner `Vec` —
815    /// and neither has an `IterPlan` in scope, because neither is the source.
816    /// Generalizing either would put a `SetPayload` under `praxis_vec_get`, which
817    /// is the exact wrong-type read `IterPlan` exists to prevent.
818    ///
819    /// It is also what keeps the instantiation path total: an `Iterable` names
820    /// ten types, so `pattern_to_type` has no single answer for one, and the
821    /// receiver is the only position that never asks it for one.
822    #[test]
823    fn finish_rejects_an_iterable_written_outside_the_receiver() {
824        let iterable = || TypePattern::iterable(TypePattern::var("T"));
825        let base = MethodEntry {
826            receiver: iterable(),
827            name: "zip",
828            params: vec![],
829            result: TypePattern::Unit,
830            purity: Purity::Pure,
831            lowering: MethodLowering::Intrinsic("seq_zip"),
832            doc: "Pair elements with another sequence.",
833        };
834        // Bare in a parameter, nested inside one, in the result, and nested
835        // inside the receiver's own item.
836        for offender in [
837            MethodEntry {
838                params: vec![iterable()],
839                ..base.clone()
840            },
841            MethodEntry {
842                params: vec![TypePattern::Function {
843                    params: vec![TypePattern::var("T")],
844                    result: Box::new(iterable()),
845                }],
846                ..base.clone()
847            },
848            MethodEntry {
849                result: TypePattern::Collection {
850                    ctor: CollectionCtor::Vec,
851                    args: vec![iterable()],
852                },
853                ..base.clone()
854            },
855            MethodEntry {
856                receiver: TypePattern::iterable(iterable()),
857                ..base.clone()
858            },
859        ] {
860            let err = MethodCatalog::build().entry(offender).finish().unwrap_err();
861            assert!(
862                matches!(err, MethodCatalogError::IterableOutsideReceiver { .. }),
863                "expected the parameter rule, got {err}"
864            );
865        }
866        // The receiver itself is the one position that may be one.
867        assert!(MethodCatalog::build().entry(base).finish().is_ok());
868    }
869
870    #[test]
871    fn entry_reports_capabilities() {
872        let e = vec_push();
873        assert_eq!(e.arity(), 1);
874        // `allocates` is the manifest's answer, not a field the row restates.
875        // Both of these are safepoints, and `len` is the interesting one:
876        // "reading a length allocates nothing" is the wrong reading —
877        // `praxis_vec_len` boxes the count into a fresh `Int`, so a collection
878        // can run inside it.
879        assert!(e.allocates());
880        assert!(vec_len().allocates());
881        // `praxis_vec_push` calls `adopt_or_reject`, which ends in
882        // `set_fault(ctx, TYPE_MISMATCH)`, so a row declaring "Allocates, not
883        // AllocatesAndFaults" would be restating the manifest rather than
884        // reading it. `praxis_vec_len` is the contrast that keeps the assertion
885        // meaningful: it really cannot fault.
886        assert!(e.can_fault(), "praxis_vec_push raises TypeMismatch");
887        assert!(!vec_len().can_fault());
888        assert_eq!(e.purity, Purity::Impure);
889    }
890}