Skip to main content

praxis_stdlib/
prelude.rs

1//! The Praxis prelude (§16.1): symbols automatically available in every
2//! program, with no `use` required.
3//!
4//! Kept as data so name resolution and the language server's completion, hover
5//! and signature tables all read the same single list.
6
7use crate::abi::RuntimeSymbol;
8
9/// The names automatically imported into every Praxis program (§16.1).
10///
11/// Sorted by category so the LSP can present them sensibly; within a category
12/// the order is alphabetical. The list is checked for emptiness and duplicates
13/// by the unit test.
14pub const PRELUDE: &[PreludeEntry] = &[
15    // Output / control
16    PreludeEntry::new(
17        "out",
18        "Write one value to stdout, followed by a newline. Renders through the value's own formatter, so any type may be written.",
19    ),
20    PreludeEntry::new(
21        "dbg",
22        "Write one value to **stderr** and return it unchanged, so `dbg(e)` can wrap any subexpression without changing what the program computes.",
23    ),
24    PreludeEntry::new(
25        "panic",
26        "Stop with an explicit message. Raises an ordinary fault, so it enters the crash debugger on a terminal; its result is `Never`, so a function may end on one.",
27    ),
28    PreludeEntry::new(
29        "assert",
30        "Stop if a condition is false. Takes a `Bool` and nothing else — there is no message parameter.",
31    ),
32    // Numeric helpers. All seven are `Int` functions and none is generic
33    // (ADR-058), so each doc string says `Int` rather than "a number": `Float`
34    // carries its own `abs`/`sign`/`min`/`max` as *methods* and has no `clamp`,
35    // `gcd` or `lcm` at all, and a reader who hovers here has already asked.
36    PreludeEntry::new(
37        "abs",
38        "Absolute value of an `Int`. Faults on `Int`'s minimum, which has no positive counterpart. `Float` has its own `x.abs()`.",
39    ),
40    PreludeEntry::new(
41        "sign",
42        "`-1`, `0` or `1`, by the sign of an `Int`. Total. `Float` has its own `x.sign()`.",
43    ),
44    PreludeEntry::new(
45        "min",
46        "The smaller of two `Int`s. `Float` has its own `x.min(y)` method.",
47    ),
48    PreludeEntry::new(
49        "max",
50        "The larger of two `Int`s. `Float` has its own `x.max(y)` method.",
51    ),
52    PreludeEntry::new(
53        "clamp",
54        "`clamp(value, low, high)` — an `Int` held inside an inclusive range. Faults if `low > high`.",
55    ),
56    PreludeEntry::new(
57        "gcd",
58        "Non-negative greatest common divisor of two `Int`s. `gcd(0, 0)` is `0`.",
59    ),
60    PreludeEntry::new(
61        "lcm",
62        "Non-negative least common multiple of two `Int`s, or `0` if either operand is. Faults if the result leaves `Int`.",
63    ),
64    // Nullary **functions**, not constants: `pi()` is the value and `pi` is
65    // `() -> Float` (§4.12). The doc string is what hover shows, so it must say
66    // so rather than call them constants.
67    PreludeEntry::new("pi", "π as a Float. A nullary function: write `pi()`."),
68    PreludeEntry::new(
69        "e",
70        "Euler's number as a Float. A nullary function: write `e()`.",
71    ),
72    // Collections
73    // `Vec` and `Grid` are the two constructors with a sized form, so their doc
74    // strings say both — this string is what LSP hover puts in front of the one
75    // reader who has already asked (ADR-146).
76    PreludeEntry::new(
77        "Vec",
78        "Grow, iterate, and pipeline over an ordered list. `Vec()` is empty; `Vec(n, fill)` is n copies of fill.",
79    ),
80    PreludeEntry::new(
81        "Deque",
82        "Double-ended queue: push and pop at either end. `Deque()` is empty.",
83    ),
84    PreludeEntry::new(
85        "Map",
86        "Hash map from keys to values. A key must be a value that cannot change. `Map()` is empty.",
87    ),
88    PreludeEntry::new(
89        "Set",
90        "Hash set of distinct values. An element must be a value that cannot change. `Set()` is empty.",
91    ),
92    PreludeEntry::new(
93        "Counter",
94        "Map whose absent values read as zero, so `c.inc(k)` needs no first-sighting case. `Counter()` is empty.",
95    ),
96    PreludeEntry::new(
97        "MinHeap",
98        "Priority queue yielding the smallest element first. Its element must be orderable. `MinHeap()` is empty.",
99    ),
100    PreludeEntry::new(
101        "MaxHeap",
102        "Priority queue yielding the largest element first. Its element must be orderable. `MaxHeap()` is empty.",
103    ),
104    PreludeEntry::new(
105        "Grid",
106        "2D grid with rectangular indexing. `Grid()` is 0x0; `Grid(w, h, fill)` is a w-by-h board of fill.",
107    ),
108    PreludeEntry::new(
109        "BitSet",
110        "Compact set of non-negative integers. Takes no type argument.",
111    ),
112    // Optionality. Option[T] is a polymorphic enum: Some(T) carries a value,
113    // None marks absence (§4.7 — "normal domain-level absence… not an error
114    // channel"). Returned by the `optional(P)` parser, by `Map.get` and
115    // `Grid.find`, and by the graph walks that may not reach their goal.
116    //
117    // Also by `find`/`position` on a sequence (ADR-082): `find` answers the
118    // *element* as an `Option[T]` and `position` its index as an `Option[Int]`.
119    // Neither uses a `-1` miss sentinel.
120    PreludeEntry::new(
121        "Option",
122        "Optional value: `Some(T)` or `None`. Domain-level absence, not an error channel — what `Map.get`, `Grid.find`, `find`/`position` and the goal-directed graph walks answer with.",
123    ),
124    PreludeEntry::variant("Some", "Wrap a value in an `Option`."),
125    PreludeEntry::variant(
126        "None",
127        "The absent `Option` value. Not a call — write `None`, never `None()`.",
128    ),
129    // Graph algorithms (§6.5). Each doc string writes the **call**, because the
130    // one thing a reader cannot guess from the name is the shape of the closures
131    // — none of these takes a graph object, so the graph *is* the neighbour
132    // function (ADR-060). Every state a walk visits is remembered, so the state
133    // type has to be usable as a key.
134    PreludeEntry::new(
135        "bfs",
136        "Breadth-first walk: `bfs(start, |s| neighbors(s))` answers every state reached, in the order it was reached.",
137    ),
138    PreludeEntry::new(
139        "bfs_distance",
140        "Steps to the first state a predicate accepts, or `None` when no goal is reachable: `bfs_distance(start, |s| neighbors(s), |s| s == goal)`.",
141    ),
142    PreludeEntry::new(
143        "dfs",
144        "Depth-first walk: `dfs(start, |s| neighbors(s))` answers every state reached, in the order it was reached.",
145    ),
146    PreludeEntry::new(
147        "dijkstra",
148        "Least cost from a start state to each reachable state, as a `Map`: `dijkstra(start, |s| neighbors(s), |a, b| weight(a, b))`. An unreachable state is absent rather than `None`.",
149    ),
150    PreludeEntry::new(
151        "a_star",
152        "Cost of the cheapest path to a goal, or `None`: `a_star(start, neighbors, weight, heuristic, goal)`, where the heuristic estimates the remaining cost from one state.",
153    ),
154    PreludeEntry::new(
155        "flood_fill",
156        "Every state reachable from a start state, unordered, as a `Set`: `flood_fill(start, |s| neighbors(s))`.",
157    ),
158];
159
160/// The built-in **type** names (§4.2's six scalars, `Never`, and `Range`), with
161/// the one-line description the editor shows for each.
162///
163/// This is the table `praxis-hir`'s name resolution seeds the root scope from,
164/// so a type name the checker accepts and a type name the editor can describe
165/// are the same list.
166///
167/// `UInt` and `Byte` are deliberately absent: §4.2 reserves them and neither is
168/// implemented, so either one in an annotation is an `N002` rather than a name
169/// with a doc string.
170pub const BUILTIN_TYPES: &[TypeEntry] = &[
171    TypeEntry::seeded("Int", "Signed 64-bit integer. Written `42` or `1_000_000`."),
172    TypeEntry::seeded(
173        "Float",
174        "IEEE-754 binary64. Written `3.5`, `1e10` or `2e-3`; `.5` is not a literal.",
175    ),
176    TypeEntry::seeded("Bool", "`true` or `false`."),
177    TypeEntry::seeded("Char", "One Unicode scalar value. Written `'p'`."),
178    TypeEntry::seeded("Text", "Immutable UTF-8 text. Written `\"praxis\"`."),
179    TypeEntry::seeded("Unit", "The type with one value, written `()`."),
180    TypeEntry::seeded(
181        "Never",
182        "The type of an expression that produces no value — `panic(...)`, `return`, `break`. It has no values, so it unifies with anything.",
183    ),
184    // **Not seeded**, and that is the whole of what `TypeEntry::seeded` and
185    // [`TypeEntry::ctor`] distinguish. `Range` is the one built-in type with no
186    // value of the same name: a range is written `0..n`, and `Range()` is
187    // `N001: 'Range' is not defined`. Binding it in the root scope would make
188    // that call resolve and then fail later, somewhere with less to say.
189    TypeEntry::ctor(
190        "Range",
191        "A half-open (`0..n`) or inclusive (`0..=n`) integer range. A type name only — there is no `Range()` constructor.",
192    ),
193];
194
195/// §6.5's graph helpers: the closure-driven algorithms that walk a graph the
196/// program never materializes (ADR-060).
197///
198/// A row is the source name, the wrapper it lowers to, the **shape** of each
199/// parameter and the shape of the result. The shapes are what inference reads
200/// to build the scheme, so a helper's signature and the wrapper it becomes are
201/// written down once; the arity that follows from `params` is checked against
202/// [`RuntimeSymbol::arity`] by a unit test, which is as close to ADR-058's
203/// "the arity is the wrapper's arity" as a family with six different shapes
204/// can get.
205///
206/// Every helper's first parameter is the start state and every other one is a
207/// function of it, which is §6.5's own spelling:
208///
209/// ```text
210/// var distance = bfs_distance(start, |s| neighbors(s), |s| s == goal)
211/// ```
212///
213/// A name in [`PRELUDE`] with no row here resolves and then has no scheme, no
214/// lowering and no implementation: inference hands out a fresh variable that
215/// unifies with anything and the call lowers as a direct call to a function
216/// nobody defined.
217pub const GRAPH_HELPERS: &[GraphHelper] = &[
218    GraphHelper::new(
219        "bfs",
220        RuntimeSymbol::Bfs,
221        &[GraphParam::Start, GraphParam::Neighbours],
222        GraphResult::VisitOrder,
223    ),
224    GraphHelper::new(
225        "bfs_distance",
226        RuntimeSymbol::BfsDistance,
227        &[GraphParam::Start, GraphParam::Neighbours, GraphParam::Goal],
228        GraphResult::Distance,
229    ),
230    GraphHelper::new(
231        "dfs",
232        RuntimeSymbol::Dfs,
233        &[GraphParam::Start, GraphParam::Neighbours],
234        GraphResult::VisitOrder,
235    ),
236    GraphHelper::new(
237        "dijkstra",
238        RuntimeSymbol::Dijkstra,
239        &[
240            GraphParam::Start,
241            GraphParam::Neighbours,
242            GraphParam::Weight,
243        ],
244        GraphResult::CostTable,
245    ),
246    GraphHelper::new(
247        "a_star",
248        RuntimeSymbol::AStar,
249        &[
250            GraphParam::Start,
251            GraphParam::Neighbours,
252            GraphParam::Weight,
253            GraphParam::Heuristic,
254            GraphParam::Goal,
255        ],
256        GraphResult::Distance,
257    ),
258    GraphHelper::new(
259        "flood_fill",
260        RuntimeSymbol::FloodFill,
261        &[GraphParam::Start, GraphParam::Neighbours],
262        GraphResult::Reached,
263    ),
264];
265
266/// One parameter of a graph helper, as a shape rather than as a type.
267///
268/// `praxis-stdlib` cannot name a `Type` — that is `praxis-typeck`, which depends
269/// on this crate — so the signature is written as the shapes inference then
270/// builds the types from. The match on this in `seed_builtin_schemes` is
271/// exhaustive, so a new shape is a compile error there rather than a parameter
272/// that silently gets the wrong type.
273#[derive(Clone, Copy, PartialEq, Eq, Debug)]
274pub enum GraphParam {
275    /// The state the walk starts from: a `T`.
276    Start,
277    /// `(T) -> Vec[T]` — the states reachable in one step from a given one.
278    /// Returning a `Vec` rather than a lazy sequence is what makes the helper
279    /// callable from the runtime, which has no way to drive a pipeline.
280    Neighbours,
281    /// `(T, T) -> Int` — the cost of the edge between two adjacent states.
282    Weight,
283    /// `(T) -> Int` — the estimated remaining cost from a state to a goal.
284    Heuristic,
285    /// `(T) -> Bool` — whether a state is a goal. A predicate rather than a
286    /// goal *value* so a search can stop on a property (`|s| s.0 == n`), which
287    /// is what §6.5's own example writes.
288    Goal,
289}
290
291/// What a graph helper answers.
292#[derive(Clone, Copy, PartialEq, Eq, Debug)]
293pub enum GraphResult {
294    /// `Vec[T]` — every state reached, in the order the walk reached it.
295    VisitOrder,
296    /// `Set[T]` — every state reached, without an order.
297    Reached,
298    /// `Map[T, Int]` — the least cost from the start to each reachable state.
299    /// A state that is not reachable is simply absent, which is why this needs
300    /// no `Option`.
301    CostTable,
302    /// `Option[Int]` — the cost of the cheapest path to a goal, or `None` when
303    /// no goal is reachable. "Unreachable" is an ordinary outcome of a search,
304    /// not a fault, and a sentinel `-1` would be a number nobody wrote.
305    Distance,
306}
307
308/// One graph prelude helper: the source name, the wrapper it lowers to, and the
309/// shape of its signature.
310#[derive(Clone, Copy, Debug)]
311pub struct GraphHelper {
312    pub name: &'static str,
313    pub symbol: RuntimeSymbol,
314    pub params: &'static [GraphParam],
315    pub result: GraphResult,
316}
317
318impl GraphHelper {
319    const fn new(
320        name: &'static str,
321        symbol: RuntimeSymbol,
322        params: &'static [GraphParam],
323        result: GraphResult,
324    ) -> GraphHelper {
325        GraphHelper {
326            name,
327            symbol,
328            params,
329            result,
330        }
331    }
332
333    /// How many arguments the source-level function takes.
334    #[inline]
335    pub const fn arity(&self) -> usize {
336        self.params.len()
337    }
338}
339
340/// The graph helper `name` denotes, or `None` for any other name.
341///
342/// The one lookup from a source name to a graph helper. Both consumers use it —
343/// inference for the scheme, MIR for the call target — so neither carries its
344/// own list.
345pub fn graph_helper(name: &str) -> Option<GraphHelper> {
346    GRAPH_HELPERS.iter().copied().find(|h| h.name == name)
347}
348
349/// The §16.1 numeric prelude helpers: the free functions that are neither
350/// output/control names nor collection constructors.
351///
352/// Every one of them is monomorphic on `Int` (ADR-058), so a row needs only the
353/// name and the wrapper it lowers to — the **arity** the type checker gives the
354/// name is [`RuntimeSymbol::arity`], read off the ABI manifest rather than
355/// restated here. That is what makes "a prelude name whose signature disagrees
356/// with the wrapper it calls" unrepresentable.
357///
358/// `Float`'s counterparts are *methods* (`0.5.abs()`, `x.min(y)` — §4.12), not
359/// entries here: a genuinely polymorphic `abs` would have to carry a numeric
360/// capability on its own binder and pick a lowering per instantiation, and
361/// nothing needs that yet.
362///
363/// `pi` and `e` are not here either. They are nullary `Float` functions rather
364/// than `Int` ones, with their own schemes and dispatch.
365pub const NUMERIC_HELPERS: &[NumericHelper] = &[
366    NumericHelper::new("abs", RuntimeSymbol::IntAbs),
367    NumericHelper::new("sign", RuntimeSymbol::IntSign),
368    NumericHelper::new("min", RuntimeSymbol::IntMin),
369    NumericHelper::new("max", RuntimeSymbol::IntMax),
370    NumericHelper::new("clamp", RuntimeSymbol::IntClamp),
371    NumericHelper::new("gcd", RuntimeSymbol::IntGcd),
372    NumericHelper::new("lcm", RuntimeSymbol::IntLcm),
373];
374
375/// One numeric prelude helper: the source name and the wrapper it lowers to.
376#[derive(Clone, Copy, Debug)]
377pub struct NumericHelper {
378    pub name: &'static str,
379    pub symbol: RuntimeSymbol,
380}
381
382impl NumericHelper {
383    const fn new(name: &'static str, symbol: RuntimeSymbol) -> NumericHelper {
384        NumericHelper { name, symbol }
385    }
386
387    /// How many `Int` parameters the source-level function takes, which is the
388    /// wrapper's arity: every helper takes `Int`s and returns one, so the two
389    /// counts are the same number and only the manifest states it.
390    #[inline]
391    pub const fn arity(&self) -> usize {
392        self.symbol.arity()
393    }
394}
395
396/// The numeric helper `name` denotes, or `None` for any other name.
397///
398/// The one lookup from a source name to a numeric helper. Both consumers use
399/// it — inference for the scheme's arity, MIR for the call target — so neither
400/// carries its own list.
401pub fn numeric_helper(name: &str) -> Option<NumericHelper> {
402    NUMERIC_HELPERS.iter().copied().find(|h| h.name == name)
403}
404
405/// The collection constructors that also have a **sized** form, in which the
406/// argument count selects the shape: `Vec(n, fill)` and `Grid(w, h, fill)`
407/// beside the nullary `Vec()` and `Grid()` (ADR-146).
408///
409/// This is the whole of the carve-out ADR-146 makes to
410/// [ADR-089](https://github.com/tljubej/praxis/blob/main/docs/decisions/089-a-name-has-one-signature.md) decision
411/// 1's "a name has exactly one signature", and it is a `const` table rather
412/// than a rule anywhere so that the narrowness is a fact a reader can count.
413/// The other seven constructors are absent on purpose: a sized `Set` is `n`
414/// copies of one element in a set, which is one element, and a sized `Map` has
415/// no answer for what its keys would be. `Vec` and `Grid` are the two whose
416/// contents are addressed by position, which is what makes "n of them" mean
417/// something.
418pub const SIZED_CTORS: &[SizedCtor] = &[
419    SizedCtor::new("Vec", RuntimeSymbol::VecFilled, 1),
420    SizedCtor::new("Grid", RuntimeSymbol::GridFilled, 2),
421];
422
423/// One sized collection constructor: the source name, the wrapper its sized
424/// form lowers to, and how many of its leading arguments are extents.
425///
426/// The fill is always the last argument and always exactly one, so `extents`
427/// determines the source arity — which is why nothing here states an arity
428/// twice.
429#[derive(Clone, Copy, Debug)]
430pub struct SizedCtor {
431    pub name: &'static str,
432    pub symbol: RuntimeSymbol,
433    /// The number of leading `Int` parameters: 1 for `Vec(n, fill)`, 2 for
434    /// `Grid(w, h, fill)`.
435    pub extents: usize,
436}
437
438impl SizedCtor {
439    const fn new(name: &'static str, symbol: RuntimeSymbol, extents: usize) -> SizedCtor {
440        SizedCtor {
441            name,
442            symbol,
443            extents,
444        }
445    }
446
447    /// The source-level arity: the extents plus the one fill.
448    #[inline]
449    #[must_use]
450    pub const fn arity(&self) -> usize {
451        self.extents + 1
452    }
453}
454
455/// The sized constructor `name` denotes, or `None` for any other name —
456/// including the seven collection constructors that have no sized form.
457///
458/// The one lookup from a source name to a sized constructor. Inference reads it
459/// to build the call site's type and MIR reads it to pick the wrapper, so
460/// neither carries its own list and neither can disagree with the other about
461/// which names are sized.
462pub fn sized_ctor(name: &str) -> Option<SizedCtor> {
463    SIZED_CTORS.iter().copied().find(|c| c.name == name)
464}
465
466/// One prelude symbol: its name and a one-line description.
467#[derive(Clone, Copy, Debug)]
468pub struct PreludeEntry {
469    pub name: &'static str,
470    pub doc: &'static str,
471    /// Whether this name is an **enum variant constructor** rather than an
472    /// ordinary value. `Some` and `None` are `Option`'s two variants, declared
473    /// by the prelude rather than by an `enum` item — and a consumer that has
474    /// to tell a constructor from a binding cannot do it from the type
475    /// (`var A = None` has `Option`'s type too), so the declaration says.
476    pub is_variant_ctor: bool,
477}
478
479impl PreludeEntry {
480    pub const fn new(name: &'static str, doc: &'static str) -> PreludeEntry {
481        PreludeEntry {
482            name,
483            doc,
484            is_variant_ctor: false,
485        }
486    }
487
488    /// An entry that constructs an enum variant.
489    pub const fn variant(name: &'static str, doc: &'static str) -> PreludeEntry {
490        PreludeEntry {
491            name,
492            doc,
493            is_variant_ctor: true,
494        }
495    }
496}
497
498/// One built-in type name and a one-line description.
499#[derive(Clone, Copy, Debug)]
500pub struct TypeEntry {
501    pub name: &'static str,
502    pub doc: &'static str,
503    /// Whether name resolution binds this name in the root scope.
504    ///
505    /// The scalars and `Never` are bound, which is how `var n: Nope` becomes an
506    /// `N002` from a failed lookup. A **type constructor** is not: `Range`, and
507    /// the collection names in [`PRELUDE`], are compiler-owned type names that
508    /// annotation checking accepts without a lookup and inference turns into
509    /// types. The distinction is not cosmetic — a bound name is also a *value*
510    /// name, and `Range` has no value.
511    pub seeded: bool,
512}
513
514impl TypeEntry {
515    /// A type name bound in the root scope.
516    const fn seeded(name: &'static str, doc: &'static str) -> TypeEntry {
517        TypeEntry {
518            name,
519            doc,
520            seeded: true,
521        }
522    }
523
524    /// A compiler-owned type constructor, which is not bound in any scope. See
525    /// [`TypeEntry::seeded`](Self::seeded)'s field documentation.
526    const fn ctor(name: &'static str, doc: &'static str) -> TypeEntry {
527        TypeEntry {
528            name,
529            doc,
530            seeded: false,
531        }
532    }
533}
534
535/// The description of the prelude **value** `name` denotes, or `None` for any
536/// other name.
537///
538/// The one lookup from a source name to its documentation, so the language
539/// server describes the prelude the compiler actually declares. A server that
540/// carried its own sentence about `bfs` would be free to describe a helper this
541/// table no longer has.
542///
543/// Callers must satisfy themselves that the name really is the prelude's: a
544/// `var out = 1` shadows it, and this function only knows the spelling.
545#[must_use]
546pub fn prelude_doc(name: &str) -> Option<&'static str> {
547    PRELUDE.iter().find(|e| e.name == name).map(|e| e.doc)
548}
549
550/// The description of the built-in **type** `name` denotes, or `None` for any
551/// other name.
552///
553/// Type position is a different question from value position and gets a
554/// different answer: `Int` is a type and never a value, `Range` is a type whose
555/// name no value shares, and the nine collection names are both — so this looks
556/// in [`BUILTIN_TYPES`] first and falls back to [`PRELUDE`], where `Vec`'s row
557/// already opens with what a `Vec` *is* before it says what `Vec()` builds.
558///
559/// The fallback is what keeps `Vec[Int]` from needing a second description of a
560/// `Vec` that could drift from the first.
561#[must_use]
562pub fn type_doc(name: &str) -> Option<&'static str> {
563    BUILTIN_TYPES
564        .iter()
565        .find(|e| e.name == name)
566        .map(|e| e.doc)
567        .or_else(|| prelude_doc(name))
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573    use crate::abi::{AbiKind, AbiRet};
574    use std::collections::HashSet;
575
576    #[test]
577    fn prelude_is_non_empty() {
578        assert!(!PRELUDE.is_empty());
579    }
580
581    #[test]
582    fn prelude_names_are_unique() {
583        let mut seen = HashSet::new();
584        for e in PRELUDE {
585            assert!(seen.insert(e.name), "duplicate prelude name {}", e.name);
586        }
587    }
588
589    /// **Every name the editor can offer carries a sentence.** Both tables are
590    /// read by hover, completion and signature help, and a row added later with
591    /// an empty `doc` would surface as a name with a blank description rather
592    /// than as a build failure.
593    ///
594    /// The length floor is the interesting half: `""` is the mistake a
595    /// copy-pasted row makes, and `"."` is the one a placeholder makes.
596    #[test]
597    fn every_documented_name_has_documentation() {
598        for e in PRELUDE {
599            assert!(
600                e.doc.len() > 10,
601                "prelude entry `{}` has no real documentation",
602                e.name
603            );
604            assert!(
605                e.doc.ends_with('.'),
606                "prelude entry `{}`'s doc is not a sentence",
607                e.name
608            );
609        }
610        for e in BUILTIN_TYPES {
611            assert!(
612                e.doc.len() > 10,
613                "type entry `{}` has no real documentation",
614                e.name
615            );
616            assert!(
617                e.doc.ends_with('.'),
618                "type entry `{}`'s doc is not a sentence",
619                e.name
620            );
621        }
622    }
623
624    /// The two lookups answer for every row and for nothing else. `prelude_doc`
625    /// is asked about *values* and `type_doc` about *types*, and the pair that
626    /// keeps them honest is `Int` (a type, never a value) and `out` (a value,
627    /// never a type).
628    #[test]
629    fn the_lookups_answer_for_exactly_their_own_names() {
630        for e in PRELUDE {
631            assert_eq!(prelude_doc(e.name), Some(e.doc), "{}", e.name);
632        }
633        for e in BUILTIN_TYPES {
634            assert_eq!(type_doc(e.name), Some(e.doc), "{}", e.name);
635        }
636        // A collection name is both, and `type_doc` falls back to the prelude
637        // row rather than to a second description of the same type.
638        assert_eq!(type_doc("Vec"), prelude_doc("Vec"));
639        assert!(type_doc("Vec").is_some());
640        // `Int` is a type and not a value; `out` is a value and not a type.
641        assert!(prelude_doc("Int").is_none());
642        assert!(type_doc("Int").is_some());
643        assert!(type_doc("out").is_none() || prelude_doc("out").is_some());
644        // Neither answers for a name the language does not have.
645        assert!(prelude_doc("nope").is_none());
646        assert!(type_doc("nope").is_none());
647        // §4.2 reserves these and neither is implemented, so neither may
648        // acquire a doc string without also acquiring a type.
649        assert!(type_doc("UInt").is_none());
650        assert!(type_doc("Byte").is_none());
651    }
652
653    /// **Every name legal in type position has a description**, which is the
654    /// property that makes "hover over an annotation says something" true by
655    /// construction rather than by a list somebody kept up to date.
656    ///
657    /// The set is `praxis-hir`'s `is_type_ctor_name` — `Option` or a collection
658    /// — plus the seeded scalars. `Seq` is excluded because it is
659    /// compiler-internal and no source name reaches it (§6.3).
660    #[test]
661    fn every_type_position_name_is_documented() {
662        for ctor in [
663            crate::CollectionCtor::Vec,
664            crate::CollectionCtor::Deque,
665            crate::CollectionCtor::Map,
666            crate::CollectionCtor::Set,
667            crate::CollectionCtor::Counter,
668            crate::CollectionCtor::MinHeap,
669            crate::CollectionCtor::MaxHeap,
670            crate::CollectionCtor::BitSet,
671            crate::CollectionCtor::Grid,
672            crate::CollectionCtor::Range,
673        ] {
674            let name = ctor.name();
675            assert!(
676                type_doc(name).is_some(),
677                "the type name `{name}` has no description"
678            );
679        }
680        assert!(type_doc("Option").is_some());
681    }
682
683    /// A **type constructor is not a scope symbol**, and `Range` is the row
684    /// that makes the distinction load-bearing: binding it would make `Range()`
685    /// resolve to a name instead of being the `N001` the book prints.
686    #[test]
687    fn only_a_type_that_has_no_value_is_left_unseeded() {
688        let unseeded: Vec<&str> = BUILTIN_TYPES
689            .iter()
690            .filter(|e| !e.seeded)
691            .map(|e| e.name)
692            .collect();
693        assert_eq!(unseeded, vec!["Range"]);
694        // The seeded names are exactly §4.2's scalars plus `Never` — the set
695        // `praxis-hir`'s `seed_type_names` binds.
696        let seeded: Vec<&str> = BUILTIN_TYPES
697            .iter()
698            .filter(|e| e.seeded)
699            .map(|e| e.name)
700            .collect();
701        assert_eq!(
702            seeded,
703            vec!["Int", "Float", "Bool", "Char", "Text", "Unit", "Never"]
704        );
705    }
706
707    #[test]
708    fn prelude_includes_design_canonical_entries() {
709        let names: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
710        for required in ["out", "dbg", "panic", "abs", "Vec", "Map", "bfs_distance"] {
711            assert!(
712                names.contains(required),
713                "missing prelude entry {required:?}"
714            );
715        }
716    }
717
718    /// Every numeric helper is a prelude name, and every name the design's
719    /// numeric line lists is a numeric helper. The two lists are written
720    /// separately — one by category for the LSP, one by lowering — and a name in
721    /// only one of them is either a phantom (it resolves, then has nowhere to
722    /// go) or an unreachable wrapper.
723    #[test]
724    fn every_numeric_helper_is_a_prelude_name() {
725        let prelude: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
726        for h in NUMERIC_HELPERS {
727            assert!(prelude.contains(h.name), "{} is not in PRELUDE", h.name);
728        }
729        // §16.1's numeric line, verbatim. `pi`/`e` are in `PRELUDE` too but are
730        // nullary `Float` functions with their own dispatch, not `Int` ones.
731        for required in ["abs", "sign", "min", "max", "clamp", "gcd", "lcm"] {
732            assert!(
733                numeric_helper(required).is_some(),
734                "§16.1 lists {required:?} and it has no helper row"
735            );
736        }
737        assert!(numeric_helper("pi").is_none());
738        assert!(numeric_helper("out").is_none());
739        assert!(numeric_helper("bfs").is_none());
740    }
741
742    /// A wrapper takes the context, then `leading_ptrs` raw pointer slots, then
743    /// only `Gc` operands, and returns one. That uniform shape is what lets the
744    /// MIR lowering be one path per table rather than a branch per helper, and
745    /// it is the same property for the numeric helpers, the graph helpers and
746    /// the sized constructors — the last of which spend their one leading
747    /// pointer on the static element descriptor.
748    fn assert_uniform_gc_wrapper(sym: RuntimeSymbol, name: &str, leading_ptrs: usize) {
749        let sig = sym.sig();
750        assert_eq!(sig.params[0], AbiKind::Ctx, "{name}");
751        let boxed_from = 1 + leading_ptrs;
752        assert!(
753            sig.params[1..boxed_from].iter().all(|k| *k == AbiKind::Ptr),
754            "{name}'s first {leading_ptrs} operand(s) after the context are not raw pointers"
755        );
756        assert!(
757            sig.params[boxed_from..].iter().all(|k| *k == AbiKind::Gc),
758            "{name} takes a non-Gc operand"
759        );
760        assert_eq!(sig.ret, AbiRet::Gc, "{name}");
761    }
762
763    /// A helper's source arity is its wrapper's arity, because the row does not
764    /// state one. This is the property the row's shape buys: `min(a)` cannot
765    /// typecheck against a two-operand wrapper, and `clamp(v, lo)` cannot
766    /// either, without anyone maintaining a second number.
767    #[test]
768    fn a_helpers_arity_is_the_wrappers_arity() {
769        assert_eq!(numeric_helper("abs").unwrap().arity(), 1);
770        assert_eq!(numeric_helper("sign").unwrap().arity(), 1);
771        assert_eq!(numeric_helper("min").unwrap().arity(), 2);
772        assert_eq!(numeric_helper("max").unwrap().arity(), 2);
773        assert_eq!(numeric_helper("gcd").unwrap().arity(), 2);
774        assert_eq!(numeric_helper("lcm").unwrap().arity(), 2);
775        assert_eq!(numeric_helper("clamp").unwrap().arity(), 3);
776        // Every helper's wrapper takes only `Gc` operands after the context and
777        // returns one — the uniform shape the MIR lowering relies on to be one
778        // path rather than seven.
779        for h in NUMERIC_HELPERS {
780            assert_uniform_gc_wrapper(h.symbol, h.name, 0);
781        }
782    }
783
784    /// No two helpers share a wrapper. A copy-pasted row that named an
785    /// already-used symbol would make one of the two names compute the other's
786    /// answer, and nothing else would notice.
787    #[test]
788    fn each_helper_has_its_own_wrapper() {
789        let mut seen = HashSet::new();
790        for h in NUMERIC_HELPERS {
791            assert!(seen.insert(h.symbol), "{} reuses a wrapper", h.name);
792        }
793        let mut seen = HashSet::new();
794        for h in GRAPH_HELPERS {
795            assert!(seen.insert(h.symbol), "{} reuses a wrapper", h.name);
796        }
797        let mut seen = HashSet::new();
798        for c in SIZED_CTORS {
799            assert!(seen.insert(c.symbol), "{} reuses a wrapper", c.name);
800        }
801    }
802
803    /// A sized constructor is a prelude name, for the reason the numeric and
804    /// graph helpers are: a row naming a name nothing declares is a wrapper no
805    /// program can reach.
806    #[test]
807    fn every_sized_ctor_is_a_prelude_name() {
808        let prelude: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
809        for c in SIZED_CTORS {
810            assert!(prelude.contains(c.name), "{} is not in PRELUDE", c.name);
811        }
812    }
813
814    /// The row's `extents` count and its wrapper's arity are one number,
815    /// checked against each other. The wrapper takes a context, an element
816    /// descriptor, every extent, and the fill — so the source arity plus the
817    /// descriptor slot is the wrapper's arity, and a row that grew an extent
818    /// without growing its wrapper would otherwise pass garbage in an unfilled
819    /// slot rather than failing here.
820    #[test]
821    fn a_sized_ctors_arity_is_the_wrappers_arity() {
822        assert_eq!(sized_ctor("Vec").expect("Vec is sized").arity(), 2);
823        assert_eq!(sized_ctor("Grid").expect("Grid is sized").arity(), 3);
824        for c in SIZED_CTORS {
825            assert_eq!(
826                c.arity() + 1,
827                c.symbol.arity(),
828                "{}'s row and its wrapper disagree about how many operands it takes",
829                c.name
830            );
831            // The one leading pointer is the static element descriptor; the
832            // extents and the fill after it are boxed (ADR-146 decision 7).
833            assert_uniform_gc_wrapper(c.symbol, c.name, 1);
834        }
835    }
836
837    /// **Only `Vec` and `Grid` are sized**, and this is the test that keeps
838    /// ADR-089 decision 1 intact everywhere else. ADR-146 is a carve-out of
839    /// exactly two names; a third row added without reopening that decision
840    /// fails here.
841    #[test]
842    fn only_vec_and_grid_have_a_sized_form() {
843        assert_eq!(SIZED_CTORS.len(), 2);
844        for absent in [
845            "Deque", "Map", "Set", "Counter", "MinHeap", "MaxHeap", "BitSet", "Range", "Option",
846            "out", "abs", "bfs",
847        ] {
848            assert!(
849                sized_ctor(absent).is_none(),
850                "{absent} has no sized form and ADR-146 says why"
851            );
852        }
853    }
854
855    /// Every graph helper is a prelude name, and every name §6.5 lists is a
856    /// graph helper. Same property as the numeric line's, for the same reason: a
857    /// name in only one list is either a phantom (it resolves, then has nowhere
858    /// to go) or an unreachable wrapper.
859    #[test]
860    fn every_graph_helper_is_a_prelude_name() {
861        let prelude: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
862        for h in GRAPH_HELPERS {
863            assert!(prelude.contains(h.name), "{} is not in PRELUDE", h.name);
864        }
865        // §6.5's six algorithms, verbatim. (Its list also names connected
866        // components and topological sort; neither is a `PRELUDE` name, so
867        // neither is a phantom.)
868        for required in [
869            "bfs",
870            "bfs_distance",
871            "dfs",
872            "dijkstra",
873            "a_star",
874            "flood_fill",
875        ] {
876            assert!(
877                graph_helper(required).is_some(),
878                "§6.5 lists {required:?} and it has no helper row"
879            );
880        }
881        assert!(graph_helper("abs").is_none());
882        assert!(graph_helper("out").is_none());
883    }
884
885    /// A helper's source arity is its wrapper's arity. `params` states the
886    /// *shape* of each argument because six helpers have five different
887    /// signatures and the manifest cannot say which is which — but the count
888    /// still has one authority, so a row that grew a parameter without growing
889    /// its wrapper is a failure here rather than a call that passes garbage in
890    /// the slot nobody filled.
891    #[test]
892    fn a_graph_helpers_arity_is_the_wrappers_arity() {
893        for h in GRAPH_HELPERS {
894            assert_eq!(
895                h.arity(),
896                h.symbol.arity(),
897                "{}'s signature and its wrapper disagree on arity",
898                h.name
899            );
900            // Every wrapper takes only `Gc` operands after the context and
901            // returns one — the uniform shape that makes the MIR lowering one
902            // path for all six rather than six branches.
903            assert_uniform_gc_wrapper(h.symbol, h.name, 0);
904        }
905    }
906
907    /// Every helper starts from a state and every other parameter is a function
908    /// of it. That is §6.5's shape — "closure-based algorithms that do not
909    /// require materializing a graph object" — and it is what lets one runtime
910    /// calling convention serve all six: the first operand is a value, the rest
911    /// are closures.
912    #[test]
913    fn a_graph_helper_takes_a_start_state_and_then_only_functions() {
914        for h in GRAPH_HELPERS {
915            assert_eq!(
916                h.params.first(),
917                Some(&GraphParam::Start),
918                "{} does not start from a state",
919                h.name
920            );
921            assert!(
922                h.params[1..].iter().all(|p| *p != GraphParam::Start),
923                "{} takes a second bare state",
924                h.name
925            );
926            assert!(
927                h.params.contains(&GraphParam::Neighbours),
928                "{} has no way to reach a second state",
929                h.name
930            );
931        }
932    }
933
934    /// A search that can fail to find anything answers with an `Option`, and a
935    /// walk that always reaches at least its own start does not. The pairing is
936    /// the rule: `dijkstra` needs no `Option` because an unreachable state is
937    /// *absent* from its table, and `bfs`/`dfs`/`flood_fill` always contain the
938    /// start.
939    #[test]
940    fn only_a_goal_directed_helper_can_answer_with_nothing() {
941        for h in GRAPH_HELPERS {
942            let goal_directed = h.params.contains(&GraphParam::Goal);
943            let optional = h.result == GraphResult::Distance;
944            assert_eq!(
945                goal_directed, optional,
946                "{} looks for a goal but cannot say it found none (or vice versa)",
947                h.name
948            );
949        }
950    }
951}