omnigraph_compiler/
error.rs1use thiserror::Error;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub struct SourceSpan {
5 pub start: usize,
6 pub end: usize,
7}
8
9impl SourceSpan {
10 pub fn new(start: usize, end: usize) -> Self {
11 Self { start, end }
12 }
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ParseDiagnostic {
17 pub message: String,
18 pub span: Option<SourceSpan>,
19}
20
21impl ParseDiagnostic {
22 pub fn new(message: String, span: Option<SourceSpan>) -> Self {
23 Self { message, span }
24 }
25}
26
27impl std::fmt::Display for ParseDiagnostic {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 write!(f, "{}", self.message)
30 }
31}
32
33impl std::error::Error for ParseDiagnostic {}
34
35pub fn render_span(span: SourceSpan) -> SourceSpan {
36 SourceSpan {
37 start: span.start,
38 end: span.end.max(span.start.saturating_add(1)),
39 }
40}
41
42pub fn decode_string_literal(raw: &str) -> Result<String> {
43 let inner = raw
44 .strip_prefix('"')
45 .and_then(|inner| inner.strip_suffix('"'))
46 .unwrap_or(raw);
47
48 let mut decoded = String::with_capacity(inner.len());
49 let mut chars = inner.chars();
50 while let Some(ch) = chars.next() {
51 if ch != '\\' {
52 decoded.push(ch);
53 continue;
54 }
55
56 let escaped = chars
57 .next()
58 .ok_or_else(|| CompilerError::Parse("unterminated escape sequence".to_string()))?;
59 match escaped {
60 '"' => decoded.push('"'),
61 '\\' => decoded.push('\\'),
62 'n' => decoded.push('\n'),
63 'r' => decoded.push('\r'),
64 't' => decoded.push('\t'),
65 other => {
66 return Err(CompilerError::Parse(format!(
67 "unsupported escape sequence: \\{}",
68 other
69 )));
70 }
71 }
72 }
73
74 Ok(decoded)
75}
76
77#[derive(Debug, Error)]
78pub enum CompilerError {
79 #[error("parse error: {0}")]
80 Parse(String),
81
82 #[error("catalog error: {0}")]
83 Catalog(String),
84
85 #[error("type error: {0}")]
86 Type(String),
87
88 #[error("storage error: {0}")]
89 Storage(String),
90
91 #[error(
92 "@unique constraint violation on {type_name}.{property}: duplicate value '{value}' at rows {first_row} and {second_row}"
93 )]
94 UniqueConstraint {
95 type_name: String,
96 property: String,
97 value: String,
98 first_row: usize,
99 second_row: usize,
100 },
101
102 #[error("plan error: {0}")]
103 Plan(String),
104
105 #[error("execution error: {0}")]
106 Execution(String),
107
108 #[error(transparent)]
109 Arrow(#[from] arrow_schema::ArrowError),
110
111 #[error("io error: {0}")]
112 Io(#[from] std::io::Error),
113
114 #[error("lance error: {0}")]
115 Lance(String),
116
117 #[error("manifest error: {0}")]
118 Manifest(String),
119}
120
121#[deprecated(note = "use CompilerError")]
122pub type NanoError = CompilerError;
123
124pub type Result<T> = std::result::Result<T, CompilerError>;
125
126#[cfg(test)]
127mod tests {
128 use std::path::Path;
129
130 use super::{CompilerError, SourceSpan, decode_string_literal, render_span};
131
132 #[test]
133 fn source_span_preserves_zero_width() {
134 let span = SourceSpan::new(7, 7);
135 assert_eq!(span.start, 7);
136 assert_eq!(span.end, 7);
137 }
138
139 #[test]
140 fn render_span_widens_zero_width_for_diagnostics() {
141 let rendered = render_span(SourceSpan::new(7, 7));
142 assert_eq!(rendered.start, 7);
143 assert_eq!(rendered.end, 8);
144 }
145
146 #[test]
147 fn decode_string_literal_supports_common_escapes() {
148 let decoded = decode_string_literal("\"a\\n\\r\\t\\\\\\\"b\"").unwrap();
149 assert_eq!(decoded, "a\n\r\t\\\"b");
150 }
151
152 #[test]
153 fn compiler_error_parse_display_is_stable() {
154 let err = CompilerError::Parse("bad token".to_string());
155 assert_eq!(err.to_string(), "parse error: bad token");
156 }
157
158 #[allow(deprecated)]
159 #[test]
160 fn legacy_nano_error_alias_constructs_variants() {
161 let err = super::NanoError::Parse("bad token".to_string());
162 assert_eq!(err.to_string(), "parse error: bad token");
163 }
164
165 #[test]
166 fn legacy_name_is_confined_to_alias_and_compatibility_test() {
167 let legacy_name = ["Nano", "Error"].concat();
168 let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
169 .parent()
170 .and_then(Path::parent)
171 .expect("compiler crate should live under crates/");
172 let allowed_file = workspace_root.join("crates/omnigraph-compiler/src/error.rs");
173 let mut offenders = Vec::new();
174
175 visit_rs_files(workspace_root, &mut |path| {
176 let text = std::fs::read_to_string(path).expect("source file should be readable");
177 let count = text.matches(&legacy_name).count();
178 if path == allowed_file {
179 if count != 2 {
180 offenders.push(format!(
181 "{} contains {count} legacy-name occurrences; expected exactly 2",
182 display_path(workspace_root, path)
183 ));
184 }
185 } else if count > 0 {
186 offenders.push(format!(
187 "{} contains {count} legacy-name occurrence(s)",
188 display_path(workspace_root, path)
189 ));
190 }
191 });
192
193 assert!(
194 offenders.is_empty(),
195 "legacy compiler error name should stay compatibility-only:\n{}",
196 offenders.join("\n")
197 );
198 }
199
200 fn visit_rs_files(dir: &Path, visit: &mut impl FnMut(&Path)) {
201 for entry in std::fs::read_dir(dir).expect("source directory should be readable") {
202 let entry = entry.expect("source entry should be readable");
203 let path = entry.path();
204 if path.is_dir() {
205 if matches!(
206 path.file_name().and_then(|name| name.to_str()),
207 Some(".git" | "target")
208 ) {
209 continue;
210 }
211 visit_rs_files(&path, visit);
212 } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
213 visit(&path);
214 }
215 }
216 }
217
218 fn display_path(root: &Path, path: &Path) -> String {
219 path.strip_prefix(root)
220 .unwrap_or(path)
221 .to_string_lossy()
222 .into_owned()
223 }
224}