Skip to content

Improve error handling on invalid code / upstream artic changes from language server development - #39

Open
TimGrun wants to merge 8 commits into
AnyDSL:masterfrom
DFOP-HD:upstreamable
Open

TimGrun wants to merge 8 commits into
AnyDSL:masterfrom
DFOP-HD:upstreamable

Conversation

@TimGrun

@TimGrun TimGrun commented Aug 5, 2026 •

Copy link
Copy Markdown

Produce errors instead of crashing when processing invalid code. Also motivated by the requirement of the language server to run further compilation stages after parsing has failed.
Should not change behaviour on valid input.

Error handling improvements:

  • lexer now keeps going when it reaches an error token, instead of ending the parse
  • fixed some possible null pointer dereferences
  • asserts reachable from ordinary source turned into diagnostics
  • skip to next declaration instead of cascading errors: On a file whose first function is missing a closing paren, master reports 8 errors and parses no declaration beyond it; this branch reports 1 and parses the rest of the file.
    expect() consumed the token it had just rejected, usually the fn the parser needed to resynchronise on, and parse_error_decl() skipped a single token, so the top-level loop walked straight back into the same error. expect() now leaves the token in place, and skip_to_decl() runs on to the next declaration boundary, counting braces as it goes.

Other improvment:

  • no unused-parameter warning for function prototypes, which have no body to use them in

Note:

  • I cherry picked these changes from the language server artic fork. The fork still has more changes (mostly guarded by preprocessor macros) but these are mostly just relevant to language server integration. You could consider integrating them in the future if we want to improve maintenance of the language server

Tim Grun added 8 commits August 5, 2026 10:40
Loc() = default leaves �egin/end with indeterminate values, and start_decl is
only assigned when name binding resolves the path, so both are read uninitialized on
any path where an earlier stage bailed out.
log::error() formats the message into err but ends the line on out, so on master
every error leaves stderr unterminated and unflushed and prints a stray newline to
stdout. It only looks right when both streams are the same console.
An error token is indistinguishable from end-of-file to the parser, so one stray
character truncated the token stream and every declaration after it was silently lost.
The error is already reported; carry on with the next character instead.
Three problems compounded each other. expect() consumed the offending token even on a
mismatch, so the token that would have resynchronised the parse -- usually the n
opening the next declaration -- was eaten. parse_error_decl() skipped exactly one token,
so the top-level loop re-entered it on the next one. And every attempt reported again.
Together, one bad declaration cost an error per token to the end of the file and every
declaration after it was lost.

expect() now leaves a mismatched token in place, parse_error_decl() consumes at least
one token and then skips to the next token that can begin a declaration (counting braces
so a nested declaration does not end the skip early), and reported_at() suppresses a
second message about a token already complained about. parse_fn_decl() builds an error
pattern for a missing parameter list instead of leaving param null.
The four error nodes had no inference rule, so they fell through to Node::infer and
produced 'cannot infer type for expression' on top of the parse error that had already
been reported. Giving them the error type instead lets should_report_error() silence
everything downstream that touches them.

CallExpr::is_jumping() asserted on a null type, which a call reaches whenever an earlier
stage bailed out (e.g. let a = return(;). The printer dereferenced FnExpr::body and
FnDecl's parameter pattern unconditionally, so --print-ast crashed on any file with a
function the parser could not finish.
…serting

These are all reachable from ordinary source, so in a release build they were silent
undefined behaviour and in a debug build they aborted the compiler. Using anything other
than an immutable initialized static as an array size, and a break or continue whose
enclosing loop is neither a while nor a for, now produce a diagnostic and the error type.

The three array-size paths repeated the same checks, so the message lives in the new
TypeChecker::invalid_array_size.
A parse error can produce a named declaration whose identifier is empty, so the assert
aborted a debug build on invalid input; in a release build name[0] then read past the
end of an empty string.
A prototype binds its parameters and has no body to use them in, so every declaration
like n f(x: i32) -> i32; warned about x, and the only way to silence it was to
rename the parameter to _x in an interface that documents its argument names.
@TimGrun TimGrun changed the title Upstream Artic changes from Language Server Development Improve crash resilience when facing invalid code / Upstream Artic changes from Language Server Development Aug 5, 2026
@TimGrun TimGrun changed the title Improve crash resilience when facing invalid code / Upstream Artic changes from Language Server Development Improve crash resilience when facing invalid code / upstream artic changes from language server development Aug 5, 2026
@TimGrun
TimGrun marked this pull request as ready for review August 5, 2026 10:30
@TimGrun TimGrun changed the title Improve crash resilience when facing invalid code / upstream artic changes from language server development Improve error handling on invalid code / upstream artic changes from language server development Aug 5, 2026
@Hugobros3 Hugobros3 self-assigned this Aug 5, 2026
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.

2 participants