diff --git a/src/error.rs b/src/error.rs index 6522331..48681ac 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, error::Error, fmt::Display, ops::Range}; +use std::{cell::RefCell, collections::VecDeque, error::Error, fmt::Display, ops::Range}; use crate::world::World; @@ -118,46 +118,136 @@ impl Error for CompileMessage {} impl CompileMessage { pub fn rich_format(&self, world: &World) -> String { - format!( - "{}\n {}", - self.to_string(), - self.location.rich_format(&world) - ) - } -} + // Get base error message + let message = self.to_string(); -impl Location { - pub fn rich_format(&self, world: &World) -> String { - let board = self.board.map(|i| &world.boards[i]); - let stat = self.stat.and_then(|i| board.map(|b| &b.stats[i])); - - let mut parts: Vec> = vec![]; - parts.push(self.file_path.clone()); - parts.push(board.map(|board| board.name.clone())); - parts.push(stat.map(|stat| format!("stat({},{})", stat.x, stat.y))); - parts.push(self.span.as_ref().and_then(|span| { - stat.map(|stat| { - let mut line = 1; - let mut col = 1; - for c in (&stat.code[0..span.start]).chars() { - if c == '\n' { - line += 1; - col = 1; - } else { - col += 1; - } - } - format!("line {line}, column {col}") - }) - })); - - while parts.len() > 0 && parts[parts.len() - 1].is_none() { - parts.pop(); + // Build hierarchy string: world -> board -> stat -> span of code + let mut breadcrumbs = vec![]; + let location = &self.location; + if let Some(path) = &location.file_path { + breadcrumbs.push(path.clone()); + } + let board = location.board.map(|i| &world.boards[i]); + if let Some(board) = board { + breadcrumbs.push(board.name.clone()); + } + let stat = board.and_then(|board| location.stat.map(|i| &board.stats[i])); + if let Some(stat) = stat { + let first_line = stat.code.lines().next(); + let name = first_line.filter(|x| x.starts_with("@")).unwrap_or("stat"); + let (x, y) = (stat.x, stat.y); + breadcrumbs.push(format!("{name} ({x},{y})")); + } + let span = stat.and_then(|stat| { + location + .span + .as_ref() + .map(|span| RichSpan::new(&span, &stat.code)) + }); + if let Some(ref span) = span { + let line = span.line_number; + let col = span.line_span.start + 1; + breadcrumbs.push(format!("line {line}:{col}")) + } + let breadcrumbs = format!(" => {}", breadcrumbs.join(" -> ")); + + // Build context block + let context = span.map(|span| { + let mut block = vec![]; + + // Add padding line at start + let last_line_number = span.nearby_lines.last().unwrap().0; + let number_width = last_line_number.to_string().len(); + let prefix = format!(" {:number_width$} |", ""); + block.push(prefix.clone()); + + // Add each of the context lines + let mut needs_end_padding = false; + for (line_number, line) in &span.nearby_lines { + block.push(format!(" {line_number:>number_width$} | {line}")); + needs_end_padding = true; + + // Add highlight + if line_number == &span.line_number { + block.push(format!( + "{prefix} {}{}", + " ".repeat(span.line_span.start), + "^".repeat(span.line_span.len()) + )); + needs_end_padding = false; + } + } + + // Add padding line at end + if needs_end_padding { + block.push(prefix); + } + + block.join("\n") + }); + + let mut parts = vec![message, breadcrumbs]; + if let Some(context) = context { + parts.push(context); + } + parts.join("\n") + } +} + +struct RichSpan<'a> { + line_number: usize, + line_span: Range, + nearby_lines: Vec<(usize, &'a str)>, +} + +impl<'a> RichSpan<'a> { + fn new(span: &Range, code: &'a str) -> Self { + // Track byte ranges and line numbers for each line + let mut offset = 0; + let mut current_line_number = 0; + let mut lines = code.lines().map(|line| { + let end_offset = offset + line.len() + 1; + let range = Range { + start: offset, + end: end_offset, + }; + offset = end_offset; + current_line_number += 1; + (range, (current_line_number, line)) + }); + + // Find the line where the span starts. + // While we search, keep track of the immediately preceding lines. + let num_context_lines = 3; + let mut recent = VecDeque::with_capacity(num_context_lines * 2 + 1); + let mut found_line_number = None; + let mut found_line_span = None; + for (range, numbered_line) in lines.by_ref() { + if recent.len() > num_context_lines { + recent.pop_front(); + } + recent.push_back(numbered_line); + if range.contains(&span.start) { + found_line_number = Some(numbered_line.0); + let offset = range.start; + found_line_span = Some(Range { + start: span.start - offset, + end: span.end - offset, + }); + break; + } + } + let found_line_number = found_line_number.expect("span outside range of code"); + let found_line_span = found_line_span.unwrap(); + + // Gather following context lines + recent.extend(lines.take(num_context_lines).map(|(_, line)| line)); + recent.make_contiguous(); + + Self { + line_number: found_line_number, + line_span: found_line_span, + nearby_lines: recent.into_iter().collect(), } - let parts: Vec<_> = parts - .into_iter() - .map(|x| x.unwrap_or("???".into())) - .collect(); - parts.join(", ") } } diff --git a/src/labels/process.rs b/src/labels/process.rs index 12b6c7e..abd55d6 100644 --- a/src/labels/process.rs +++ b/src/labels/process.rs @@ -166,7 +166,7 @@ fn anonymous_forward_pass(stats: &mut [ParsedStat], registry: &mut Registry, ctx name.name = backward.clone(); } else { ctx.with_span(name.span.clone()) - .error("backward reference @b without prior anonymous label :@"); + .error("backward reference needs an anonymous label"); } } } @@ -200,7 +200,7 @@ fn anonymous_backward_pass(stats: &mut [ParsedStat], ctx: &Context) { name.name = forward.clone(); } else { ctx.with_span(name.span.clone()) - .error("forward reference @f without following anonymous label :@"); + .error("forward reference needs an anonymous label"); } } }