pub mod emacs;
pub mod input_emacs;
pub mod operators_nvim;
pub mod operators_sublime;
pub mod snippets_emacs;
pub mod snippets_nvim;
pub mod snippets_vscode;
pub mod sublime;
pub mod textmate;
pub mod vim;
pub mod zed;
use rossi::builtins::BUILTIN_WORDS;
use rossi::keywords::{KEYWORDS, KeywordGroup};
use rossi::operators::{OPERATOR_SPELLINGS, OperatorCategory};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchKind {
Word,
Symbol,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scope {
KeywordControl,
KeywordOther,
ConstantLanguage,
SupportFunction,
KeywordOperator,
}
impl Scope {
pub fn textmate(self) -> &'static str {
match self {
Scope::KeywordControl => "keyword.control.eventb",
Scope::KeywordOther => "keyword.other.eventb",
Scope::ConstantLanguage => "constant.language.eventb",
Scope::SupportFunction => "support.function.eventb",
Scope::KeywordOperator => "keyword.operator.eventb",
}
}
}
#[derive(Debug, Clone)]
pub struct TokenGroup {
pub scope: Scope,
pub kind: MatchKind,
pub case_insensitive: bool,
pub members: Vec<String>,
}
impl TokenGroup {
pub fn regex_oniguruma(&self) -> String {
let alts = self
.members
.iter()
.map(|m| escape_oniguruma(m))
.collect::<Vec<_>>()
.join("|");
match (self.kind, self.case_insensitive) {
(MatchKind::Word, true) => format!("(?i)\\b({alts})\\b"),
(MatchKind::Word, false) => format!("\\b({alts})\\b"),
(MatchKind::Symbol, _) => format!("({alts})"),
}
}
}
fn is_word(spelling: &str) -> bool {
spelling
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
}
const BOOLEAN_WORDS: &[&str] = &["TRUE", "FALSE", "BOOL", "true", "false", "bool"];
const BOOLEAN_SYMBOLS: &[&str] = &["⊤", "⊥"];
pub struct Model {
pub groups: Vec<TokenGroup>,
}
impl Model {
pub fn build() -> Model {
let mut keyword_control: Vec<String> = Vec::new();
let mut keyword_other: Vec<String> = Vec::new();
let mut operator_words: Vec<String> = Vec::new();
let mut operator_symbols: Vec<String> = Vec::new();
let mut constant_words: Vec<String> = Vec::new();
let mut constant_symbols: Vec<String> = Vec::new();
let mut builtins: Vec<String> = Vec::new();
for kw in KEYWORDS {
let bucket = match kw.group {
KeywordGroup::Status | KeywordGroup::Inline => &mut keyword_other,
_ => &mut keyword_control,
};
for spelling in kw.spellings {
bucket.push(spelling.to_lowercase());
}
}
for word in BOOLEAN_WORDS {
constant_words.push((*word).to_string());
}
for sym in BOOLEAN_SYMBOLS {
constant_symbols.push((*sym).to_string());
}
for op in OPERATOR_SPELLINGS {
let is_atom = op.category == OperatorCategory::ExpressionAtom;
let (words, symbols) = if is_atom {
(&mut constant_words, &mut constant_symbols)
} else {
(&mut operator_words, &mut operator_symbols)
};
push_spelling(words, symbols, op.unicode);
push_spelling(words, symbols, op.ascii);
}
for word in BUILTIN_WORDS {
let covered = operator_words
.iter()
.chain(&constant_words)
.any(|o| o.eq_ignore_ascii_case(word));
if !covered {
builtins.push((*word).to_string());
}
}
let groups = vec![
word_group(Scope::KeywordControl, keyword_control, true),
word_group(Scope::KeywordOther, keyword_other, true),
symbol_group(Scope::ConstantLanguage, constant_symbols),
word_group(Scope::ConstantLanguage, constant_words, false),
word_group(Scope::SupportFunction, builtins, false),
symbol_group(Scope::KeywordOperator, operator_symbols),
word_group(Scope::KeywordOperator, operator_words, false),
];
Model { groups }
}
}
fn push_spelling(words: &mut Vec<String>, symbols: &mut Vec<String>, spelling: &str) {
if is_word(spelling) {
words.push(spelling.to_string());
} else {
symbols.push(spelling.to_string());
}
}
fn word_group(scope: Scope, mut members: Vec<String>, case_insensitive: bool) -> TokenGroup {
members.sort();
members.dedup();
TokenGroup {
scope,
kind: MatchKind::Word,
case_insensitive,
members,
}
}
pub(super) fn longest_first(a: &str, b: &str) -> std::cmp::Ordering {
b.len().cmp(&a.len()).then_with(|| a.cmp(b))
}
fn symbol_group(scope: Scope, mut members: Vec<String>) -> TokenGroup {
members.sort();
members.dedup();
members.sort_by(|a, b| longest_first(a, b));
TokenGroup {
scope,
kind: MatchKind::Symbol,
case_insensitive: false,
members,
}
}
pub fn escape_oniguruma(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if matches!(
c,
'\\' | '.' | '^' | '$' | '|' | '?' | '*' | '+' | '(' | ')' | '[' | ']' | '{' | '}'
) {
out.push('\\');
}
out.push(c);
}
out
}
pub(super) fn elisp_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
if c == '\\' || c == '"' {
out.push('\\');
}
out.push(c);
}
out.push('"');
out
}
pub mod paths {
pub const TEXTMATE: &str = "editors/vscode/syntaxes/eventb.tmLanguage.json";
pub const SUBLIME: &str = "editors/sublime/EventB/EventB.sublime-syntax";
pub const VIM: &str = "editors/neovim/syntax/eventb.vim";
pub const EMACS: &str = "editors/emacs/eventb-mode.el";
pub const TS_GRAMMAR: &str = "editors/tree-sitter-eventb/grammar.js";
pub const TS_HIGHLIGHTS: &str = "editors/tree-sitter-eventb/queries/highlights.scm";
pub const TS_TOKENS: &str = "editors/tree-sitter-eventb/test/tokens.json";
pub const ZED_HIGHLIGHTS: &str = "editors/zed/languages/eventb/highlights.scm";
pub const TS_EXAMPLES_DIR: &str = "editors/tree-sitter-eventb/examples";
pub const SNIPPETS_VSCODE: &str = "editors/vscode/snippets/eventb.json";
pub const NVIM_SNIPPETS_PACKAGE: &str = "editors/neovim/snippets/package.json";
pub const NVIM_SNIPPETS_JSON: &str = "editors/neovim/snippets/eventb.json";
pub const ZED_SNIPPETS: &str = "editors/zed/snippets/event-b.json";
pub const COPIES: &[(&str, &str)] = &[
(SNIPPETS_VSCODE, ZED_SNIPPETS),
(TS_HIGHLIGHTS, ZED_HIGHLIGHTS),
(
"crates/rossi/examples/bank_account_machine.eventb",
"editors/tree-sitter-eventb/examples/bank_account_machine.eventb",
),
(
"crates/rossi/examples/counter.eventb",
"editors/tree-sitter-eventb/examples/counter.eventb",
),
(
"crates/rossi/examples/simple_machine.eventb",
"editors/tree-sitter-eventb/examples/simple_machine.eventb",
),
(
"crates/rossi/examples/traffic_light_machine.eventb",
"editors/tree-sitter-eventb/examples/traffic_light_machine.eventb",
),
];
pub const EMACS_SNIPPETS_DIR: &str = "editors/emacs/snippets/eventb-mode";
pub const NVIM_OPERATORS: &str = "editors/neovim/lua/eventb/operators.lua";
pub const EMACS_INPUT: &str = "editors/emacs/eventb-input.el";
pub const SUBLIME_OPERATORS: &str = "editors/sublime/EventB/operators.py";
}
pub struct Markers {
pub begin: &'static str,
pub end: &'static str,
}
#[cfg(test)]
mod tests {
use super::*;
fn group(model: &Model, scope: Scope, kind: MatchKind) -> Vec<String> {
model
.groups
.iter()
.filter(|g| g.scope == scope && g.kind == kind)
.flat_map(|g| g.members.iter().cloned())
.collect()
}
fn all_members(model: &Model) -> Vec<String> {
model
.groups
.iter()
.flat_map(|g| g.members.iter().cloned())
.collect()
}
#[test]
fn every_canonical_spelling_is_classified_once() {
let model = Model::build();
let all = all_members(&model);
assert!(all.contains(&"context".to_string()));
assert!(all.contains(&"status".to_string()));
assert!(all.contains(&"theorem".to_string()));
assert!(all.contains(&"skip".to_string()));
assert!(all.iter().any(|m| m == "<=>"));
assert!(all.iter().any(|m| m == "|->"));
assert!(all.iter().any(|m| m == "∈"));
assert!(all.iter().any(|m| m == "ℙ"));
assert!(all.contains(&"POW".to_string()));
assert!(!all.contains(&"pow".to_string()));
assert!(all.contains(&"card".to_string()));
assert!(all.contains(&"partition".to_string()));
}
#[test]
fn word_case_policy_mirrors_the_grammar() {
let model = Model::build();
let find = |member: &str| {
model
.groups
.iter()
.find(|g| g.members.iter().any(|m| m == member))
.unwrap_or_else(|| panic!("{member:?} not classified"))
};
for ci in ["context", "skip"] {
assert!(find(ci).case_insensitive, "{ci:?} must match (?i)");
}
for exact in [
"true", "TRUE", "BOOL", "NAT1", "UNION", "INTER", "dom", "ran", "mod", "or", "POW",
"POW1", "card", "finite",
] {
assert!(
!find(exact).case_insensitive,
"{exact:?} must match exact-case (Rodin reserves exact spellings; \
`nat`, `Union`, `DOM`, `Card`, `pow` are ordinary identifiers)"
);
}
assert!(find("dom").regex_oniguruma().starts_with("\\b("));
assert!(find("context").regex_oniguruma().starts_with("(?i)"));
}
#[test]
fn symbols_are_longest_first() {
let model = Model::build();
let ops = group(&model, Scope::KeywordOperator, MatchKind::Symbol);
let pos = |needle: &str| ops.iter().position(|m| m == needle);
assert!(pos("<=>") < pos("<="));
assert!(pos("<=") < pos("<"));
assert!(pos("|->") < pos("|"));
assert!(pos(":=") < pos(":"));
}
#[test]
fn no_stale_tokens_leak_in() {
let model = Model::build();
let all = all_members(&model);
for stale in ["extended", "℘", "⁻¹", "⊲", ">-<", ":<-"] {
assert!(
!all.iter().any(|m| m == stale),
"stale token {stale:?} leaked into the model"
);
}
}
#[test]
fn booleans_read_as_constants_not_builtins() {
let model = Model::build();
let consts = group(&model, Scope::ConstantLanguage, MatchKind::Word);
let funcs = group(&model, Scope::SupportFunction, MatchKind::Word);
for b in ["TRUE", "FALSE", "BOOL", "true", "false", "bool"] {
assert!(consts.contains(&b.to_string()), "{b} should be a constant");
assert!(!funcs.contains(&b.to_string()));
}
assert!(funcs.contains(&"card".to_string()));
assert!(funcs.contains(&"finite".to_string()));
let const_words = model
.groups
.iter()
.find(|g| g.scope == Scope::ConstantLanguage && g.kind == MatchKind::Word)
.expect("constant word group");
assert!(!const_words.case_insensitive);
}
#[test]
fn boolean_words_are_reserved_tokens() {
for w in BOOLEAN_WORDS {
assert!(
rossi::builtins::is_reserved_name(w),
"BOOLEAN_WORDS entry {w:?} is not a reserved keyword token"
);
}
}
#[test]
fn boolean_symbols_render_the_literals() {
use rossi::ast::predicate::{Predicate, PredicateKind};
let pp = rossi::pretty::PrettyPrinter::new();
let glyph = |kind| pp.print_predicate(&Predicate::from(kind));
assert!(BOOLEAN_SYMBOLS.contains(&glyph(PredicateKind::True).as_str()));
assert!(BOOLEAN_SYMBOLS.contains(&glyph(PredicateKind::False).as_str()));
assert_eq!(BOOLEAN_SYMBOLS.len(), 2);
}
#[test]
fn nvim_operators_match_lsp_rows() {
use eventb_lsp::server::operator_rows;
let rendered = operators_nvim::render();
let rows = operator_rows();
let emitted = rendered.matches("{ ascii = ").count();
assert_eq!(
emitted,
rows.len(),
"operators.lua emitted {emitted} rows but the LSP serves {}",
rows.len()
);
let s = operators_nvim::lua_string;
let mut cursor = 0usize;
for row in &rows {
let aliases = row
.aliases
.iter()
.map(|a| s(a))
.collect::<Vec<_>>()
.join(", ");
let aliases = if aliases.is_empty() {
String::new()
} else {
format!(" {aliases} ")
};
let needle = format!(
"{{ ascii = {}, unicode = {}, aliases = {{{}}}, symbolic = {}, eager = {} }}",
s(&row.ascii),
s(&row.unicode),
aliases,
row.symbolic,
row.eager
);
let at = rendered[cursor..].find(&needle).unwrap_or_else(|| {
panic!("operators.lua is missing row {needle} (or it is out of order)")
});
cursor += at + needle.len();
}
}
#[test]
fn emacs_quail_matches_lsp_rows() {
use eventb_lsp::server::operator_rows;
let rendered = input_emacs::render();
let rows = operator_rows();
let es = super::elisp_string;
let mut expected_rules = 0usize;
for row in &rows {
let unicode = es(&row.unicode);
for alias in &row.aliases {
let rule = format!("({} {})", es(&format!("\\{alias}")), unicode);
assert!(
rendered.contains(&rule),
"eventb-input.el is missing alias rule {rule}"
);
expected_rules += 1;
}
if !row.symbolic && !row.aliases.contains(&row.ascii) {
let rule = format!("({} {})", es(&format!("\\{}", row.ascii)), unicode);
assert!(
rendered.contains(&rule),
"eventb-input.el is missing alphabetic rule {rule}"
);
expected_rules += 1;
}
}
let emitted = rendered.matches("(\"\\\\").count();
assert_eq!(
emitted, expected_rules,
"eventb-input.el emitted {emitted} rules but {expected_rules} were expected"
);
}
#[test]
fn sublime_operators_match_lsp_rows() {
use eventb_lsp::server::operator_rows;
let rendered = operators_sublime::render();
let rows = operator_rows();
let emitted = rendered.matches("\"ascii\":").count();
assert_eq!(
emitted,
rows.len(),
"operators.py emitted {emitted} rows but the LSP serves {}",
rows.len()
);
let s = operators_sublime::py_string;
let b = operators_sublime::py_bool;
let mut cursor = 0usize;
for row in &rows {
let aliases = row
.aliases
.iter()
.map(|a| s(a))
.collect::<Vec<_>>()
.join(", ");
let needle = format!(
"{{\"ascii\": {}, \"unicode\": {}, \"aliases\": [{}], \"symbolic\": {}, \"eager\": {}}}",
s(&row.ascii),
s(&row.unicode),
aliases,
b(row.symbolic),
b(row.eager),
);
let at = rendered[cursor..].find(&needle).unwrap_or_else(|| {
panic!("operators.py is missing row {needle} (or it is out of order)")
});
cursor += at + needle.len();
}
}
}