Skip to content

Commit

Permalink
feat: add line/column information to error reports.
Browse files Browse the repository at this point in the history
  • Loading branch information
plusvic committed Sep 20, 2024
1 parent fdde450 commit 760d3c1
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 5 deletions.
4 changes: 4 additions & 0 deletions go/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ type Label struct {
Level string `json:"level"`
// Origin of the code where the error occurred.
CodeOrigin string `json:"code_origin"`
// Line number
Line int64 `json:"line"`
// Column number
Column int64 `json:"column"`
// The code span highlighted by this label.
Span Span `json:"span"`
// Text associated to the label.
Expand Down
14 changes: 11 additions & 3 deletions go/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,11 @@ func TestErrors(t *testing.T) {
Title: "invariant boolean expression",
Labels: []Label{
{
Level: "warning",
Span: Span{Start: 25, End: 29},
Text: "this expression is always true",
Level: "warning",
Line: 1,
Column: 26,
Span: Span{Start: 25, End: 29},
Text: "this expression is always true",
},
},
Footers: []Footer{
Expand Down Expand Up @@ -197,6 +199,8 @@ func TestErrors(t *testing.T) {
{
Level: "error",
CodeOrigin: "test.yar",
Line: 1,
Column: 26,
Span: Span{Start: 25, End: 28},
Text: "this identifier has not been declared",
},
Expand Down Expand Up @@ -295,6 +299,8 @@ func TestWarnings(t *testing.T) {
{
Level: "warning",
CodeOrigin: "",
Line: 1,
Column: 31,
Span: Span{Start: 30, End: 40},
Text: "these consecutive jumps will be treated as [0-2]",
},
Expand All @@ -314,6 +320,8 @@ func TestWarnings(t *testing.T) {
{
Level: "warning",
CodeOrigin: "",
Line: 1,
Column: 22,
Span: Span{Start: 21, End: 43},
Text: "this pattern may slow down the scan",
},
Expand Down
103 changes: 101 additions & 2 deletions lib/src/compiler/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl From<Span> for CodeLoc {
/// - `title`: The title of the report (e.g., "unexpected negative number").
/// - `labels`: A collection of labels included in the report. Each label
/// contains a level, a span, and associated text.
/// - `footers`: A collection notes that appear after the end of the report.
#[derive(Clone)]
pub(crate) struct Report {
code_cache: Arc<CodeCache>,
Expand Down Expand Up @@ -110,12 +111,24 @@ impl Report {
code_loc.source_id.unwrap_or(self.default_source_id);

let code_cache = self.code_cache.read();
let code_origin =
code_cache.get(&source_id).unwrap().origin.clone();
let cache_entry = code_cache.get(&source_id).unwrap();
let code_origin = cache_entry.origin.clone();

// This could be faster if we maintain an ordered vector with the
// byte offset where each line begins. By doing a binary search
// on that vector, we can locate the line number in O(log(N))
// instead of O(N).
let (line, column) = byte_offset_to_line_col(
&cache_entry.code,
code_loc.span.start(),
)
.unwrap();

Label {
level: level_as_text(*level),
code_origin,
line,
column,
span: code_loc.span.clone(),
text,
}
Expand Down Expand Up @@ -232,6 +245,8 @@ impl Display for Report {
pub struct Label<'a> {
level: &'a str,
code_origin: Option<String>,
line: usize,
column: usize,
span: Span,
text: &'a str,
}
Expand Down Expand Up @@ -436,3 +451,87 @@ fn level_as_text(level: Level) -> &'static str {
Level::Help => "help",
}
}

/// Given a text slice and a position indicated as a byte offset, returns
/// the same position as a (line, column) pair.
fn byte_offset_to_line_col(
text: &str,
byte_offset: usize,
) -> Option<(usize, usize)> {
// Check if the byte_offset is valid
if byte_offset > text.len() {
return None; // Out of bounds
}

let mut line = 1;
let mut col = 1;

// Iterate through the characters (not bytes) in the string
for (i, c) in text.char_indices() {
if i == byte_offset {
return Some((line, col));
}
if c == '\n' {
line += 1;
col = 1; // Reset column to 1 after a newline
} else {
col += 1;
}
}

// If the byte_offset points to the last byte of the string, return the final position
if byte_offset == text.len() {
return Some((line, col));
}

None
}

#[cfg(test)]
mod tests {
use crate::compiler::report::byte_offset_to_line_col;

#[test]
fn byte_offset_to_line_col_single_line() {
let text = "Hello, World!";
assert_eq!(byte_offset_to_line_col(text, 0), Some((1, 1))); // Start of the string
assert_eq!(byte_offset_to_line_col(text, 7), Some((1, 8))); // Byte offset of 'W'
assert_eq!(byte_offset_to_line_col(text, 12), Some((1, 13))); // Byte offset of '!'
}

#[test]
fn byte_offset_to_line_col_multiline() {
let text = "Hello\nRust\nWorld!";
assert_eq!(byte_offset_to_line_col(text, 0), Some((1, 1))); // First character
assert_eq!(byte_offset_to_line_col(text, 5), Some((1, 6))); // End of first line (newline)
assert_eq!(byte_offset_to_line_col(text, 6), Some((2, 1))); // Start of second line ('R')
assert_eq!(byte_offset_to_line_col(text, 9), Some((2, 4))); // Byte offset of 't' in "Rust"
assert_eq!(byte_offset_to_line_col(text, 11), Some((3, 1))); // Start of third line ('W')
}

#[test]
fn byte_offset_to_line_col_empty_string() {
let text = "";
assert_eq!(byte_offset_to_line_col(text, 0), Some((1, 1)));
}

#[test]
fn byte_offset_to_line_col_out_of_bounds() {
let text = "Hello, World!";
assert_eq!(byte_offset_to_line_col(text, text.len() + 1), None);
}

#[test]
fn byte_offset_to_line_col_end_of_string() {
let text = "Hello, World!";
assert_eq!(byte_offset_to_line_col(text, text.len()), Some((1, 14))); // Last position after '!'
}

#[test]
fn byte_offset_to_line_col_multibyte_characters() {
let text = "Hello, 你好!";
assert_eq!(byte_offset_to_line_col(text, 7), Some((1, 8))); // Position of '你'
assert_eq!(byte_offset_to_line_col(text, 10), Some((1, 9))); // Position of '好'
assert_eq!(byte_offset_to_line_col(text, 13), Some((1, 10))); // Position of '!'
}
}
2 changes: 2 additions & 0 deletions lib/src/compiler/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,8 @@ fn errors_serialization() {
{
"level": "error",
"code_origin": "test.yar",
"line": 1,
"column": 23,
"span": { "start": 22, "end": 25 },
"text": "this identifier has not been declared"
}
Expand Down

0 comments on commit 760d3c1

Please sign in to comment.