Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ does not require Java, Node.js, the ANTLR tool jar, or an intermediate
`.interp` file. The repository's differential tests use ANTLR `4.13.2` only as
an explicit compatibility oracle.

Grammar sources are read as UTF-8. A leading byte order mark is skipped like
Java ANTLR's `UnicodeBOM` rule, and `\n`, `\r\n`, and lone `\r` line endings are
all accepted, so grammars saved by Windows editors need no conversion. The byte
order mark still occupies a column, which keeps reported positions identical to
ANTLR's.

### 2. Install the Rust ANTLR runtime tools

Each ANTLR target language needs a runtime package used by generated parsers.
Expand Down
95 changes: 95 additions & 0 deletions src/bin_support/grammar/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,101 @@ mod tests {
}
}

/// A UTF-8 byte order mark is skipped rather than stripped, so it keeps
/// occupying a column exactly as Java ANTLR's `UnicodeBOM` rule leaves it.
#[test]
fn utf8_byte_order_mark_is_skipped_without_shifting_spans() {
let with_bom = "\u{feff}grammar A; a: 'x';";
let file = parse_source(SourceId::new(0), "memory:bom", with_bom)
.expect("a byte order mark must not fail the frontend");
assert_eq!(
file.tokens()
.iter()
.filter(
|token| token.channel == antlr4_runtime::token::DEFAULT_CHANNEL
&& token.token_type != RUNTIME_TOKEN_EOF
)
.map(|token| file.token_text(token))
.collect::<Vec<_>>(),
["grammar", "A", ";", "a", ":", "'x'", ";"],
);
// The mark keeps its own byte span, so later spans stay anchored to the
// real file offsets and Java ANTLR's column numbers are reproduced.
assert_eq!(file.line_column(with_bom.len() as u32 - 1), Some((1, 18)));

let without_bom = "grammar A; a: 'x';";
let plain = parse_source(SourceId::new(1), "memory:no-bom", without_bom)
.expect("the control grammar should parse");
assert_eq!(
plain.line_column(without_bom.len() as u32 - 1),
Some((1, 17))
);
}

/// A byte order mark is only whitespace outside literals; inside a string
/// literal or a character set it stays ordinary content.
#[test]
fn byte_order_mark_inside_literals_remains_content() {
let text = "lexer grammar A; A: '\u{feff}'; B: [\u{feff}a-z]+;";
let file = parse_source(SourceId::new(0), "memory:bom-literal", text)
.expect("a byte order mark inside literals should parse");
let literals = file
.tokens()
.iter()
.filter(|token| {
matches!(
token.token_type,
STRING_LITERAL | super::super::generated::antlr_v4_lexer::LEXER_CHAR_SET
)
})
.map(|token| file.token_text(token))
.collect::<Vec<_>>();
assert_eq!(literals, ["'\u{feff}'", "[\u{feff}a-z]"]);
}

/// Windows and classic-Mac line endings are plain whitespace, and the line
/// map counts them the way Java ANTLR does.
#[test]
fn carriage_returns_are_whitespace_and_end_lines() {
for (name, text) in [
("crlf", "grammar A;\r\na: 'x';\r\n"),
("cr", "grammar A;\ra: 'x';\r"),
] {
let file = parse_source(SourceId::new(0), name, text)
.unwrap_or_else(|error| panic!("{name}: {:?}", error.diagnostics()));
assert_eq!(
file.tokens()
.iter()
.filter(
|token| token.channel == antlr4_runtime::token::DEFAULT_CHANNEL
&& token.token_type != RUNTIME_TOKEN_EOF
)
.map(|token| file.token_text(token))
.collect::<Vec<_>>(),
["grammar", "A", ";", "a", ":", "'x'", ";"],
"{name}",
);
}

// A CRLF pair must advance the line once, not twice, so diagnostics on
// the second line report the same column as the LF-only spelling.
let crlf = parse_source(SourceId::new(0), "crlf-lines", "grammar A;\r\na: 'x';\r\n")
.expect("CRLF grammar should parse");
let lf = parse_source(SourceId::new(1), "lf-lines", "grammar A;\na: 'x';\n")
.expect("LF grammar should parse");
let semicolon = |file: &SourceFile| {
let token = file
.tokens()
.iter()
.filter(|token| token.token_type == SEMI)
.nth(1)
.expect("both grammars have two semicolons");
file.line_column(token.span.bytes.start)
};
assert_eq!(semicolon(&crlf), Some((2, 6)));
assert_eq!(semicolon(&crlf), semicolon(&lf));
}
Comment on lines +1127 to +1168

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert bare-CR coordinates.

The lone-CR case is parsed, but Lines 1151-1167 only verify CRLF coordinates. Assert the second semicolon’s position for \r against the LF case too, so a lone-CR line_starts regression cannot pass.

As per coding guidelines, maintain coverage using the documented cargo-llvm-cov workflows.

Suggested test addition
         let lf = parse_source(SourceId::new(1), "lf-lines", "grammar A;\na: 'x';\n")
             .expect("LF grammar should parse");
+        let cr = parse_source(SourceId::new(2), "cr-lines", "grammar A;\ra: 'x';\r")
+            .expect("CR grammar should parse");
         let semicolon = |file: &SourceFile| {
             // ...
         };
         assert_eq!(semicolon(&crlf), Some((2, 6)));
         assert_eq!(semicolon(&crlf), semicolon(&lf));
+        assert_eq!(semicolon(&cr), semicolon(&lf));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Windows and classic-Mac line endings are plain whitespace, and the line
/// map counts them the way Java ANTLR does.
#[test]
fn carriage_returns_are_whitespace_and_end_lines() {
for (name, text) in [
("crlf", "grammar A;\r\na: 'x';\r\n"),
("cr", "grammar A;\ra: 'x';\r"),
] {
let file = parse_source(SourceId::new(0), name, text)
.unwrap_or_else(|error| panic!("{name}: {:?}", error.diagnostics()));
assert_eq!(
file.tokens()
.iter()
.filter(
|token| token.channel == antlr4_runtime::token::DEFAULT_CHANNEL
&& token.token_type != RUNTIME_TOKEN_EOF
)
.map(|token| file.token_text(token))
.collect::<Vec<_>>(),
["grammar", "A", ";", "a", ":", "'x'", ";"],
"{name}",
);
}
// A CRLF pair must advance the line once, not twice, so diagnostics on
// the second line report the same column as the LF-only spelling.
let crlf = parse_source(SourceId::new(0), "crlf-lines", "grammar A;\r\na: 'x';\r\n")
.expect("CRLF grammar should parse");
let lf = parse_source(SourceId::new(1), "lf-lines", "grammar A;\na: 'x';\n")
.expect("LF grammar should parse");
let semicolon = |file: &SourceFile| {
let token = file
.tokens()
.iter()
.filter(|token| token.token_type == SEMI)
.nth(1)
.expect("both grammars have two semicolons");
file.line_column(token.span.bytes.start)
};
assert_eq!(semicolon(&crlf), Some((2, 6)));
assert_eq!(semicolon(&crlf), semicolon(&lf));
}
/// Windows and classic-Mac line endings are plain whitespace, and the line
/// map counts them the way Java ANTLR does.
#[test]
fn carriage_returns_are_whitespace_and_end_lines() {
for (name, text) in [
("crlf", "grammar A;\r\na: 'x';\r\n"),
("cr", "grammar A;\ra: 'x';\r"),
] {
let file = parse_source(SourceId::new(0), name, text)
.unwrap_or_else(|error| panic!("{name}: {:?}", error.diagnostics()));
assert_eq!(
file.tokens()
.iter()
.filter(
|token| token.channel == antlr4_runtime::token::DEFAULT_CHANNEL
&& token.token_type != RUNTIME_TOKEN_EOF
)
.map(|token| file.token_text(token))
.collect::<Vec<_>>(),
["grammar", "A", ";", "a", ":", "'x'", ";"],
"{name}",
);
}
// A CRLF pair must advance the line once, not twice, so diagnostics on
// the second line report the same column as the LF-only spelling.
let crlf = parse_source(SourceId::new(0), "crlf-lines", "grammar A;\r\na: 'x';\r\n")
.expect("CRLF grammar should parse");
let lf = parse_source(SourceId::new(1), "lf-lines", "grammar A;\na: 'x';\n")
.expect("LF grammar should parse");
let cr = parse_source(SourceId::new(2), "cr-lines", "grammar A;\ra: 'x';\r")
.expect("CR grammar should parse");
let semicolon = |file: &SourceFile| {
let token = file
.tokens()
.iter()
.filter(|token| token.token_type == SEMI)
.nth(1)
.expect("both grammars have two semicolons");
file.line_column(token.span.bytes.start)
};
assert_eq!(semicolon(&crlf), Some((2, 6)));
assert_eq!(semicolon(&crlf), semicolon(&lf));
assert_eq!(semicolon(&cr), semicolon(&lf));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bin_support/grammar/frontend.rs` around lines 1127 - 1168, The test
carriage_returns_are_whitespace_and_end_lines currently compares coordinates
only for CRLF input; add a parsed bare-CR fixture and assert its
second-semicolon line_column matches the LF fixture, preserving the existing
CRLF assertion. Keep the coverage focused on lone-CR line_starts behavior and
follow the documented cargo-llvm-cov workflow.

Source: Coding guidelines


#[test]
fn tparser_preserves_named_action_rule_and_argument_spans() {
const ACTION_BLOCK_RULE: usize = 14;
Expand Down
6 changes: 3 additions & 3 deletions src/bin_support/grammar/generated/antlr_v4_lexer.rs

Large diffs are not rendered by default.

Loading
Loading