Skip to content

Release 0.3.5 — trivia round-trip + %recover wiring#26

Merged
siy merged 15 commits into
mainfrom
release-0.3.5
May 1, 2026
Merged

Release 0.3.5 — trivia round-trip + %recover wiring#26
siy merged 15 commits into
mainfrom
release-0.3.5

Conversation

@siy

@siy siy commented May 1, 2026

Copy link
Copy Markdown
Owner

Summary

Trivia attribution correctness — full byte-equal round-trip on all 22 perf-corpus fixtures — plus %recover directive wiring on the interpreter side. RoundTripTest re-enabled.

Five distinct trivia-attribution bugs (Bug A through Bug C'') diagnosed and fixed in this release, plus the %recover end-to-end wiring fix:

  • Bug AParsingContext.savePending/restorePending changed from size-only to full List<Trivia> so backtracked branches don't permanently lose drained pending trivia.
  • Bug B — Cache-hit path rebuilds leading trivia (drain pending + skipWhitespace + reattach) so cache hits behave identically to fresh parses.
  • Bug C — Generator's cache stored the wrapped-with-leading body; the attachLeadingTrivia short-circuit then preserved stale leading on empty-pending hits, duplicating trivia. Fix: cache an empty-leading version, return the leading-applied version.
  • Bug C' — Trivia consumed by the body's last inter-element skipWhitespace (e.g. before an empty ZoM/Optional) ends up in pending with no claimant. At rule exit, attach to the last child's trailingTrivia. Pos is not rewound — predicate combinators rely on it.
  • Bug C'' — Generator's emitted Sequence didn't roll back outer children on element failure. Symptom: a successful trailing comma appeared as a child of both the inner ZoM-NT and the outer Sequence. Fix: snapshot children at Sequence start, restore on element failure.
  • %recover — Override popped in finally before parseWithRecovery consulted it. Fix: capture into per-context field with deepest-wins semantics; new regression test proves override and default produce distinct recovery landing points.

Generator-side %recover per-rule overrides remain deferred to 0.3.6.

large/FactoryClassGenerator.java.txt CST hash baseline regenerated (the Bug C'' fix removes a duplicate trailing comma in enum-constant lists). Other 21 fixtures unchanged.

Test plan

  • All 896 tests pass (875 + 21 net from RoundTripTest going skipped → 22 passing)
  • RoundTripTest 22/22 byte-equal via generated parser
  • Interpreter and generator parity preserved
  • CorpusParityTest, Phase1ParityTest, Phase1InterpreterParityTest, Phase2*ParityTest all green
  • GeneratorFlagInertnessTest green (no leakage)
  • RecoverDirectiveProofTest validates wiring

Summary by CodeRabbit

  • New Features

    • Interpreter-level %recover wiring completed; recovery overrides are honored.
  • Bug Fixes

    • Corrected trivia attribution across backtracking and cache hits.
    • Fixed generator caching, sequence child rollback, and rule-exit trailing trivia.
    • Restored byte-equal source round-trip for corpus fixtures.
  • Tests

    • Re-enabled round-trip verification.
    • Added regression test proving %recover changes landing points.
  • Documentation

    • Updated changelog, trivia-attribution guide, and release plan.

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Parser correctness fixes for trivia attribution across backtracking, packrat cache-hits, and rule-exit handling. %recover is wired end-to-end using a per-context pending override consumed during recovery selection. Pending-leading trivia snapshots switched to full-list semantics; RoundTripTest is re-enabled and one CST baseline was regenerated.

Changes

Cohort / File(s) Summary
Release notes & docs
CHANGELOG.md, docs/RELEASE-PLAN-0.3.5-0.4.0.md, docs/TRIVIA-ATTRIBUTION.md
Document shipped fixes for trivia attribution, packrat/cache behavior, Sequence child rollback, %recover wiring, RoundTripTest re-enablement, and regenerated CST baseline(s).
Version bumps
README.md, pom.xml, peglib-core/pom.xml, peglib-formatter/pom.xml, peglib-incremental/pom.xml, peglib-maven-plugin/pom.xml, peglib-playground/pom.xml
Bumped parent/module Maven versions from 0.3.40.3.5.
ParsingContext: recovery & trivia snapshot API
peglib-core/src/main/java/org/pragmatica/peg/parser/ParsingContext.java
Adds recordFailureRecoveryOverride and clearPendingRecoveryOverride; changes pending-leading trivia snapshot/restore sigs from intList<Trivia> and consumes pending recovery override during skip-to-recovery.
PegEngine: cache-hit/trailing-trivia & recovery plumbing
peglib-core/src/main/java/org/pragmatica/peg/parser/PegEngine.java
Reconstructs leading trivia on packrat cache hits, attaches unclaimed pending-leading trivia as rule-exit trailing trivia (via attachTrailingToTail), records/clears failure recovery overrides at appropriate points, and adds combineTrivia helper.
ParserGenerator: generated-cache & child rollback
peglib-core/src/main/java/org/pragmatica/peg/generator/ParserGenerator.java
Generates cache-hit paths that rebuild leading trivia instead of returning cached node verbatim; emits rule-exit trailing-trivia attribution; adjusts LR-cache behavior; snapshots/restores Sequence children list for element-failure rollback.
Tests: recovery proof & round-trip
peglib-core/src/test/java/org/pragmatica/peg/error/RecoverDirectiveProofTest.java, peglib-core/src/test/java/org/pragmatica/peg/perf/RoundTripTest.java
Adds regression test proving %recover landing-point override; re-enables RoundTripTest (removed @Disabled).
Baselines / metrics
peglib-core/src/test/resources/perf-corpus-baseline/large/FactoryClassGenerator.java.txt.hash, .../FactoryClassGenerator.java.txt.ruleHits.txt, .../ruleCoverage.txt
Regenerated CST hash baseline for one large fixture and adjusted small rule-hit/coverage counts due to legitimate shape change from Sequence child-rollback fix.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Parser Caller
  participant Peg as PegEngine
  participant Context as ParsingContext
  participant Cache as PackratCache
  participant Gen as GeneratedRule (ParserGenerator)

  Caller->>Peg: parseRule(rule)
  Peg->>Context: savePendingLeadingTrivia()
  Peg->>Cache: lookup(rule,pos)
  alt cache hit (success)
    Cache-->>Peg: cachedNode + endPos
    Peg->>Context: drain pendingLeadingTrivia
    Peg->>Context: skipWhitespace(if tokenBoundaryDepth==0)
    Peg->>Gen: attach reconstructed leading trivia to cachedNode
    Peg-->>Caller: return success with cachedNode at endPos
  else cache miss / parse body
    Peg->>Gen: execute generated rule method
    Gen->>Context: may recordFailureRecoveryOverride(term) on failure
    Gen-->>Peg: node or null
    alt success
      Peg->>Context: clearPendingRecoveryOverride()
      Peg->>Peg: attachTrailingToTail(unclaimed pendingLeadingTrivia)
      Peg->>Cache: store adjusted cacheable node
      Peg-->>Caller: return success
    else failure
      Peg->>Context: recordFailureRecoveryOverride(term)
      Peg-->>Caller: return failure (recovery selection consults pending override)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐇 I nibbled at bytes and chased each stray space,

Cache-hits now wear the right leading lace,
%recover points now pause where they should, not roam,
Sequences roll back, and the CST finds its home,
Hooray — a clean round trip, carrot cake for the comb! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Release 0.3.5 — trivia round-trip + %recover wiring' directly and accurately summarizes the two primary accomplishments of this release: restoring byte-equal round-trip functionality and wiring the %recover directive.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release-0.3.5

Review rate limit: 3/5 reviews remaining, refill in 16 minutes and 20 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
peglib-core/src/main/java/org/pragmatica/peg/parser/PegEngine.java (1)

1929-1934: 💤 Low value

Consider consolidating duplicate trivia concatenation methods.

concatTrivia (line 1929) and combineTrivia (line 2025) perform nearly identical operations. The difference is that combineTrivia has early-return optimizations for empty lists. Consider consolidating into a single method with the optimizations.

♻️ Proposed consolidation
-    private static List<Trivia> concatTrivia(List<Trivia> first, List<Trivia> second) {
-        var combined = new ArrayList<Trivia>(first.size() + second.size());
-        combined.addAll(first);
-        combined.addAll(second);
-        return List.copyOf(combined);
-    }
-
     // ... later in the file ...

     private static List<Trivia> combineTrivia(List<Trivia> first, List<Trivia> second) {
         if (first.isEmpty()) return second;
         if (second.isEmpty()) return first;
         var combined = new ArrayList<Trivia>(first.size() + second.size());
         combined.addAll(first);
         combined.addAll(second);
         return List.copyOf(combined);
     }

Then replace all concatTrivia calls with combineTrivia (which has the early-return optimizations).

Also applies to: 2025-2032

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@peglib-core/src/main/java/org/pragmatica/peg/parser/PegEngine.java` around
lines 1929 - 1934, Replace the two near-duplicate helpers by keeping a single
optimized method (preferably combineTrivia) that implements the early-return
checks for empty lists and concatenation logic, then remove concatTrivia and
update all callers to call combineTrivia instead; locate the methods named
concatTrivia and combineTrivia in PegEngine, copy combineTrivia's
empty-check/early-return behavior into the retained method, ensure it returns an
immutable List (List.copyOf) and update any references to concatTrivia to use
combineTrivia.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@peglib-core/src/main/java/org/pragmatica/peg/generator/ParserGenerator.java`:
- Around line 2694-2704: The left-recursive wrapper path sets finalResult and
cacheableResult from lastSeed without draining pendingLeadingTrivia, so trailing
trivia consumed by the LR body remains unclaimed; modify the LR wrapper exit
(the branch that uses lastSeed, attachLeadingTrivia, restoreLocation and
finalResult = CstParseResult.success(...)) to drain pendingLeadingTrivia and
attach it as trailing trivia to the returned node (similar to the non-LR
parseRule fix), ensure the node passed to CstParseResult.success includes those
trailing Trivia nodes, and update cacheableResult to reflect the
drained/modified lastSeed so the cached seed contains the trivia changes.

In `@peglib-core/src/main/java/org/pragmatica/peg/parser/ParsingContext.java`:
- Around line 393-406: Backtracking combinators currently restore location and
pending-leading-trivia but do not restore or clear
pendingFailureRecoveryOverride, letting a failed alternative's override leak
into subsequent alternatives; update each backtracking path in
parseChoiceWithMode, parseOptionalWithMode, parseZeroOrMoreWithMode,
parseOneOrMoreWithMode, and parseRepetitionWithMode to save the current
pendingFailureRecoveryOverride before trying an alternative and restore it on
backtrack (or call clearPendingRecoveryOverride() on entry/after restoring
state) so that recordFailureRecoveryOverride() is isolated to the attempt
context and first-write-wins only applies within the same attempt.

---

Nitpick comments:
In `@peglib-core/src/main/java/org/pragmatica/peg/parser/PegEngine.java`:
- Around line 1929-1934: Replace the two near-duplicate helpers by keeping a
single optimized method (preferably combineTrivia) that implements the
early-return checks for empty lists and concatenation logic, then remove
concatTrivia and update all callers to call combineTrivia instead; locate the
methods named concatTrivia and combineTrivia in PegEngine, copy combineTrivia's
empty-check/early-return behavior into the retained method, ensure it returns an
immutable List (List.copyOf) and update any references to concatTrivia to use
combineTrivia.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f9fe090-6c58-46a5-b2ee-7b2865aec541

📥 Commits

Reviewing files that changed from the base of the PR and between 0a10223 and 8fbea57.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • README.md
  • docs/RELEASE-PLAN-0.3.5-0.4.0.md
  • docs/TRIVIA-ATTRIBUTION.md
  • peglib-core/pom.xml
  • peglib-core/src/main/java/org/pragmatica/peg/generator/ParserGenerator.java
  • peglib-core/src/main/java/org/pragmatica/peg/parser/ParsingContext.java
  • peglib-core/src/main/java/org/pragmatica/peg/parser/PegEngine.java
  • peglib-core/src/test/java/org/pragmatica/peg/error/RecoverDirectiveProofTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/perf/RoundTripTest.java
  • peglib-core/src/test/resources/perf-corpus-baseline/large/FactoryClassGenerator.java.txt.hash
  • peglib-core/src/test/resources/perf-corpus-baseline/large/FactoryClassGenerator.java.txt.ruleHits.txt
  • peglib-core/src/test/resources/perf-corpus-baseline/ruleCoverage.txt
  • peglib-formatter/pom.xml
  • peglib-incremental/pom.xml
  • peglib-maven-plugin/pom.xml
  • peglib-playground/pom.xml
  • pom.xml

Comment on lines 2694 to +2704
sb.append(" if (lastSeed.isSuccess()) {\n");
sb.append(" restoreLocation(lastSeed.endLocation.unwrap());\n");
sb.append(" var node = attachLeadingTrivia(lastSeed.node.unwrap(), leadingTrivia);\n");
sb.append(" finalResult = CstParseResult.success(node, lastSeed.text.or(\"\"), lastSeed.endLocation.unwrap());\n");
// Bug C fix: cache the empty-leading lastSeed (body emission attaches no
// leading trivia). The wrapped node above is for return only. Bug C' is
// intentionally NOT applied at the LR settle path: PegEngine's LR path
// does not apply rule-exit trailing-trivia attribution either, and the
// interpreter's parseRule (non-LR) fix alone is sufficient for full
// round-trip equivalence on the corpus.
sb.append(" cacheableResult = lastSeed;\n");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Apply the rule-exit trailing-trivia fix to left-recursive wrappers too.

This branch still returns/caches lastSeed without draining pendingLeadingTrivia, so any trivia consumed by the LR body’s final inter-element skipWhitespace() is left to be claimed by the next sibling instead of this rule’s tail. That means Bug C' is still present for left-recursive rules even though the non-LR path was fixed. Based on learnings: Group whitespace and comments as Trivia nodes in the CST for lossless tree representation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@peglib-core/src/main/java/org/pragmatica/peg/generator/ParserGenerator.java`
around lines 2694 - 2704, The left-recursive wrapper path sets finalResult and
cacheableResult from lastSeed without draining pendingLeadingTrivia, so trailing
trivia consumed by the LR body remains unclaimed; modify the LR wrapper exit
(the branch that uses lastSeed, attachLeadingTrivia, restoreLocation and
finalResult = CstParseResult.success(...)) to drain pendingLeadingTrivia and
attach it as trailing trivia to the returned node (similar to the non-LR
parseRule fix), ensure the node passed to CstParseResult.success includes those
trailing Trivia nodes, and update cacheableResult to reflect the
drained/modified lastSeed so the cached seed contains the trivia changes.

Comment on lines +393 to +406
public void recordFailureRecoveryOverride(String terminator) {
if (terminator != null && !terminator.isEmpty() && pendingFailureRecoveryOverride.isEmpty()) {
pendingFailureRecoveryOverride = Option.some(terminator);
}
}

/**
* 0.3.5 (Phase 5) — clear any pending failure override. Called on rule
* SUCCESS so that an override recorded by a failed inner alternative of
* a backtracking combinator (Choice/Optional/etc.) does not leak forward
* into a later, unrelated recovery cycle.
*/
public void clearPendingRecoveryOverride() {
pendingFailureRecoveryOverride = Option.none();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Call sites for pending recovery override APIs =="
rg -n -C4 '\brecordFailureRecoveryOverride\s*\(|\bclearPendingRecoveryOverride\s*\(' \
  peglib-core/src/main/java/org/pragmatica/peg/parser

echo
echo "== Nearby backtracking / recovery logic in PegEngine =="
rg -n -C6 'clearPendingRecoveryOverride|recordFailureRecoveryOverride|restoreLocation|Choice|Optional|And|Not' \
  peglib-core/src/main/java/org/pragmatica/peg/parser/PegEngine.java

Repository: siy/java-peglib

Length of output: 31059


Confirm: pending recovery override leaks between alternatives in backtracking combinators.

When a Choice/Optional/ZeroOrMore alternative fails and backtracks, it restores the parser location and pending-leading-trivia, but NOT the pending-failure-recovery-override. This allows a failed alternative's override to pollute subsequent alternatives:

  1. Alt1 fails, records override via its rule's %recover
  2. Choice restores location + pending-leading-trivia only
  3. Alt2 fails, attempts recordFailureRecoveryOverride(), but first-write-wins prevents it from overwriting Alt1's stale override
  4. Alt2 is forced to use Alt1's terminator when recovery is needed

This violates the isolation expectation for distinct attempt contexts. The fix is to save/restore or explicitly clear pendingFailureRecoveryOverride when backtracking in parseChoiceWithMode, parseOptionalWithMode, parseZeroOrMoreWithMode, parseOneOrMoreWithMode, and parseRepetitionWithMode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@peglib-core/src/main/java/org/pragmatica/peg/parser/ParsingContext.java`
around lines 393 - 406, Backtracking combinators currently restore location and
pending-leading-trivia but do not restore or clear
pendingFailureRecoveryOverride, letting a failed alternative's override leak
into subsequent alternatives; update each backtracking path in
parseChoiceWithMode, parseOptionalWithMode, parseZeroOrMoreWithMode,
parseOneOrMoreWithMode, and parseRepetitionWithMode to save the current
pendingFailureRecoveryOverride before trying an alternative and restore it on
backtrack (or call clearPendingRecoveryOverride() on entry/after restoring
state) so that recordFailureRecoveryOverride() is isolated to the attempt
context and first-write-wins only applies within the same attempt.

@siy siy merged commit 3371903 into main May 1, 2026
1 of 2 checks passed
@siy siy deleted the release-0.3.5 branch May 1, 2026 12:56
@coderabbitai coderabbitai Bot mentioned this pull request May 14, 2026
5 tasks
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