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