use crate::abi::RuntimeSymbol;
pub const PRELUDE: &[PreludeEntry] = &[
PreludeEntry::new(
"out",
"Write one value to stdout, followed by a newline. Renders through the value's own formatter, so any type may be written.",
),
PreludeEntry::new(
"dbg",
"Write one value to **stderr** and return it unchanged, so `dbg(e)` can wrap any subexpression without changing what the program computes.",
),
PreludeEntry::new(
"panic",
"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.",
),
PreludeEntry::new(
"assert",
"Stop if a condition is false. Takes a `Bool` and nothing else — there is no message parameter.",
),
PreludeEntry::new(
"abs",
"Absolute value of an `Int`. Faults on `Int`'s minimum, which has no positive counterpart. `Float` has its own `x.abs()`.",
),
PreludeEntry::new(
"sign",
"`-1`, `0` or `1`, by the sign of an `Int`. Total. `Float` has its own `x.sign()`.",
),
PreludeEntry::new(
"min",
"The smaller of two `Int`s. `Float` has its own `x.min(y)` method.",
),
PreludeEntry::new(
"max",
"The larger of two `Int`s. `Float` has its own `x.max(y)` method.",
),
PreludeEntry::new(
"clamp",
"`clamp(value, low, high)` — an `Int` held inside an inclusive range. Faults if `low > high`.",
),
PreludeEntry::new(
"gcd",
"Non-negative greatest common divisor of two `Int`s. `gcd(0, 0)` is `0`.",
),
PreludeEntry::new(
"lcm",
"Non-negative least common multiple of two `Int`s, or `0` if either operand is. Faults if the result leaves `Int`.",
),
PreludeEntry::new("pi", "π as a Float. A nullary function: write `pi()`."),
PreludeEntry::new(
"e",
"Euler's number as a Float. A nullary function: write `e()`.",
),
PreludeEntry::new(
"Vec",
"Grow, iterate, and pipeline over an ordered list. `Vec()` is empty; `Vec(n, fill)` is n copies of fill.",
),
PreludeEntry::new(
"Deque",
"Double-ended queue: push and pop at either end. `Deque()` is empty.",
),
PreludeEntry::new(
"Map",
"Hash map from keys to values. A key must be a value that cannot change. `Map()` is empty.",
),
PreludeEntry::new(
"Set",
"Hash set of distinct values. An element must be a value that cannot change. `Set()` is empty.",
),
PreludeEntry::new(
"Counter",
"Map whose absent values read as zero, so `c.inc(k)` needs no first-sighting case. `Counter()` is empty.",
),
PreludeEntry::new(
"MinHeap",
"Priority queue yielding the smallest element first. Its element must be orderable. `MinHeap()` is empty.",
),
PreludeEntry::new(
"MaxHeap",
"Priority queue yielding the largest element first. Its element must be orderable. `MaxHeap()` is empty.",
),
PreludeEntry::new(
"Grid",
"2D grid with rectangular indexing. `Grid()` is 0x0; `Grid(w, h, fill)` is a w-by-h board of fill.",
),
PreludeEntry::new(
"BitSet",
"Compact set of non-negative integers. Takes no type argument.",
),
PreludeEntry::new(
"Option",
"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.",
),
PreludeEntry::variant("Some", "Wrap a value in an `Option`."),
PreludeEntry::variant(
"None",
"The absent `Option` value. Not a call — write `None`, never `None()`.",
),
PreludeEntry::new(
"bfs",
"Breadth-first walk: `bfs(start, |s| neighbors(s))` answers every state reached, in the order it was reached.",
),
PreludeEntry::new(
"bfs_distance",
"Steps to the first state a predicate accepts, or `None` when no goal is reachable: `bfs_distance(start, |s| neighbors(s), |s| s == goal)`.",
),
PreludeEntry::new(
"bfs_path",
"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)`.",
),
PreludeEntry::new(
"dfs",
"Depth-first walk: `dfs(start, |s| neighbors(s))` answers every state reached, in the order it was reached.",
),
PreludeEntry::new(
"dfs_distance",
"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)`.",
),
PreludeEntry::new(
"dfs_path",
"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)`.",
),
PreludeEntry::new(
"dijkstra",
"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`.",
),
PreludeEntry::new(
"dijkstra_distance",
"Cost of the cheapest route to a goal, or `None`: `dijkstra_distance(start, |s| neighbors(s), |a, b| weight(a, b), |s| s == goal)`.",
),
PreludeEntry::new(
"dijkstra_path",
"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)`.",
),
PreludeEntry::new(
"a_star_distance",
"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.",
),
PreludeEntry::new(
"a_star_path",
"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.",
),
PreludeEntry::new(
"flood_fill",
"Every state reachable from a start state, unordered, as a `Set`: `flood_fill(start, |s| neighbors(s))`.",
),
];
pub const BUILTIN_TYPES: &[TypeEntry] = &[
TypeEntry::seeded("Int", "Signed 64-bit integer. Written `42` or `1_000_000`."),
TypeEntry::seeded(
"Float",
"IEEE-754 binary64. Written `3.5`, `1e10` or `2e-3`; `.5` is not a literal.",
),
TypeEntry::seeded("Bool", "`true` or `false`."),
TypeEntry::seeded("Char", "One Unicode scalar value. Written `'p'`."),
TypeEntry::seeded("Text", "Immutable UTF-8 text. Written `\"praxis\"`."),
TypeEntry::seeded("Unit", "The type with one value, written `()`."),
TypeEntry::seeded(
"Never",
"The type of an expression that produces no value — `panic(...)`, `return`, `break`. It has no values, so it unifies with anything.",
),
TypeEntry::ctor(
"Range",
"A half-open (`0..n`) or inclusive (`0..=n`) integer range. A type name only — there is no `Range()` constructor.",
),
];
pub const GRAPH_HELPERS: &[GraphHelper] = &[
GraphHelper::new(
"bfs",
RuntimeSymbol::Bfs,
&[GraphParam::Start, GraphParam::Neighbours],
GraphResult::VisitOrder,
),
GraphHelper::new(
"bfs_distance",
RuntimeSymbol::BfsDistance,
&[GraphParam::Start, GraphParam::Neighbours, GraphParam::Goal],
GraphResult::Distance,
),
GraphHelper::new(
"bfs_path",
RuntimeSymbol::BfsPath,
&[GraphParam::Start, GraphParam::Neighbours, GraphParam::Goal],
GraphResult::Path,
),
GraphHelper::new(
"dfs",
RuntimeSymbol::Dfs,
&[GraphParam::Start, GraphParam::Neighbours],
GraphResult::VisitOrder,
),
GraphHelper::new(
"dfs_distance",
RuntimeSymbol::DfsDistance,
&[GraphParam::Start, GraphParam::Neighbours, GraphParam::Goal],
GraphResult::Distance,
),
GraphHelper::new(
"dfs_path",
RuntimeSymbol::DfsPath,
&[GraphParam::Start, GraphParam::Neighbours, GraphParam::Goal],
GraphResult::Path,
),
GraphHelper::new(
"dijkstra",
RuntimeSymbol::Dijkstra,
&[
GraphParam::Start,
GraphParam::Neighbours,
GraphParam::Weight,
],
GraphResult::CostTable,
),
GraphHelper::new(
"dijkstra_distance",
RuntimeSymbol::DijkstraDistance,
&[
GraphParam::Start,
GraphParam::Neighbours,
GraphParam::Weight,
GraphParam::Goal,
],
GraphResult::Distance,
),
GraphHelper::new(
"dijkstra_path",
RuntimeSymbol::DijkstraPath,
&[
GraphParam::Start,
GraphParam::Neighbours,
GraphParam::Weight,
GraphParam::Goal,
],
GraphResult::Path,
),
GraphHelper::new(
"a_star_distance",
RuntimeSymbol::AStarDistance,
&[
GraphParam::Start,
GraphParam::Neighbours,
GraphParam::Weight,
GraphParam::Heuristic,
GraphParam::Goal,
],
GraphResult::Distance,
),
GraphHelper::new(
"a_star_path",
RuntimeSymbol::AStarPath,
&[
GraphParam::Start,
GraphParam::Neighbours,
GraphParam::Weight,
GraphParam::Heuristic,
GraphParam::Goal,
],
GraphResult::Path,
),
GraphHelper::new(
"flood_fill",
RuntimeSymbol::FloodFill,
&[GraphParam::Start, GraphParam::Neighbours],
GraphResult::Reached,
),
];
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum GraphParam {
Start,
Neighbours,
Weight,
Heuristic,
Goal,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum GraphResult {
VisitOrder,
Reached,
CostTable,
Distance,
Path,
}
#[derive(Clone, Copy, Debug)]
pub struct GraphHelper {
pub name: &'static str,
pub symbol: RuntimeSymbol,
pub params: &'static [GraphParam],
pub result: GraphResult,
}
impl GraphHelper {
const fn new(
name: &'static str,
symbol: RuntimeSymbol,
params: &'static [GraphParam],
result: GraphResult,
) -> GraphHelper {
GraphHelper {
name,
symbol,
params,
result,
}
}
#[inline]
pub const fn arity(&self) -> usize {
self.params.len()
}
}
pub fn graph_helper(name: &str) -> Option<GraphHelper> {
GRAPH_HELPERS.iter().copied().find(|h| h.name == name)
}
pub const NUMERIC_HELPERS: &[NumericHelper] = &[
NumericHelper::new("abs", RuntimeSymbol::IntAbs),
NumericHelper::new("sign", RuntimeSymbol::IntSign),
NumericHelper::new("min", RuntimeSymbol::IntMin),
NumericHelper::new("max", RuntimeSymbol::IntMax),
NumericHelper::new("clamp", RuntimeSymbol::IntClamp),
NumericHelper::new("gcd", RuntimeSymbol::IntGcd),
NumericHelper::new("lcm", RuntimeSymbol::IntLcm),
];
#[derive(Clone, Copy, Debug)]
pub struct NumericHelper {
pub name: &'static str,
pub symbol: RuntimeSymbol,
}
impl NumericHelper {
const fn new(name: &'static str, symbol: RuntimeSymbol) -> NumericHelper {
NumericHelper { name, symbol }
}
#[inline]
pub const fn arity(&self) -> usize {
self.symbol.arity()
}
}
pub fn numeric_helper(name: &str) -> Option<NumericHelper> {
NUMERIC_HELPERS.iter().copied().find(|h| h.name == name)
}
pub const SIZED_CTORS: &[SizedCtor] = &[
SizedCtor::new("Vec", RuntimeSymbol::VecFilled, 1),
SizedCtor::new("Grid", RuntimeSymbol::GridFilled, 2),
];
#[derive(Clone, Copy, Debug)]
pub struct SizedCtor {
pub name: &'static str,
pub symbol: RuntimeSymbol,
pub extents: usize,
}
impl SizedCtor {
const fn new(name: &'static str, symbol: RuntimeSymbol, extents: usize) -> SizedCtor {
SizedCtor {
name,
symbol,
extents,
}
}
#[inline]
#[must_use]
pub const fn arity(&self) -> usize {
self.extents + 1
}
}
pub fn sized_ctor(name: &str) -> Option<SizedCtor> {
SIZED_CTORS.iter().copied().find(|c| c.name == name)
}
#[derive(Clone, Copy, Debug)]
pub struct PreludeEntry {
pub name: &'static str,
pub doc: &'static str,
pub is_variant_ctor: bool,
}
impl PreludeEntry {
pub const fn new(name: &'static str, doc: &'static str) -> PreludeEntry {
PreludeEntry {
name,
doc,
is_variant_ctor: false,
}
}
pub const fn variant(name: &'static str, doc: &'static str) -> PreludeEntry {
PreludeEntry {
name,
doc,
is_variant_ctor: true,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TypeEntry {
pub name: &'static str,
pub doc: &'static str,
pub seeded: bool,
}
impl TypeEntry {
const fn seeded(name: &'static str, doc: &'static str) -> TypeEntry {
TypeEntry {
name,
doc,
seeded: true,
}
}
const fn ctor(name: &'static str, doc: &'static str) -> TypeEntry {
TypeEntry {
name,
doc,
seeded: false,
}
}
}
#[must_use]
pub fn prelude_doc(name: &str) -> Option<&'static str> {
PRELUDE.iter().find(|e| e.name == name).map(|e| e.doc)
}
#[must_use]
pub fn type_doc(name: &str) -> Option<&'static str> {
BUILTIN_TYPES
.iter()
.find(|e| e.name == name)
.map(|e| e.doc)
.or_else(|| prelude_doc(name))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::{AbiKind, AbiRet};
use std::collections::HashSet;
#[test]
fn prelude_is_non_empty() {
assert!(!PRELUDE.is_empty());
}
#[test]
fn prelude_names_are_unique() {
let mut seen = HashSet::new();
for e in PRELUDE {
assert!(seen.insert(e.name), "duplicate prelude name {}", e.name);
}
}
#[test]
fn every_documented_name_has_documentation() {
for e in PRELUDE {
assert!(
e.doc.len() > 10,
"prelude entry `{}` has no real documentation",
e.name
);
assert!(
e.doc.ends_with('.'),
"prelude entry `{}`'s doc is not a sentence",
e.name
);
}
for e in BUILTIN_TYPES {
assert!(
e.doc.len() > 10,
"type entry `{}` has no real documentation",
e.name
);
assert!(
e.doc.ends_with('.'),
"type entry `{}`'s doc is not a sentence",
e.name
);
}
}
#[test]
fn the_lookups_answer_for_exactly_their_own_names() {
for e in PRELUDE {
assert_eq!(prelude_doc(e.name), Some(e.doc), "{}", e.name);
}
for e in BUILTIN_TYPES {
assert_eq!(type_doc(e.name), Some(e.doc), "{}", e.name);
}
assert_eq!(type_doc("Vec"), prelude_doc("Vec"));
assert!(type_doc("Vec").is_some());
assert!(prelude_doc("Int").is_none());
assert!(type_doc("Int").is_some());
assert!(type_doc("out").is_none() || prelude_doc("out").is_some());
assert!(prelude_doc("nope").is_none());
assert!(type_doc("nope").is_none());
assert!(type_doc("UInt").is_none());
assert!(type_doc("Byte").is_none());
}
#[test]
fn every_type_position_name_is_documented() {
for ctor in [
crate::CollectionCtor::Vec,
crate::CollectionCtor::Deque,
crate::CollectionCtor::Map,
crate::CollectionCtor::Set,
crate::CollectionCtor::Counter,
crate::CollectionCtor::MinHeap,
crate::CollectionCtor::MaxHeap,
crate::CollectionCtor::BitSet,
crate::CollectionCtor::Grid,
crate::CollectionCtor::Range,
] {
let name = ctor.name();
assert!(
type_doc(name).is_some(),
"the type name `{name}` has no description"
);
}
assert!(type_doc("Option").is_some());
}
#[test]
fn only_a_type_that_has_no_value_is_left_unseeded() {
let unseeded: Vec<&str> = BUILTIN_TYPES
.iter()
.filter(|e| !e.seeded)
.map(|e| e.name)
.collect();
assert_eq!(unseeded, vec!["Range"]);
let seeded: Vec<&str> = BUILTIN_TYPES
.iter()
.filter(|e| e.seeded)
.map(|e| e.name)
.collect();
assert_eq!(
seeded,
vec!["Int", "Float", "Bool", "Char", "Text", "Unit", "Never"]
);
}
#[test]
fn prelude_includes_design_canonical_entries() {
let names: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
for required in ["out", "dbg", "panic", "abs", "Vec", "Map", "bfs_distance"] {
assert!(
names.contains(required),
"missing prelude entry {required:?}"
);
}
}
#[test]
fn every_numeric_helper_is_a_prelude_name() {
let prelude: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
for h in NUMERIC_HELPERS {
assert!(prelude.contains(h.name), "{} is not in PRELUDE", h.name);
}
for required in ["abs", "sign", "min", "max", "clamp", "gcd", "lcm"] {
assert!(
numeric_helper(required).is_some(),
"§16.1 lists {required:?} and it has no helper row"
);
}
assert!(numeric_helper("pi").is_none());
assert!(numeric_helper("out").is_none());
assert!(numeric_helper("bfs").is_none());
}
fn assert_uniform_gc_wrapper(sym: RuntimeSymbol, name: &str, leading_ptrs: usize) {
let sig = sym.sig();
assert_eq!(sig.params[0], AbiKind::Ctx, "{name}");
let boxed_from = 1 + leading_ptrs;
assert!(
sig.params[1..boxed_from].iter().all(|k| *k == AbiKind::Ptr),
"{name}'s first {leading_ptrs} operand(s) after the context are not raw pointers"
);
assert!(
sig.params[boxed_from..].iter().all(|k| *k == AbiKind::Gc),
"{name} takes a non-Gc operand"
);
assert_eq!(sig.ret, AbiRet::Gc, "{name}");
}
#[test]
fn a_helpers_arity_is_the_wrappers_arity() {
assert_eq!(numeric_helper("abs").unwrap().arity(), 1);
assert_eq!(numeric_helper("sign").unwrap().arity(), 1);
assert_eq!(numeric_helper("min").unwrap().arity(), 2);
assert_eq!(numeric_helper("max").unwrap().arity(), 2);
assert_eq!(numeric_helper("gcd").unwrap().arity(), 2);
assert_eq!(numeric_helper("lcm").unwrap().arity(), 2);
assert_eq!(numeric_helper("clamp").unwrap().arity(), 3);
for h in NUMERIC_HELPERS {
assert_uniform_gc_wrapper(h.symbol, h.name, 0);
}
}
#[test]
fn each_helper_has_its_own_wrapper() {
let mut seen = HashSet::new();
for h in NUMERIC_HELPERS {
assert!(seen.insert(h.symbol), "{} reuses a wrapper", h.name);
}
let mut seen = HashSet::new();
for h in GRAPH_HELPERS {
assert!(seen.insert(h.symbol), "{} reuses a wrapper", h.name);
}
let mut seen = HashSet::new();
for c in SIZED_CTORS {
assert!(seen.insert(c.symbol), "{} reuses a wrapper", c.name);
}
}
#[test]
fn every_sized_ctor_is_a_prelude_name() {
let prelude: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
for c in SIZED_CTORS {
assert!(prelude.contains(c.name), "{} is not in PRELUDE", c.name);
}
}
#[test]
fn a_sized_ctors_arity_is_the_wrappers_arity() {
assert_eq!(sized_ctor("Vec").expect("Vec is sized").arity(), 2);
assert_eq!(sized_ctor("Grid").expect("Grid is sized").arity(), 3);
for c in SIZED_CTORS {
assert_eq!(
c.arity() + 1,
c.symbol.arity(),
"{}'s row and its wrapper disagree about how many operands it takes",
c.name
);
assert_uniform_gc_wrapper(c.symbol, c.name, 1);
}
}
#[test]
fn only_vec_and_grid_have_a_sized_form() {
assert_eq!(SIZED_CTORS.len(), 2);
for absent in [
"Deque", "Map", "Set", "Counter", "MinHeap", "MaxHeap", "BitSet", "Range", "Option",
"out", "abs", "bfs",
] {
assert!(
sized_ctor(absent).is_none(),
"{absent} has no sized form and ADR-146 says why"
);
}
}
#[test]
fn every_graph_helper_is_a_prelude_name() {
let prelude: HashSet<_> = PRELUDE.iter().map(|e| e.name).collect();
for h in GRAPH_HELPERS {
assert!(prelude.contains(h.name), "{} is not in PRELUDE", h.name);
}
for required in [
"bfs",
"bfs_distance",
"bfs_path",
"dfs",
"dfs_distance",
"dfs_path",
"dijkstra",
"dijkstra_distance",
"dijkstra_path",
"a_star_distance",
"a_star_path",
"flood_fill",
] {
assert!(
graph_helper(required).is_some(),
"§6.5 lists {required:?} and it has no helper row"
);
}
assert!(graph_helper("abs").is_none());
assert!(graph_helper("out").is_none());
assert!(graph_helper("a_star").is_none());
}
#[test]
fn a_graph_helpers_arity_is_the_wrappers_arity() {
for h in GRAPH_HELPERS {
assert_eq!(
h.arity(),
h.symbol.arity(),
"{}'s signature and its wrapper disagree on arity",
h.name
);
assert_uniform_gc_wrapper(h.symbol, h.name, 0);
}
}
#[test]
fn a_graph_helper_takes_a_start_state_and_then_only_functions() {
for h in GRAPH_HELPERS {
assert_eq!(
h.params.first(),
Some(&GraphParam::Start),
"{} does not start from a state",
h.name
);
assert!(
h.params[1..].iter().all(|p| *p != GraphParam::Start),
"{} takes a second bare state",
h.name
);
assert!(
h.params.contains(&GraphParam::Neighbours),
"{} has no way to reach a second state",
h.name
);
}
}
#[test]
fn only_a_goal_directed_helper_can_answer_with_nothing() {
for h in GRAPH_HELPERS {
let goal_directed = h.params.contains(&GraphParam::Goal);
let optional = matches!(h.result, GraphResult::Distance | GraphResult::Path);
assert_eq!(
goal_directed, optional,
"{} looks for a goal but cannot say it found none (or vice versa)",
h.name
);
}
}
#[test]
fn a_goal_directed_helper_answers_both_the_number_and_the_route() {
for h in GRAPH_HELPERS {
let (family, twin_suffix, twin_result) = match h.result {
GraphResult::Distance => (
h.name.strip_suffix("_distance").unwrap_or_else(|| {
panic!("{} answers a distance but is not `_distance`", h.name)
}),
"_path",
GraphResult::Path,
),
GraphResult::Path => (
h.name
.strip_suffix("_path")
.unwrap_or_else(|| panic!("{} answers a path but is not `_path`", h.name)),
"_distance",
GraphResult::Distance,
),
_ => continue,
};
let twin_name = format!("{family}{twin_suffix}");
let twin =
graph_helper(&twin_name).unwrap_or_else(|| panic!("{} has no {twin_name}", h.name));
assert_eq!(
twin.result, twin_result,
"{twin_name} answers the wrong shape"
);
assert_eq!(
twin.params, h.params,
"{} and {twin_name} are two forms of one search and must take the same arguments",
h.name
);
}
}
}