Skip to content

feat: add RecoverTo option for fault-tolerant parsing - #472

Open
Pastalikek65 wants to merge 1 commit into
alecthomas:masterfrom
Pastalikek65:feat-342-error-recovery
Open

feat: add RecoverTo option for fault-tolerant parsing#472
Pastalikek65 wants to merge 1 commit into
alecthomas:masterfrom
Pastalikek65:feat-342-error-recovery

Conversation

@Pastalikek65

Copy link
Copy Markdown
Contributor

Closes #342

Implements opt-in fault-tolerant parsing via a new RecoverTo(types...) build option.

What it does

When parsing fails, the parser scans forward from the error position for the first token that could begin one of the registered productions, skips the malformed region, and resumes parsing from that synchronization point. The partial AST up to the failure is retained (per participle's existing best-effort behavior).

type Statements struct {
    Stmts []*Stmt `@@*`
}

parser := participle.MustBuild[Statements](
    participle.RecoverTo(&Stmt{}),
)

Input like call one broken call two parses to [call:one, call:two]broken is skipped.

Design

  • Start-set analysis (recovery.go): computes, per registered production, the token predicates that can begin it. Handles literal (incl. type constraints and case-insensitivity), reference, strct, disjunction, sequence (with nullability propagation through nullable heads), capture, group and union. Opaque nodes (custom, parseable, negation, lookaheadGroup) contribute nothing.
  • Forward scan (maybeRecover): scans via a speculative branch (Next() skips elided tokens), never crosses structural literals (tokens appearing verbatim in the grammar that aren't sync points — e.g. closing delimiters), and guarantees termination via:
    1. the failed token is always consumed, and
    2. a lastRecovery cursor guard prevents re-recovering at the same position.
  • Hooks (nodes.go): only inside repetition loops (group */+), on both the error path and the zero-match path. This keeps the change surgical and avoids the O(n²) blow-up a strct-level hook produced during development.
  • Validation: RecoverTo on a type not in the grammar is a build error.

Testing

11 unit tests covering: skip-malformed, resume-after-brace (nested blocks, } not crossed), no-sync-token falls back to original error, no-option no-op, multiple malformed, UseLookahead interaction, case-insensitive literals, unknown-type build error, unions, @@+ (one-or-more), and struct-level sync on a recursive grammar.

Fuzzed 45s / ~300k executions with no hang, panic or explosion (a fuzzer found two non-termination cases during development that the lastRecovery guard + structural-literal rules resolved).

Open question for maintainer

RecoverTo is deliberately sync-only (productions are not re-parsed into the AST after recovery); re-parsing recovered nodes would be a natural follow-up but adds complexity. Happy to extend if desired.

RecoverTo(types...) enables skipping malformed input regions and resuming
parse at the next token that can begin a registered production. It works
inside repetition loops (struct slices parsed with @@* or @@+): when an
element fails, the lexer scans forward for a synchronization token while
never crossing structural literals (such as closing delimiters), then
restarts the loop there.

- recovery.go: startSet/nullable analysis over the node graph (literal,
  reference, strct, disjunction, sequence, capture, group, union), build-time
  resolution, and maybeRecover forward scan with lastRecovery progress guard.
- nodes.go: hooks in group.Parse for both error and empty-match paths.
- context.go/parser.go: recoverNode+structural sets threaded into parseContext.

Full test suite green; fuzzed 45s / ~300k execs with no hang, panic or
explosion. Closes alecthomas#342.
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.

Error recovery ideas

1 participant