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