PEGO

110. No Recovered-Error Rollback Without Recovery

  • Status: Implemented
  • Date: 2026-10-22

Problem and Mechanism

Every backtracking point of a generated parser saved len(p.Recovered) next to the position and truncated the list when it went back. p.Recovered is only appended by recovery (#recover); memo entries copy only errors that exist. In a grammar without #recover, the list stays empty, so saving its length and truncating it are no-ops.

The generator now decides once per program whether any rule can recover (a #recover attribute or a recovery expression). If none can, the saved variable, the p.Restore(start, rec) calls and the length stores are not generated; a rollback is p.Pos = start. Grammars with #recover generate what they did before. TestGeneratedRecoveredRollbackOnlyWithRecovery checks both forms, and TestGeneratedParsersMatchEngine, TestGeneratedTypes, TestGeneratedDocumentsMatchBatchOnCorpus, TestGeneratedUnsharedParsersMatchEngine and TestGeneratedConstructedFieldChecks fail when the rollback is dropped under recovery.

Results

parser.go with -typed-only, bytes and lines before and after:

Parser Bytes before Bytes after Lines before Lines after
duckdb 2,337,553 2,207,830 116,542 114,168
typescript 1,427,281 1,352,046 69,180 68,061
python 845,854 805,025 39,373 38,640
golang 784,767 706,034 39,207 36,536
cue 518,213 490,503 24,488 23,951
cel 159,089 146,061 7,782 7,349

The duckdb test binary shrinks from 17,656,658 to 17,078,738 bytes (-3.3%); its cold compile time (best of three, clean build cache) is 7.0 s before and 7.4 s after, which is the spread of that measurement. Parse speed (30 interleaved rounds, minimum, an identical copy as the control) is within noise: python -1.25% (control -1.21%), cue -2.01% (-0.29%), duckdb -1.76% (-0.03%), cel +1.14% (+1.05%); cue and duckdb are 1.7 points faster than their controls, python and cel within 0.1 point.

Decision and Limits

Adopted. The optimization applies to the whole program: one #recover anywhere keeps the rollback everywhere.