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}