use std::collections::BTreeSet;
use blue_lang_syntax::{
keyword_doc, lex, TokenKind, BLOCK_KEYWORDS, FORMS, INFIX, SURFACE_KEYWORDS,
};
use serde::Serialize;
use tatara_lisp_eval::{Arity, HeadBinding, Interpreter, Value};
#[derive(Serialize)]
pub struct Reference {
operators: Vec<Operator>,
keywords: Vec<Keyword>,
forms: Vec<Form>,
bluefile: Vec<blue_lang_pkg::bluefile::WordDoc>,
topics: Vec<Topic>,
names: Vec<Name>,
uncallable: usize,
}
#[derive(Serialize)]
struct Operator {
op: &'static str,
left: u8,
right: u8,
callee: &'static str,
doc: &'static str,
}
#[derive(Serialize)]
struct Keyword {
word: &'static str,
kind: &'static str,
doc: &'static str,
}
#[derive(Serialize)]
struct Form {
example: &'static str,
lowers_to: &'static str,
doc: &'static str,
}
#[derive(Serialize)]
struct Topic {
slug: String,
title: &'static str,
}
#[derive(Serialize)]
pub struct Name {
pub name: String,
pub topic: String,
pub arbiter: &'static str,
pub arity: Option<String>,
pub signature: &'static str,
pub doc: &'static str,
}
#[must_use]
pub fn arity_of(value: &Value) -> Option<String> {
match value {
Value::NativeFn(f) => Some(match f.arity {
Arity::Exact(n) => n.to_string(),
Arity::AtLeast(n) => format!("{n}+"),
Arity::Range(a, b) => format!("{a}..{b}"),
Arity::Any => "any".into(),
}),
Value::Closure(c) => Some(if c.rest.is_some() {
format!("{}+", c.params.len())
} else {
c.params.len().to_string()
}),
_ => None,
}
}
pub fn names_of<H: 'static>(interp: &Interpreter<H>) -> (Vec<Name>, usize) {
let mut names = Vec::new();
let mut uncallable = 0;
let all: BTreeSet<String> = interp
.reserved_head_names()
.iter()
.map(ToString::to_string)
.collect();
for name in all {
if !blue_lang_syntax::is_callable_name(&name) {
uncallable += 1;
continue;
}
let value = interp.lookup_global(&name);
let arbiter = match interp.resolve_head(&name) {
Some(HeadBinding::SpecialForm) => "special form",
Some(HeadBinding::Macro) => "macro",
_ => match value {
Some(Value::NativeFn(_) | Value::Closure(_)) => "function",
_ => "value",
},
};
let doc = blue_lang_runtime::docs::doc_of(&name);
names.push(Name {
arity: value.as_ref().and_then(arity_of),
topic: doc.map(|d| slug(d.topic)).unwrap_or_default(),
signature: doc.map_or("", |d| d.signature),
doc: doc.map_or("", |d| d.doc),
arbiter,
name,
});
}
(names, uncallable)
}
fn slug(t: blue_lang_runtime::docs::Topic) -> String {
format!("{t:?}").to_lowercase()
}
pub fn reference() -> Reference {
let interp = blue_lang_runtime::interpreter(&mut ());
let (names, uncallable) = names_of(&interp);
Reference {
operators: INFIX
.iter()
.map(|i| Operator {
op: i.op,
left: i.power.0,
right: i.power.1,
callee: i.callee,
doc: i.doc,
})
.collect(),
keywords: SURFACE_KEYWORDS
.iter()
.map(|w| (w, "form"))
.chain(BLOCK_KEYWORDS.iter().map(|w| (w, "block")))
.map(|(w, kind)| Keyword {
word: w,
kind,
doc: keyword_doc(w),
})
.collect(),
forms: FORMS
.iter()
.map(|f| Form {
example: f.example,
lowers_to: f.lowers_to,
doc: f.doc,
})
.collect(),
bluefile: blue_lang_pkg::bluefile::words().collect(),
topics: blue_lang_runtime::docs::Topic::ALL
.iter()
.map(|t| Topic {
slug: slug(*t),
title: t.title(),
})
.collect(),
names,
uncallable,
}
}
fn declared_params(signature: &str) -> Option<(usize, usize, bool)> {
let open = signature.find('(')?;
let close = signature.rfind(')')?;
let inner = &signature[open + 1..close];
let (mut outside, mut optional) = (String::new(), 0);
let mut rest = inner;
while let Some(start) = rest.find('[') {
outside.push_str(&rest[..start]);
let end = rest[start..].find(']')? + start;
optional += rest[start + 1..end]
.split(',')
.filter(|p| !p.trim().is_empty())
.count();
rest = &rest[end + 1..];
}
outside.push_str(rest);
let params: Vec<&str> = outside
.split(',')
.map(str::trim)
.filter(|p| !p.is_empty())
.collect();
let variadic = params.iter().any(|p| p.ends_with("..."));
let required = params.iter().filter(|p| !p.ends_with("...")).count();
Some((required, optional, variadic))
}
fn agrees(arity: &str, (required, optional, variadic): (usize, usize, bool)) -> bool {
let (min, max) = match arity.split_once("..") {
Some((a, b)) => (a.parse().ok(), b.parse().ok()),
None if arity == "any" => (Some(0), None),
None => match arity.strip_suffix('+') {
Some(n) => (n.parse().ok(), None),
None => (arity.parse().ok(), arity.parse().ok()),
},
};
let declared_max = if variadic {
None
} else {
Some(required + optional)
};
min == Some(required) && max == declared_max
}
#[cfg(test)]
mod tests {
use super::*;
fn bound() -> Vec<Name> {
names_of(&blue_lang_runtime::interpreter(&mut ())).0
}
#[test]
fn the_probe_sees_the_full_runtime() {
let names = bound();
assert!(names.len() >= 220, "only {} names", names.len());
assert!(names.iter().any(|n| n.name == "read_file"), "no sys layer");
}
#[test]
fn every_bound_name_is_described() {
let missing: Vec<String> = bound()
.into_iter()
.filter(|n| n.doc.is_empty())
.map(|n| n.name)
.collect();
assert!(
missing.is_empty(),
"bound with no row in blue_lang_runtime::docs: {missing:?}"
);
}
#[test]
fn every_row_names_a_bound_name() {
let names: BTreeSet<String> = bound().into_iter().map(|n| n.name).collect();
let stale: Vec<&str> = blue_lang_runtime::docs::NAMES
.iter()
.map(|d| d.name)
.filter(|n| !names.contains(*n))
.collect();
assert!(
stale.is_empty(),
"rows naming nothing the runtime binds: {stale:?}"
);
let mut seen = BTreeSet::new();
for d in blue_lang_runtime::docs::NAMES {
assert!(seen.insert(d.name), "`{}` has two rows", d.name);
}
}
#[test]
fn every_signature_matches_its_arity() {
let mut wrong = Vec::new();
for n in bound() {
let Some(arity) = &n.arity else { continue };
match declared_params(n.signature) {
Some(p) if agrees(arity, p) => {}
_ => wrong.push(format!(
"{}: `{}` against arity {arity}",
n.name, n.signature
)),
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
#[test]
fn the_signature_grammar_reads_what_it_says() {
assert_eq!(declared_params("f(a, b)"), Some((2, 0, false)));
assert_eq!(declared_params("f(a[, b][, c])"), Some((1, 2, false)));
assert_eq!(declared_params("f(g, [init, ]xs)"), Some((2, 1, false)));
assert_eq!(declared_params("f(a, more...)"), Some((1, 0, true)));
assert_eq!(declared_params("f()"), Some((0, 0, false)));
assert!(agrees("1..3", (1, 2, false)));
assert!(agrees("2+", (2, 0, true)));
assert!(agrees("any", (0, 0, true)));
assert!(!agrees("2", (1, 0, false)));
assert!(!agrees("2", (2, 0, true)));
}
}