const INVENTORY: &str = include_str!("commands.tsv");
const SYNTAX: &str = include_str!("syntax-index.tsv");
pub fn signatures(name: &str) -> Option<&'static [crate::syntax::signature::Signature]> {
use std::collections::HashMap;
use std::sync::OnceLock;
static SIGS: OnceLock<HashMap<String, Vec<crate::syntax::signature::Signature>>> =
OnceLock::new();
SIGS.get_or_init(|| {
entries()
.into_iter()
.filter(|e| e.library.is_empty())
.filter_map(|e| {
crate::syntax::signature::parse_cell(&e.synopsis)
.ok()
.map(|s| (e.name, s))
})
.collect()
})
.get(name)
.map(Vec::as_slice)
}
pub fn options_of(name: &str) -> Option<Vec<&'static crate::syntax::signature::OptSpec>> {
let forms = signatures(name)?;
let mut out: Vec<&'static crate::syntax::signature::OptSpec> = Vec::new();
for o in forms.iter().flat_map(|f| f.options.iter()) {
if !out.iter().any(|seen| {
(seen.long.is_some() && seen.long == o.long)
|| (seen.short.is_some() && seen.short == o.short)
}) {
out.push(o);
}
}
Some(out)
}
pub fn declares_long(written: &str, long: &str) -> bool {
use std::collections::HashMap;
use std::sync::OnceLock;
static LONGS: OnceLock<HashMap<String, Vec<String>>> = OnceLock::new();
LONGS
.get_or_init(|| {
let mut out: HashMap<String, Vec<String>> = HashMap::new();
for e in entries() {
let Ok(forms) = crate::syntax::signature::parse_cell(&e.synopsis) else {
continue;
};
out.entry(e.written()).or_default().extend(
forms
.iter()
.flat_map(|f| f.options.iter())
.filter_map(|o| o.long.clone()),
);
}
out
})
.get(written)
.is_some_and(|longs| longs.iter().any(|l| l == long))
}
pub fn takes_of_by_signature(name: &str) -> Option<bool> {
options_of(name).map(|opts| opts.iter().any(|o| o.long.as_deref() == Some("--of")))
}
#[derive(Debug, Clone)]
pub struct Entry {
pub name: String,
pub library: String,
pub pipes: String,
pub synopsis: String,
pub kind: String,
pub typed: String,
pub onnull: String,
pub en: String,
pub ru: String,
pub note_en: String,
pub note_ru: String,
pub anchor: String,
}
impl Entry {
pub fn written(&self) -> String {
if self.library.is_empty() {
self.name.clone()
} else {
format!("{}:{}", self.library, self.name)
}
}
}
pub fn entries() -> Vec<Entry> {
let mut header: Option<Vec<&str>> = None;
let mut out = Vec::new();
for line in INVENTORY.lines() {
if line.trim().is_empty() || line.starts_with("# ") {
continue;
}
let cells: Vec<&str> = line.split('\t').collect();
let Some(h) = &header else {
header = Some(cells);
continue;
};
let col = |name: &str| -> String {
h.iter()
.position(|c| *c == name)
.and_then(|i| cells.get(i))
.map(|s| s.to_string())
.unwrap_or_default()
};
out.push(Entry {
name: col("name"),
library: col("library"),
pipes: col("pipes"),
synopsis: col("synopsis"),
kind: col("kind"),
typed: col("typed"),
onnull: col("onnull"),
en: col("en"),
ru: col("ru"),
note_en: col("note_en"),
note_ru: col("note_ru"),
anchor: col("anchor"),
});
}
out
}
pub fn entry(name: &str) -> Option<Entry> {
entries().into_iter().find(|e| e.written() == name)
}
pub fn is_scalar_kind(name: &str) -> bool {
static SCALARS: std::sync::OnceLock<std::collections::HashSet<String>> =
std::sync::OnceLock::new();
SCALARS
.get_or_init(|| {
entries()
.into_iter()
.filter(|e| e.kind == "scalar")
.map(|e| e.written())
.collect()
})
.contains(name)
}
pub fn page(e: &Entry, ru: bool) -> String {
let (line, note) = if ru {
(&e.ru, &e.note_ru)
} else {
(&e.en, &e.note_en)
};
let mut s = format!("{} — {}\n", e.written(), line);
let row = |s: &mut String, label: &str, text: &str| {
if !text.is_empty() {
s.push_str(&format!(" {label:<9} {text}\n"));
}
};
row(&mut s, "synopsis", &e.synopsis);
row(&mut s, "pipes", &e.pipes);
row(&mut s, "type", &e.typed);
row(&mut s, "on null", &e.onnull);
row(&mut s, "note", note);
row(&mut s, "spec", &e.anchor);
s.trim_end().to_string()
}
pub fn list(pipe: Option<&str>, ru: bool) -> String {
let rows: Vec<Entry> = entries()
.into_iter()
.filter(|e| pipe.is_none_or(|p| e.pipes.split_whitespace().any(|x| x == p)))
.collect();
let width = rows
.iter()
.map(|e| e.written().chars().count())
.max()
.unwrap_or(0);
rows.iter()
.map(|e| {
let name = e.written();
let pad = " ".repeat(width - name.chars().count());
format!("{name}{pad} {}", if ru { &e.ru } else { &e.en })
})
.collect::<Vec<_>>()
.join("\n")
}
#[derive(Clone, Debug)]
pub struct Glyph {
pub id: String,
pub glyph: String,
pub name_en: String,
pub name_ru: String,
pub anchor: String,
pub entry: String,
pub en: String,
pub ru: String,
pub kind: String,
}
impl Glyph {
pub fn marks(&self) -> String {
let mut out = String::new();
let mut held = false;
for c in self.glyph.chars() {
match c {
'⟨' => held = true,
'⟩' => held = false,
'␣' | ' ' => {}
c if !held => out.push(c),
_ => {}
}
}
out
}
pub fn is_written_as(&self, written: &str) -> bool {
if self.marks().is_empty() {
return false;
}
enum Part {
Run(String),
Var {
digits: bool,
},
Body,
}
let mut parts: Vec<Part> = Vec::new();
let mut name: Option<String> = None;
for c in self.glyph.chars() {
match (c, &mut name) {
('⟨', None) => name = Some(String::new()),
('⟩', Some(n)) => {
let digits = matches!(n.as_str(), "N" | "n" | "k" | "m");
parts.push(Part::Var { digits });
name = None;
}
(c, Some(n)) => n.push(c),
('␣', None) => parts.push(Part::Body),
(c, None) => match parts.last_mut() {
Some(Part::Run(run)) => run.push(c),
_ => parts.push(Part::Run(c.to_string())),
},
}
}
if parts.iter().all(|p| matches!(p, Part::Run(_))) {
return false;
}
fn fits(parts: &[Part], text: &str) -> bool {
match parts.split_first() {
None => text.is_empty(),
Some((Part::Run(run), rest)) => text
.strip_prefix(run.as_str())
.is_some_and(|t| fits(rest, t)),
Some((Part::Body, rest)) => text
.char_indices()
.map(|(at, _)| at)
.chain([text.len()])
.any(|at| fits(rest, &text[at..])),
Some((Part::Var { digits }, rest)) => {
let mut ends = text
.char_indices()
.map(|(at, _)| at)
.skip(1)
.chain([text.len()]);
!text.is_empty()
&& ends.any(|at| {
(!*digits || text[..at].chars().all(|c| c.is_ascii_digit()))
&& fits(rest, &text[at..])
})
}
}
}
fits(&parts, written)
}
}
pub fn glyphs() -> Vec<Glyph> {
let mut lines = SYNTAX.lines();
let header: Vec<&str> = lines
.next()
.unwrap_or_default()
.trim_start_matches('#')
.trim_start()
.split('\t')
.collect();
let at = |name: &str| header.iter().position(|c| *c == name);
let (id, glyph, name_en, name_ru, anchor, entry) = (
at("id"),
at("glyph"),
at("name_en"),
at("name_ru"),
at("anchor"),
at("entry"),
);
let (en, ru, kind) = (at("does_en"), at("does_ru"), at("kind"));
lines
.filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
.map(|line| {
let cells: Vec<&str> = line.split('\t').collect();
let cell = |i: Option<usize>| -> String {
i.and_then(|i| cells.get(i))
.map(|c| c.to_string())
.unwrap_or_default()
};
Glyph {
id: cell(id),
glyph: cell(glyph),
name_en: cell(name_en),
name_ru: cell(name_ru),
anchor: cell(anchor),
entry: cell(entry),
en: cell(en),
ru: cell(ru),
kind: cell(kind),
}
})
.filter(|g| g.entry != "-")
.collect()
}
pub fn glyphs_of(word: &str) -> Vec<Glyph> {
let all = glyphs();
let bare: String = word.chars().filter(|c| !c.is_whitespace()).collect();
let filled = word
.chars()
.any(|c| c.is_alphanumeric() || c.is_whitespace());
let readings: [&dyn Fn(&Glyph) -> bool; 5] = [
&|g| g.glyph == word,
&|g| !bare.is_empty() && g.marks() == bare,
&|g| filled && g.is_written_as(word),
&|g| g.entry == word && g.entry != "model",
&|g| g.id == word,
];
for (i, reading) in readings.into_iter().enumerate() {
let mut rows: Vec<Glyph> = all.iter().filter(|g| reading(g)).cloned().collect();
if rows.is_empty() {
continue;
}
if i == 2 {
let most = rows
.iter()
.map(|g| g.marks().chars().count())
.max()
.unwrap_or(0);
rows.retain(|g| g.marks().chars().count() == most);
}
return rows;
}
Vec::new()
}
fn reference_url(entry: &str) -> String {
format!("https://quarb.org/reference/{entry}.html")
}
pub fn glyph_page(g: &Glyph, ru: bool) -> String {
let all = glyphs();
let canonical = g
.anchor
.strip_prefix("see:")
.and_then(|id| all.iter().find(|c| c.id == id));
let name = |g: &Glyph| {
if ru {
g.name_ru.clone()
} else {
g.name_en.clone()
}
};
let what = |g: &Glyph| if ru { g.ru.clone() } else { g.en.clone() };
let mut s = format!("{} — {}\n", g.glyph, name(g));
let row = |s: &mut String, label: &str, text: &str| {
if !text.is_empty() {
s.push_str(&format!(" {label:<9} {text}\n"));
}
};
let sentence = match what(g) {
w if w.is_empty() => canonical.map(&what).unwrap_or_default(),
w => w,
};
row(&mut s, "what", &sentence);
if !g.kind.is_empty() {
row(&mut s, "kind", &g.kind);
}
if let Some(c) = canonical {
row(&mut s, "see", &format!("{} — {}", c.glyph, name(c)));
}
let anchor = canonical.map_or(g.anchor.as_str(), |c| c.anchor.as_str());
if !anchor.starts_with("see:") {
row(&mut s, "spec", anchor);
}
let entry = match g.entry.as_str() {
"-" | "" => canonical.map_or("", |c| c.entry.as_str()),
e => e,
};
if !ru && !matches!(entry, "" | "-" | "model") {
row(&mut s, "reference", &reference_url(entry));
}
s.trim_end().to_string()
}
pub fn glyph_list(rows: &[Glyph], ru: bool) -> String {
let width = |of: &dyn Fn(&Glyph) -> usize| rows.iter().map(of).max().unwrap_or(0);
let glyph = width(&|g| g.glyph.chars().count());
let kind = width(&|g| g.kind.chars().count());
rows.iter()
.map(|g| {
let pad = " ".repeat(glyph - g.glyph.chars().count());
let name = if ru { &g.name_ru } else { &g.name_en };
if kind == 0 {
format!("{}{pad} {name}", g.glyph)
} else {
let gap = " ".repeat(kind - g.kind.chars().count());
format!("{}{pad} {}{gap} {name}", g.glyph, g.kind)
}
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn kinds() -> Vec<String> {
let mut kinds: Vec<String> = Vec::new();
for g in glyphs() {
if !g.kind.is_empty() && !kinds.contains(&g.kind) {
kinds.push(g.kind);
}
}
kinds
}
pub fn man(args: &[&str]) -> Result<String, String> {
let ru = args.iter().any(|a| *a == "--ru");
let words: Vec<&str> = args.iter().copied().filter(|a| *a != "--ru").collect();
let pages = |rows: &[Glyph]| -> String {
rows.iter()
.map(|g| glyph_page(g, ru))
.collect::<Vec<_>>()
.join("\n\n")
};
match words.as_slice() {
[] => Ok(list(None, ru)),
[pipe] if matches!(*pipe, "|" | "@|" | "$|") => {
let rows = glyphs_of(pipe);
let commands = list(Some(pipe), ru);
Ok(if rows.is_empty() {
commands
} else {
format!("{}\n\n{commands}", pages(&rows))
})
}
[word] if *word == "syntax" && entry(word).is_none() => Ok(glyph_list(&glyphs(), ru)),
[word, kind] if *word == "syntax" && entry(word).is_none() => {
let kinds = kinds();
let named = kinds.iter().find(|k| {
k.as_str() == *kind
|| format!("{k}s") == *kind
|| k.strip_suffix('y')
.is_some_and(|stem| format!("{stem}ies") == *kind)
});
match named {
Some(k) => {
let rows: Vec<Glyph> = glyphs().into_iter().filter(|g| g.kind == *k).collect();
Ok(glyph_list(&rows, ru))
}
None if kinds.is_empty() => Err("the syntax index names no kinds".into()),
None => Err(format!(
"no kind '{kind}' of syntax; the kinds: {}",
kinds.join(", ")
)),
}
}
[name] => {
let rows = glyphs_of(name);
match (entry(name), rows.is_empty()) {
(Some(e), false) => Ok(format!("{}\n\n{}", page(&e, ru), pages(&rows))),
(Some(e), true) => Ok(page(&e, ru)),
(None, false) => Ok(pages(&rows)),
(None, true) => Err(unknown(name, ru)),
}
}
_ => Err(
"man takes one name, one glyph, or the word syntax: man upper, man '@|', \
man '<=>', man syntax, man syntax operator, man --ru upper"
.into(),
),
}
}
pub fn line(line: &str) -> Option<Result<String, String>> {
let t = line.trim();
let rest = t.strip_prefix(":man").or_else(|| t.strip_prefix("man"))?;
if !(rest.is_empty() || rest.starts_with(char::is_whitespace)) {
return None;
}
let words: Vec<&str> = rest
.split_whitespace()
.map(|w| {
['\'', '"']
.iter()
.find_map(|q| {
w.strip_prefix(*q)
.and_then(|w| w.strip_suffix(*q))
.filter(|w| !w.is_empty())
})
.unwrap_or(w)
})
.collect();
Some(man(&words))
}
fn unknown(name: &str, ru: bool) -> String {
let near: Vec<String> = entries()
.iter()
.map(Entry::written)
.filter(|n| {
n.to_lowercase().contains(&name.to_lowercase())
|| (n.chars().count() >= 3 && name.to_lowercase().contains(&n.to_lowercase()))
})
.take(5)
.collect();
let symbolic = !name.chars().any(char::is_alphanumeric);
let part_of: Vec<Glyph> = if symbolic {
glyphs()
.into_iter()
.filter(|g| g.marks().contains(name))
.take(12)
.collect()
} else {
Vec::new()
};
if !part_of.is_empty() {
return format!(
"no glyph '{name}' alone; it is part of:\n{}",
glyph_list(&part_of, ru)
);
}
if near.is_empty() {
format!(
"no command '{name}', and no glyph (`man` alone lists the commands, `man syntax` \
the glyphs)"
)
} else {
format!("no command '{name}'; the nearest: {}", near.join(", "))
}
}
pub fn operand_bound(name: &str) -> Option<usize> {
use std::collections::HashMap;
use std::sync::OnceLock;
static BOUNDS: OnceLock<HashMap<String, Option<usize>>> = OnceLock::new();
BOUNDS
.get_or_init(|| {
entries()
.into_iter()
.filter(|e| e.library.is_empty())
.map(|e| {
let bound = signatures(&e.name).and_then(|forms| {
forms
.iter()
.map(crate::syntax::signature::Signature::max_operands)
.try_fold(0usize, |acc, m| m.map(|m| acc.max(m)))
});
(e.name.clone(), bound)
})
.collect()
})
.get(name)
.copied()
.flatten()
}
pub fn synopsis(name: &str) -> Option<String> {
entry(name).map(|e| e.synopsis)
}
pub fn registry_tsv() -> String {
use crate::syntax::registry::{Agg, Layer, OnNull, SPECS};
let mut s = String::from("name\tkind\tpipes\tlayer\tonnull\tdoc\n");
for spec in SPECS {
let kind = match (spec.scalar, spec.agg) {
(true, None) => "scalar",
(true, Some(Agg::Reducing)) => "both",
(true, Some(Agg::Context)) if spec.name == "number" => "both",
(true, Some(Agg::Context)) if spec.name == "shift" => "context",
(true, Some(Agg::Context)) if spec.name == "tree" => "context",
(true, Some(Agg::Context)) => "rendering",
(false, Some(Agg::Reducing)) => "reducing",
(false, Some(Agg::Context)) if spec.name == "collapse" => "thread",
(false, Some(Agg::Context)) => "context",
(_, Some(Agg::Keyed)) => "thread",
(_, Some(Agg::Association { .. })) => "assoc",
(false, None) => "none",
};
let mut pipes = Vec::new();
if spec.scalar {
pipes.push("|");
}
if spec.agg.is_some() {
pipes.push("@|");
}
if spec.name == "session" {
continue;
}
let layer = match spec.layer {
Layer::Value => "value",
Layer::Node => "node",
};
let onnull = entry(spec.name).map(|e| e.onnull).unwrap_or_else(|| {
match spec.null {
OnNull::Null => "null",
OnNull::Nothing => "no thread",
OnNull::Keep => "",
}
.to_string()
});
s.push_str(&format!(
"{}\t{kind}\t{}\t{layer}\t{onnull}\t{}\n",
spec.name,
pipes.join(" "),
spec.doc
));
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_inventory_and_the_registry_name_the_same_commands() {
let names: std::collections::BTreeSet<String> =
entries().iter().map(Entry::written).collect();
for e in entries() {
if !e.library.is_empty() {
if e.library == "graph" {
use crate::pipeline::library::CommandLibrary as _;
assert!(
crate::graph::library::GRAPH.synopsis(&e.name).is_some(),
"inventory row without a library command: {}",
e.written()
);
}
if e.library == "syntax" {
use crate::pipeline::library::CommandLibrary as _;
assert!(
crate::syntax::construct::SYNTAX.synopsis(&e.name).is_some(),
"inventory row without a library command: {}",
e.written()
);
}
continue;
}
assert!(
e.written() == "now" || crate::syntax::registry::spec(&e.written()).is_some(),
"inventory row without a command: {}",
e.written()
);
}
use crate::pipeline::library::CommandLibrary as _;
for syn in crate::graph::library::GRAPH.commands() {
assert!(
names.contains(&format!("graph:{}", syn.name)),
"library command without an inventory row: graph:{}",
syn.name
);
}
for syn in crate::syntax::construct::SYNTAX.commands() {
assert!(
names.contains(&format!("syntax:{}", syn.name)),
"library command without an inventory row: syntax:{}",
syn.name
);
}
for spec in crate::syntax::registry::SPECS {
assert!(
spec.name == "#" || names.contains(spec.name),
"command without an inventory row: {}",
spec.name
);
}
}
#[test]
fn every_built_in_row_writes_the_notation() {
let mut bad = Vec::new();
for e in entries().into_iter().filter(|e| e.library.is_empty()) {
let opens_as_call = e.synopsis.starts_with(&format!("{}(", e.name))
|| e.synopsis.starts_with(&format!("{}{{", e.name));
if !e.name.chars().all(|c| c.is_alphanumeric() || c == '_') || !opens_as_call {
continue;
}
if let Err(why) = crate::syntax::signature::parse_cell(&e.synopsis) {
bad.push(format!("{}: {why}", e.name));
}
}
assert!(bad.is_empty(), "{}", bad.join("\n"));
}
#[test]
fn the_row_gives_the_operand_bound() {
assert_eq!(operand_bound("round"), Some(1));
assert_eq!(operand_bound("sum"), Some(0));
assert_eq!(operand_bound("csv"), None);
assert_eq!(operand_bound("morph:pos"), None);
assert_eq!(operand_bound("#"), None);
}
#[test]
fn the_door_answers() {
let p = man(&["upper"]).unwrap();
assert!(p.starts_with("upper — the text in upper case\n"), "{p}");
assert!(p.contains(" on null null"), "{p}");
assert!(p.contains(" spec sec:value-commands"), "{p}");
let ru = man(&["--ru", "upper"]).unwrap();
assert!(ru.contains("верхнем"), "{ru}");
let all = man(&[]).unwrap();
assert!(all.lines().count() > 150, "{}", all.lines().count());
let aggs = man(&["@|"]).unwrap();
assert!(aggs.lines().any(|l| l.starts_with("tree ")), "{aggs}");
assert!(!aggs.lines().any(|l| l.starts_with("upper ")), "{aggs}");
let e = man(&["uper"]).unwrap_err();
assert!(e.contains("no command 'uper'"), "{e}");
assert!(
registry_tsv()
.lines()
.any(|l| l.starts_with("tree\tcontext\t| @|\tnode\t"))
);
}
}