use nom_language::error::{VerboseError, VerboseErrorKind};
use std::fmt::Display;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KipErrorCode {
InvalidSyntax,
InvalidIdentifier,
TypeMismatch,
ConstraintViolation,
InvalidValueType,
ReferenceError,
NotFound,
DuplicateExists,
ImmutableTarget,
ExecutionTimeout,
ResourceExhausted,
InternalError,
}
impl KipErrorCode {
pub fn code(&self) -> &'static str {
match self {
Self::InvalidSyntax => "KIP_1001",
Self::InvalidIdentifier => "KIP_1002",
Self::TypeMismatch => "KIP_2001",
Self::ConstraintViolation => "KIP_2002",
Self::InvalidValueType => "KIP_2003",
Self::ReferenceError => "KIP_3001",
Self::NotFound => "KIP_3002",
Self::DuplicateExists => "KIP_3003",
Self::ImmutableTarget => "KIP_3004",
Self::ExecutionTimeout => "KIP_4001",
Self::ResourceExhausted => "KIP_4002",
Self::InternalError => "KIP_4003",
}
}
pub fn name(&self) -> &'static str {
match self {
Self::InvalidSyntax => "InvalidSyntax",
Self::InvalidIdentifier => "InvalidIdentifier",
Self::TypeMismatch => "TypeMismatch",
Self::ConstraintViolation => "ConstraintViolation",
Self::InvalidValueType => "InvalidValueType",
Self::ReferenceError => "ReferenceError",
Self::NotFound => "NotFound",
Self::DuplicateExists => "DuplicateExists",
Self::ImmutableTarget => "ImmutableTarget",
Self::ExecutionTimeout => "ExecutionTimeout",
Self::ResourceExhausted => "ResourceExhausted",
Self::InternalError => "InternalError",
}
}
pub fn hint(&self) -> &'static str {
match self {
Self::InvalidSyntax => {
"Check parenthesis matching, keyword spelling, and statement structure. Ensure JSON data format is valid."
}
Self::InvalidIdentifier => "Identifiers must match regex `[a-zA-Z_][a-zA-Z0-9_]*`.",
Self::TypeMismatch => {
"Execute `DESCRIBE` to confirm type names. Remember types are case-sensitive (`Drug` vs `drug`)."
}
Self::ConstraintViolation => "Supply the missing required attributes.",
Self::InvalidValueType => "Correct the JSON value type.",
Self::ReferenceError => {
"Ensure the variable is defined and bound in the WHERE clause (for KQL) or the CONCEPT block is placed before referencing clauses (for KML)."
}
Self::NotFound => {
"Target may have been deleted or never created. Try `SEARCH` or `FIND` to confirm existence first."
}
Self::DuplicateExists => {
"If intent is update, check if `UPSERT` should be used instead of creation logic."
}
Self::ImmutableTarget => {
"**Operation Prohibited.** Do not attempt to modify system meta-definitions or core identity nodes."
}
Self::ExecutionTimeout => {
"Optimize query. Reduce `UNION` usage, lower `LIMIT`, or reduce regex/hops."
}
Self::ResourceExhausted => "Must use `LIMIT` and `CURSOR` for pagination.",
Self::InternalError => "Contact system administrator or retry later.",
}
}
}
impl Display for KipErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.code())
}
}
#[derive(Error, Debug, Clone)]
#[error("{code}: {message}")]
pub struct KipError {
pub code: KipErrorCode,
pub message: String,
}
impl KipError {
pub fn new(code: KipErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
pub fn code_str(&self) -> &'static str {
self.code.code()
}
pub fn name(&self) -> &'static str {
self.code.name()
}
pub fn hint(&self) -> &'static str {
self.code.hint()
}
pub fn invalid_syntax(err: impl Display) -> Self {
Self::new(KipErrorCode::InvalidSyntax, format!("{err}"))
}
pub fn invalid_identifier(err: impl Display) -> Self {
Self::new(KipErrorCode::InvalidIdentifier, format!("{err}"))
}
pub fn type_mismatch(err: impl Display) -> Self {
Self::new(KipErrorCode::TypeMismatch, format!("{err}"))
}
pub fn constraint_violation(err: impl Display) -> Self {
Self::new(KipErrorCode::ConstraintViolation, format!("{err}"))
}
pub fn invalid_value_type(err: impl Display) -> Self {
Self::new(KipErrorCode::InvalidValueType, format!("{err}"))
}
pub fn reference_error(err: impl Display) -> Self {
Self::new(KipErrorCode::ReferenceError, format!("{err}"))
}
pub fn not_found(err: impl Display) -> Self {
Self::new(KipErrorCode::NotFound, format!("{err}"))
}
pub fn duplicate_exists(err: impl Display) -> Self {
Self::new(KipErrorCode::DuplicateExists, format!("{err}"))
}
pub fn immutable_target(err: impl Display) -> Self {
Self::new(KipErrorCode::ImmutableTarget, format!("{err}"))
}
pub fn execution_timeout(err: impl Display) -> Self {
Self::new(KipErrorCode::ExecutionTimeout, format!("{err}"))
}
pub fn resource_exhausted(err: impl Display) -> Self {
Self::new(KipErrorCode::ResourceExhausted, format!("{err}"))
}
pub fn internal_error(err: impl Display) -> Self {
Self::new(KipErrorCode::InternalError, format!("{err}"))
}
}
pub fn format_nom_error(input: &str, err: nom::Err<VerboseError<&str>>) -> KipError {
let message = match err {
nom::Err::Incomplete(needed) => {
format!("Parse incomplete, need more input: {needed:?}")
}
nom::Err::Error(ve) | nom::Err::Failure(ve) => format_verbose_error(input, ve),
};
KipError::invalid_syntax(message)
}
fn format_verbose_error(input: &str, ve: VerboseError<&str>) -> String {
let mut msg = String::new();
let contexts: Vec<&str> = ve
.errors
.iter()
.filter_map(|(_, kind)| match kind {
VerboseErrorKind::Context(ctx) => Some(*ctx),
_ => None,
})
.collect();
let deepest = ve.errors.first();
let is_at_start = deepest
.map(|(slice, _)| slice.len() == input.len())
.unwrap_or(false);
if !contexts.is_empty() && !is_at_start {
msg.push_str("Parsing context: ");
let display_contexts: Vec<&str> = contexts.iter().rev().copied().collect();
let start_idx = if display_contexts.len() > 3 {
display_contexts.len() - 3
} else {
0
};
for (i, ctx) in display_contexts[start_idx..].iter().enumerate() {
if i > 0 {
msg.push_str(" > ");
}
msg.push_str(ctx);
}
msg.push('\n');
}
if let Some((slice, kind)) = deepest {
let error_desc = match kind {
VerboseErrorKind::Char(c) => {
let got = slice.chars().next();
match got {
Some(g) => format!("Expected '{}', but found '{}'", c, g),
None => format!("Expected '{}', but reached end of input", c),
}
}
VerboseErrorKind::Context(ctx) => format!("Failed to parse: {ctx}"),
VerboseErrorKind::Nom(e) => match e {
nom::error::ErrorKind::Tag => {
if is_at_start {
let got = take_first_chars(slice, 20);
format!(
"Unrecognized KIP command starting with: \"{}\". \
A KIP statement must begin with FIND, UPSERT, DELETE, DESCRIBE, or SEARCH (case-sensitive).",
got
)
} else {
let got = take_first_chars(slice, 30);
format!("Expected a keyword, but found: \"{}\"", got)
}
}
nom::error::ErrorKind::Char => "Unexpected character".to_string(),
nom::error::ErrorKind::Alpha => {
let got = slice.chars().next();
match got {
Some(g) => {
format!("Expected a letter (a-z, A-Z), but found '{}'", g)
}
None => {
"Expected a letter (a-z, A-Z), but reached end of input".to_string()
}
}
}
nom::error::ErrorKind::Verify => "Validation check failed".to_string(),
nom::error::ErrorKind::MapRes => "Value conversion/validation failed".to_string(),
nom::error::ErrorKind::Eof => {
let remaining = take_first_chars(slice, 60);
format!(
"Unexpected trailing content after valid KIP statement: \"{}\"",
remaining
)
}
nom::error::ErrorKind::Alt => {
if is_at_start {
if slice.is_empty() {
"Empty input. A KIP statement must begin with FIND, UPSERT, DELETE, DESCRIBE, or SEARCH.".to_string()
} else {
let got = take_first_chars(slice, 20);
format!(
"Unrecognized KIP command starting with: \"{}\". \
A KIP statement must begin with FIND, UPSERT, DELETE, DESCRIBE, or SEARCH (case-sensitive).",
got
)
}
} else {
let got = take_first_chars(slice, 40);
format!("No valid KIP syntax matched at: \"{}\"", got)
}
}
_ => format!("Parser {:?} failed", e),
},
};
msg.push_str(&error_desc);
msg.push('\n');
let offset = input.len() - slice.len();
let (line, col) = compute_line_col(input, offset);
msg.push_str(&format!("Location: line {}, column {}\n", line, col));
if !input.is_empty() {
msg.push_str(&format_error_context_window(input, offset));
}
}
if let Some(suggestion) = generate_recovery_suggestion(&contexts, deepest, is_at_start) {
msg.push_str("\nSuggestion: ");
msg.push_str(&suggestion);
msg.push('\n');
}
msg
}
fn compute_line_col(input: &str, offset: usize) -> (usize, usize) {
let prefix = &input[..offset];
let line = prefix.chars().filter(|c| *c == '\n').count() + 1;
let last_newline = prefix.rfind('\n').map(|p| p + 1).unwrap_or(0);
let col = input[last_newline..offset].chars().count() + 1;
(line, col)
}
fn format_error_context_window(input: &str, offset: usize) -> String {
let mut result = String::new();
let lines: Vec<&str> = input.lines().collect();
let prefix = &input[..offset];
let error_line_idx = prefix.chars().filter(|c| *c == '\n').count(); let last_newline = prefix.rfind('\n').map(|p| p + 1).unwrap_or(0);
let error_col = input[last_newline..offset].chars().count();
let start = error_line_idx.saturating_sub(2);
let end = (error_line_idx + 2).min(lines.len());
result.push_str("Context:\n");
for (i, line_content) in lines.iter().enumerate().take(end).skip(start) {
let line_num = i + 1;
let trimmed = take_first_chars(line_content, 120);
if i == error_line_idx {
result.push_str(&format!(" --> | {}\n", trimmed));
let pointer_offset = error_col;
result.push_str(&format!(" | {}^\n", " ".repeat(pointer_offset)));
} else {
result.push_str(&format!(" {:>3} | {}\n", line_num, trimmed));
}
}
result
}
fn generate_recovery_suggestion(
contexts: &[&str],
deepest: Option<&(&str, VerboseErrorKind)>,
is_at_start: bool,
) -> Option<String> {
if is_at_start {
return Some(
"A KIP statement must start with one of: \
FIND (for queries), UPSERT/DELETE (for modifications), \
or DESCRIBE/SEARCH (for schema exploration). \
All keywords are case-sensitive and must be UPPERCASE."
.to_string(),
);
}
let innermost = contexts.first().copied().unwrap_or("");
if innermost.contains("JSON string") {
return Some(
"Check for unterminated strings: ensure every '\"' has a matching closing '\"'. \
Inside JSON strings, special characters must be escaped: \
use \\\" for quotes, \\\\ for backslashes, \\n for newlines."
.to_string(),
);
}
if innermost.contains("JSON object") || innermost.contains("key-value") {
if let Some((_, VerboseErrorKind::Char(c))) = deepest {
if *c == '}' {
return Some(
"Unclosed JSON object. Ensure every '{' has a matching '}'. \
Check for missing commas between key-value pairs, \
or unterminated string values inside the object."
.to_string(),
);
}
if *c == ':' {
return Some(
"Expected ':' after key in object. Format: { key: value } or { \"key\": value }. \
Keys can be unquoted identifiers (letters, digits, underscores) or quoted strings."
.to_string(),
);
}
}
return Some(
"Check JSON object syntax: { key: value, key2: value2 }. \
Keys can be identifiers or quoted strings. \
Values can be strings, numbers, booleans, null, arrays, or nested objects. \
Trailing commas are allowed."
.to_string(),
);
}
if innermost.contains("JSON array") {
return Some(
"Check JSON array syntax: [value1, value2, ...]. \
Ensure every '[' has a matching ']'. \
Trailing commas are allowed."
.to_string(),
);
}
if innermost.contains("FIND") {
return Some(
"FIND clause syntax: FIND(?variable) or FIND(?var1, ?var2, COUNT(?var3)). \
Variables start with '?' followed by an identifier. \
Aggregation functions: COUNT, SUM, AVG, MIN, MAX."
.to_string(),
);
}
if innermost.contains("WHERE") {
return Some(
"WHERE clause syntax: WHERE { <clauses> }. \
Each clause is either: a concept match (?var {type: \"T\", name: \"N\"}), \
a proposition match (?s, \"predicate\", ?o), \
FILTER(...), OPTIONAL {...}, NOT {...}, or UNION {...}."
.to_string(),
);
}
if innermost.contains("CONCEPT") && innermost.contains("?local_handle") {
return Some(
"CONCEPT block syntax: CONCEPT [?handle] { {type: \"T\", name: \"N\"} [SET ATTRIBUTES {...}] [SET PROPOSITIONS {...}] }. \
The concept matcher {type: \"...\", name: \"...\"} or {id: \"...\"} or {type: \"...\"} or {name: \"...\"} is required. \
Handle is optional: CONCEPT { ... } is also valid."
.to_string(),
);
}
if innermost.contains("PROPOSITION") && innermost.contains("?local_handle") {
return Some(
"PROPOSITION block syntax: PROPOSITION [?handle] { (subject, \"predicate\", object) [SET ATTRIBUTES {...}] }. \
Subject/object can be: ?variable, {type: \"T\", name: \"N\"}, {id: \"...\"}, or {type: \"...\"}, or {name: \"...\"}."
.to_string(),
);
}
if innermost.contains("SET ATTRIBUTES") {
return Some(
"SET ATTRIBUTES syntax: SET ATTRIBUTES { key: value, key2: value2 }. \
Keys are identifiers or quoted strings. Values are JSON values."
.to_string(),
);
}
if innermost.contains("SET PROPOSITIONS") {
return Some(
"SET PROPOSITIONS syntax: SET PROPOSITIONS { (\"predicate\", target) [WITH METADATA {...}] ... }. \
Target can be: ?variable, {type: \"T\", name: \"N\"}, {id: \"...\"}, or {type: \"...\"}, or {name: \"...\"}."
.to_string(),
);
}
if innermost.contains("WITH METADATA") {
return Some(
"WITH METADATA syntax: WITH METADATA { key: value, ... }. \
Common metadata keys: source, author, confidence (0.0-1.0), status."
.to_string(),
);
}
if innermost.contains("UPSERT") {
return Some(
"UPSERT block syntax: UPSERT { CONCEPT ... | PROPOSITION ... } [WITH METADATA {...}]. \
Must contain at least one CONCEPT or PROPOSITION block."
.to_string(),
);
}
if innermost.contains("DELETE") {
return Some(
"DELETE syntax variants: \
DELETE ATTRIBUTES {\"attr1\", \"attr2\"} FROM ?var WHERE {...}, \
DELETE METADATA {\"key1\"} FROM ?var WHERE {...}, \
DELETE PROPOSITIONS ?var WHERE {...}, \
DELETE CONCEPT ?var DETACH WHERE {...}."
.to_string(),
);
}
if innermost.contains("FILTER") {
return Some(
"FILTER syntax: FILTER(expression). \
Comparisons: ?var == value, ?var != value, ?var < value, ?var > value, ?var <= value, ?var >= value. \
Functions: CONTAINS(?var, \"text\"), STARTS_WITH(?var, \"prefix\"), ENDS_WITH(?var, \"suffix\"), REGEX(?var, \"pattern\"). \
Logical: expr && expr, expr || expr, !(expr)."
.to_string(),
);
}
if innermost.contains("DESCRIBE") || innermost.contains("SEARCH") {
return Some(
"META commands: DESCRIBE PRIMER | DESCRIBE DOMAINS | \
DESCRIBE CONCEPT TYPES [LIMIT N] | DESCRIBE CONCEPT TYPE \"TypeName\" | \
DESCRIBE PROPOSITION TYPES [LIMIT N] | DESCRIBE PROPOSITION TYPE \"pred\" | \
SEARCH CONCEPT \"term\" [WITH TYPE \"T\"] [LIMIT N] | \
SEARCH PROPOSITION \"term\" [WITH TYPE \"T\"] [LIMIT N]."
.to_string(),
);
}
if innermost.contains("concept matcher") || innermost.contains("proposition matcher") {
return Some(
"Concept matcher formats: {type: \"TypeName\", name: \"Name\"} or {id: \"ID\"} or {type: \"Name\"} or {name: \"Name\"}. \
Proposition matcher formats: (?subject, \"predicate\", ?object) or (id: \"proposition_id\")."
.to_string(),
);
}
if innermost.contains("dot notation") {
return Some(
"Dot path syntax: ?variable or ?variable.field or ?variable.field.subfield. \
Variable names must start with '?' followed by a letter or underscore, \
and can contain letters, digits, and underscores. \
Path segments follow the same identifier rules."
.to_string(),
);
}
if let Some((slice, kind)) = deepest {
if slice.is_empty() {
return Some(
"Unexpected end of input. Check for unclosed brackets { }, parentheses ( ), \
or unterminated strings \"...\". The KIP statement may be incomplete."
.to_string(),
);
}
if matches!(kind, VerboseErrorKind::Nom(nom::error::ErrorKind::Eof)) {
return Some(
"There is unexpected content after a valid KIP statement. \
Each parse_kip() call should contain exactly one complete statement. \
Remove the extra text or split into separate statements."
.to_string(),
);
}
}
if contexts.is_empty() {
return Some(
"A KIP statement must start with one of: \
FIND (for queries), UPSERT/DELETE (for modifications), \
or DESCRIBE/SEARCH (for schema exploration). \
Keywords are case-sensitive and must be UPPERCASE."
.to_string(),
);
}
None
}
fn take_first_chars(s: &str, n: usize) -> String {
s.chars().take(n).collect()
}