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