1use crate::intern::Interner;
32use crate::sourcemap::{OwnershipRole, SourceMap};
33use crate::style::Style;
34use crate::token::Span;
35use serde::Deserialize;
36
37#[derive(Debug, Clone)]
39pub struct LogosError {
40 pub title: String,
41 pub explanation: String,
42 pub logos_span: Option<Span>,
43 pub suggestion: Option<String>,
44}
45
46impl std::fmt::Display for LogosError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 writeln!(f, "{}: {}", Style::bold_red("ownership error"), self.title)?;
49 writeln!(f)?;
50 writeln!(f, "{}", self.explanation)?;
51 if let Some(suggestion) = &self.suggestion {
52 writeln!(f)?;
53 writeln!(f, "{}: {}", Style::cyan("suggestion"), suggestion)?;
54 }
55 Ok(())
56 }
57}
58
59#[derive(Debug, Deserialize)]
65pub struct RustcDiagnostic {
66 pub message: String,
67 pub code: Option<RustcCode>,
68 pub level: String,
69 pub spans: Vec<RustcSpan>,
70 #[serde(default)]
71 pub children: Vec<RustcDiagnostic>,
72}
73
74#[derive(Debug, Deserialize)]
76pub struct RustcCode {
77 pub code: String,
79}
80
81#[derive(Debug, Deserialize)]
86pub struct RustcSpan {
87 pub file_name: String,
89 pub line_start: u32,
91 pub line_end: u32,
93 pub column_start: u32,
95 pub column_end: u32,
97 pub is_primary: bool,
99 pub label: Option<String>,
101 #[serde(default)]
103 pub text: Vec<RustcSpanText>,
104}
105
106#[derive(Debug, Deserialize)]
108pub struct RustcSpanText {
109 pub text: String,
111 pub highlight_start: u32,
113 pub highlight_end: u32,
115}
116
117#[derive(Debug, Deserialize)]
119#[serde(tag = "reason")]
120#[serde(rename_all = "kebab-case")]
121pub enum RustcMessage {
122 CompilerMessage { message: RustcDiagnostic },
123 #[serde(other)]
124 Other,
125}
126
127pub fn parse_rustc_json(stderr: &str) -> Vec<RustcDiagnostic> {
133 let mut diagnostics = Vec::new();
134
135 for line in stderr.lines() {
136 if !line.starts_with('{') {
138 continue;
139 }
140
141 match serde_json::from_str::<RustcMessage>(line) {
142 Ok(RustcMessage::CompilerMessage { message }) => {
143 if message.level == "error" {
144 diagnostics.push(message);
145 }
146 }
147 Ok(RustcMessage::Other) => {} Err(_) => {} }
150 }
151
152 diagnostics
153}
154
155pub fn get_error_code(diag: &RustcDiagnostic) -> Option<&str> {
159 diag.code.as_ref().map(|c| c.code.as_str())
160}
161
162pub fn get_primary_span(diag: &RustcDiagnostic) -> Option<&RustcSpan> {
167 diag.spans.iter().find(|s| s.is_primary)
168}
169
170fn extract_var_from_message(message: &str, prefix: &str, suffix: &str) -> Option<String> {
173 let start = message.find(prefix)?;
174 let after_prefix = &message[start + prefix.len()..];
175 let end = after_prefix.find(suffix)?;
176 Some(after_prefix[..end].to_string())
177}
178
179pub struct DiagnosticBridge<'a> {
189 source_map: &'a SourceMap,
191 interner: &'a Interner,
193}
194
195impl<'a> DiagnosticBridge<'a> {
196 pub fn new(source_map: &'a SourceMap, interner: &'a Interner) -> Self {
197 Self { source_map, interner }
198 }
199
200 pub fn translate(&self, diag: &RustcDiagnostic) -> Option<LogosError> {
202 let code = get_error_code(diag)?;
203 let span = get_primary_span(diag);
204
205 match code {
206 "E0382" => self.translate_use_after_move(diag, span),
207 "E0505" => self.translate_move_while_borrowed(diag, span),
208 "E0597" => self.translate_lifetime_error(diag, span),
209 _ => self.translate_generic(diag, span),
210 }
211 }
212
213 fn translate_use_after_move(&self, diag: &RustcDiagnostic, span: Option<&RustcSpan>) -> Option<LogosError> {
216 let var_name = extract_var_from_message(&diag.message, "value: `", "`")
217 .or_else(|| extract_var_from_message(&diag.message, "value `", "`"))?;
218
219 let logos_span = span.and_then(|s| self.source_map.find_nearest_span(s.line_start));
220
221 let (logos_name, role) = if let Some(origin) = self.source_map.get_var_origin(&var_name) {
223 (self.interner.resolve(origin.logos_name).to_string(), Some(origin.role))
224 } else {
225 (var_name.clone(), None)
226 };
227
228 let explanation = match role {
229 Some(OwnershipRole::GiveObject) => format!(
230 "You gave '{}' away with a Give statement, so you can't use it anymore.\n\
231 In LOGOS, 'Give X to Y' transfers ownership - X moves to Y and leaves your hands.\n\
232 This is like handing someone a physical object: once given, you no longer have it.",
233 logos_name
234 ),
235 Some(OwnershipRole::LetBinding) | None => format!(
236 "The value '{}' was moved somewhere else and can't be used again.\n\
237 Check if you used 'Give' or passed it to a function that took ownership.",
238 logos_name
239 ),
240 _ => format!(
241 "The value '{}' has been moved and is no longer available.",
242 logos_name
243 ),
244 };
245
246 let suggestion = Some(format!(
247 "If you need to use '{}' after giving it away, either:\n\
248 1. Use 'Show {} to Y' instead (this borrows, keeping ownership)\n\
249 2. Use 'a copy of {}' before the Give",
250 logos_name, logos_name, logos_name
251 ));
252
253 Some(LogosError {
254 title: format!("Cannot use '{}' after giving it away", logos_name),
255 explanation,
256 logos_span,
257 suggestion,
258 })
259 }
260
261 fn translate_move_while_borrowed(&self, diag: &RustcDiagnostic, span: Option<&RustcSpan>) -> Option<LogosError> {
264 let var_name = extract_var_from_message(&diag.message, "out of `", "`")
265 .or_else(|| extract_var_from_message(&diag.message, "move out of `", "`"))?;
266
267 let logos_span = span.and_then(|s| self.source_map.find_nearest_span(s.line_start));
268
269 let logos_name = if let Some(origin) = self.source_map.get_var_origin(&var_name) {
270 self.interner.resolve(origin.logos_name).to_string()
271 } else {
272 var_name.clone()
273 };
274
275 let explanation = format!(
276 "You showed '{}' to someone (creating a temporary view),\n\
277 but then tried to give it away before they finished looking.\n\
278 In LOGOS, 'Show' creates a promise that the data won't change or disappear\n\
279 while being viewed. You can't break that promise by giving it away.",
280 logos_name
281 );
282
283 let suggestion = Some(format!(
284 "Make sure all 'Show' usages of '{}' complete before any 'Give'.\n\
285 Alternatively, give away a copy: 'Give a copy of {} to Y'",
286 logos_name, logos_name
287 ));
288
289 Some(LogosError {
290 title: format!("Cannot give '{}' while it's being shown", logos_name),
291 explanation,
292 logos_span,
293 suggestion,
294 })
295 }
296
297 fn translate_lifetime_error(&self, diag: &RustcDiagnostic, span: Option<&RustcSpan>) -> Option<LogosError> {
300 let logos_span = span.and_then(|s| self.source_map.find_nearest_span(s.line_start));
301
302 let is_zone_related = diag.message.contains("borrowed")
304 || diag.children.iter().any(|c| c.message.contains("dropped"));
305
306 let explanation = if is_zone_related {
307 "A value created inside a Zone cannot be referenced from outside.\n\
308 Zones are memory arenas - when the Zone ends, everything inside it is released.\n\
309 This is the 'Hotel California' rule: data can check in (be created),\n\
310 but references can't check out (escape the Zone).".to_string()
311 } else {
312 "A borrowed reference is being used after the original value has gone away.\n\
313 References are temporary views - they can't outlive what they're viewing.".to_string()
314 };
315
316 let suggestion = Some(
317 "If you need the data after the Zone ends, either:\n\
318 1. Move the data out with 'Give' before the Zone closes\n\
319 2. Copy the data: 'Let result be a copy of zone_data'\n\
320 3. Restructure so the computation completes inside the Zone".to_string()
321 );
322
323 Some(LogosError {
324 title: "Reference cannot outlive its data".to_string(),
325 explanation,
326 logos_span,
327 suggestion,
328 })
329 }
330
331 fn translate_generic(&self, diag: &RustcDiagnostic, span: Option<&RustcSpan>) -> Option<LogosError> {
333 let logos_span = span.and_then(|s| self.source_map.find_nearest_span(s.line_start));
334
335 let var_hint = if let Some(start) = diag.message.find('`') {
337 if let Some(end) = diag.message[start + 1..].find('`') {
338 Some(&diag.message[start + 1..start + 1 + end])
339 } else {
340 None
341 }
342 } else {
343 None
344 };
345
346 let explanation = if let Some(var) = var_hint {
347 format!(
348 "The Rust compiler reported an error involving '{}':\n{}",
349 var, diag.message
350 )
351 } else {
352 format!("The Rust compiler reported an error:\n{}", diag.message)
353 };
354
355 Some(LogosError {
356 title: "Compilation error".to_string(),
357 explanation,
358 logos_span,
359 suggestion: None,
360 })
361 }
362}
363
364pub fn translate_diagnostics(
370 diagnostics: &[RustcDiagnostic],
371 source_map: &SourceMap,
372 interner: &Interner,
373) -> Option<LogosError> {
374 translate_diagnostics_all(diagnostics, source_map, interner)
375 .into_iter()
376 .next()
377}
378
379pub fn translate_diagnostics_all(
384 diagnostics: &[RustcDiagnostic],
385 source_map: &SourceMap,
386 interner: &Interner,
387) -> Vec<LogosError> {
388 let bridge = DiagnosticBridge::new(source_map, interner);
389 diagnostics
390 .iter()
391 .filter_map(|diag| bridge.translate(diag))
392 .collect()
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn parse_rustc_json_extracts_errors() {
401 let json_output = r#"{"reason":"compiler-message","message":{"message":"use of moved value: `x`","code":{"code":"E0382"},"level":"error","spans":[{"file_name":"src/main.rs","line_start":5,"line_end":5,"column_start":10,"column_end":11,"is_primary":true,"label":null,"text":[]}],"children":[]}}
402{"reason":"build-finished","success":false}"#;
403
404 let diagnostics = parse_rustc_json(json_output);
405 assert_eq!(diagnostics.len(), 1);
406 assert_eq!(diagnostics[0].message, "use of moved value: `x`");
407 assert_eq!(get_error_code(&diagnostics[0]), Some("E0382"));
408 }
409
410 #[test]
411 fn extract_var_from_message_works() {
412 assert_eq!(
413 extract_var_from_message("use of moved value: `data`", "value: `", "`"),
414 Some("data".to_string())
415 );
416 assert_eq!(
417 extract_var_from_message("cannot move out of `x` because", "out of `", "`"),
418 Some("x".to_string())
419 );
420 }
421
422 #[test]
423 fn translate_e0382_creates_friendly_error() {
424 let interner = Interner::new();
425 let source_map = SourceMap::new("Let data be 5.\nGive data to processor.".to_string());
426
427 let diag = RustcDiagnostic {
428 message: "use of moved value: `data`".to_string(),
429 code: Some(RustcCode { code: "E0382".to_string() }),
430 level: "error".to_string(),
431 spans: vec![RustcSpan {
432 file_name: "src/main.rs".to_string(),
433 line_start: 3,
434 line_end: 3,
435 column_start: 10,
436 column_end: 14,
437 is_primary: true,
438 label: None,
439 text: vec![],
440 }],
441 children: vec![],
442 };
443
444 let bridge = DiagnosticBridge::new(&source_map, &interner);
445 let error = bridge.translate(&diag).expect("Should translate");
446
447 assert!(error.title.contains("data"));
448 assert!(error.title.contains("giving it away"));
449 assert!(error.explanation.contains("moved"));
450 assert!(error.suggestion.is_some());
451 }
452
453 #[test]
454 fn translate_e0597_creates_hotel_california_error() {
455 let interner = Interner::new();
456 let source_map = SourceMap::new("Inside a zone:\n Let x be 5.".to_string());
457
458 let diag = RustcDiagnostic {
459 message: "borrowed value does not live long enough".to_string(),
460 code: Some(RustcCode { code: "E0597".to_string() }),
461 level: "error".to_string(),
462 spans: vec![RustcSpan {
463 file_name: "src/main.rs".to_string(),
464 line_start: 5,
465 line_end: 5,
466 column_start: 1,
467 column_end: 10,
468 is_primary: true,
469 label: None,
470 text: vec![],
471 }],
472 children: vec![],
473 };
474
475 let bridge = DiagnosticBridge::new(&source_map, &interner);
476 let error = bridge.translate(&diag).expect("Should translate");
477
478 assert!(error.title.contains("outlive"));
479 assert!(error.explanation.contains("Zone") || error.explanation.contains("borrowed"));
480 }
481}