use thiserror::Error; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SourceSpan { pub start: usize, pub end: usize, } impl SourceSpan { pub fn new(start: usize, end: usize) -> Self { Self { start, end } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParseDiagnostic { pub message: String, pub span: Option, } impl ParseDiagnostic { pub fn new(message: String, span: Option) -> Self { Self { message, span } } } impl std::fmt::Display for ParseDiagnostic { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.message) } } impl std::error::Error for ParseDiagnostic {} pub fn render_span(span: SourceSpan) -> SourceSpan { SourceSpan { start: span.start, end: span.end.max(span.start.saturating_add(1)), } } pub fn decode_string_literal(raw: &str) -> Result { let inner = raw .strip_prefix('"') .and_then(|inner| inner.strip_suffix('"')) .unwrap_or(raw); let mut decoded = String::with_capacity(inner.len()); let mut chars = inner.chars(); while let Some(ch) = chars.next() { if ch != '\\' { decoded.push(ch); continue; } let escaped = chars .next() .ok_or_else(|| CompilerError::Parse("unterminated escape sequence".to_string()))?; match escaped { '"' => decoded.push('"'), '\\' => decoded.push('\\'), 'n' => decoded.push('\n'), 'r' => decoded.push('\r'), 't' => decoded.push('\t'), other => { return Err(CompilerError::Parse(format!( "unsupported escape sequence: \\{}", other ))); } } } Ok(decoded) } #[derive(Debug, Error)] pub enum CompilerError { #[error("parse error: {0}")] Parse(String), #[error("catalog error: {0}")] Catalog(String), #[error("type error: {0}")] Type(String), #[error("storage error: {0}")] Storage(String), #[error( "@unique constraint violation on {type_name}.{property}: duplicate value '{value}' at rows {first_row} and {second_row}" )] UniqueConstraint { type_name: String, property: String, value: String, first_row: usize, second_row: usize, }, #[error("plan error: {0}")] Plan(String), #[error("execution error: {0}")] Execution(String), #[error(transparent)] Arrow(#[from] arrow_schema::ArrowError), #[error("io error: {0}")] Io(#[from] std::io::Error), #[error("lance error: {0}")] Lance(String), #[error("manifest error: {0}")] Manifest(String), } #[deprecated(note = "use CompilerError")] pub type NanoError = CompilerError; pub type Result = std::result::Result; #[cfg(test)] mod tests { use std::path::Path; use super::{CompilerError, SourceSpan, decode_string_literal, render_span}; #[test] fn source_span_preserves_zero_width() { let span = SourceSpan::new(7, 7); assert_eq!(span.start, 7); assert_eq!(span.end, 7); } #[test] fn render_span_widens_zero_width_for_diagnostics() { let rendered = render_span(SourceSpan::new(7, 7)); assert_eq!(rendered.start, 7); assert_eq!(rendered.end, 8); } #[test] fn decode_string_literal_supports_common_escapes() { let decoded = decode_string_literal("\"a\\n\\r\\t\\\\\\\"b\"").unwrap(); assert_eq!(decoded, "a\n\r\t\\\"b"); } #[test] fn compiler_error_parse_display_is_stable() { let err = CompilerError::Parse("bad token".to_string()); assert_eq!(err.to_string(), "parse error: bad token"); } #[allow(deprecated)] #[test] fn legacy_nano_error_alias_constructs_variants() { let err = super::NanoError::Parse("bad token".to_string()); assert_eq!(err.to_string(), "parse error: bad token"); } #[test] fn legacy_name_is_confined_to_alias_and_compatibility_test() { let legacy_name = ["Nano", "Error"].concat(); let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .and_then(Path::parent) .expect("compiler crate should live under crates/"); let allowed_file = workspace_root.join("crates/omnigraph-compiler/src/error.rs"); let mut offenders = Vec::new(); visit_rs_files(&workspace_root.join("crates"), &mut |path| { let text = std::fs::read_to_string(path).expect("source file should be readable"); let count = text.matches(&legacy_name).count(); if path == allowed_file { if count != 2 { offenders.push(format!( "{} contains {count} legacy-name occurrences; expected exactly 2", display_path(workspace_root, path) )); } } else if count > 0 { offenders.push(format!( "{} contains {count} legacy-name occurrence(s)", display_path(workspace_root, path) )); } }); assert!( offenders.is_empty(), "legacy compiler error name should stay compatibility-only:\n{}", offenders.join("\n") ); } fn visit_rs_files(dir: &Path, visit: &mut impl FnMut(&Path)) { for entry in std::fs::read_dir(dir).expect("source directory should be readable") { let entry = entry.expect("source entry should be readable"); let path = entry.path(); if path.is_dir() { visit_rs_files(&path, visit); } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") { visit(&path); } } } fn display_path(root: &Path, path: &Path) -> String { path.strip_prefix(root) .unwrap_or(path) .to_string_lossy() .into_owned() } }