Skip to main content

praxis_stdlib/
type_pattern.rs

1//! Schema-level type patterns used to describe receivers, parameters, and
2//! results in the method catalog (§16.2).
3//!
4//! This is **not** the inference type system — that lives in `praxis-typeck`.
5//! `TypePattern` is a small, self-describing shape language, enough to populate
6//! the catalog and to be unified with the real type representation. Keeping it
7//! separate is what keeps `praxis-stdlib` from depending on `praxis-typeck`.
8
9use std::fmt;
10
11/// What a catalog type variable is required to be.
12///
13/// Without a way to state this, `sum` would accept `Vec[Bool]` *and*
14/// `Vec[Float]` — the first a nonsense addition of booleans, the second a
15/// silent reinterpretation of float bits as an integer.
16///
17/// # Why the scalar shape is not a capability, and why the other one is
18///
19/// `sum`, `product`, `min` and `max` each lower to an `ExtractScalar` at
20/// `ScalarKind::Int` followed by an `IntBinOp` or an `IntCmp`, so
21/// [`CapKind`](crate::CapKind)`::Numeric` — which is `Int`, `UInt`, `Byte`
22/// *and* `Float` — would bless `Vec[Float].sum()` and return the float's bits
23/// added as an integer. A capability is the wrong *width* for an Int-only
24/// lowering.
25///
26/// The capabilities the catalog would otherwise want are already enforced from
27/// the receiver's **type** rather than per row, which is stronger: a `Map` key
28/// must be hash-stable and a heap element orderable wherever that collection is
29/// built, not only when a particular method is called
30/// (`Inferer::require_collection_invariants`, ADR-057 Decision 3).
31///
32/// So the scalar arm is not a capability. The **second** arm is: `sorted`
33/// orders its elements through the element descriptor's `compare` callback, and
34/// a `Vec[T]` whose `T` is a function value has none. That is `CapKind::Ord`,
35/// and it is a fact about the row rather than about the receiver's *type* — a
36/// `Vec` is a perfectly good `Vec` of unorderable things right up until someone
37/// sorts it — so `require_collection_invariants` is the wrong door for it and
38/// the row has to say it itself.
39///
40/// The match on this enum in `praxis_hir`'s `apply_bounds` is exhaustive, so a
41/// third arm is a compile error to add halfway rather than a silent omission.
42#[derive(Clone, Copy, PartialEq, Eq, Debug)]
43pub enum Bound {
44    /// Exactly one scalar, and nothing else. Discharged by **unification**, so a
45    /// failure is the ordinary `expected Int, found Bool` reported at the method
46    /// name, and an element type nothing has named yet is *pinned* rather than
47    /// merely permitted — which is what `v.map(f).sum()` needs.
48    Is(ScalarType),
49    /// One capability, and any type that has it. Discharged through the
50    /// **constraint channel**, not by unification: a bound on a variable nothing
51    /// has pinned yet cannot be answered, and `fn top(v) { v.sorted() }` is
52    /// exactly that shape until a call site says what `v` holds. That is the
53    /// whole reason this arm is not spelled as a set of scalars.
54    Kind(crate::CapKind),
55}
56
57/// A pattern describing a type shape in a catalog entry.
58///
59/// # There is no placeholder arm
60///
61/// Every row writes a concrete pattern. A placeholder for rows whose shape is
62/// not worked out yet would have to arrive *together with* the rejection that
63/// makes it safe: the only thing `pattern_to_type` could instantiate one as is a
64/// fresh inference variable, which unifies with anything, so "the type checker
65/// rejects it if it is still present at use time" is a promise nothing keeps by
66/// default.
67#[derive(Clone, PartialEq, Eq, Debug)]
68pub enum TypePattern {
69    /// A specific scalar type, e.g. `Int`.
70    Scalar(ScalarType),
71    /// A built-in collection type constructor applied to element type(s), e.g.
72    /// `Vec[Int]` or `Map[Text, Int]`.
73    Collection {
74        ctor: CollectionCtor,
75        /// Element type parameters. Length must match the constructor's arity.
76        args: Vec<TypePattern>,
77    },
78    /// A type variable used inside a generic method's signature, e.g. `T` in
79    /// `Vec[T].push(T)`. Two occurrences of the *same* variable name inside one
80    /// entry refer to the same type; that equality is what the type checker
81    /// enforces at a call site.
82    ///
83    /// `bound` is what the variable must satisfy. It is a fact about the
84    /// *variable*, not about the position it is written in, so an entry
85    /// declares it once — at whichever occurrence reads best — and
86    /// [`MethodEntry::bounds`](crate::MethodEntry::bounds) finds it wherever it
87    /// is. Declaring two different bounds for one name in one entry is a catalog
88    /// authoring mistake and [`MethodCatalog::build`](crate::MethodCatalog::build)
89    /// refuses it.
90    Var {
91        name: &'static str,
92        bound: Option<Bound>,
93    },
94    /// The function type `(params) -> result`. Used for higher-order methods
95    /// like `Vec[T].map`.
96    Function {
97        params: Vec<TypePattern>,
98        result: Box<TypePattern>,
99    },
100    /// The unit type, used for methods like `Vec[T].push` that return nothing.
101    Unit,
102    /// A tuple `(T, U, ...)`. Used by grid methods that return/accept `(x, y)`
103    /// points (§6.4). Structural identity is the element-type sequence.
104    Tuple(Vec<TypePattern>),
105    /// `Option[T]` — the prelude enum, applied to one argument (§4.7, F12).
106    ///
107    /// Its own arm rather than a `Collection` ctor because `Option` is not a
108    /// collection: it is the one *generic enum def* the language has, and a
109    /// catalog row spelling it has to lower to `TypeDb::option_of`, which names
110    /// the single canonical def every `Option[T]` in a program shares.
111    ///
112    /// §4.7: "Option[T] represents normal domain-level absence. It is not an
113    /// error channel." `Map.get` and `Grid.find` are the rows that need it: a
114    /// miss is an absent value, not a `V` or an `(Int, Int)` standing in for
115    /// one.
116    Option(Box<TypePattern>),
117    /// A **nominal prelude record** — one name, one declared field order, e.g.
118    /// `Around4 { up, left, right, down }`.
119    ///
120    /// # The field order written here is the runtime's layout, and that is not a convention
121    ///
122    /// A field read compiles to a **slot index** taken from the def's field
123    /// order, while the value is laid out in its schema's order. ADR-152 makes
124    /// those agree for an *anonymous* shape by permuting every registration
125    /// into the order the shape was first written anywhere in the program —
126    /// which a runtime-built record cannot participate in, because the runtime
127    /// built its schema before the program was read. So a catalog record is
128    /// nominal: `TypeDb::register_record` permutes only when the name is
129    /// absent, and a nominal def keeps this list's order verbatim.
130    ///
131    /// The consequence is that **this list and the runtime schema's field list
132    /// are one order in two places**. `praxis_runtime::records::around4_schema`
133    /// is the other, and `praxis_runtime::records::tests::around_schemas_match_the_catalog`
134    /// is what keeps them from drifting — it is the one test that can see both
135    /// lists at once. A disagreement is a silently wrong field read, not a
136    /// crash.
137    ///
138    /// A record is a method *result*, never a receiver:
139    /// `praxis_hir::catalog::type_to_pattern` answers `None` for
140    /// `TypeData::Record`, so no row can dispatch on one.
141    Record {
142        /// The declared type name, which is the record's identity (§4.5) and
143        /// the `SchemaIdentity::Nominal` the runtime builds values under.
144        name: &'static str,
145        /// The fields, **in declaration order**. See the type-level note.
146        fields: Vec<(&'static str, TypePattern)>,
147    },
148    /// A receiver the pipeline walks: any of the ten iterables named by
149    /// [`is_pipeline_receiver`], binding what it yields to `item` (ADR-127).
150    ///
151    /// # It is the one pattern that is not unified with the receiver
152    ///
153    /// Everywhere else a catalog receiver is instantiated and unified with the
154    /// actual receiver — that is what pins `T` in `Vec[T].push(T)`. This one
155    /// cannot be: it accepts ten different constructors, and unifying against
156    /// any one of them pins the other nine out. What is unified is the **item**,
157    /// against `capability::iter_item`'s answer for the receiver — the `for`
158    /// loop's own answer to "what does this yield".
159    ///
160    /// One consequence is load-bearing and Decision 4 uses it: a row constrains
161    /// *which* iterables it accepts by writing a shape into `item`.
162    /// `Iterable { item: Tuple[K, V] }` is "a `Map` or a `Counter`", because
163    /// those are the two whose item is a pair — and `[1, 2].to_map()` is an
164    /// ordinary unification failure at the method name, not a row that resolves
165    /// and then faults.
166    Iterable { item: Box<TypePattern> },
167}
168
169/// The collection constructors a [`TypePattern::Iterable`] receiver accepts
170/// (ADR-127 decision 1) — the `for` loop's list minus `Grid` and `Seq`.
171///
172/// **`Grid[T]` is excluded, and `grid.map` is why.** §6.4 requires `grid.map(fn)`
173/// and it means the shape-preserving one, `Grid[T] -> Grid[U]`, cells in place. A
174/// generic row would claim the name and answer `Vec[U]` instead. A grid enters a
175/// pipeline through `grid.cells()` or `grid.positions()`, which already answer
176/// `Vec`s. The exclusion is enforced rather than intended:
177/// [`MethodCatalogBuilder::finish`](crate::catalog::MethodCatalogBuilder::finish)
178/// refuses a concrete row that shares a `(name, arity)` with a generic one *on a
179/// receiver in this list*, so a future `Grid[T].map/1` is allowed and a
180/// `Set[T].map/1` is a build failure.
181///
182/// **`Seq[T]` is excluded because it has no values.** `praxis-repr` says a `Seq`
183/// has no runtime representation, and nothing produces or consumes one
184/// (ADR-127).
185///
186/// `Text` is the tenth receiver and is not here, because it is not a collection:
187/// it is the one *scalar* with members (§4.13). [`is_pipeline_receiver`] is the
188/// predicate that answers for all ten.
189pub const PIPELINE_RECEIVERS: &[CollectionCtor] = &[
190    CollectionCtor::Vec,
191    CollectionCtor::Deque,
192    CollectionCtor::Set,
193    CollectionCtor::MinHeap,
194    CollectionCtor::MaxHeap,
195    CollectionCtor::Range,
196    CollectionCtor::BitSet,
197    CollectionCtor::Map,
198    CollectionCtor::Counter,
199];
200
201/// Whether a *concrete* receiver pattern is one of the ten a
202/// [`TypePattern::Iterable`] row accepts (ADR-127 decision 1).
203///
204/// A pure pattern-level test — ctor membership in [`PIPELINE_RECEIVERS`], or the
205/// `Text` scalar — so it needs no `TypeDb` and both callers can ask it from
206/// inside an immutable borrow. It deliberately says nothing about the row's
207/// `item`: a row whose item shape excludes this receiver still *matches*, and
208/// the item unification is what reports.
209#[must_use]
210pub fn is_pipeline_receiver(concrete: &TypePattern) -> bool {
211    match concrete {
212        TypePattern::Collection { ctor, .. } => PIPELINE_RECEIVERS.contains(ctor),
213        TypePattern::Scalar(ScalarType::Text) => true,
214        _ => false,
215    }
216}
217
218/// Whether a catalog receiver pattern accepts a concrete runtime pattern.
219///
220/// `Var("T")` in the catalog entry is a type-variable wildcard: it matches any
221/// concrete element (so `Vec[T].len()` matches `Vec[Int].len()`). A
222/// [`TypePattern::Iterable`] receiver matches any of the ten
223/// [`PIPELINE_RECEIVERS`]. All other variants require exact equality.
224///
225/// **This lives here because two callers ask the same question.**
226/// `praxis_hir::catalog::lookup` decides dispatch and
227/// `praxis_lsp::completion::dot_items` decides what `set.` offers. If the two
228/// disagree the editor offers a method the compiler refuses; one function is
229/// what makes that unrepresentable rather than merely unlikely.
230#[must_use]
231pub fn pattern_matches(catalog_pat: &TypePattern, concrete_pat: &TypePattern) -> bool {
232    match (catalog_pat, concrete_pat) {
233        (TypePattern::Var { .. }, _) => true,
234        // The generic pipeline receiver (ADR-127). Note what is *not* consulted:
235        // the row's `item`. `Iterable { item: (K, V) }` matches a `Set[Int]`
236        // here, and the item unification `bind_receiver` performs is what
237        // reports "expected `(K, V)`, found `Int`" at the method name.
238        (TypePattern::Iterable { .. }, concrete) => is_pipeline_receiver(concrete),
239        (
240            TypePattern::Collection { ctor: c1, args: a1 },
241            TypePattern::Collection { ctor: c2, args: a2 },
242        ) => {
243            c1 == c2
244                && a1.len() == a2.len()
245                && a1.iter().zip(a2).all(|(x, y)| pattern_matches(x, y))
246        }
247        // Tuples match element-wise (so a catalog `Tuple[Int, Int]` point
248        // pattern matches a concrete `(Int, Int)`).
249        (TypePattern::Tuple(a1), TypePattern::Tuple(a2)) => {
250            a1.len() == a2.len() && a1.iter().zip(a2).all(|(x, y)| pattern_matches(x, y))
251        }
252        // `Option[T]` matches through its argument, for the same reason a
253        // collection does.
254        (TypePattern::Option(a), TypePattern::Option(b)) => pattern_matches(a, b),
255        // A nominal record matches by name first — that *is* its identity — and
256        // then field-wise through the pattern, positionally, because a nominal
257        // record's field order is part of its declaration. Reached only through
258        // a nested position; a record is never a receiver.
259        (
260            TypePattern::Record {
261                name: n1,
262                fields: f1,
263            },
264            TypePattern::Record {
265                name: n2,
266                fields: f2,
267            },
268        ) => {
269            n1 == n2
270                && f1.len() == f2.len()
271                && f1
272                    .iter()
273                    .zip(f2)
274                    .all(|((na, pa), (nb, pb))| na == nb && pattern_matches(pa, pb))
275        }
276        _ => catalog_pat == concrete_pat,
277    }
278}
279
280impl TypePattern {
281    /// An unconstrained type variable — `T` in `Vec[T].push(T)`.
282    ///
283    /// The overwhelmingly common case, and the reason [`TypePattern::Var`] is a
284    /// struct variant rather than a second enum arm: a bound is an optional fact
285    /// about a variable, so there is one kind of variable and not two.
286    #[must_use]
287    pub const fn var(name: &'static str) -> TypePattern {
288        TypePattern::Var { name, bound: None }
289    }
290
291    /// A type variable that must satisfy `bound`.
292    #[must_use]
293    pub const fn bounded(name: &'static str, bound: Bound) -> TypePattern {
294        TypePattern::Var {
295            name,
296            bound: Some(bound),
297        }
298    }
299
300    /// A type variable required to be exactly `scalar` — the Int-only sinks.
301    #[must_use]
302    pub const fn is_scalar(name: &'static str, scalar: ScalarType) -> TypePattern {
303        TypePattern::bounded(name, Bound::Is(scalar))
304    }
305
306    /// The pipeline receiver yielding `item` — `Iterable { item }`, spelled
307    /// without the `Box` every row would otherwise write (ADR-127).
308    #[must_use]
309    pub fn iterable(item: TypePattern) -> TypePattern {
310        TypePattern::Iterable {
311            item: Box::new(item),
312        }
313    }
314
315    /// A type variable required to have `kind` — the barrier combinators, whose
316    /// runtime wrappers read a descriptor callback the element may not have
317    /// (`sorted` needs `compare`, `frequencies` and `unique` need a key that
318    /// stays findable after it is stored).
319    #[must_use]
320    pub const fn of_kind(name: &'static str, kind: crate::CapKind) -> TypePattern {
321        TypePattern::bounded(name, Bound::Kind(kind))
322    }
323
324    /// Append every `(name, bound)` this pattern declares, recursing into
325    /// composites. Order is source order, which is what makes a duplicate
326    /// declaration reportable at the first occurrence.
327    pub(crate) fn collect_bounds(&self, into: &mut Vec<(&'static str, Bound)>) {
328        match self {
329            TypePattern::Var { name, bound } => {
330                if let Some(b) = bound {
331                    into.push((name, *b));
332                }
333            }
334            TypePattern::Collection { args, .. } | TypePattern::Tuple(args) => {
335                for a in args {
336                    a.collect_bounds(into);
337                }
338            }
339            // A bound on the pipeline receiver's item is the row's own — `sum`'s
340            // `Bound::Is(Int)` lives here — so the sweep has to reach it. Its
341            // load-bearing half is that an item type nothing has pinned yet is
342            // *pinned* to `Int` rather than merely permitted.
343            TypePattern::Iterable { item } => item.collect_bounds(into),
344            TypePattern::Option(inner) => inner.collect_bounds(into),
345            TypePattern::Record { fields, .. } => {
346                for (_, f) in fields {
347                    f.collect_bounds(into);
348                }
349            }
350            TypePattern::Function { params, result } => {
351                for p in params {
352                    p.collect_bounds(into);
353                }
354                result.collect_bounds(into);
355            }
356            TypePattern::Scalar(_) | TypePattern::Unit => {}
357        }
358    }
359}
360
361/// Built-in scalar types (§4.3). The full set is named here even though `UInt`
362/// has no runtime object of its own (§7.4: its type is `Int`) — these names
363/// must not be reused for anything else.
364#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
365pub enum ScalarType {
366    Bool,
367    Int,
368    UInt,
369    Float,
370    Byte,
371    Char,
372    Text,
373}
374
375/// Built-in collection constructors (§6.1). `Range` and `BitSet` take no type
376/// arguments; the others take one (`Vec`, `Set`, ...) or two (`Map`).
377///
378/// **`Seq` has no rows and no values.** It is the compiler-internal pipeline
379/// source (§6.3), threading an element type through what a lazy chain would
380/// need; the pipeline is eager (ADR-028 decision 2), so no row answers one.
381/// Nothing produces a `Seq`, nothing consumes one, and retiring the constructor
382/// itself is a mechanical follow-up rather than a decision.
383#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
384pub enum CollectionCtor {
385    Vec,
386    Deque,
387    Map,
388    Set,
389    Counter,
390    MinHeap,
391    MaxHeap,
392    BitSet,
393    Grid,
394    Range,
395    /// Compiler-internal lazy sequence (§6.3). Never appears in source.
396    Seq,
397}
398
399impl CollectionCtor {
400    /// The number of element type parameters this constructor takes.
401    pub fn arity(self) -> usize {
402        match self {
403            CollectionCtor::Map => 2,
404            // `BitSet` and `Range` are nullary in user syntax; the rest take one
405            // element type.
406            CollectionCtor::BitSet | CollectionCtor::Range => 0,
407            _ => 1,
408        }
409    }
410
411    /// The constructor a source name denotes, or `None` for any other name.
412    ///
413    /// The inverse of [`name`](Self::name), and the one authority for the
414    /// mapping: HIR resolves a constructor call through it and MIR picks the
415    /// allocation's ctor through it, so the two cannot come to disagree about
416    /// which names construct a collection. `Seq` is deliberately absent — it is
417    /// compiler-internal and no source name reaches it (§6.3).
418    #[must_use]
419    pub fn from_name(name: &str) -> Option<CollectionCtor> {
420        Some(match name {
421            "Vec" => CollectionCtor::Vec,
422            "Deque" => CollectionCtor::Deque,
423            "Map" => CollectionCtor::Map,
424            "Set" => CollectionCtor::Set,
425            "Counter" => CollectionCtor::Counter,
426            "MinHeap" => CollectionCtor::MinHeap,
427            "MaxHeap" => CollectionCtor::MaxHeap,
428            "BitSet" => CollectionCtor::BitSet,
429            "Grid" => CollectionCtor::Grid,
430            "Range" => CollectionCtor::Range,
431            _ => return None,
432        })
433    }
434
435    /// The user-facing name of this collection constructor, e.g. `Vec`. `Seq`
436    /// is internal and has no user-facing name; `name()` returns `"Seq"` only
437    /// for diagnostics/debugging.
438    pub fn name(self) -> &'static str {
439        match self {
440            CollectionCtor::Vec => "Vec",
441            CollectionCtor::Deque => "Deque",
442            CollectionCtor::Map => "Map",
443            CollectionCtor::Set => "Set",
444            CollectionCtor::Counter => "Counter",
445            CollectionCtor::MinHeap => "MinHeap",
446            CollectionCtor::MaxHeap => "MaxHeap",
447            CollectionCtor::BitSet => "BitSet",
448            CollectionCtor::Grid => "Grid",
449            CollectionCtor::Range => "Range",
450            CollectionCtor::Seq => "Seq",
451        }
452    }
453}
454
455impl ScalarType {
456    pub fn name(self) -> &'static str {
457        match self {
458            ScalarType::Bool => "Bool",
459            ScalarType::Int => "Int",
460            ScalarType::UInt => "UInt",
461            ScalarType::Float => "Float",
462            ScalarType::Byte => "Byte",
463            ScalarType::Char => "Char",
464            ScalarType::Text => "Text",
465        }
466    }
467}
468
469impl fmt::Display for TypePattern {
470    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471        match self {
472            TypePattern::Scalar(s) => f.write_str(s.name()),
473            TypePattern::Unit => f.write_str("Unit"),
474            TypePattern::Tuple(els) => {
475                f.write_str("(")?;
476                for (i, e) in els.iter().enumerate() {
477                    if i > 0 {
478                        f.write_str(", ")?;
479                    }
480                    write!(f, "{e}")?;
481                }
482                f.write_str(")")
483            }
484            TypePattern::Option(inner) => write!(f, "Option[{inner}]"),
485            // The *name*, not the shape. A nominal record is its name (§4.5),
486            // and that is what hover and completion should read; the field list
487            // is documentation's job, and the row's `doc` carries it.
488            TypePattern::Record { name, .. } => f.write_str(name),
489            // Not a type a user can write — no annotation names it — but the
490            // completion table renders every receiver, and "the thing a `for`
491            // walks" is what this says.
492            TypePattern::Iterable { item } => write!(f, "Iterable[{item}]"),
493            // The bound is not part of the type's spelling: it is a rule the
494            // compiler enforces, and §5.4 forbids surfacing capability names to
495            // the user. Completion and signature help show `T`.
496            TypePattern::Var { name, .. } => write!(f, "{name}"),
497            TypePattern::Collection { ctor, args } => {
498                write!(f, "{ctor:?}")?;
499                if !args.is_empty() {
500                    f.write_str("[")?;
501                    for (i, a) in args.iter().enumerate() {
502                        if i > 0 {
503                            f.write_str(", ")?;
504                        }
505                        write!(f, "{a}")?;
506                    }
507                    f.write_str("]")?;
508                }
509                Ok(())
510            }
511            TypePattern::Function { params, result } => {
512                f.write_str("(")?;
513                for (i, p) in params.iter().enumerate() {
514                    if i > 0 {
515                        f.write_str(", ")?;
516                    }
517                    write!(f, "{p}")?;
518                }
519                write!(f, ") -> {result}")
520            }
521        }
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn collection_arity_matches_design() {
531        assert_eq!(CollectionCtor::Vec.arity(), 1);
532        assert_eq!(CollectionCtor::Map.arity(), 2);
533        assert_eq!(CollectionCtor::Set.arity(), 1);
534        assert_eq!(CollectionCtor::BitSet.arity(), 0);
535        assert_eq!(CollectionCtor::Range.arity(), 0);
536        assert_eq!(CollectionCtor::Grid.arity(), 1);
537    }
538
539    #[test]
540    fn scalar_names_match_user_syntax() {
541        assert_eq!(ScalarType::Int.name(), "Int");
542        assert_eq!(ScalarType::Text.name(), "Text");
543    }
544
545    #[test]
546    fn pattern_display_matches_design_syntax() {
547        assert_eq!(TypePattern::Scalar(ScalarType::Int).to_string(), "Int");
548        assert_eq!(
549            TypePattern::Collection {
550                ctor: CollectionCtor::Vec,
551                args: vec![TypePattern::var("T")],
552            }
553            .to_string(),
554            "Vec[T]"
555        );
556        assert_eq!(
557            TypePattern::Collection {
558                ctor: CollectionCtor::Map,
559                args: vec![
560                    TypePattern::Scalar(ScalarType::Text),
561                    TypePattern::Scalar(ScalarType::Int)
562                ],
563            }
564            .to_string(),
565            "Map[Text, Int]"
566        );
567        let func = TypePattern::Function {
568            params: vec![TypePattern::var("T")],
569            result: Box::new(TypePattern::var("U")),
570        };
571        assert_eq!(func.to_string(), "(T) -> U");
572        assert_eq!(
573            TypePattern::iterable(TypePattern::var("T")).to_string(),
574            "Iterable[T]"
575        );
576    }
577
578    fn collection(ctor: CollectionCtor, args: Vec<TypePattern>) -> TypePattern {
579        TypePattern::Collection { ctor, args }
580    }
581
582    /// **ADR-127 decision 1.** The pipeline's receiver list is the `for` loop's
583    /// minus two, and each exclusion is a decision rather than an oversight:
584    /// `Grid` because §6.4 owes `grid.map` a shape-preserving row, `Seq` because
585    /// it has no values.
586    #[test]
587    fn the_pipeline_walks_ten_receivers_and_not_a_grid() {
588        let accepted = [
589            collection(CollectionCtor::Vec, vec![TypePattern::var("T")]),
590            collection(CollectionCtor::Deque, vec![TypePattern::var("T")]),
591            collection(CollectionCtor::Set, vec![TypePattern::var("T")]),
592            collection(CollectionCtor::MinHeap, vec![TypePattern::var("T")]),
593            collection(CollectionCtor::MaxHeap, vec![TypePattern::var("T")]),
594            collection(CollectionCtor::Range, vec![]),
595            collection(CollectionCtor::BitSet, vec![]),
596            collection(
597                CollectionCtor::Map,
598                vec![TypePattern::var("K"), TypePattern::var("V")],
599            ),
600            collection(CollectionCtor::Counter, vec![TypePattern::var("T")]),
601            TypePattern::Scalar(ScalarType::Text),
602        ];
603        assert_eq!(
604            accepted.len(),
605            PIPELINE_RECEIVERS.len() + 1,
606            "`Text` is the tenth receiver and the only one that is not a ctor"
607        );
608        for pat in &accepted {
609            assert!(is_pipeline_receiver(pat), "{pat} is walked by a `for`");
610        }
611
612        for refused in [
613            collection(CollectionCtor::Grid, vec![TypePattern::var("T")]),
614            collection(CollectionCtor::Seq, vec![TypePattern::var("T")]),
615            TypePattern::Scalar(ScalarType::Int),
616            TypePattern::Tuple(vec![TypePattern::var("K"), TypePattern::var("V")]),
617        ] {
618            assert!(!is_pipeline_receiver(&refused), "{refused} is not walked");
619        }
620    }
621
622    /// The `Iterable` arm matches on the *receiver's shape alone*. A row whose
623    /// item is a pair still matches a `Set`, and the failure it earns is the
624    /// item unification's — an ordinary "expected `(K, V)`, found `Int`" at the
625    /// method name, rather than "no method `to_map`", which would be a worse
626    /// message for the same mistake.
627    #[test]
628    fn an_iterable_row_matches_by_receiver_and_reports_by_item() {
629        let to_map = TypePattern::iterable(TypePattern::Tuple(vec![
630            TypePattern::var("K"),
631            TypePattern::var("V"),
632        ]));
633        let set_of_int = collection(
634            CollectionCtor::Set,
635            vec![TypePattern::Scalar(ScalarType::Int)],
636        );
637        assert!(pattern_matches(&to_map, &set_of_int));
638        // …and a `Grid` is refused at the door, which is what keeps `grid.map`
639        // §6.4's row rather than this one's.
640        let grid = collection(
641            CollectionCtor::Grid,
642            vec![TypePattern::Scalar(ScalarType::Int)],
643        );
644        assert!(!pattern_matches(&to_map, &grid));
645    }
646
647    /// A nominal record displays as its **name**, and matches on it.
648    ///
649    /// Two things ride on the name alone: the completion table renders a result
650    /// through `Display`, and `pattern_matches` is what would let one shape
651    /// stand in for another. One field list under two names is two types (§4.5),
652    /// and the arity is not what decides it.
653    #[test]
654    fn a_nominal_record_is_its_name() {
655        let point = TypePattern::Option(Box::new(TypePattern::Tuple(vec![
656            TypePattern::Scalar(ScalarType::Int),
657            TypePattern::Scalar(ScalarType::Int),
658        ])));
659        let plus = |name| TypePattern::Record {
660            name,
661            fields: vec![
662                ("up", point.clone()),
663                ("left", point.clone()),
664                ("right", point.clone()),
665                ("down", point.clone()),
666            ],
667        };
668        assert_eq!(plus("Around4").to_string(), "Around4");
669        assert!(pattern_matches(&plus("Around4"), &plus("Around4")));
670        assert!(!pattern_matches(&plus("Around4"), &plus("Corners")));
671        // …and the field *order* is part of the shape, because a nominal
672        // record's order is its declaration's and a field read is a slot index.
673        let reordered = TypePattern::Record {
674            name: "Around4",
675            fields: vec![
676                ("left", point.clone()),
677                ("up", point.clone()),
678                ("right", point.clone()),
679                ("down", point.clone()),
680            ],
681        };
682        assert!(!pattern_matches(&plus("Around4"), &reordered));
683    }
684
685    /// A bound written inside a record field is still the row's, so the sweep
686    /// reaches through the arm. No row does this today — the two prelude
687    /// records hold points — and the arm exists so that the day one does, the
688    /// bound is not silently dropped.
689    #[test]
690    fn a_bound_inside_a_record_field_is_found() {
691        let mut bounds = Vec::new();
692        TypePattern::Record {
693            name: "R",
694            fields: vec![("f", TypePattern::is_scalar("T", ScalarType::Int))],
695        }
696        .collect_bounds(&mut bounds);
697        assert_eq!(bounds, vec![("T", Bound::Is(ScalarType::Int))]);
698    }
699
700    /// `sum`'s `Int` bound lives on the pipeline receiver's *item*, and there is
701    /// nowhere else in the row for it to live — so the sweep has to reach
702    /// through the `Iterable` arm.
703    #[test]
704    fn a_bound_on_the_item_is_found() {
705        let mut bounds = Vec::new();
706        TypePattern::iterable(TypePattern::is_scalar("T", ScalarType::Int))
707            .collect_bounds(&mut bounds);
708        assert_eq!(bounds, vec![("T", Bound::Is(ScalarType::Int))]);
709    }
710}