use crate::intern::Interner;
use crate::sourcemap::{OwnershipRole, SourceMap};
use crate::style::Style;
use crate::token::Span;
use serde::Deserialize;
#[derive(Debug, Clone)]
pub struct LogosError {
pub title: String,
pub explanation: String,
pub logos_span: Option<Span>,
pub suggestion: Option<String>,
}
impl std::fmt::Display for LogosError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}: {}", Style::bold_red("ownership error"), self.title)?;
writeln!(f)?;
writeln!(f, "{}", self.explanation)?;
if let Some(suggestion) = &self.suggestion {
writeln!(f)?;
writeln!(f, "{}: {}", Style::cyan("suggestion"), suggestion)?;
}
Ok(())
}
}
#[derive(Debug, Deserialize)]
pub struct RustcDiagnostic {
pub message: String,
pub code: Option<RustcCode>,
pub level: String,
pub spans: Vec<RustcSpan>,
#[serde(default)]
pub children: Vec<RustcDiagnostic>,
}
#[derive(Debug, Deserialize)]
pub struct RustcCode {
pub code: String,
}
#[derive(Debug, Deserialize)]
pub struct RustcSpan {
pub file_name: String,
pub line_start: u32,
pub line_end: u32,
pub column_start: u32,
pub column_end: u32,
pub is_primary: bool,
pub label: Option<String>,
#[serde(default)]
pub text: Vec<RustcSpanText>,
}
#[derive(Debug, Deserialize)]
pub struct RustcSpanText {
pub text: String,
pub highlight_start: u32,
pub highlight_end: u32,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "reason")]
#[serde(rename_all = "kebab-case")]
pub enum RustcMessage {
CompilerMessage { message: RustcDiagnostic },
#[serde(other)]
Other,
}
pub fn parse_rustc_json(stderr: &str) -> Vec<RustcDiagnostic> {
let mut diagnostics = Vec::new();
for line in stderr.lines() {
if !line.starts_with('{') {
continue;
}
match serde_json::from_str::<RustcMessage>(line) {
Ok(RustcMessage::CompilerMessage { message }) => {
if message.level == "error" {
diagnostics.push(message);
}
}
Ok(RustcMessage::Other) => {} Err(_) => {} }
}
diagnostics
}
pub fn get_error_code(diag: &RustcDiagnostic) -> Option<&str> {
diag.code.as_ref().map(|c| c.code.as_str())
}
pub fn get_primary_span(diag: &RustcDiagnostic) -> Option<&RustcSpan> {
diag.spans.iter().find(|s| s.is_primary)
}
fn extract_var_from_message(message: &str, prefix: &str, suffix: &str) -> Option<String> {
let start = message.find(prefix)?;
let after_prefix = &message[start + prefix.len()..];
let end = after_prefix.find(suffix)?;
Some(after_prefix[..end].to_string())
}
pub struct DiagnosticBridge<'a> {
source_map: &'a SourceMap,
interner: &'a Interner,
}
impl<'a> DiagnosticBridge<'a> {
pub fn new(source_map: &'a SourceMap, interner: &'a Interner) -> Self {
Self { source_map, interner }
}
pub fn translate(&self, diag: &RustcDiagnostic) -> Option<LogosError> {
let code = get_error_code(diag)?;
let span = get_primary_span(diag);
match code {
"E0382" => self.translate_use_after_move(diag, span),
"E0505" => self.translate_move_while_borrowed(diag, span),
"E0597" => self.translate_lifetime_error(diag, span),
_ => self.translate_generic(diag, span),
}
}
fn translate_use_after_move(&self, diag: &RustcDiagnostic, span: Option<&RustcSpan>) -> Option<LogosError> {
let var_name = extract_var_from_message(&diag.message, "value: `", "`")
.or_else(|| extract_var_from_message(&diag.message, "value `", "`"))?;
let logos_span = span.and_then(|s| self.source_map.find_nearest_span(s.line_start));
let (logos_name, role) = if let Some(origin) = self.source_map.get_var_origin(&var_name) {
(self.interner.resolve(origin.logos_name).to_string(), Some(origin.role))
} else {
(var_name.clone(), None)
};
let explanation = match role {
Some(OwnershipRole::GiveObject) => format!(
"You gave '{}' away with a Give statement, so you can't use it anymore.\n\
In LOGOS, 'Give X to Y' transfers ownership - X moves to Y and leaves your hands.\n\
This is like handing someone a physical object: once given, you no longer have it.",
logos_name
),
Some(OwnershipRole::LetBinding) | None => format!(
"The value '{}' was moved somewhere else and can't be used again.\n\
Check if you used 'Give' or passed it to a function that took ownership.",
logos_name
),
_ => format!(
"The value '{}' has been moved and is no longer available.",
logos_name
),
};
let suggestion = Some(format!(
"If you need to use '{}' after giving it away, either:\n\
1. Use 'Show {} to Y' instead (this borrows, keeping ownership)\n\
2. Use 'a copy of {}' before the Give",
logos_name, logos_name, logos_name
));
Some(LogosError {
title: format!("Cannot use '{}' after giving it away", logos_name),
explanation,
logos_span,
suggestion,
})
}
fn translate_move_while_borrowed(&self, diag: &RustcDiagnostic, span: Option<&RustcSpan>) -> Option<LogosError> {
let var_name = extract_var_from_message(&diag.message, "out of `", "`")
.or_else(|| extract_var_from_message(&diag.message, "move out of `", "`"))?;
let logos_span = span.and_then(|s| self.source_map.find_nearest_span(s.line_start));
let logos_name = if let Some(origin) = self.source_map.get_var_origin(&var_name) {
self.interner.resolve(origin.logos_name).to_string()
} else {
var_name.clone()
};
let explanation = format!(
"You showed '{}' to someone (creating a temporary view),\n\
but then tried to give it away before they finished looking.\n\
In LOGOS, 'Show' creates a promise that the data won't change or disappear\n\
while being viewed. You can't break that promise by giving it away.",
logos_name
);
let suggestion = Some(format!(
"Make sure all 'Show' usages of '{}' complete before any 'Give'.\n\
Alternatively, give away a copy: 'Give a copy of {} to Y'",
logos_name, logos_name
));
Some(LogosError {
title: format!("Cannot give '{}' while it's being shown", logos_name),
explanation,
logos_span,
suggestion,
})
}
fn translate_lifetime_error(&self, diag: &RustcDiagnostic, span: Option<&RustcSpan>) -> Option<LogosError> {
let logos_span = span.and_then(|s| self.source_map.find_nearest_span(s.line_start));
let is_zone_related = diag.message.contains("borrowed")
|| diag.children.iter().any(|c| c.message.contains("dropped"));
let explanation = if is_zone_related {
"A value created inside a Zone cannot be referenced from outside.\n\
Zones are memory arenas - when the Zone ends, everything inside it is released.\n\
This is the 'Hotel California' rule: data can check in (be created),\n\
but references can't check out (escape the Zone).".to_string()
} else {
"A borrowed reference is being used after the original value has gone away.\n\
References are temporary views - they can't outlive what they're viewing.".to_string()
};
let suggestion = Some(
"If you need the data after the Zone ends, either:\n\
1. Move the data out with 'Give' before the Zone closes\n\
2. Copy the data: 'Let result be a copy of zone_data'\n\
3. Restructure so the computation completes inside the Zone".to_string()
);
Some(LogosError {
title: "Reference cannot outlive its data".to_string(),
explanation,
logos_span,
suggestion,
})
}
fn translate_generic(&self, diag: &RustcDiagnostic, span: Option<&RustcSpan>) -> Option<LogosError> {
let logos_span = span.and_then(|s| self.source_map.find_nearest_span(s.line_start));
let var_hint = if let Some(start) = diag.message.find('`') {
if let Some(end) = diag.message[start + 1..].find('`') {
Some(&diag.message[start + 1..start + 1 + end])
} else {
None
}
} else {
None
};
let explanation = if let Some(var) = var_hint {
format!(
"The Rust compiler reported an error involving '{}':\n{}",
var, diag.message
)
} else {
format!("The Rust compiler reported an error:\n{}", diag.message)
};
Some(LogosError {
title: "Compilation error".to_string(),
explanation,
logos_span,
suggestion: None,
})
}
}
pub fn translate_diagnostics(
diagnostics: &[RustcDiagnostic],
source_map: &SourceMap,
interner: &Interner,
) -> Option<LogosError> {
let bridge = DiagnosticBridge::new(source_map, interner);
for diag in diagnostics {
if let Some(error) = bridge.translate(diag) {
return Some(error);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_rustc_json_extracts_errors() {
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":[]}}
{"reason":"build-finished","success":false}"#;
let diagnostics = parse_rustc_json(json_output);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].message, "use of moved value: `x`");
assert_eq!(get_error_code(&diagnostics[0]), Some("E0382"));
}
#[test]
fn extract_var_from_message_works() {
assert_eq!(
extract_var_from_message("use of moved value: `data`", "value: `", "`"),
Some("data".to_string())
);
assert_eq!(
extract_var_from_message("cannot move out of `x` because", "out of `", "`"),
Some("x".to_string())
);
}
#[test]
fn translate_e0382_creates_friendly_error() {
let interner = Interner::new();
let source_map = SourceMap::new("Let data be 5.\nGive data to processor.".to_string());
let diag = RustcDiagnostic {
message: "use of moved value: `data`".to_string(),
code: Some(RustcCode { code: "E0382".to_string() }),
level: "error".to_string(),
spans: vec![RustcSpan {
file_name: "src/main.rs".to_string(),
line_start: 3,
line_end: 3,
column_start: 10,
column_end: 14,
is_primary: true,
label: None,
text: vec![],
}],
children: vec![],
};
let bridge = DiagnosticBridge::new(&source_map, &interner);
let error = bridge.translate(&diag).expect("Should translate");
assert!(error.title.contains("data"));
assert!(error.title.contains("giving it away"));
assert!(error.explanation.contains("moved"));
assert!(error.suggestion.is_some());
}
#[test]
fn translate_e0597_creates_hotel_california_error() {
let interner = Interner::new();
let source_map = SourceMap::new("Inside a zone:\n Let x be 5.".to_string());
let diag = RustcDiagnostic {
message: "borrowed value does not live long enough".to_string(),
code: Some(RustcCode { code: "E0597".to_string() }),
level: "error".to_string(),
spans: vec![RustcSpan {
file_name: "src/main.rs".to_string(),
line_start: 5,
line_end: 5,
column_start: 1,
column_end: 10,
is_primary: true,
label: None,
text: vec![],
}],
children: vec![],
};
let bridge = DiagnosticBridge::new(&source_map, &interner);
let error = bridge.translate(&diag).expect("Should translate");
assert!(error.title.contains("outlive"));
assert!(error.explanation.contains("Zone") || error.explanation.contains("borrowed"));
}
}