use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum CompileError {
#[error("{0}")]
Parse(elenchus_parser::Diagnostics),
#[error("'{name}' redefined with a different body")]
PremiseRedefinition {
name: String,
},
#[error("{file}: missing a DOMAIN declaration (every file must start with `DOMAIN <name>`)")]
MissingDomain {
file: String,
},
#[error("{file}: more than one DOMAIN declaration (a file has exactly one domain)")]
DuplicateDomain {
file: String,
},
#[error("unknown domain '{domain}' — declare it with DOMAIN, or IMPORT it in this file")]
UnknownDomain {
domain: String,
},
#[error("domain name '{alias}' is bound to two different imports (disambiguate with AS)")]
DomainAliasClash {
alias: String,
},
#[error("import not found: {0}")]
ImportNotFound(String),
#[error("circular import: {0}")]
CircularImport(String),
#[error("rule '{name}' cannot derive a disjunction (OR in THEN); use a PREMISE instead")]
RuleDisjunctiveConsequent {
name: String,
},
#[error(transparent)]
UnknownValue(Box<UnknownValue>),
#[error("{file}:{line}: FOR EACH ranges over '{set}', which is not a declared SET{suggestion}")]
UnknownSet {
file: String,
line: u32,
set: String,
suggestion: String,
},
#[error(
"{file}:{line}: relation '{relation}' has a cycle (`{node}` reaches itself) \
— CLOSE … TRANSITIVE requires a DAG"
)]
CyclicRelation {
file: String,
line: u32,
relation: String,
node: String,
},
#[error(
"{file}:{line}: '{name}' is a bare proposition but no VAR declares it \
— add `VAR {name}`{suggestion}"
)]
UndeclaredPort {
file: String,
line: u32,
name: String,
suggestion: String,
},
#[error("no VAR declares the port '{name}' that an external value sets{suggestion}")]
UnknownPort {
name: String,
suggestion: String,
},
#[error(
"'{name}' is declared in multiple domains ({domains}); qualify which one as `<domain>.{name}`"
)]
AmbiguousPort {
name: String,
domains: String,
},
#[error(
"no atom '{name}' is used in the program, so an external value cannot set it \
(use it in a FACT/PREMISE/RULE, or fix a typo)"
)]
UnknownExternalAtom {
name: String,
},
#[error(
"the port '{name}' is set to two different values: {a_value} (from {a_origin}) \
and {b_value} (from {b_origin})"
)]
PortConflict {
name: String,
a_value: bool,
a_origin: String,
b_value: bool,
b_origin: String,
},
#[error("{file}:{line}: a data file may only contain PROVIDE (and DOMAIN), not this statement")]
DataFileStatement {
file: String,
line: u32,
},
}
#[derive(Debug, Error, PartialEq, Eq)]
#[error(
"{file}:{line}: '{value}' is not a declared value of '{subject} {predicate}' \
— ONEOF declares {{ {declared} }}{suggestion}"
)]
pub struct UnknownValue {
pub file: String,
pub line: u32,
pub subject: String,
pub predicate: String,
pub value: String,
pub declared: String,
pub suggestion: String,
}
pub(crate) fn nearest_set_suggestion(set: &str, sets: &BTreeMap<String, Vec<String>>) -> String {
let names: Vec<&str> = sets.keys().map(String::as_str).collect();
did_you_mean(set, &names)
}
pub fn levenshtein(a: &[char], b: &[char]) -> usize {
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut cur = vec![0usize; b.len() + 1];
for (i, &ca) in a.iter().enumerate() {
cur[0] = i + 1;
for (j, &cb) in b.iter().enumerate() {
let cost = usize::from(ca != cb);
cur[j + 1] = (prev[j + 1] + 1).min(cur[j] + 1).min(prev[j] + cost);
}
core::mem::swap(&mut prev, &mut cur);
}
prev[b.len()]
}
pub(crate) fn nearest<'a>(word: &str, candidates: &[&'a str]) -> Option<&'a str> {
let budget = word.chars().count() / 3;
if budget == 0 {
return None;
}
let w: Vec<char> = word.chars().collect();
candidates
.iter()
.map(|&c| (levenshtein(&w, &c.chars().collect::<Vec<char>>()), c))
.filter(|&(d, _)| d <= budget)
.min_by_key(|&(d, _)| d)
.map(|(_, c)| c)
}
pub(crate) fn did_you_mean(word: &str, candidates: &[&str]) -> String {
match nearest(word, candidates) {
Some(s) => alloc::format!(" — did you mean `{s}`?"),
None => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;
#[test]
fn levenshtein_basics() {
fn lev(a: &str, b: &str) -> usize {
levenshtein(
&a.chars().collect::<Vec<char>>(),
&b.chars().collect::<Vec<char>>(),
)
}
assert_eq!(lev("", ""), 0);
assert_eq!(lev("abc", "abc"), 0);
assert_eq!(lev("censoredmtp", "censored_mtp"), 1);
assert_eq!(lev("norml", "normal"), 1);
assert_eq!(lev("kitten", "sitting"), 3);
}
#[test]
fn nearest_respects_the_length_budget() {
let cands = ["censored", "censored_mtp", "uncensored"];
assert_eq!(nearest("censoredmtp", &cands), Some("censored_mtp"));
assert_eq!(nearest("zzz", &cands), None);
}
#[test]
fn nearest_offers_nothing_for_very_short_values() {
assert_eq!(nearest("七", &["一", "二", "三"]), None);
assert_eq!(nearest("us", &["uk", "eu", "jp"]), None);
assert_eq!(nearest("中文字", &["中文学", "日本語"]), Some("中文学"));
}
}