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    //
135    // The names spell out what a helper answers: **the bare name is the whole
136    // walk, `_distance` is the number of the cheapest route to a goal, and
137    // `_path` is the route.** So a reader who knows one family knows all four,
138    // and a family that has no whole-walk meaning has no bare name — which is
139    // why A\* is only `a_star_distance` and `a_star_path`. `flood_fill` is the
140    // unordered twin of `bfs` and takes no goal at all: a `Set` has no route.
141    PreludeEntry::new(
142        "bfs",
143        "Breadth-first walk: `bfs(start, |s| neighbors(s))` answers every state reached, in the order it was reached.",
144    ),
145    PreludeEntry::new(
146        "bfs_distance",
147        "Steps to the first state a predicate accepts, or `None` when no goal is reachable: `bfs_distance(start, |s| neighbors(s), |s| s == goal)`.",
148    ),
149    PreludeEntry::new(
150        "bfs_path",
151        "A shortest route to the first state a predicate accepts, start to goal inclusive, or `None`: `bfs_path(start, |s| neighbors(s), |s| s == goal)`.",
152    ),
153    PreludeEntry::new(
154        "dfs",
155        "Depth-first walk: `dfs(start, |s| neighbors(s))` answers every state reached, in the order it was reached.",
156    ),
157    PreludeEntry::new(
158        "dfs_distance",
159        "Steps along the route depth-first search reaches a goal by, which need not be the fewest, or `None`: `dfs_distance(start, |s| neighbors(s), |s| s == goal)`.",
160    ),
161    PreludeEntry::new(
162        "dfs_path",
163        "The route depth-first search reaches a goal by, which need not be a shortest one, or `None`: `dfs_path(start, |s| neighbors(s), |s| s == goal)`.",
164    ),
165    PreludeEntry::new(
166        "dijkstra",
167        "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`.",
168    ),
169    PreludeEntry::new(
170        "dijkstra_distance",
171        "Cost of the cheapest route to a goal, or `None`: `dijkstra_distance(start, |s| neighbors(s), |a, b| weight(a, b), |s| s == goal)`.",
172    ),
173    PreludeEntry::new(
174        "dijkstra_path",
175        "The cheapest route to a goal, start to goal inclusive, or `None`: `dijkstra_path(start, |s| neighbors(s), |a, b| weight(a, b), |s| s == goal)`.",
176    ),
177    PreludeEntry::new(
178        "a_star_distance",
179        "Cost of the cheapest route to a goal, or `None`: `a_star_distance(start, neighbors, weight, heuristic, goal)`, where the heuristic estimates the remaining cost from one state.",
180    ),
181    PreludeEntry::new(
182        "a_star_path",
183        "The cheapest route to a goal, start to goal inclusive, or `None`: `a_star_path(start, neighbors, weight, heuristic, goal)`, where the heuristic estimates the remaining cost from one state.",
184    ),
185    PreludeEntry::new(
186        "flood_fill",
187        "Every state reachable from a start state, unordered, as a `Set`: `flood_fill(start, |s| neighbors(s))`.",
188    ),
189];
190
191/// The built-in **type** names (§4.2's six scalars, `Never`, and `Range`), with
192/// the one-line description the editor shows for each.
193///
194/// This is the table `praxis-hir`'s name resolution seeds the root scope from,
195/// so a type name the checker accepts and a type name the editor can describe
196/// are the same list.
197///
198/// `UInt` and `Byte` are deliberately absent: §4.2 reserves them and neither is
199/// implemented, so either one in an annotation is an `N002` rather than a name
200/// with a doc string.
201pub const BUILTIN_TYPES: &[TypeEntry] = &[
202    TypeEntry::seeded("Int", "Signed 64-bit integer. Written `42` or `1_000_000`."),
203    TypeEntry::seeded(
204        "Float",
205        "IEEE-754 binary64. Written `3.5`, `1e10` or `2e-3`; `.5` is not a literal.",
206    ),
207    TypeEntry::seeded("Bool", "`true` or `false`."),
208    TypeEntry::seeded("Char", "One Unicode scalar value. Written `'p'`."),
209    TypeEntry::seeded("Text", "Immutable UTF-8 text. Written `\"praxis\"`."),
210    TypeEntry::seeded("Unit", "The type with one value, written `()`."),
211    TypeEntry::seeded(
212        "Never",
213        "The type of an expression that produces no value — `panic(...)`, `return`, `break`. It has no values, so it unifies with anything.",
214    ),
215    // **Not seeded**, and that is the whole of what `TypeEntry::seeded` and
216    // [`TypeEntry::ctor`] distinguish. `Range` is the one built-in type with no
217    // value of the same name: a range is written `0..n`, and `Range()` is
218    // `N001: 'Range' is not defined`. Binding it in the root scope would make
219    // that call resolve and then fail later, somewhere with less to say.
220    TypeEntry::ctor(
221        "Range",
222        "A half-open (`0..n`) or inclusive (`0..=n`) integer range. A type name only — there is no `Range()` constructor.",
223    ),
224];
225
226/// §6.5's graph helpers: the closure-driven algorithms that walk a graph the
227/// program never materializes (ADR-060).
228///
229/// A row is the source name, the wrapper it lowers to, the **shape** of each
230/// parameter and the shape of the result. The shapes are what inference reads
231/// to build the scheme, so a helper's signature and the wrapper it becomes are
232/// written down once; the arity that follows from `params` is checked against
233/// [`RuntimeSymbol::arity`] by a unit test, which is as close to ADR-058's
234/// "the arity is the wrapper's arity" as a family with this many shapes can
235/// get.
236///
237/// The naming is a rule and not a habit: **the bare name is the whole walk,
238/// `_distance` is the number of the cheapest route to a goal, and `_path` is
239/// the route.** So every goal-directed search comes in both forms, over the
240/// same parameters, and a name tells a reader which of the three questions it
241/// answers before they read the row.
242///
243/// Every helper's first parameter is the start state and every other one is a
244/// function of it, which is §6.5's own spelling:
245///
246/// ```text
247/// var distance = bfs_distance(start, |s| neighbors(s), |s| s == goal)
248/// ```
249///
250/// A name in [`PRELUDE`] with no row here resolves and then has no scheme, no
251/// lowering and no implementation: inference hands out a fresh variable that
252/// unifies with anything and the call lowers as a direct call to a function
253/// nobody defined.
254pub const GRAPH_HELPERS: &[GraphHelper] = &[
255    GraphHelper::new(
256        "bfs",
257        RuntimeSymbol::Bfs,
258        &[GraphParam::Start, GraphParam::Neighbours],
259        GraphResult::VisitOrder,
260    ),
261    GraphHelper::new(
262        "bfs_distance",
263        RuntimeSymbol::BfsDistance,
264        &[GraphParam::Start, GraphParam::Neighbours, GraphParam::Goal],
265        GraphResult::Distance,
266    ),
267    GraphHelper::new(
268        "bfs_path",
269        RuntimeSymbol::BfsPath,
270        &[GraphParam::Start, GraphParam::Neighbours, GraphParam::Goal],
271        GraphResult::Path,
272    ),
273    GraphHelper::new(
274        "dfs",
275        RuntimeSymbol::Dfs,
276        &[GraphParam::Start, GraphParam::Neighbours],
277        GraphResult::VisitOrder,
278    ),
279    GraphHelper::new(
280        "dfs_distance",
281        RuntimeSymbol::DfsDistance,
282        &[GraphParam::Start, GraphParam::Neighbours, GraphParam::Goal],
283        GraphResult::Distance,
284    ),
285    GraphHelper::new(
286        "dfs_path",
287        RuntimeSymbol::DfsPath,
288        &[GraphParam::Start, GraphParam::Neighbours, GraphParam::Goal],
289        GraphResult::Path,
290    ),
291    GraphHelper::new(
292        "dijkstra",
293        RuntimeSymbol::Dijkstra,
294        &[
295            GraphParam::Start,
296            GraphParam::Neighbours,
297            GraphParam::Weight,
298        ],
299        GraphResult::CostTable,
300    ),
301    GraphHelper::new(
302        "dijkstra_distance",
303        RuntimeSymbol::DijkstraDistance,
304        &[
305            GraphParam::Start,
306            GraphParam::Neighbours,
307            GraphParam::Weight,
308            GraphParam::Goal,
309        ],
310        GraphResult::Distance,
311    ),
312    GraphHelper::new(
313        "dijkstra_path",
314        RuntimeSymbol::DijkstraPath,
315        &[
316            GraphParam::Start,
317            GraphParam::Neighbours,
318            GraphParam::Weight,
319            GraphParam::Goal,
320        ],
321        GraphResult::Path,
322    ),
323    GraphHelper::new(
324        "a_star_distance",
325        RuntimeSymbol::AStarDistance,
326        &[
327            GraphParam::Start,
328            GraphParam::Neighbours,
329            GraphParam::Weight,
330            GraphParam::Heuristic,
331            GraphParam::Goal,
332        ],
333        GraphResult::Distance,
334    ),
335    GraphHelper::new(
336        "a_star_path",
337        RuntimeSymbol::AStarPath,
338        &[
339            GraphParam::Start,
340            GraphParam::Neighbours,
341            GraphParam::Weight,
342            GraphParam::Heuristic,
343            GraphParam::Goal,
344        ],
345        GraphResult::Path,
346    ),
347    GraphHelper::new(
348        "flood_fill",
349        RuntimeSymbol::FloodFill,
350        &[GraphParam::Start, GraphParam::Neighbours],
351        GraphResult::Reached,
352    ),
353];
354
355/// One parameter of a graph helper, as a shape rather than as a type.
356///
357/// `praxis-stdlib` cannot name a `Type` — that is `praxis-typeck`, which depends
358/// on this crate — so the signature is written as the shapes inference then
359/// builds the types from. The match on this in `seed_builtin_schemes` is
360/// exhaustive, so a new shape is a compile error there rather than a parameter
361/// that silently gets the wrong type.
362#[derive(Clone, Copy, PartialEq, Eq, Debug)]
363pub enum GraphParam {
364    /// The state the walk starts from: a `T`.
365    Start,
366    /// `(T) -> Vec[T]` — the states reachable in one step from a given one.
367    /// Returning a `Vec` rather than a lazy sequence is what makes the helper
368    /// callable from the runtime, which has no way to drive a pipeline.
369    Neighbours,
370    /// `(T, T) -> Int` — the cost of the edge between two adjacent states.
371    Weight,
372    /// `(T) -> Int` — the estimated remaining cost from a state to a goal.
373    Heuristic,
374    /// `(T) -> Bool` — whether a state is a goal. A predicate rather than a
375    /// goal *value* so a search can stop on a property (`|s| s.0 == n`), which
376    /// is what §6.5's own example writes.
377    Goal,
378}
379
380/// What a graph helper answers.
381#[derive(Clone, Copy, PartialEq, Eq, Debug)]
382pub enum GraphResult {
383    /// `Vec[T]` — every state reached, in the order the walk reached it.
384    VisitOrder,
385    /// `Set[T]` — every state reached, without an order.
386    Reached,
387    /// `Map[T, Int]` — the least cost from the start to each reachable state.
388    /// A state that is not reachable is simply absent, which is why this needs
389    /// no `Option`.
390    CostTable,
391    /// `Option[Int]` — the cost of the cheapest path to a goal, or `None` when
392    /// no goal is reachable. "Unreachable" is an ordinary outcome of a search,
393    /// not a fault, and a sentinel `-1` would be a number nobody wrote.
394    Distance,
395    /// `Option[Vec[T]]` — the route to the goal a search stopped at, from the
396    /// start to the goal inclusive, or `None` when no goal is reachable.
397    ///
398    /// The `Option` is [`Distance`](Self::Distance)'s, for the same reason: a
399    /// helper that takes a goal may not find one. An empty `Vec` could not
400    /// stand for that, because a route that *was* found always holds at least
401    /// its own start — so "no route" and "a route of nothing" would be the same
402    /// value.
403    Path,
404}
405
406/// One graph prelude helper: the source name, the wrapper it lowers to, and the
407/// shape of its signature.
408#[derive(Clone, Copy, Debug)]
409pub struct GraphHelper {
410    pub name: &'static str,
411    pub symbol: RuntimeSymbol,
412    pub params: &'static [GraphParam],
413    pub result: GraphResult,
414}
415
416impl GraphHelper {
417    const fn new(
418        name: &'static str,
419        symbol: RuntimeSymbol,
420        params: &'static [GraphParam],
421        result: GraphResult,
422    ) -> GraphHelper {
423        GraphHelper {
424            name,
425            symbol,
426            params,
427            result,
428        }
429    }
430
431    /// How many arguments the source-level function takes.
432    #[inline]
433    pub const fn arity(&self) -> usize {
434        self.params.len()
435    }
436}
437
438/// The graph helper `name` denotes, or `None` for any other name.
439///
440/// The one lookup from a source name to a graph helper. Both consumers use it —
441/// inference for the scheme, MIR for the call target — so neither carries its
442/// own list.
443pub fn graph_helper(name: &str) -> Option<GraphHelper> {
444    GRAPH_HELPERS.iter().copied().find(|h| h.name == name)
445}
446
447/// The §16.1 numeric prelude helpers: the free functions that are neither
448/// output/control names nor collection constructors.
449///
450/// Every one of them is monomorphic on `Int` (ADR-058), so a row needs only the
451/// name and the wrapper it lowers to — the **arity** the type checker gives the
452/// name is [`RuntimeSymbol::arity`], read off the ABI manifest rather than
453/// restated here. That is what makes "a prelude name whose signature disagrees
454/// with the wrapper it calls" unrepresentable.
455///
456/// `Float`'s counterparts are *methods* (`0.5.abs()`, `x.min(y)` — §4.12), not
457/// entries here: a genuinely polymorphic `abs` would have to carry a numeric
458/// capability on its own binder and pick a lowering per instantiation, and
459/// nothing needs that yet.
460///
461/// `pi` and `e` are not here either. They are nullary `Float` functions rather
462/// than `Int` ones, with their own schemes and dispatch.
463pub const NUMERIC_HELPERS: &[NumericHelper] = &[
464    NumericHelper::new("abs", RuntimeSymbol::IntAbs),
465    NumericHelper::new("sign", RuntimeSymbol::IntSign),
466    NumericHelper::new("min", RuntimeSymbol::IntMin),
467    NumericHelper::new("max", RuntimeSymbol::IntMax),
468    NumericHelper::new("clamp", RuntimeSymbol::IntClamp),
469    NumericHelper::new("gcd", RuntimeSymbol::IntGcd),
470    NumericHelper::new("lcm", RuntimeSymbol::IntLcm),
471];
472
473/// One numeric prelude helper: the source name and the wrapper it lowers to.
474#[derive(Clone, Copy, Debug)]
475pub struct NumericHelper {
476    pub name: &'static str,
477    pub symbol: RuntimeSymbol,
478}
479
480impl NumericHelper {
481    const fn new(name: &'static str, symbol: RuntimeSymbol) -> NumericHelper {
482        NumericHelper { name, symbol }
483    }
484
485    /// How many `Int` parameters the source-level function takes, which is the
486    /// wrapper's arity: every helper takes `Int`s and returns one, so the two
487    /// counts are the same number and only the manifest states it.
488    #[inline]
489    pub const fn arity(&self) -> usize {
490        self.symbol.arity()
491    }
492}
493
494/// The numeric helper `name` denotes, or `None` for any other name.
495///
496/// The one lookup from a source name to a numeric helper. Both consumers use
497/// it — inference for the scheme's arity, MIR for the call target — so neither
498/// carries its own list.
499pub fn numeric_helper(name: &str) -> Option<NumericHelper> {
500    NUMERIC_HELPERS.iter().copied().find(|h| h.name == name)
501}
502
503/// The collection constructors that also have a **sized** form, in which the
504/// argument count selects the shape: `Vec(n, fill)` and `Grid(w, h, fill)`
505/// beside the nullary `Vec()` and `Grid()` (ADR-146).
506///
507/// This is the whole of the carve-out ADR-146 makes to
508/// [ADR-089](https://github.com/tljubej/praxis/blob/main/docs/decisions/089-a-name-has-one-signature.md) decision
509/// 1's "a name has exactly one signature", and it is a `const` table rather
510/// than a rule anywhere so that the narrowness is a fact a reader can count.
511/// The other seven constructors are absent on purpose: a sized `Set` is `n`
512/// copies of one element in a set, which is one element, and a sized `Map` has
513/// no answer for what its keys would be. `Vec` and `Grid` are the two whose
514/// contents are addressed by position, which is what makes "n of them" mean
515/// something.
516pub const SIZED_CTORS: &[SizedCtor] = &[
517    SizedCtor::new("Vec", RuntimeSymbol::VecFilled, 1),
518    SizedCtor::new("Grid", RuntimeSymbol::GridFilled, 2),
519];
520
521/// One sized collection constructor: the source name, the wrapper its sized
522/// form lowers to, and how many of its leading arguments are extents.
523///
524/// The fill is always the last argument and always exactly one, so `extents`
525/// determines the source arity — which is why nothing here states an arity
526/// twice.
527#[derive(Clone, Copy, Debug)]
528pub struct SizedCtor {
529    pub name: &'static str,
530    pub symbol: RuntimeSymbol,
531    /// The number of leading `Int` parameters: 1 for `Vec(n, fill)`, 2 for
532    /// `Grid(w, h, fill)`.
533    pub extents: usize,
534}
535
536impl SizedCtor {
537    const fn new(name: &'static str, symbol: RuntimeSymbol, extents: usize) -> SizedCtor {
538        SizedCtor {
539            name,
540            symbol,
541            extents,
542        }
543    }
544
545    /// The source-level arity: the extents plus the one fill.
546    #[inline]
547    #[must_use]
548    pub const fn arity(&self) -> usize {
549        self.extents + 1
550    }
551}
552
553/// The sized constructor `name` denotes, or `None` for any other name —
554/// including the seven collection constructors that have no sized form.
555///
556/// The one lookup from a source name to a sized constructor. Inference reads it
557/// to build the call site's type and MIR reads it to pick the wrapper, so
558/// neither carries its own list and neither can disagree with the other about
559/// which names are sized.
560pub fn sized_ctor(name: &str) -> Option<SizedCtor> {
561    SIZED_CTORS.iter().copied().find(|c| c.name == name)
562}
563
564/// One prelude symbol: its name and a one-line description.
565#[derive(Clone, Copy, Debug)]
566pub struct PreludeEntry {
567    pub name: &'static str,
568    pub doc: &'static str,
569    /// Whether this name is an **enum variant constructor** rather than an
570    /// ordinary value. `Some` and `None` are `Option`'s two variants, declared
571    /// by the prelude rather than by an `enum` item — and a consumer that has
572    /// to tell a constructor from a binding cannot do it from the type
573    /// (`var A = None` has `Option`'s type too), so the declaration says.
574    pub is_variant_ctor: bool,
575}
576
577impl PreludeEntry {
578    pub const fn new(name: &'static str, doc: &'static str) -> PreludeEntry {
579        PreludeEntry {
580            name,
581            doc,
582            is_variant_ctor: false,
583        }
584    }
585
586    /// An entry that constructs an enum variant.
587    pub const fn variant(name: &'static str, doc: &'static str) -> PreludeEntry {
588        PreludeEntry {
589            name,
590            doc,
591            is_variant_ctor: true,
592        }
593    }
594}
595
596/// One built-in type name and a one-line description.
597#[derive(Clone, Copy, Debug)]
598pub struct TypeEntry {
599    pub name: &'static str,
600    pub doc: &'static str,
601    /// Whether name resolution binds this name in the root scope.
602    ///
603    /// The scalars and `Never` are bound, which is how `var n: Nope` becomes an
604    /// `N002` from a failed lookup. A **type constructor** is not: `Range`, and
605    /// the collection names in [`PRELUDE`], are compiler-owned type names that
606    /// annotation checking accepts without a lookup and inference turns into
607    /// types. The distinction is not cosmetic — a bound name is also a *value*
608    /// name, and `Range` has no value.
609    pub seeded: bool,
610}
611
612impl TypeEntry {
613    /// A type name bound in the root scope.
614    const fn seeded(name: &'static str, doc: &'static str) -> TypeEntry {
615        TypeEntry {
616            name,
617            doc,
618            seeded: true,
619        }
620    }
621
622    /// A compiler-owned type constructor, which is not bound in any scope. See
623    /// [`TypeEntry::seeded`](Self::seeded)'s field documentation.
624    const fn ctor(name: &'static str, doc: &'static str) -> TypeEntry {
625        TypeEntry {
626            name,
627            doc,
628            seeded: false,
629        }
630    }
631}
632
633/// The description of the prelude **value** `name` denotes, or `None` for any
634/// other name.
635///
636/// The one lookup from a source name to its documentation, so the language
637/// server describes the prelude the compiler actually declares. A server that
638/// carried its own sentence about `bfs` would be free to describe a helper this
639/// table no longer has.
640///
641/// Callers must satisfy themselves that the name really is the prelude's: a
642/// `var out = 1` shadows it, and this function only knows the spelling.
643#[must_use]
644pub fn prelude_doc(name: &str) -> Option<&'static str> {
645    PRELUDE.iter().find(|e| e.name == name).map(|e| e.doc)
646}
647
648/// The description of the built-in **type** `name` denotes, or `None` for any
649/// other name.
650///
651/// Type position is a different question from value position and gets a
652/// different answer: `Int` is a type and never a value, `Range` is a type whose
653/// name no value shares, and the nine collection names are both — so this looks
654/// in [`BUILTIN_TYPES`] first and falls back to [`PRELUDE`], where `Vec`'s row
655/// already opens with what a `Vec` *is* before it says what `Vec()` builds.
656///
657/// The fallback is what keeps `Vec[Int]` from needing a second description of a
658/// `Vec` that could drift from the first.
659#[must_use]
660pub fn type_doc(name: &str) -> Option<&'static str> {
661    BUILTIN_TYPES
662        .iter()
663        .find(|e| e.name == name)
664        .map(|e| e.doc)
665        .or_else(|| prelude_doc(name))
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671    use crate::abi::{AbiKind, AbiRet};
672    use std::collections::HashSet;
673
674    #[test]
675    fn prelude_is_non_empty() {
676        assert!(!PRELUDE.is_empty());
677    }
678
679    #[test]
680    fn prelude_names_are_unique() {
681        let mut seen = HashSet::new();
682        for e in PRELUDE {
683            assert!(seen.insert(e.name), "duplicate prelude name {}", e.name);
684        }
685    }
686
687    /// **Every name the editor can offer carries a sentence.** Both tables are
688    /// read by hover, completion and signature help, and a row added later with
689    /// an empty `doc` would surface as a name with a blank description rather
690    /// than as a build failure.
691    ///
692    /// The length floor is the interesting half: `""` is the mistake a
693    /// copy-pasted row makes, and `"."` is the one a placeholder makes.
694    #[test]
695    fn every_documented_name_has_documentation() {
696        for e in PRELUDE {
697            assert!(
698                e.doc.len() > 10,
699                "prelude entry `{}` has no real documentation",
700                e.name
701            );
702            assert!(
703                e.doc.ends_with('.'),
704                "prelude entry `{}`'s doc is not a sentence",
705                e.name
706            );
707        }
708        for e in BUILTIN_TYPES {
709            assert!(
710                e.doc.len() > 10,
711                "type entry `{}` has no real documentation",
712                e.name
713            );
714            assert!(
715                e.doc.ends_with('.'),
716                "type entry `{}`'s doc is not a sentence",
717                e.name
718            );
719        }
720    }
721
722    /// The two lookups answer for every row and for nothing else. `prelude_doc`
723    /// is asked about *values* and `type_doc` about *types*, and the pair that
724    /// keeps them honest is `Int` (a type, never a value) and `out` (a value,
725    /// never a type).
726    #[test]
727    fn the_lookups_answer_for_exactly_their_own_names() {
728        for e in PRELUDE {
729            assert_eq!(prelude_doc(e.name), Some(e.doc), "{}", e.name);
730        }
731        for e in BUILTIN_TYPES {
732            assert_eq!(type_doc(e.name), Some(e.doc), "{}", e.name);
733        }
734        // A collection name is both, and `type_doc` falls back to the prelude
735        // row rather than to a second description of the same type.
736        assert_eq!(type_doc("Vec"), prelude_doc("Vec"));
737        assert!(type_doc("Vec").is_some());
738        // `Int` is a type and not a value; `out` is a value and not a type.
739        assert!(prelude_doc("Int").is_none());
740        assert!(type_doc("Int").is_some());
741        assert!(type_doc("out").is_none() || prelude_doc("out").is_some());
742        // Neither answers for a name the language does not have.
743        assert!(prelude_doc("nope").is_none());
744        assert!(type_doc("nope").is_none());
745        // §4.2 reserves these and neither is implemented, so neither may
746        // acquire a doc string without also acquiring a type.
747        assert!(type_doc("UInt").is_none());
748        assert!(type_doc("Byte").is_none());
749    }
750
751    /// **Every name legal in type position has a description**, which is the
752    /// property that makes "hover over an annotation says something" true by
753    /// construction rather than by a list somebody kept up to date.
754    ///
755    /// The set is `praxis-hir`'s `is_type_ctor_name` — `Option` or a collection
756    /// — plus the seeded scalars. `Seq` is excluded because it is
757    /// compiler-internal and no source name reaches it (§6.3).
758    #[test]
759    fn every_type_position_name_is_documented() {
760        for ctor in [
761            crate::CollectionCtor::Vec,
762            crate::CollectionCtor::Deque,
763            crate::CollectionCtor::Map,
764            crate::CollectionCtor::Set,
765            crate::CollectionCtor::Counter,
766            crate::CollectionCtor::MinHeap,
767            crate::CollectionCtor::MaxHeap,
768            crate::CollectionCtor::BitSet,
769            crate::CollectionCtor::Grid,
770            crate::CollectionCtor::Range,
771        ] {
772            let name = ctor.name();
773            assert!(
774                type_doc(name).is_some(),
775                "the type name `{name}` has no description"
776            );
777        }
778        assert!(type_doc("Option").is_some());
779    }
780
781    /// A **type constructor is not a scope symbol**, and `Range` is the row
782    /// that makes the distinction load-bearing: binding it would make `Range()`
783    /// resolve to a name instead of being the `N001` the book prints.
784    #[test]
785    fn only_a_type_that_has_no_value_is_left_unseeded() {
786        let unseeded: Vec<&str> = BUILTIN_TYPES
787            .iter()
788            .filter(|e| !e.seeded)
789            .map(|e| e.name)
790            .collect();
791        assert_eq!(unseeded, vec!["Range"]);
792        // The seeded names are exactly §4.2's scalars plus `Never` — the set
793        // `praxis-hir`'s `seed_type_names` binds.
794        let seeded: Vec<&str> = BUILTIN_TYPES
795            .iter()
796            .filter(|e| e.seeded)
797            .map(|e| e.name)
798            .collect();
799        assert_eq!(
800            seeded,
801            vec!["Int", "Float", "Bool", "Char", "Text", "Unit", "Never"]
802        );
803    }
804
805    #[test]
806    fn prelude_includes_design_canonical_entries() {
807        let names: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
808        for required in ["out", "dbg", "panic", "abs", "Vec", "Map", "bfs_distance"] {
809            assert!(
810                names.contains(required),
811                "missing prelude entry {required:?}"
812            );
813        }
814    }
815
816    /// Every numeric helper is a prelude name, and every name the design's
817    /// numeric line lists is a numeric helper. The two lists are written
818    /// separately — one by category for the LSP, one by lowering — and a name in
819    /// only one of them is either a phantom (it resolves, then has nowhere to
820    /// go) or an unreachable wrapper.
821    #[test]
822    fn every_numeric_helper_is_a_prelude_name() {
823        let prelude: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
824        for h in NUMERIC_HELPERS {
825            assert!(prelude.contains(h.name), "{} is not in PRELUDE", h.name);
826        }
827        // §16.1's numeric line, verbatim. `pi`/`e` are in `PRELUDE` too but are
828        // nullary `Float` functions with their own dispatch, not `Int` ones.
829        for required in ["abs", "sign", "min", "max", "clamp", "gcd", "lcm"] {
830            assert!(
831                numeric_helper(required).is_some(),
832                "§16.1 lists {required:?} and it has no helper row"
833            );
834        }
835        assert!(numeric_helper("pi").is_none());
836        assert!(numeric_helper("out").is_none());
837        assert!(numeric_helper("bfs").is_none());
838    }
839
840    /// A wrapper takes the context, then `leading_ptrs` raw pointer slots, then
841    /// only `Gc` operands, and returns one. That uniform shape is what lets the
842    /// MIR lowering be one path per table rather than a branch per helper, and
843    /// it is the same property for the numeric helpers, the graph helpers and
844    /// the sized constructors — the last of which spend their one leading
845    /// pointer on the static element descriptor.
846    fn assert_uniform_gc_wrapper(sym: RuntimeSymbol, name: &str, leading_ptrs: usize) {
847        let sig = sym.sig();
848        assert_eq!(sig.params[0], AbiKind::Ctx, "{name}");
849        let boxed_from = 1 + leading_ptrs;
850        assert!(
851            sig.params[1..boxed_from].iter().all(|k| *k == AbiKind::Ptr),
852            "{name}'s first {leading_ptrs} operand(s) after the context are not raw pointers"
853        );
854        assert!(
855            sig.params[boxed_from..].iter().all(|k| *k == AbiKind::Gc),
856            "{name} takes a non-Gc operand"
857        );
858        assert_eq!(sig.ret, AbiRet::Gc, "{name}");
859    }
860
861    /// A helper's source arity is its wrapper's arity, because the row does not
862    /// state one. This is the property the row's shape buys: `min(a)` cannot
863    /// typecheck against a two-operand wrapper, and `clamp(v, lo)` cannot
864    /// either, without anyone maintaining a second number.
865    #[test]
866    fn a_helpers_arity_is_the_wrappers_arity() {
867        assert_eq!(numeric_helper("abs").unwrap().arity(), 1);
868        assert_eq!(numeric_helper("sign").unwrap().arity(), 1);
869        assert_eq!(numeric_helper("min").unwrap().arity(), 2);
870        assert_eq!(numeric_helper("max").unwrap().arity(), 2);
871        assert_eq!(numeric_helper("gcd").unwrap().arity(), 2);
872        assert_eq!(numeric_helper("lcm").unwrap().arity(), 2);
873        assert_eq!(numeric_helper("clamp").unwrap().arity(), 3);
874        // Every helper's wrapper takes only `Gc` operands after the context and
875        // returns one — the uniform shape the MIR lowering relies on to be one
876        // path rather than seven.
877        for h in NUMERIC_HELPERS {
878            assert_uniform_gc_wrapper(h.symbol, h.name, 0);
879        }
880    }
881
882    /// No two helpers share a wrapper. A copy-pasted row that named an
883    /// already-used symbol would make one of the two names compute the other's
884    /// answer, and nothing else would notice.
885    #[test]
886    fn each_helper_has_its_own_wrapper() {
887        let mut seen = HashSet::new();
888        for h in NUMERIC_HELPERS {
889            assert!(seen.insert(h.symbol), "{} reuses a wrapper", h.name);
890        }
891        let mut seen = HashSet::new();
892        for h in GRAPH_HELPERS {
893            assert!(seen.insert(h.symbol), "{} reuses a wrapper", h.name);
894        }
895        let mut seen = HashSet::new();
896        for c in SIZED_CTORS {
897            assert!(seen.insert(c.symbol), "{} reuses a wrapper", c.name);
898        }
899    }
900
901    /// A sized constructor is a prelude name, for the reason the numeric and
902    /// graph helpers are: a row naming a name nothing declares is a wrapper no
903    /// program can reach.
904    #[test]
905    fn every_sized_ctor_is_a_prelude_name() {
906        let prelude: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
907        for c in SIZED_CTORS {
908            assert!(prelude.contains(c.name), "{} is not in PRELUDE", c.name);
909        }
910    }
911
912    /// The row's `extents` count and its wrapper's arity are one number,
913    /// checked against each other. The wrapper takes a context, an element
914    /// descriptor, every extent, and the fill — so the source arity plus the
915    /// descriptor slot is the wrapper's arity, and a row that grew an extent
916    /// without growing its wrapper would otherwise pass garbage in an unfilled
917    /// slot rather than failing here.
918    #[test]
919    fn a_sized_ctors_arity_is_the_wrappers_arity() {
920        assert_eq!(sized_ctor("Vec").expect("Vec is sized").arity(), 2);
921        assert_eq!(sized_ctor("Grid").expect("Grid is sized").arity(), 3);
922        for c in SIZED_CTORS {
923            assert_eq!(
924                c.arity() + 1,
925                c.symbol.arity(),
926                "{}'s row and its wrapper disagree about how many operands it takes",
927                c.name
928            );
929            // The one leading pointer is the static element descriptor; the
930            // extents and the fill after it are boxed (ADR-146 decision 7).
931            assert_uniform_gc_wrapper(c.symbol, c.name, 1);
932        }
933    }
934
935    /// **Only `Vec` and `Grid` are sized**, and this is the test that keeps
936    /// ADR-089 decision 1 intact everywhere else. ADR-146 is a carve-out of
937    /// exactly two names; a third row added without reopening that decision
938    /// fails here.
939    #[test]
940    fn only_vec_and_grid_have_a_sized_form() {
941        assert_eq!(SIZED_CTORS.len(), 2);
942        for absent in [
943            "Deque", "Map", "Set", "Counter", "MinHeap", "MaxHeap", "BitSet", "Range", "Option",
944            "out", "abs", "bfs",
945        ] {
946            assert!(
947                sized_ctor(absent).is_none(),
948                "{absent} has no sized form and ADR-146 says why"
949            );
950        }
951    }
952
953    /// Every graph helper is a prelude name, and every name §6.5 lists is a
954    /// graph helper. Same property as the numeric line's, for the same reason: a
955    /// name in only one list is either a phantom (it resolves, then has nowhere
956    /// to go) or an unreachable wrapper.
957    #[test]
958    fn every_graph_helper_is_a_prelude_name() {
959        let prelude: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
960        for h in GRAPH_HELPERS {
961            assert!(prelude.contains(h.name), "{} is not in PRELUDE", h.name);
962        }
963        // §6.5's algorithms, in the three questions each family can answer.
964        // (Its list also names connected components and topological sort;
965        // neither is a `PRELUDE` name, so neither is a phantom.)
966        for required in [
967            "bfs",
968            "bfs_distance",
969            "bfs_path",
970            "dfs",
971            "dfs_distance",
972            "dfs_path",
973            "dijkstra",
974            "dijkstra_distance",
975            "dijkstra_path",
976            "a_star_distance",
977            "a_star_path",
978            "flood_fill",
979        ] {
980            assert!(
981                graph_helper(required).is_some(),
982                "§6.5 lists {required:?} and it has no helper row"
983            );
984        }
985        assert!(graph_helper("abs").is_none());
986        assert!(graph_helper("out").is_none());
987        // A\* has no whole-walk meaning — "every state A\* reaches" is not a
988        // question anyone asks — so the bare name is not a helper, and the
989        // family is its two goal-directed forms alone.
990        assert!(graph_helper("a_star").is_none());
991    }
992
993    /// A helper's source arity is its wrapper's arity. `params` states the
994    /// *shape* of each argument because the helpers have several different
995    /// signatures and the manifest cannot say which is which — but the count
996    /// still has one authority, so a row that grew a parameter without growing
997    /// its wrapper is a failure here rather than a call that passes garbage in
998    /// the slot nobody filled.
999    #[test]
1000    fn a_graph_helpers_arity_is_the_wrappers_arity() {
1001        for h in GRAPH_HELPERS {
1002            assert_eq!(
1003                h.arity(),
1004                h.symbol.arity(),
1005                "{}'s signature and its wrapper disagree on arity",
1006                h.name
1007            );
1008            // Every wrapper takes only `Gc` operands after the context and
1009            // returns one — the uniform shape that makes the MIR lowering one
1010            // path for the whole table rather than a branch per name.
1011            assert_uniform_gc_wrapper(h.symbol, h.name, 0);
1012        }
1013    }
1014
1015    /// Every helper starts from a state and every other parameter is a function
1016    /// of it. That is §6.5's shape — "closure-based algorithms that do not
1017    /// require materializing a graph object" — and it is what lets one runtime
1018    /// calling convention serve all six: the first operand is a value, the rest
1019    /// are closures.
1020    #[test]
1021    fn a_graph_helper_takes_a_start_state_and_then_only_functions() {
1022        for h in GRAPH_HELPERS {
1023            assert_eq!(
1024                h.params.first(),
1025                Some(&GraphParam::Start),
1026                "{} does not start from a state",
1027                h.name
1028            );
1029            assert!(
1030                h.params[1..].iter().all(|p| *p != GraphParam::Start),
1031                "{} takes a second bare state",
1032                h.name
1033            );
1034            assert!(
1035                h.params.contains(&GraphParam::Neighbours),
1036                "{} has no way to reach a second state",
1037                h.name
1038            );
1039        }
1040    }
1041
1042    /// **A helper that takes a goal may not find one, and that is what the
1043    /// `Option` is for — whether the answer is the number or the route.** A
1044    /// walk that always reaches at least its own start needs no `Option` and
1045    /// does not get one: `dijkstra`'s unreachable state is *absent* from its
1046    /// table, and `bfs`/`dfs`/`flood_fill` always contain the start.
1047    ///
1048    /// The biconditional is the whole rule. A goal-directed helper whose result
1049    /// is not optional would have to invent an answer for "no goal"; a
1050    /// non-goal-directed one that answered `None` would be claiming an absence
1051    /// nothing can produce.
1052    #[test]
1053    fn only_a_goal_directed_helper_can_answer_with_nothing() {
1054        for h in GRAPH_HELPERS {
1055            let goal_directed = h.params.contains(&GraphParam::Goal);
1056            let optional = matches!(h.result, GraphResult::Distance | GraphResult::Path);
1057            assert_eq!(
1058                goal_directed, optional,
1059                "{} looks for a goal but cannot say it found none (or vice versa)",
1060                h.name
1061            );
1062        }
1063    }
1064
1065    /// **Every goal-directed search comes in both forms**, over the same
1066    /// parameters: for each `X_distance` there is an `X_path` and for each
1067    /// `X_path` an `X_distance`. That is the harmonization the family is named
1068    /// for, and it is checked rather than intended — a new search added with
1069    /// only one of its two forms fails here instead of leaving a reader to
1070    /// discover that `dijkstra_path` exists and `dijkstra_distance` does not.
1071    ///
1072    /// The parameters have to match too. Two forms of one search that disagreed
1073    /// about their arguments would be two searches sharing a prefix.
1074    #[test]
1075    fn a_goal_directed_helper_answers_both_the_number_and_the_route() {
1076        for h in GRAPH_HELPERS {
1077            let (family, twin_suffix, twin_result) = match h.result {
1078                GraphResult::Distance => (
1079                    h.name.strip_suffix("_distance").unwrap_or_else(|| {
1080                        panic!("{} answers a distance but is not `_distance`", h.name)
1081                    }),
1082                    "_path",
1083                    GraphResult::Path,
1084                ),
1085                GraphResult::Path => (
1086                    h.name
1087                        .strip_suffix("_path")
1088                        .unwrap_or_else(|| panic!("{} answers a path but is not `_path`", h.name)),
1089                    "_distance",
1090                    GraphResult::Distance,
1091                ),
1092                _ => continue,
1093            };
1094            let twin_name = format!("{family}{twin_suffix}");
1095            let twin =
1096                graph_helper(&twin_name).unwrap_or_else(|| panic!("{} has no {twin_name}", h.name));
1097            assert_eq!(
1098                twin.result, twin_result,
1099                "{twin_name} answers the wrong shape"
1100            );
1101            assert_eq!(
1102                twin.params, h.params,
1103                "{} and {twin_name} are two forms of one search and must take the same arguments",
1104                h.name
1105            );
1106        }
1107    }
1108}