use nibli_types::error::NibliError;
use nibli_types::logic::LogicBuffer;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GateError {
Syntax(String),
Semantic(String),
RoundTrip(String),
Verification(String),
}
impl GateError {
pub fn gate(&self) -> &'static str {
match self {
GateError::Syntax(_) => "nibli-kr",
GateError::Semantic(_) => "semantics",
GateError::RoundTrip(_) => "round-trip",
GateError::Verification(_) => "semantic verifier",
}
}
pub fn message(&self) -> &str {
match self {
GateError::Syntax(m)
| GateError::Semantic(m)
| GateError::RoundTrip(m)
| GateError::Verification(m) => m,
}
}
}
pub fn local_gates(candidate: &str) -> Result<LogicBuffer, GateError> {
let ast = nibli_kr::parse_checked(candidate).map_err(syntax)?;
let buf = nibli_semantics::compile_from_ast(ast.clone()).map_err(semantic)?;
nibli_kr_round_trip(&ast, &buf)?;
Ok(buf)
}
fn nibli_kr_round_trip(
ast: &nibli_types::ast::AstBuffer,
buf: &LogicBuffer,
) -> Result<(), GateError> {
let rendered = nibli_kr::render::render(ast).map_err(|e| {
GateError::RoundTrip(format!(
"the canonical renderer could not re-spell the statement: {e}"
))
})?;
let ast2 = nibli_kr::parse_checked(&rendered).map_err(|e| {
GateError::RoundTrip(format!(
"the canonical re-spelling {rendered:?} failed to re-parse: {e}"
))
})?;
let buf2 = nibli_semantics::compile_from_ast(ast2).map_err(|e| {
GateError::RoundTrip(format!(
"the canonical re-spelling {rendered:?} failed to re-compile: {e}"
))
})?;
if buf2 != *buf {
return Err(GateError::RoundTrip(format!(
"the statement compiles, but its canonical re-spelling {rendered:?} compiles to \
different logic — prefer the canonical spelling"
)));
}
Ok(())
}
pub fn validate(candidate: &str) -> Result<LogicBuffer, GateError> {
local_gates(candidate)
}
pub fn validate_kb(text: &str) -> Result<(), GateError> {
for (i, raw) in text.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
validate(line).map(|_| ()).map_err(|e| tag_line(e, i + 1))?;
}
Ok(())
}
fn tag_line(e: GateError, line_no: usize) -> GateError {
let msg = format!("(KB line {line_no}) {}", e.message());
match e {
GateError::Syntax(_) => GateError::Syntax(msg),
GateError::Semantic(_) => GateError::Semantic(msg),
GateError::RoundTrip(_) => GateError::RoundTrip(msg),
GateError::Verification(_) => GateError::Verification(msg),
}
}
fn syntax(e: NibliError) -> GateError {
GateError::Syntax(e.to_string())
}
fn semantic(e: NibliError) -> GateError {
GateError::Semantic(e.to_string())
}
pub fn feedback_for(err: &GateError) -> String {
if let GateError::Verification(issues) = err {
return format!(
"That is grammatically valid but does not MEAN what the source says. An \
independent reading of what your nibli KR actually claims reported these \
mismatches:\n{issues}\nRevise so the meaning matches the source; output ONLY \
the corrected nibli KR — no explanation."
);
}
let (what, tool) = match err {
GateError::Syntax(_) => ("is not valid nibli KR", "nibli-kr compiler"),
GateError::Semantic(_) => (
"parses but failed semantic compilation (e.g. a predicate got the wrong number of arguments)",
"nibli-semantics compiler",
),
GateError::RoundTrip(_) => (
"compiles but is not canonical nibli KR (its canonical re-spelling does not compile to the same logic)",
"round-trip gate",
),
GateError::Verification(_) => unreachable!("handled above"),
};
format!(
"That {what}. The {tool} reported:\n{}\nFix it and output ONLY the corrected nibli KR — no explanation.",
err.message()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_nibli_kr_passes_local_gates() {
local_gates("dog(Adam).").expect("valid nibli KR should pass the three local gates");
}
#[test]
fn nibli_kr_garbage_fails_at_the_grammar_gate() {
let err = local_gates("dog(Adam") .expect_err("malformed nibli KR must be rejected");
assert!(
matches!(err, GateError::Syntax(_)),
"expected Syntax, got {err:?}"
);
assert_eq!(err.gate(), "nibli-kr");
}
#[test]
fn nibli_kr_unknown_alias_fails_closed_at_the_grammar_gate() {
let err = local_gates("zzyzxq(Adam).").expect_err("unknown alias must fail closed");
assert!(
matches!(err, GateError::Syntax(_)),
"expected Syntax (resolve), got {err:?}"
);
}
#[test]
fn nibli_kr_round_trip_gate_holds_on_shipped_shapes() {
for s in [
"dog(Adam).",
"animal(every dog).",
"beautiful(every person where ~cat).",
"Kim = Adam.",
"past dog(Dan).",
"red(exactly 2 red).",
"~eats(Adam).",
] {
validate(s)
.unwrap_or_else(|e| panic!("round-trip gate rejected shipped shape {s:?}: {e:?}"));
}
}
#[test]
fn feedback_names_the_nibli_kr_gates() {
let fb = feedback_for(&GateError::Syntax("[Syntax Error] line 1:5: nope".into()));
assert!(fb.contains("nibli-kr compiler"));
assert!(fb.contains("line 1:5"));
assert!(fb.contains("corrected nibli KR"));
let fb = feedback_for(&GateError::RoundTrip(
"canonical re-spelling differs".into(),
));
assert!(fb.contains("round-trip gate"));
assert!(fb.contains("corrected nibli KR"));
let fb = feedback_for(&GateError::Verification("1. off".into()));
assert!(fb.contains("your nibli KR"));
assert!(fb.contains("corrected nibli KR"));
}
#[test]
fn validate_kb_passes_valid_multiline_and_skips_blanks_and_comments() {
validate_kb("dog(Adam).\n# a note\n\neats(Adam).")
.expect("every non-comment line is valid nibli KR");
}
#[test]
fn validate_kb_reports_the_failing_line_number() {
let err = validate_kb("dog(Adam).\ndog(Adam").expect_err("line 2 is malformed nibli KR");
assert!(err.message().contains("KB line 2"), "got {err:?}");
}
}