oxur_comp/
error_translator.rs

1//! Error translation
2//!
3//! Translates rustc compilation errors from generated Rust code positions
4//! back to original Oxur source code positions using the SourceMap.
5//!
6//! # Current Implementation
7//!
8//! Stage 1.10 provides the infrastructure for error translation but doesn't
9//! yet implement full position lookup. Error messages show:
10//! - The error message from rustc
11//! - The generated Rust file position (as fallback)
12//! - A note that full translation is not yet implemented
13//!
14//! # Future Enhancement (Phase 2)
15//!
16//! Full position translation requires a reverse index:
17//! - Rust Position (file:line:col) → Rust NodeId
18//! - Built during lowering when generating syn nodes
19//! - Fast lookup at error translation time
20//!
21//! This will enable errors like:
22//! ```text
23//! error: cannot find value `x` in this scope
24//!   --> example.oxur:2:8
25//! ```
26//!
27//! Instead of:
28//! ```text
29//! error: cannot find value `x` in this scope
30//!   --> generated.rs:5:10
31//! ```
32
33use crate::RustcDiagnostic;
34use oxur_smap::SourceMap;
35
36/// Translates rustc error positions to Oxur source positions
37pub struct ErrorTranslator {
38    source_map: SourceMap,
39}
40
41impl ErrorTranslator {
42    /// Create a new error translator with the given source map
43    pub fn new(source_map: SourceMap) -> Self {
44        Self { source_map }
45    }
46
47    /// Translate a rustc diagnostic to Oxur source positions
48    ///
49    /// Returns a formatted error message with Oxur positions where possible.
50    /// If translation is not possible, falls back to showing Rust positions.
51    pub fn translate_diagnostic(&self, diagnostic: &RustcDiagnostic) -> String {
52        let mut output = String::new();
53
54        // Extract primary position from rustc diagnostic
55        if let Some((rust_file, rust_line, rust_col)) = diagnostic.primary_position() {
56            // TODO: Look up Rust position in reverse index
57            // For now, show Rust position as fallback
58
59            output.push_str(&format!("error: {}\n", diagnostic.message));
60
61            // Show Rust position (fallback until reverse index implemented)
62            output.push_str(&format!("  --> {}:{}:{}\n", rust_file, rust_line, rust_col));
63
64            // TODO: Show Oxur position when translation available
65            output.push_str("  (Note: Error position translation not yet implemented)\n");
66        } else {
67            // No position available
68            output.push_str(&format!("error: {}\n", diagnostic.message));
69        }
70
71        // Show error code if available
72        if let Some(code) = &diagnostic.code {
73            output.push_str(&format!("  code: {}\n", code.code));
74        }
75
76        output
77    }
78
79    /// Translate all diagnostics and format as a single error message
80    pub fn translate_diagnostics(&self, diagnostics: &[RustcDiagnostic]) -> String {
81        diagnostics
82            .iter()
83            .filter(|d| d.is_error())
84            .map(|d| self.translate_diagnostic(d))
85            .collect::<Vec<_>>()
86            .join("\n")
87    }
88
89    /// Get a reference to the source map
90    pub fn source_map(&self) -> &SourceMap {
91        &self.source_map
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::RustcDiagnostic;
99
100    #[test]
101    fn test_translator_creation() {
102        let source_map = oxur_smap::SourceMap::new();
103        let translator = ErrorTranslator::new(source_map);
104        assert_eq!(translator.source_map().stats().surface_nodes, 0);
105    }
106
107    #[test]
108    fn test_translate_diagnostic_with_position() {
109        let source_map = oxur_smap::SourceMap::new();
110        let translator = ErrorTranslator::new(source_map);
111
112        let json = r#"{
113            "message": "cannot find value `x` in this scope",
114            "code": {
115                "code": "E0425",
116                "explanation": null
117            },
118            "level": "error",
119            "spans": [
120                {
121                    "file_name": "generated.rs",
122                    "byte_start": 42,
123                    "byte_end": 43,
124                    "line_start": 5,
125                    "line_end": 5,
126                    "column_start": 10,
127                    "column_end": 11,
128                    "is_primary": true,
129                    "text": [],
130                    "label": "not found in this scope",
131                    "suggested_replacement": null,
132                    "suggestion_applicability": null,
133                    "expansion": null
134                }
135            ],
136            "children": [],
137            "rendered": null
138        }"#;
139
140        let diagnostic = RustcDiagnostic::from_json(json).unwrap();
141        let output = translator.translate_diagnostic(&diagnostic);
142
143        // Should contain error message
144        assert!(output.contains("cannot find value `x`"));
145
146        // Should contain Rust position (fallback)
147        assert!(output.contains("generated.rs:5:10"));
148
149        // Should contain error code
150        assert!(output.contains("E0425"));
151
152        // Should note that translation isn't implemented yet
153        assert!(output.contains("translation not yet implemented"));
154    }
155
156    #[test]
157    fn test_translate_diagnostic_without_position() {
158        let source_map = oxur_smap::SourceMap::new();
159        let translator = ErrorTranslator::new(source_map);
160
161        let json = r#"{
162            "message": "aborting due to previous error",
163            "code": null,
164            "level": "error",
165            "spans": [],
166            "children": [],
167            "rendered": null
168        }"#;
169
170        let diagnostic = RustcDiagnostic::from_json(json).unwrap();
171        let output = translator.translate_diagnostic(&diagnostic);
172
173        // Should contain error message
174        assert!(output.contains("aborting due to previous error"));
175
176        // Should not contain position
177        assert!(!output.contains("-->"));
178    }
179
180    #[test]
181    fn test_translate_multiple_diagnostics() {
182        let source_map = oxur_smap::SourceMap::new();
183        let translator = ErrorTranslator::new(source_map);
184
185        let json_lines = r#"{"message": "error 1", "code": null, "level": "error", "spans": [], "children": [], "rendered": null}
186{"message": "warning 1", "code": null, "level": "warning", "spans": [], "children": [], "rendered": null}
187{"message": "error 2", "code": null, "level": "error", "spans": [], "children": [], "rendered": null}"#;
188
189        let diagnostics = RustcDiagnostic::from_json_lines(json_lines).unwrap();
190        let output = translator.translate_diagnostics(&diagnostics);
191
192        // Should contain errors but not warnings
193        assert!(output.contains("error 1"));
194        assert!(output.contains("error 2"));
195        assert!(!output.contains("warning 1"));
196    }
197
198    #[test]
199    fn test_translator_with_populated_source_map() {
200        use oxur_lang::{Expander, Parser};
201
202        // Create a source map with actual mappings
203        let source = r#"(deffn main ()
204  (println! "Hello"))"#;
205
206        let mut parser = Parser::new(source.to_string());
207        let surface_forms = parser.parse().unwrap();
208
209        let mut expander = Expander::new();
210        let _core_forms = expander.expand(surface_forms).unwrap();
211        let source_map = expander.source_map().clone();
212
213        // Verify source map has mappings
214        let stats = source_map.stats();
215        assert!(stats.surface_nodes > 0, "Should have surface mappings");
216
217        let translator = ErrorTranslator::new(source_map);
218
219        // Verify translator has access to source map
220        assert!(translator.source_map().stats().surface_nodes > 0);
221    }
222
223    #[test]
224    fn test_source_map_accessor() {
225        let mut source_map = oxur_smap::SourceMap::new();
226        let node_id = oxur_smap::new_node_id();
227        let pos = oxur_smap::SourcePos::new("test.oxur".to_string(), 1, 1, 1);
228        source_map.record_surface_node(node_id, pos);
229
230        let translator = ErrorTranslator::new(source_map);
231
232        // Should be able to access source map through translator
233        let retrieved_pos = translator.source_map().get_surface_position(&node_id);
234        assert!(retrieved_pos.is_some());
235    }
236}