Skip to content

fix: bound the rendering window when formatting an error line - #264

Open
minh-anm-nguyen wants to merge 1 commit into
WerWolv:masterfrom
minh-anm-nguyen:fix/error-line-render-bounds
Open

minh-anm-nguyen wants to merge 1 commit into
WerWolv:masterfrom
minh-anm-nguyen:fix/error-line-render-bounds

Conversation

@minh-anm-nguyen

@minh-anm-nguyen minh-anm-nguyen commented Oct 24, 2026 •

Copy link
Copy Markdown

formatLines() renders the offending source line with a caret under the reported column, trimming a window around it when the line is longer than 40 characters:

auto start = column > 20 ? column - 20 : 0;
auto end = column + 20 < errorLine.length() ? column + 20 : errorLine.length();
for (auto i = 0; i < 10; ++i) {
    if (start > 0 && errorLine[start] != ' ') --start;
    if (end < errorLine.length() && errorLine[end] != ' ') ++end;
}

end is bounded above by the line's length. start is bounded only below, by start > 0, and never above. location.column can legitimately exceed the length of the line it names, which is what gets reported when a token is left unterminated at the end of a line, and then start indexes past the end of the string.

The effect is that a malformed pattern can take the interpreter down while it is building the message that would have explained the problem. Under a hardened standard library std::string::operator[] trips its precondition and aborts; otherwise it is an out-of-range read.

Two nearby locations reach for something that is not there either:

  • location.source == nullptr, which Location::Empty() produces, is dereferenced by location.source->content.
  • location.line == 0 reaches lines[location.line - 1], one before the first line.

arrowPosition is also computed by unsigned subtraction in two places that can wrap, after which std::string(lineNumberPrefix.length() + arrowPosition, ' ') asks for an enormous string.

The fix normalizes the reported column once into a zero-based caret clamped to the line, where a caret equal to the line's length denotes the position just past the final character, which is the right place for an end-of-input marker. The window is then derived so that start <= caret <= end <= errorLine.size() holds throughout.

The window arithmetic still runs on the clamped one-based column, so the trimmed output and the caret placement stay byte-identical for every input that already rendered correctly. Only the previously broken cases change.

The regression test calls formatLines() directly rather than driving it through a malformed pattern. A pattern-driven test would depend on how the lexer numbers columns, and would quietly stop covering this code if that bookkeeping were ever tidied up.

formatLines() trims a window around the reported column when the
offending line is longer than 40 characters. `end` was bounded above by
the line's length, but `start` was only bounded below, so a column past
the end of its line -- which is what gets reported when a token is left
unterminated there -- indexed past the end of the string. Rendering a
syntax error could therefore take the interpreter down instead of
printing the error.

Normalize the reported column once into a zero-based caret clamped to the
line, where a caret equal to the line's length denotes the position just
past the final character, and derive the window from it so that
start <= caret <= end <= size() holds throughout. The window arithmetic
still runs on the clamped one-based column, so the trimmed output stays
identical for every input that already rendered correctly.

Also return early for a null source, which Location::Empty() produces,
and for line zero, which would reach one before the first line.

Add a regression test that calls formatLines() directly rather than
driving it through a malformed pattern, so the coverage does not quietly
disappear if the lexer's column bookkeeping ever changes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant