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}