Skip to content

release: peglib 0.6.0 — tokens-first redesign#34

Merged
siy merged 39 commits into
mainfrom
release-0.6.0
May 11, 2026
Merged

release: peglib 0.6.0 — tokens-first redesign#34
siy merged 39 commits into
mainfrom
release-0.6.0

Conversation

@siy

@siy siy commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

Clean-slate redesign delivering parity with javac on Java parsing.

Performance (real-world Java25)

Workload 0.5.x gen 0.6.0 javac v6 vs 0.5.x v6 vs javac
1900-LOC parse 33.24 ms 2.71 ms 2.25 ms 12.3× faster 1.20× of javac
40K-LOC parse 1141 ms 95.65 ms 52.25 ms 11.9× faster 1.83× of javac
Memory (1900 LOC) 77 MB 8.03 MB 3.17 MB 9.6× less 2.53× of javac
Incremental edit n/a 0.70 ms p50 / 1.5 ms p99

Highlights

  • 9 architectural decisions per spec §3 — all met or documented
  • New peglib-runtime (25KB) — standalone-parser invariant
  • DFA Unicode support — handles non-ASCII in real-world Java
  • True partial reparse for incremental editing
  • JBCT 0.25.0 conformance: 0 lint errors

Tests

1440 passing across 7 modules. Java25 corpus 20/20 clean. FactoryClassGenerator (1900 LOC real-world JBCT generator): 0 diagnostics.

Migration

See docs/MIGRATION-0.5-TO-0.6.md.

Known limitations

Documented in docs/HANDOVER.md under "Known limitations". Bounded; non-blocking for ship.

Test plan

  • Full reactor mvn install clean
  • Java25 corpus 20/20 round-trip
  • Real-world fixture (FactoryClassGenerator) parses cleanly
  • JMH bench vs 0.5.x-gen and javac
  • Incremental edit latency p50/p99 measured
  • V6Formatter corpus round-trip
  • Final review before merge + Maven Central deploy

Summary by CodeRabbit

  • New Features

    • Introduced peglib v0.6.0 with tokens-first architecture, flat-array CST, panic-mode recovery, and incremental editing support.
    • Added visitor-based code generation for CST traversal.
    • Added CST pretty-printer formatter with customizable trivia handling.
    • Added command-line REPL and playground for grammar testing.
  • Documentation

    • Added comprehensive migration guide from v0.5 to v0.6.
    • Updated README and handover documentation for v0.6.0 release.
  • Chores

    • Updated project version to 0.6.0.
    • Updated dependencies and Maven plugin configurations.

siy added 30 commits May 9, 2026 23:07
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces PEG v6: new lexer DFA/engine, parser generator/compiler, flat CST runtime with diagnostics, incremental editing, visitor/formatter, Maven Mojo, playground, benchmarks, grammar/resource updates, extensive tests, and documentation; modules/poms bump to 0.6.0 and add peglib-runtime.

Changes

PEG v6 End-To-End

Layer / File(s) Summary
Runtime (Tokens/CST/Diag)
peglib-runtime/...
Flat TokenArray (+splice), CstArray (+builder, views), ParseResult, Diagnostic/Severity; traversal/trivia APIs and tests.
Generators/Compilers
peglib-core/.../generator/*
Lexer/Parser/Visitor generators and in-memory compilers with identifier validation and emitted entry points.
Entry Points/Analyzers
peglib-core/.../v6/{PegParser,Parser}, .../analyzer/*
fromGrammar compile/cache pipeline; Parser facade; left-recursion and named-capture detectors/causes.
Lexer Pipeline
peglib-core/.../v6/lexer/*
Dfa/DfaBuilder, LexerEngine, RuleClassifier/RuleKind with keyword resolution, aliasing, delimited-blocks, Unicode.
Incremental
peglib-core/.../v6/incremental/IncrementalParser.java, tests
Snapshot/restore, checkpoint partial reparse, CST splice, diagnostics merge; TokenArray.spliceLex tests.
Grammar/%recover
peglib-core/.../grammar/*, tests
Grammar.recoverSets; GrammarParser parses %recover [chars] RuleName; Resolver plumbs sets.
Formatter v6
peglib-formatter/.../v6/*
V6Formatter, config, rules, trivia policy, context, package docs; formatter tests/corpus gate.
Maven/Playground
peglib-maven-plugin/..., peglib-playground/...
GenerateV6Mojo goal; v6 playground engine and interactive REPL with tests.
Benchmarks
peglib-core/src/jmh/...
Cold-compile/parse, v5.1 comparison, javac parse-only, incremental edit microbenchmarks.
Java25 Grammar/Resources
peglib-core/src/test/resources/*
Local-var annotations allowed; shift/assign via helper rules; baseline hashes/ruleHits/coverage updates.
Docs/Build
CHANGELOG.md, README.md, docs/*, */pom.xml, .gitignore
Adds 0.6.0 release notes/migration/handover; bumps to 0.6.0; adds peglib-runtime; ignore .claude/.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Possibly related PRs

  • siy/java-peglib#15 — Recovery directive handling and propagation overlaps with %recover parsing/wiring here.
  • siy/java-peglib#24 — Original formatter subsystem; this PR adds the v6 formatter counterparts.
  • siy/java-peglib#20 — Left-recursion analysis APIs; related to v6 LeftRecursionDetector/Cause introduced here.

Poem

I nibbled through tokens, hop by hop,
Built DFAs where pauses stop;
A parser burrow, CST bright,
Recovery lanterns in the night.
With visitors, formats, speedy leaps—
v6 awakens; the meadow peeps. 🐇✨

✨ 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.6.0

@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: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
peglib-core/src/main/java/org/pragmatica/peg/grammar/GrammarResolver.java (1)

169-176: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Imported %recover sets are still dropped during grammar composition

At Line 176, only root.recoverSets() is forwarded. Imported rules are renamed/inlined, but their recovery sets are never remapped into the composed grammar, so recovery behavior for imported rules is silently lost.

💡 Proposed fix
@@
-        // Produce composed Grammar. imports are cleared (surface-level composition is complete).
+        // Compose recover sets: root + imported rules (with rename mapping).
+        var composedRecoverSets = new LinkedHashMap<String, Set<Character>>();
+        root.recoverSets().forEach((name, chars) -> composedRecoverSets.put(name, Set.copyOf(chars)));
+
+        // Produce composed Grammar. imports are cleared (surface-level composition is complete).
@@
             for (var originalName : closure) {
                 var rule = ruleMap.get(originalName);
                 String renamedName;
@@
                 var rewritten = rewriteReferences(rule,
                                                   renamedName,
                                                   ref -> {
@@
                                                   });
                 composedRules.put(renamedName, rewritten);
+
+                var importedRecover = g.recoverSets().get(originalName);
+                if (importedRecover != null) {
+                    composedRecoverSets.putIfAbsent(renamedName, Set.copyOf(importedRecover));
+                }
             }
         }
@@
         return Grammar.grammar(
         new ArrayList<>(composedRules.values()),
         root.startRule(),
         root.whitespace(),
         root.word(),
         root.suggestRules(),
         List.of(),
-        root.recoverSets());
+        Map.copyOf(composedRecoverSets));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@peglib-core/src/main/java/org/pragmatica/peg/grammar/GrammarResolver.java`
around lines 169 - 176, The composed Grammar currently only forwards
root.recoverSets(), dropping any recovery sets from imported/inlined grammars;
update GrammarResolver to collect and remap recovery sets from imported grammars
(using the same renaming/inline mapping applied to rules) and merge them with
root.recoverSets() before calling Grammar.grammar. Locate the composition logic
around composedRules.values() and the Grammar.grammar(...) call and produce a
combined List of remapped recovery sets (preserving rule name remapping used
when inlining imports) to replace the empty List.of() argument so imported
%recover sets are preserved in the final grammar.
README.md (2)

410-410: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update version number in Maven plugin example.

The Maven plugin configuration example shows version 0.5.0, inconsistent with the 0.6.0 release being documented.

📝 Suggested fix
 <plugin>
     <groupId>org.pragmatica-lite</groupId>
     <artifactId>peglib-maven-plugin</artifactId>
-    <version>0.5.0</version>
+    <version>0.6.0</version>
     <executions>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 410, Update the Maven plugin example version tag shown as
<version>0.5.0</version> to match the documented release (0.6.0); locate the
example in README.md where the <version>0.5.0</version> snippet appears and
change the value to 0.6.0 so the sample configuration is consistent with the
0.6.0 release.

62-62: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update version number in dependency example.

The Quick Start section shows version 0.5.0 in the Maven dependency example, but this README is documenting the 0.6.0 release. Update to reflect the current version being released.

📝 Suggested fix
 <dependency>
     <groupId>org.pragmatica-lite</groupId>
     <artifactId>peglib</artifactId>
-    <version>0.5.0</version>
+    <version>0.6.0</version>
 </dependency>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 62, Update the Maven dependency example's version tag from
0.5.0 to the release version 0.6.0 by replacing the <version>0.5.0</version>
element in the Quick Start dependency snippet; search for the
<version>...</version> line in the Maven example and change its value to 0.6.0
so the README matches the documented release.
🟡 Minor comments (11)
peglib-runtime/src/main/java/org/pragmatica/peg/v6/diagnostic/Diagnostic.java-69-70 (1)

69-70: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize offsets and CRLF handling in diagnostic rendering.

Line 70 should clamp negative offsets, and Lines 82-87 should trim \r before rendering the line text. Otherwise Rust-style output can show invalid columns or malformed line display on CRLF inputs.

🩹 Proposed fix
-        int clamped = Math.min(offset, input.length());
+        int clamped = Math.max(0, Math.min(offset, input.length()));
@@
-        int end = input.indexOf('\n', lineStart);
-        return end < 0
-               ? input.length()
-               : end;
+        int end = input.indexOf('\n', lineStart);
+        int lineEnd = end < 0 ? input.length() : end;
+        if (lineEnd > lineStart && input.charAt(lineEnd - 1) == '\r') {
+            lineEnd--;
+        }
+        return lineEnd;

Also applies to: 82-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-runtime/src/main/java/org/pragmatica/peg/v6/diagnostic/Diagnostic.java`
around lines 69 - 70, The locate method in Diagnostic currently clamps only the
upper bound; update Diagnostic.locate to also clamp negative offsets (e.g., use
Math.max(0, Math.min(offset, input.length()))) so offsets below zero are
normalized, and when rendering the extracted line text (the variables used in
the rendering block around lines 82-87), strip any trailing '\r' before
computing column positions and before printing the line so CRLF inputs don't
produce invalid columns or malformed line display.
peglib-core/src/test/java/org/pragmatica/peg/v6/analyzer/LeftRecursionDetectorTest.java-183-186 (1)

183-186: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use classpath resource loading for java25.peg to avoid cwd-dependent test failures

Line 184 hardcodes a relative filesystem path. This is brittle outside module-root execution contexts.

💡 Proposed fix
-        var grammarPath = Paths.get("src/test/resources/java25.peg");
-        var grammarText = Files.readString(grammarPath, StandardCharsets.UTF_8);
+        var resource = LeftRecursionDetectorTest.class.getResource("/java25.peg");
+        assertTrue(resource != null, "Missing test resource: /java25.peg");
+        var grammarText = Files.readString(Path.of(resource.toURI()), StandardCharsets.UTF_8);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/test/java/org/pragmatica/peg/v6/analyzer/LeftRecursionDetectorTest.java`
around lines 183 - 186, Replace filesystem-based loading of the test grammar in
java25Grammar_isFreeOfLeftRecursion by reading the resource from the classpath;
locate the test method java25Grammar_isFreeOfLeftRecursion and change the
Files.readString(Paths.get("src/test/resources/java25.peg"), ...) usage to use
getResourceAsStream or ClassLoader.getResource to load "java25.peg" from the
test resources, then convert the stream to a String (e.g., using new
String(stream.readAllBytes(), StandardCharsets.UTF_8)) before passing it to
org.pragmatica.peg.grammar.GrammarParser.parse to avoid cwd-dependent failures.
peglib-playground/src/test/java/org/pragmatica/peg/playground/v6/PlaygroundReplV6Test.java-35-36 (1)

35-36: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verify non-quit commands do not signal exit.

These tests currently ignore handleCommand(...)’s boolean for normal commands, so an accidental true (exit) would not be caught.

Proposed patch
-        repl.handleCommand("12 + 34");
+        boolean exit = repl.handleCommand("12 + 34");
+        assertThat(exit).isFalse();
@@
-        repl.handleCommand("a#x");
+        boolean exit = repl.handleCommand("a#x");
+        assertThat(exit).isFalse();
@@
-        repl.handleCommand(":status");
+        boolean exit = repl.handleCommand(":status");
+        assertThat(exit).isFalse();

Also applies to: 73-74, 90-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-playground/src/test/java/org/pragmatica/peg/playground/v6/PlaygroundReplV6Test.java`
around lines 35 - 36, The tests call PlaygroundReplV6.handleCommand("...") for
non-quit inputs but ignore its boolean return (which signals exit), so update
each invocation (e.g., in PlaygroundReplV6Test where repl.handleCommand("12 +
34"), other occurrences) to capture and assert the result is false (use the test
framework's assertFalse/assertEquals(false, ...) on the returned boolean) to
ensure non-quit commands do not signal exit; apply this change to all similar
calls in the test class (including the other places noted).
peglib-core/src/test/java/org/pragmatica/peg/v6/generator/RecoverDirectiveGeneratorTest.java-172-183 (1)

172-183: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

doesNotContain("{}") is a no-op for this extracted arrayBody.

arrayBody excludes the closing }, so this assertion does not actually prove the sync set is populated with |. Please assert presence of the | token kind directly.

Suggested fix
         var idx = generated.source()
                            .indexOf("DEFAULT_SYNC = new int[] {");
+        assertThat(idx).isPositive();
         var end = generated.source()
                            .indexOf('}', idx);
         var arrayBody = generated.source()
                                  .substring(idx, end);
-        // '|' must be present (proves override populated it); no commas in
-        // the kind-list interior means the array is non-empty (a single int
-        // followed by '}' with no leading comma).
-        assertThat(arrayBody)
-        .doesNotContain("{}");
+        var pipeKind = built.kinds().inlineLiteralToKind().get("|/cs");
+        assertThat(pipeKind).isNotNull();
+        assertThat(arrayBody).contains(String.valueOf(pipeKind));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/test/java/org/pragmatica/peg/v6/generator/RecoverDirectiveGeneratorTest.java`
around lines 172 - 183, The current test extracts arrayBody from
generated.source() between "DEFAULT_SYNC = new int[] {" and '}' but then checks
doesNotContain("{}") which is a no-op because arrayBody excludes the closing
brace; replace that assertion with a direct presence check for the '|' token
kind (e.g. assertThat(arrayBody).contains("|")) so the test actually verifies
the sync set was populated; update the assertion near variables generated, idx,
end, arrayBody and the DEFAULT_SYNC extraction accordingly.
peglib-formatter/src/test/java/org/pragmatica/peg/formatter/v6/V6FormatterCorpusGateTest.java-48-58 (1)

48-58: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix double negation in assertion.

Line 50 uses assertFalse(!Files.isReadable(...)), which is a double negation and reduces readability. Use assertTrue directly.

🐛 Proposed fix
-    assertFalse(!Files.isReadable(GRAMMAR_PATH),
+    assertTrue(Files.isReadable(GRAMMAR_PATH),
         "java25 grammar not readable at " + GRAMMAR_PATH.toAbsolutePath());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-formatter/src/test/java/org/pragmatica/peg/formatter/v6/V6FormatterCorpusGateTest.java`
around lines 48 - 58, Replace the double-negated assertion in the setUp method:
change the assertFalse(!Files.isReadable(GRAMMAR_PATH), ...) to use assertTrue
with Files.isReadable(GRAMMAR_PATH) so the check is clearer; update the message
string as-is (it can remain "java25 grammar not readable at " +
GRAMMAR_PATH.toAbsolutePath()) and ensure this change is applied in the setUp()
method where GRAMMAR_PATH, PegParser.clearCache(), parser =
PegParser.fromGrammar(grammar).unwrap(), and fixtures = listFixtures() are used.
peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6Formatter.java-89-96 (1)

89-96: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't attribute every formatter exception to the root rule.

Any exception below the root — a child formatter rule, trivia policy, or even renderer bug — is currently wrapped as RuleFailed(cst.kindNameAt(root), e). That makes failures misleading. Capture the failing node/rule where applyUserRule(...) runs and propagate that metadata upward instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6Formatter.java`
around lines 89 - 96, The catch block is attributing every exception to the root
rule; change the error propagation so the actual failing CST node/rule is
reported: when invoking user rules (applyUserRule(...)) and other per-node logic
(e.g., inside walk, wrapRootWithFileTrivia or Renderer.render call sites), wrap
runtime exceptions in a small carrier (or throw a V6FormatterError.RuleFailed
with cst.kindNameAt(failingNode)) that carries the specific node/kind where the
failure occurred, then let that propagate to the outer try/catch so the final
catch constructs V6FormatterError.RuleFailed using the failing node/kind from
that carrier rather than always using root; update walk and applyUserRule call
sites to annotate exceptions with the node identity.
peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6FormatterConfig.java-55-60 (1)

55-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate all Builder constructor inputs, not just rules.

Builder is a public type, so callers can bypass V6FormatterConfig.builder() and construct it directly with defaultIndent < 0, maxLineWidth <= 0, or triviaPolicy == null. Mirror the outer record's validation here so the builder can't exist in an invalid state.

Suggested fix
         public Builder {
+            if (defaultIndent < 0) {
+                throw new IllegalArgumentException("defaultIndent must be >= 0");
+            }
+            if (maxLineWidth <= 0) {
+                throw new IllegalArgumentException("maxLineWidth must be > 0");
+            }
+            if (triviaPolicy == null) {
+                throw new IllegalArgumentException("triviaPolicy must not be null");
+            }
             if (rules == null) {
                 throw new IllegalArgumentException("rules must not be null");
             }
             rules = Map.copyOf(rules);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6FormatterConfig.java`
around lines 55 - 60, The Builder constructor currently only validates rules;
add the same checks as the outer V6FormatterConfig record so the Builder cannot
be instantiated with invalid values: verify defaultIndent is not negative,
maxLineWidth is greater than 0, and triviaPolicy is not null (in addition to the
existing rules null check), and call Map.copyOf(rules) as before; update the
Builder constructor (class Builder) to throw IllegalArgumentException with clear
messages for each failing parameter (defaultIndent, maxLineWidth, triviaPolicy,
rules) so direct construction mirrors V6FormatterConfig validation and prevents
invalid Builder state.
peglib-runtime/src/main/java/org/pragmatica/peg/v6/cst/CstArrayBuilder.java-169-177 (1)

169-177: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Enforce the advertised single-shot contract with an explicit state check.

build() nulls the backing arrays, so a second build() call — or any later mutation — currently fails with an incidental NullPointerException instead of the documented fail-fast behavior. Guard build() and the mutators with if (built) throw new IllegalStateException(...).

Suggested fix
     public CstArray build(int rootIndex) {
+        if (built) {
+            throw new IllegalStateException("CstArrayBuilder is single-use");
+        }
         var trimmed = Arrays.copyOf(nodes, nodeCount * CstArray.NODE_STRIDE);
         var ruleTableCopy = ruleTable.clone();
         built = true;
         nodes = null;
         lastChild = null;

You'd want the same guard on beginNode(...), endNode(...), setFlag(...), and truncate(...) as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@peglib-runtime/src/main/java/org/pragmatica/peg/v6/cst/CstArrayBuilder.java`
around lines 169 - 177, The builder currently nulls its backing arrays and sets
built=true in CstArrayBuilder.build(), but does not enforce the single-shot
contract; add explicit state checks that throw IllegalStateException when
reused: at the start of CstArrayBuilder.build() guard with "if (built) throw new
IllegalStateException(...)" and do the same guard in the mutator methods
beginNode(...), endNode(...), setFlag(...), and truncate(...); ensure the
message clearly indicates the builder has already been built so callers
fail-fast instead of getting a NullPointerException.
peglib-core/src/test/java/org/pragmatica/peg/v6/generator/LexerGeneratorTest.java-257-278 (1)

257-278: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail if the DFA probe never finds an accepting byte.

If no start-state transition reaches an accepting state, this test falls through after checking only empty-input parity and still passes. Add an explicit assertion after the loop so the probe actually guards what the comment describes.

Suggested fix
-        for (int b = 0; b < 256; b++ ) {
+        boolean foundAcceptedByte = false;
+        for (int b = 0; b < 256; b++ ) {
             int next = built.dfa()
                             .dfa()
                             .transition(0, b);
@@
             var engineTokens = built.engine()
                                     .lex(probe);
             var compiledTokens = compiled.lex(probe);
             assertTokenArraysEqual(engineTokens, compiledTokens);
-            return;
+            foundAcceptedByte = true;
+            break;
         }
+        assertThat(foundAcceptedByte)
+        .as("java25 DFA should accept at least one start-state byte")
+        .isTrue();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/test/java/org/pragmatica/peg/v6/generator/LexerGeneratorTest.java`
around lines 257 - 278, The loop in LexerGeneratorTest that probes bytes using
built.dfa().dfa().transition(...) and built.dfa().dfa().acceptKind(...) can fall
through without finding an accepting byte and the test will still pass; after
the for-loop add an explicit assertion (e.g., fail or assertTrue) that ensures
at least one accepting byte was found so the probe actually validates the DFA
parity check, referencing the same built and compiled lex(...) comparisons
already used in the loop to determine success.
peglib-core/src/main/java/org/pragmatica/peg/v6/generator/LexerGenerator.java-72-73 (1)

72-73: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The empty-match warning text is stale.

The generated lex() no longer throws here; it forces progress by emitting a synthetic one-character whitespace token on no-match. Since this warning is surfaced to users via the mojo, it currently describes the wrong runtime behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/main/java/org/pragmatica/peg/v6/generator/LexerGenerator.java`
around lines 72 - 73, The warning message added in LexerGenerator when
dfa.acceptKind(Dfa.START_STATE) != Dfa.NO_ACCEPT is stale; update the string
passed to warnings.add to describe the new behavior: instead of generated lex()
throwing on any input, the lexer now forces progress by emitting a synthetic
one-character whitespace token when no rule matches. Locate the warning
construction in LexerGenerator (the warnings.add(...) call that mentions "DFA
start state is accepting") and replace the message text to explain the synthetic
whitespace-token fallback and advise tightening the offending rule (e.g., use
'+' instead of '*').
docs/MIGRATION-0.5-TO-0.6.md-22-22 (1)

22-22: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the temporary nature of the v6 package prefix.

The table indicates the entry point is org.pragmatica.peg.v6.PegParser with a note "(during 0.6.0 development; the .v6 suffix will collapse to org.pragmatica.peg at GA)". Since this is documentation for the 0.6.0 release (not a pre-release), clarify whether users should expect the .v6 prefix in the released version or if it will be removed before the release is finalized.

If .v6 is present in 0.6.0 release, consider whether this might cause confusion given it's called "0.6.0" (suggesting stability) rather than "0.6.0-rc".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/MIGRATION-0.5-TO-0.6.md` at line 22, The table row referencing the entry
point currently shows org.pragmatica.peg.v6.PegParser with an ambiguous
parenthetical; clarify whether the .v6 package prefix will actually appear in
the 0.6.0 release or will be removed before release. Update the text for the
PegParser entry to explicitly state the release behavior (e.g., "In 0.6.0 the
entry point is org.pragmatica.peg.v6.PegParser — the .v6 package prefix is
temporary and will be collapsed to org.pragmatica.peg at GA" if .v6 ships, or
change to "org.pragmatica.peg.PegParser" if it will be removed), so readers know
exactly which class name to use and why the versioned package exists.
🧹 Nitpick comments (10)
peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/DfaBuilderTest.java (1)

26-26: ⚡ Quick win

Fix spacing around comparison operator.

Missing space before the < operator: Dfa.START_STATE< dfa.stateCount() should be Dfa.START_STATE < dfa.stateCount().

🎨 Proposed fix
-        if (Dfa.START_STATE< dfa.stateCount()) {
+        if (Dfa.START_STATE < dfa.stateCount()) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/DfaBuilderTest.java` at
line 26, Fix the spacing around the comparison operator in the test: change the
expression using Dfa.START_STATE and dfa.stateCount() to use a space around the
'<' operator so it reads "Dfa.START_STATE < dfa.stateCount()"; update the
occurrence in the method that contains the conditional referencing
Dfa.START_STATE and dfa.stateCount() to match this spacing style.
peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/RuleClassifierTest.java (1)

247-254: 💤 Low value

Consider using a test logger instead of System.out.

The diagnostic output at lines 247-253 uses System.out.println for reporting classifier results. While acceptable in tests, using a proper logging framework (or JUnit's standard output capture) would be more maintainable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/RuleClassifierTest.java`
around lines 247 - 254, Replace the ad-hoc System.out.println diagnostic prints
in RuleClassifierTest (the block that prints counts and
classification.warnings()) with a test logger or JUnit/Slf4j logging: locate the
prints in RuleClassifierTest around the variable classification and counts,
create or reuse a logger (e.g., a private static final Logger in the test or use
JUnit's TestInfo/TestReporter) and replace the System.out.println calls with
logger.info/debug or TestReporter.publishEntry calls so test output is captured
and configurable instead of printing directly to stdout.
peglib-core/src/main/java/org/pragmatica/peg/v6/lexer/DfaBuilder.java (1)

956-998: ⚡ Quick win

Consider reformatting the large switch expression for readability.

The compileExpression switch expression spans 43 lines with dense, single-line case arms. While functionally correct, the formatting makes it difficult to scan and review. Consider breaking each case onto multiple lines or extracting complex case logic into helper methods.

Example reformatting:

-return switch (expr) {case Expression.Literal lit -> Result.success(compileLiteral(nfa, lit));case Expression.CharClass cc -> Result.success(compileCharClass(nfa,
-                                                                                                                                                                      cc));
+return switch (expr) {
+    case Expression.Literal lit -> Result.success(compileLiteral(nfa, lit));
+    case Expression.CharClass cc -> Result.success(compileCharClass(nfa, cc));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@peglib-core/src/main/java/org/pragmatica/peg/v6/lexer/DfaBuilder.java` around
lines 956 - 998, The large switch expression in compileExpression is hard to
read; split each case into its own multi-line arm and move complex logic into
small helpers: extract the Sequence case body (including the delimited-block
check) into a helper that calls compileDelimitedBlock and compileSequence, break
chained map calls into separate statements or helpers for
ZeroOrMore/OneOrMore/Optional (wrapZeroOrMore, wrapOneOrMore, wrapOptional), and
put compileChoice/compileRepetition/unsupported(...) cases on separate lines
with clear formatting; ensure each case uses a clear yield/return path so the
switch is easy to scan.
peglib-playground/src/test/java/org/pragmatica/peg/playground/v6/PlaygroundEngineV6Test.java (1)

55-57: ⚡ Quick win

Assert success before calling unwrap() in the trivia test.

If this case regresses to failure, the test currently fails via unwrap() instead of a clear assertion on engine status.

Proposed patch
     `@Test`
     void run_triviaCount_reflectsLexedWhitespace() {
         var request = new ParseRequest(GRAMMAR, "1 + 2");
-        var outcome = PlaygroundEngineV6.run(request).unwrap();
+        var result = PlaygroundEngineV6.run(request);
+        assertThat(result.isSuccess()).isTrue();
+        var outcome = result.unwrap();
         // Two whitespace runs lexed as trivia tokens.
         assertThat(outcome.stats().triviaCount()).isGreaterThan(0);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-playground/src/test/java/org/pragmatica/peg/playground/v6/PlaygroundEngineV6Test.java`
around lines 55 - 57, The test should assert the engine returned a successful
result before calling unwrap(); locate the ParseRequest creation and the call to
PlaygroundEngineV6.run (the variables request and outcome) and add an assertion
that the run result indicates success (e.g. check outcome.isSuccess() or the
equivalent status accessor on the run result) immediately after obtaining
outcome, then call unwrap() only after that assertion to avoid panics when the
engine fails.
peglib-core/src/test/java/org/pragmatica/peg/v6/generator/ParserGeneratorTest.java (1)

277-286: 💤 Low value

Consider using streams for countParserRules.

The manual counter loop can be expressed more idiomatically using Java streams, especially since this targets Java 25.

♻️ Proposed refactor using streams
 private static int countParserRules(RuleClassifier.Classification c) {
-    int n = 0;
-    for (var k : c.kinds()
-                  .values()) {
-        if (k == org.pragmatica.peg.v6.lexer.RuleKind.PARSER || k == org.pragmatica.peg.v6.lexer.RuleKind.MIXED) {
-            n++ ;
-        }
-    }
-    return n;
+    return (int) c.kinds()
+        .values()
+        .stream()
+        .filter(k -> k == org.pragmatica.peg.v6.lexer.RuleKind.PARSER 
+                  || k == org.pragmatica.peg.v6.lexer.RuleKind.MIXED)
+        .count();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/test/java/org/pragmatica/peg/v6/generator/ParserGeneratorTest.java`
around lines 277 - 286, Refactor the countParserRules method to replace the
manual loop with a Java Stream: use c.kinds().values().stream().filter(k -> k ==
org.pragmatica.peg.v6.lexer.RuleKind.PARSER || k ==
org.pragmatica.peg.v6.lexer.RuleKind.MIXED).count() and convert the long result
to int (or use Math.toIntExact) so the method returns the correct int; update
the implementation in countParserRules to use this stream-based count instead of
the explicit counter and loop.
peglib-core/src/test/java/org/pragmatica/peg/v6/PegParserGateTest.java (1)

179-195: 💤 Low value

Consider optimizing StringBuilder capacity in escape(String).

The escape(String) method allocates a StringBuilder with s.length() + 4, but escape sequences can expand significantly (e.g., \u00XX is 6 chars). For strings with many control characters, this may trigger multiple reallocations.

♻️ Proposed optimization
 private static String escape(String s) {
-    var sb = new StringBuilder(s.length() + 4);
+    var sb = new StringBuilder((int) (s.length() * 1.2) + 4);
     for (int i = 0; i < s.length(); i++) {
         sb.append(escape(s.charAt(i)));
     }
     return sb.toString();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@peglib-core/src/test/java/org/pragmatica/peg/v6/PegParserGateTest.java`
around lines 179 - 195, The StringBuilder in escape(String) is under-provisioned
causing reallocations when many chars expand (escape(String) and escape(char));
fix by initializing the StringBuilder with a safe worst-case capacity (e.g.,
s.length() * 6 to accommodate sequences like "\u00xx") or compute needed
capacity by scanning and summing expansion lengths using escape(char) logic
before appending, then use that capacity when constructing the StringBuilder.
peglib-core/src/test/java/org/pragmatica/peg/v6/generator/Java25ParserGateTest.java (1)

344-355: 💤 Low value

Consider simplifying switch expression syntax.

The switch expression uses traditional case arrow syntax with braces. For simple single-statement cases, Java 14+ allows omitting braces for more concise code.

♻️ Proposed simplification
 private static String escape(char c) {
     return switch (c) {
-        case'\n' -> "\\n";
-        case'\r' -> "\\r";
-        case'\t' -> "\\t";
-        case'"' -> "\\\"";
-        case'\\' -> "\\\\";
-        default -> c < 32 || c == 127
-                   ? String.format("\\u%04x", (int) c)
-                   : String.valueOf(c);
+        case '\n' -> "\\n";
+        case '\r' -> "\\r";
+        case '\t' -> "\\t";
+        case '"' -> "\\\"";
+        case '\\' -> "\\\\";
+        default -> c < 32 || c == 127 ? String.format("\\u%04x", (int) c) : String.valueOf(c);
     };
 }

Note: Also added spacing after case for consistency (line 346-350).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/test/java/org/pragmatica/peg/v6/generator/Java25ParserGateTest.java`
around lines 344 - 355, The switch inside the escape(char c) method should use
the concise, consistent switch-expression style: add a space after each case
token (e.g., change case'\n' to case '\n') and normalize all case labels to the
compact form already used for single-statement arms; keep the default arm as an
expression but format it consistently (e.g., case '\n' -> "\\n"; ... default ->
(c < 32 || c == 127) ? String.format("\\u%04x", (int) c) : String.valueOf(c);)
so the switch arms are uniform and concise.
peglib-core/src/main/java/org/pragmatica/peg/v6/generator/VisitorGenerator.java (1)

64-78: ⚡ Quick win

Generated visitor source order is nondeterministic due raw Map iteration

Iterating unsorted ruleKinds can produce unstable source ordering across runs/JDKs, which hurts reproducible builds and golden-file tests.

Emit in deterministic key order
-        for ( var e : ruleKinds.entrySet()) {
+        for ( var e : ruleKinds.entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
@@
-        for ( var e : ruleKinds.entrySet()) {
+        for ( var e : ruleKinds.entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
@@
-        for ( var name : ruleKinds.keySet()) {
+        for ( var name : ruleKinds.keySet().stream().sorted().toList()) {

Also applies to: 96-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/main/java/org/pragmatica/peg/v6/generator/VisitorGenerator.java`
around lines 64 - 78, The visitor generator iterates ruleKinds in
insertion/HashMap order causing nondeterministic output; modify VisitorGenerator
to iterate entries in a deterministic key order (e.g., sort ruleKinds.keySet()
or use a TreeMap view) when emitting the RULE_... constants and the switch/case
visit methods (both the block that builds the protected static final int
RULE_..._KIND lines and the switch case emission around visit... calls, also the
similar emission at the other location referenced around lines 96-101). Ensure
you consistently use the same sorted iteration for all places where ruleKinds is
traversed so generated source order is stable across runs.
peglib-runtime/src/main/java/org/pragmatica/peg/v6/cst/CstArray.java (1)

83-85: 💤 Low value

Consider returning a defensive copy of ruleTable.

The ruleTable() method returns the internal String[] directly, which could allow callers to mutate it. If the array is intended to be immutable, consider returning ruleTable.clone() or wrapping in an unmodifiable view.

Proposed fix
 public String[] ruleTable() {
-    return ruleTable;
+    return ruleTable.clone();
 }

Alternatively, if performance is critical and callers are trusted, document the immutability contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@peglib-runtime/src/main/java/org/pragmatica/peg/v6/cst/CstArray.java` around
lines 83 - 85, The ruleTable() method in class CstArray exposes the internal
String[] ruleTable directly allowing external mutation; change ruleTable() to
return a defensive copy (e.g., clone or Arrays.copyOf(ruleTable,
ruleTable.length)) so callers cannot mutate the internal array, and update any
relevant Javadoc to note the returned array is a copy; if you deliberately want
to avoid the copy for performance, instead document the immutability/trust
contract on CstArray.ruleTable and ruleTable().
CHANGELOG.md (1)

62-62: 💤 Low value

Consider breaking up the extremely long JBCT conformance paragraph.

Line 62 contains a single sentence of approximately 1,900 characters describing the JBCT 0.25.0 conformance changes. While technically correct, this creates readability challenges. Consider breaking this into multiple sentences or a bulleted list to improve scanability for users reviewing the changelog.

♻️ Suggested restructuring
- **JBCT 0.25.0 conformance pass on v6** (2026-05-10): 
  - All 123 strict-mode lint errors eliminated in both framework code and generated parser/lexer source
  - Defensive null/bounds checks removed from internal helpers (callers within v6 are trusted; JVM catches genuine OOB)
  - `Result<Void>` converted to `Result<Unit>` on `ParserGenerator` emit visitors
  - Null returns converted to `Option<T>` in `DfaBuilder`, `IncrementalParser`, `RuleClassifier` methods
  - Reflection wrappers rewritten as `Result.lift(...).unwrap()` for typed failure causes
  - Generated parser/lexer no longer throw exceptions for stall/null/unknown-rule conditions
  - Hot-path void mutators carry `@SuppressWarnings("JBCT-RET-01"/"JBCT-EX-01")`
  - Lint passes with 0 errors, 287 style warnings
  - Bench: reference `7.06 ± 0.19 ms` (vs `~7.4ms` baseline), selfhost `97.4 ± 27.9 ms` (vs `~97ms` baseline) — within noise, no regression
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 62, The JBCT conformance paragraph on "JBCT 0.25.0
conformance pass on v6" is extremely long and hurts readability; split it into
multiple sentences or, preferably, a short bulleted list (as suggested) that
separates the major changes (lint error removal, removed defensive guards in
CstArray/CstArrayBuilder/TokenArray/TokenArrayBuilder/Diagnostic,
Result<Void>→Result<Unit> in ParserGenerator, null→Option<T> conversions in
DfaBuilder/IncrementalParser.tryPartialReparse/RuleClassifier, reflection unwrap
changes in LexerCompiler.CompiledLexer.lex and
ParserCompiler.CompiledParser.{parse, parseRuleFrom, ruleKinds}, generated
parser/lexer behavior changes, `@SuppressWarnings` on hot-path mutators, and
lint/bench results) so each key point (use the symbols above to locate text)
becomes its own list item or short sentence for better scanability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ba6c3cb-5e0d-4248-b340-6f0ec62598e8

📥 Commits

Reviewing files that changed from the base of the PR and between 6929c73 and 31c3b3c.

📒 Files selected for processing (117)
  • .gitignore
  • CHANGELOG.md
  • README.md
  • docs/HANDOVER.md
  • docs/MIGRATION-0.5-TO-0.6.md
  • peglib-core/pom.xml
  • peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/IncrementalEditBenchmark.java
  • peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/Java25LargeFixturesBenchmark.java
  • peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/Java25LargeFixturesV51Benchmark.java
  • peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/Java25V51GeneratedParseBenchmark.java
  • peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/Java25V51ParseBenchmark.java
  • peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/Java25V6ColdCompileBenchmark.java
  • peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/Java25V6ParseBenchmark.java
  • peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/JavacParseOnlyBenchmark.java
  • peglib-core/src/main/java/org/pragmatica/peg/grammar/Grammar.java
  • peglib-core/src/main/java/org/pragmatica/peg/grammar/GrammarParser.java
  • peglib-core/src/main/java/org/pragmatica/peg/grammar/GrammarResolver.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/Parser.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/PegParser.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/analyzer/LeftRecursionCause.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/analyzer/LeftRecursionDetector.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/analyzer/NamedCaptureCause.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/analyzer/NamedCaptureDetector.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/generator/LexerCompiler.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/generator/LexerGenerator.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/generator/ParserCompiler.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/generator/ParserGenerator.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/generator/VisitorGenerator.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/incremental/IncrementalParser.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/lexer/Dfa.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/lexer/DfaBuilder.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/lexer/LexerEngine.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/lexer/RuleClassifier.java
  • peglib-core/src/main/java/org/pragmatica/peg/v6/lexer/RuleKind.java
  • peglib-core/src/test/java/org/pragmatica/peg/grammar/RecoverDirectiveTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/perf/SelfhostFixtureGenerator.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/PegParserGateTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/PegParserTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/analyzer/LeftRecursionDetectorTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/diagnostic/FactoryClassGeneratorDiagTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/CutOperatorTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/IdentifierFallbackTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/Java25BisectTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/Java25ParserGateTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/LexerGeneratorTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/NamedCaptureTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/ParserGeneratorTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/RecoverDirectiveGeneratorTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/RecoveryTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/generator/VisitorGeneratorTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/incremental/IncrementalParserTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/AliasDetectionTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/DfaBuilderTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/Java25CorpusGateTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/KeywordResolutionTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/LexerEngineTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/RuleClassifierTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/TriviaClassificationTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/lexer/UnicodeAndCommentsTest.java
  • peglib-core/src/test/java/org/pragmatica/peg/v6/token/TokenArraySpliceTest.java
  • peglib-core/src/test/resources/bench-fixtures/Java25SelfHost-v51.java.txt
  • peglib-core/src/test/resources/java25.peg
  • peglib-core/src/test/resources/perf-corpus-baseline/format-examples/Annotations.java.hash
  • peglib-core/src/test/resources/perf-corpus-baseline/format-examples/Annotations.java.ruleHits.txt
  • peglib-core/src/test/resources/perf-corpus-baseline/format-examples/CompoundAssignments.java.hash
  • peglib-core/src/test/resources/perf-corpus-baseline/format-examples/CompoundAssignments.java.ruleHits.txt
  • peglib-core/src/test/resources/perf-corpus-baseline/ruleCoverage.txt
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/Annotations.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/BlankLines.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/Comments.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/CompoundAssignments.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/ExhaustiveSwitchPatterns.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/Imports.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/KeywordPrefixedIdentifiers.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/Lambdas.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/LineWrapping.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/MultilineArguments.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/TernaryOperators.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/format-examples/TextBlocks.java.hash
  • peglib-core/src/test/resources/perf-corpus-interpreter-baseline/large/FactoryClassGenerator.java.txt.hash
  • peglib-formatter/pom.xml
  • peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6FormatContext.java
  • peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6Formatter.java
  • peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6FormatterConfig.java
  • peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6FormatterRule.java
  • peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6TriviaPolicy.java
  • peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/package-info.java
  • peglib-formatter/src/test/java/org/pragmatica/peg/formatter/v6/V6FormatterCorpusGateTest.java
  • peglib-formatter/src/test/java/org/pragmatica/peg/formatter/v6/V6FormatterTest.java
  • peglib-incremental/pom.xml
  • peglib-maven-plugin/pom.xml
  • peglib-maven-plugin/src/main/java/org/pragmatica/peg/maven/GenerateV6Mojo.java
  • peglib-maven-plugin/src/test/java/org/pragmatica/peg/maven/MojoIntegrationTest.java
  • peglib-playground/pom.xml
  • peglib-playground/src/main/java/org/pragmatica/peg/playground/v6/PlaygroundEngineV6.java
  • peglib-playground/src/main/java/org/pragmatica/peg/playground/v6/PlaygroundReplV6.java
  • peglib-playground/src/test/java/org/pragmatica/peg/playground/v6/PlaygroundEngineV6Test.java
  • peglib-playground/src/test/java/org/pragmatica/peg/playground/v6/PlaygroundReplV6Test.java
  • peglib-runtime/pom.xml
  • peglib-runtime/src/main/java/org/pragmatica/peg/v6/cst/CstArray.java
  • peglib-runtime/src/main/java/org/pragmatica/peg/v6/cst/CstArrayBuilder.java
  • peglib-runtime/src/main/java/org/pragmatica/peg/v6/cst/CstNode.java
  • peglib-runtime/src/main/java/org/pragmatica/peg/v6/cst/ParseResult.java
  • peglib-runtime/src/main/java/org/pragmatica/peg/v6/diagnostic/Diagnostic.java
  • peglib-runtime/src/main/java/org/pragmatica/peg/v6/diagnostic/Severity.java
  • peglib-runtime/src/main/java/org/pragmatica/peg/v6/token/LexFn.java
  • peglib-runtime/src/main/java/org/pragmatica/peg/v6/token/TokenArray.java
  • peglib-runtime/src/main/java/org/pragmatica/peg/v6/token/TokenArrayBuilder.java
  • peglib-runtime/src/test/java/org/pragmatica/peg/v6/cst/CstArrayBuilderTest.java
  • peglib-runtime/src/test/java/org/pragmatica/peg/v6/cst/CstArrayTest.java
  • peglib-runtime/src/test/java/org/pragmatica/peg/v6/cst/CstNodeViewTest.java
  • peglib-runtime/src/test/java/org/pragmatica/peg/v6/cst/FindCheckpointAncestorTest.java
  • peglib-runtime/src/test/java/org/pragmatica/peg/v6/cst/ParseResultTest.java
  • peglib-runtime/src/test/java/org/pragmatica/peg/v6/cst/SpliceSubtreeTest.java
  • peglib-runtime/src/test/java/org/pragmatica/peg/v6/diagnostic/DiagnosticTest.java
  • peglib-runtime/src/test/java/org/pragmatica/peg/v6/token/TokenArrayTest.java
  • pom.xml

Comment thread docs/HANDOVER.md
mvn install
| | |
|---|---|
| **Active branch** | `release-0.6.0` at `4a2799d`, tagged `v0.6.0-candidate` |

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the commit and tag referenced in handover
echo "Checking commit 4a2799d:"
git log --oneline -1 4a2799d 2>/dev/null || echo "✗ Commit 4a2799d not found"

echo -e "\nChecking tag v0.6.0-candidate:"
git tag -l "v0.6.0-candidate" | grep -q "v0.6.0-candidate" && \
    echo "✓ Tag v0.6.0-candidate exists" || \
    echo "✗ Tag v0.6.0-candidate not found"

echo -e "\nChecking branch release-0.6.0:"
git branch -r | grep -q "origin/release-0.6.0" && \
    echo "✓ Branch release-0.6.0 exists" || \
    echo "✗ Branch release-0.6.0 not found"

Repository: siy/java-peglib

Length of output: 336


Update handover references: tag v0.6.0-candidate and branch release-0.6.0 do not exist.

The handover document at line 13 references commit 4a2799d on branch release-0.6.0 with tag v0.6.0-candidate. While the commit exists, neither the tag nor the branch can be found in the repository. Update the document with the correct branch/tag information or remove these references if they are no longer applicable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/HANDOVER.md` at line 13, The HANDOVER.md references a non-existent
branch `release-0.6.0` and tag `v0.6.0-candidate` for commit `4a2799d`; edit the
line that currently reads "| **Active branch** | `release-0.6.0` at `4a2799d`,
tagged `v0.6.0-candidate` |" to either replace `release-0.6.0` and
`v0.6.0-candidate` with the actual existing branch/tag names that point to
`4a2799d` or remove the branch/tag portion entirely and keep only the commit SHA
`4a2799d` (or update to the correct commit if needed) so the document no longer
references missing refs.

Comment on lines +111 to +138
private static Class<?> compileAndLoad(String source, String fqcn) throws Exception {
Path tempDir = Files.createTempDirectory("peglib-v51gen-bench");
var packagePath = fqcn.substring(0, fqcn.lastIndexOf('.')).replace('.', '/');
var simpleClassName = fqcn.substring(fqcn.lastIndexOf('.') + 1);

var packageDir = tempDir.resolve(packagePath);
Files.createDirectories(packageDir);

var sourceFile = packageDir.resolve(simpleClassName + ".java");
Files.writeString(sourceFile, source);

var compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("JDK compiler not available (not running on a JDK?)");
}
int rc = compiler.run(null, null, null,
"-d", tempDir.toString(),
"-cp", System.getProperty("java.class.path"),
sourceFile.toString());
if (rc != 0) {
throw new RuntimeException("Compilation failed for " + fqcn + " (rc=" + rc + ")");
}

var classLoader = new URLClassLoader(
new URL[]{tempDir.toUri().toURL()},
Java25V51GeneratedParseBenchmark.class.getClassLoader());
return classLoader.loadClass(fqcn);
}

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 | ⚖️ Poor tradeoff

Resource leak: temp directory and URLClassLoader not cleaned up.

The compileAndLoad method creates a temporary directory (line 112) and a URLClassLoader (line 134-136) but neither is cleaned up. Over multiple benchmark trials/forks, this accumulates temp directories in the system temp space.

Considerations:

  • JMH benchmarks typically run for extended periods, and temp directory accumulation can exhaust disk space.
  • The URLClassLoader holds references to the temp directory, preventing OS cleanup even after JVM exit on some platforms.
  • Since this is a benchmark measuring parse performance (not classloading), cleanup overhead is outside the measurement and acceptable.
♻️ Proposed fix: Add cleanup and use try-with-resources
 private static Class<?> compileAndLoad(String source, String fqcn) throws Exception {
     Path tempDir = Files.createTempDirectory("peglib-v51gen-bench");
+    try {
         var packagePath = fqcn.substring(0, fqcn.lastIndexOf('.')).replace('.', '/');
         var simpleClassName = fqcn.substring(fqcn.lastIndexOf('.') + 1);
 
         var packageDir = tempDir.resolve(packagePath);
         Files.createDirectories(packageDir);
 
         var sourceFile = packageDir.resolve(simpleClassName + ".java");
         Files.writeString(sourceFile, source);
 
         var compiler = ToolProvider.getSystemJavaCompiler();
         if (compiler == null) {
             throw new IllegalStateException("JDK compiler not available (not running on a JDK?)");
         }
         int rc = compiler.run(null, null, null,
                 "-d", tempDir.toString(),
                 "-cp", System.getProperty("java.class.path"),
                 sourceFile.toString());
         if (rc != 0) {
             throw new RuntimeException("Compilation failed for " + fqcn + " (rc=" + rc + ")");
         }
 
-        var classLoader = new URLClassLoader(
+        try (var classLoader = new URLClassLoader(
                 new URL[]{tempDir.toUri().toURL()},
-                Java25V51GeneratedParseBenchmark.class.getClassLoader());
-        return classLoader.loadClass(fqcn);
+                Java25V51GeneratedParseBenchmark.class.getClassLoader())) {
+            return classLoader.loadClass(fqcn);
+        }
+    } finally {
+        // Clean up temp directory after class is loaded
+        if (Files.exists(tempDir)) {
+            Files.walk(tempDir)
+                .sorted(java.util.Comparator.reverseOrder())
+                .forEach(p -> {
+                    try { Files.deleteIfExists(p); } catch (Exception ignored) {}
+                });
+        }
+    }
 }

Note: Closing the URLClassLoader immediately after loadClass may cause issues if the JVM needs to access class resources later. An alternative is to store the classLoader as a field and clean it up in a @TearDown(Level.Trial) method.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static Class<?> compileAndLoad(String source, String fqcn) throws Exception {
Path tempDir = Files.createTempDirectory("peglib-v51gen-bench");
var packagePath = fqcn.substring(0, fqcn.lastIndexOf('.')).replace('.', '/');
var simpleClassName = fqcn.substring(fqcn.lastIndexOf('.') + 1);
var packageDir = tempDir.resolve(packagePath);
Files.createDirectories(packageDir);
var sourceFile = packageDir.resolve(simpleClassName + ".java");
Files.writeString(sourceFile, source);
var compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("JDK compiler not available (not running on a JDK?)");
}
int rc = compiler.run(null, null, null,
"-d", tempDir.toString(),
"-cp", System.getProperty("java.class.path"),
sourceFile.toString());
if (rc != 0) {
throw new RuntimeException("Compilation failed for " + fqcn + " (rc=" + rc + ")");
}
var classLoader = new URLClassLoader(
new URL[]{tempDir.toUri().toURL()},
Java25V51GeneratedParseBenchmark.class.getClassLoader());
return classLoader.loadClass(fqcn);
}
private static Class<?> compileAndLoad(String source, String fqcn) throws Exception {
Path tempDir = Files.createTempDirectory("peglib-v51gen-bench");
try {
var packagePath = fqcn.substring(0, fqcn.lastIndexOf('.')).replace('.', '/');
var simpleClassName = fqcn.substring(fqcn.lastIndexOf('.') + 1);
var packageDir = tempDir.resolve(packagePath);
Files.createDirectories(packageDir);
var sourceFile = packageDir.resolve(simpleClassName + ".java");
Files.writeString(sourceFile, source);
var compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("JDK compiler not available (not running on a JDK?)");
}
int rc = compiler.run(null, null, null,
"-d", tempDir.toString(),
"-cp", System.getProperty("java.class.path"),
sourceFile.toString());
if (rc != 0) {
throw new RuntimeException("Compilation failed for " + fqcn + " (rc=" + rc + ")");
}
try (var classLoader = new URLClassLoader(
new URL[]{tempDir.toUri().toURL()},
Java25V51GeneratedParseBenchmark.class.getClassLoader())) {
return classLoader.loadClass(fqcn);
}
} finally {
// Clean up temp directory after class is loaded
if (Files.exists(tempDir)) {
Files.walk(tempDir)
.sorted(java.util.Comparator.reverseOrder())
.forEach(p -> {
try { Files.deleteIfExists(p); } catch (Exception ignored) {}
});
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/Java25V51GeneratedParseBenchmark.java`
around lines 111 - 138, The compileAndLoad method creates a tempDir and a
URLClassLoader that are never cleaned up; modify
compileAndLoad/Java25V51GeneratedParseBenchmark to ensure the temp directory is
deleted and the URLClassLoader is closed when no longer needed: either (a)
return both the loaded Class and its classloader and close the URLClassLoader
and delete tempDir in a new `@TearDown`(Level.Trial) method, or (b) store the
URLClassLoader and tempDir as fields on Java25V51GeneratedParseBenchmark and
close the classLoader and recursively delete tempDir in `@TearDown`, ensuring you
reference the tempDir and URLClassLoader created in compileAndLoad and avoid
closing the loader while the benchmark still needs the loaded classes.

Comment on lines +41 to +47
private static final Path GRAMMAR_PATH = Path.of("src/test/resources/java25.peg");

private String grammarText;

@Setup(Level.Iteration)
public void setup() throws Exception {
grammarText = Files.readString(GRAMMAR_PATH, StandardCharsets.UTF_8);

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 | ⚡ Quick win

Use classpath resource loading instead of a working-directory-relative path.

Path.of("src/test/resources/java25.peg") makes this benchmark fragile outside module-root execution. It can fail in CI/JAR/plugin-driven runs.

Proposed patch
-    private static final Path GRAMMAR_PATH = Path.of("src/test/resources/java25.peg");
+    private static final String GRAMMAR_RESOURCE = "/java25.peg";
@@
     `@Setup`(Level.Iteration)
     public void setup() throws Exception {
-        grammarText = Files.readString(GRAMMAR_PATH, StandardCharsets.UTF_8);
+        try (var in = Java25V6ColdCompileBenchmark.class.getResourceAsStream(GRAMMAR_RESOURCE)) {
+            if (in == null) {
+                throw new IllegalStateException("Missing benchmark resource: " + GRAMMAR_RESOURCE);
+            }
+            grammarText = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        }
         PegParser.clearCache();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/Java25V6ColdCompileBenchmark.java`
around lines 41 - 47, Replace the working-directory-dependent Path usage with
classpath resource loading: remove the GRAMMAR_PATH constant and, in the setup()
method, load "java25.peg" via getClass().getResourceAsStream("/java25.peg") or
Thread.currentThread().getContextClassLoader().getResourceAsStream("java25.peg")
and read the stream into grammarText (e.g., new
String(inputStream.readAllBytes(), StandardCharsets.UTF_8)), failing fast with a
clear error if the resource is null; update references to
GRAMMAR_PATH/grammarText accordingly.

Comment on lines +47 to +49
private static final Path GRAMMAR_PATH = Path.of("src/test/resources/java25.peg");
private static final Path FIXTURE_DIR = Path.of("src/test/resources/perf-corpus/format-examples");

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 | ⚡ Quick win

Resource paths are brittle for JMH runs; load grammar/fixtures from classpath.

Using src/test/resources/... ties execution to a specific working directory layout and can break in benchmark tooling environments.

Proposed patch
-    private static final Path GRAMMAR_PATH = Path.of("src/test/resources/java25.peg");
-    private static final Path FIXTURE_DIR = Path.of("src/test/resources/perf-corpus/format-examples");
+    private static final String GRAMMAR_RESOURCE = "/java25.peg";
+    private static final String FIXTURE_PREFIX = "/perf-corpus/format-examples/";
@@
     `@Setup`(Level.Trial)
     public void setup() throws Exception {
-        String grammarText = Files.readString(GRAMMAR_PATH, StandardCharsets.UTF_8);
+        String grammarText = readResource(GRAMMAR_RESOURCE);
         PegParser.clearCache();
         parser = PegParser.fromGrammar(grammarText).unwrap();
-        input = Files.readString(FIXTURE_DIR.resolve(fixture), StandardCharsets.UTF_8);
+        input = readResource(FIXTURE_PREFIX + fixture);
     }
+
+    private static String readResource(String path) throws Exception {
+        try (var in = Java25V6ParseBenchmark.class.getResourceAsStream(path)) {
+            if (in == null) {
+                throw new IllegalStateException("Missing benchmark resource: " + path);
+            }
+            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        }
+    }

Also applies to: 71-75

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/Java25V6ParseBenchmark.java`
around lines 47 - 49, Replace hard-coded filesystem paths GRAMMAR_PATH and
FIXTURE_DIR with classpath resource lookups: use the class loader to locate
"java25.peg" and the "perf-corpus/format-examples" resource directory (e.g.,
ClassLoader.getResource / getResourceAsStream or getResources and then
Paths.get(resource.toURI()) when available) inside the Java25V6ParseBenchmark
class so benchmarks work regardless of working directory; update any code using
GRAMMAR_PATH and FIXTURE_DIR to accept an InputStream/URL/Path obtained from the
classpath lookup and handle the case where the resource is missing by throwing a
clear exception.

Comment on lines +99 to +104
// Fresh task per invocation: parsing state is per-task. null DiagnosticListener
// and null Writer route diagnostics to System.err; we don't expect any on the
// reference fixture (hand-written Java 25). For selfhost (synthesized parser
// source), syntactic diagnostics may appear but parse() still returns a tree.
JavacTask task = (JavacTask) compiler.getTask(null, null, null, null, null, sources);
Iterable<? extends CompilationUnitTree> trees = task.parse();

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify benchmark call sites still using null output/listener for JavaCompiler#getTask.
rg -nP --type=java 'getTask\(\s*null\s*,\s*null\s*,\s*null'

Repository: siy/java-peglib

Length of output: 238


Remove stderr I/O from benchmark timing path by suppressing compiler diagnostics.

Line 103 uses null writer and listener, which route javac diagnostics to System.err. Even diagnostic-free fixtures risk unexpected output from synthesized sources, introducing unmeasured I/O overhead that skews parse timing. Suppress diagnostics explicitly using a null writer and no-op listener.

🔧 Proposed fix
-        JavacTask task = (JavacTask) compiler.getTask(null, null, null, null, null, sources);
+        JavacTask task = (JavacTask) compiler.getTask(
+            java.io.Writer.nullWriter(),
+            null,
+            diagnostic -> {},
+            null,
+            null,
+            sources
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Fresh task per invocation: parsing state is per-task. null DiagnosticListener
// and null Writer route diagnostics to System.err; we don't expect any on the
// reference fixture (hand-written Java 25). For selfhost (synthesized parser
// source), syntactic diagnostics may appear but parse() still returns a tree.
JavacTask task = (JavacTask) compiler.getTask(null, null, null, null, null, sources);
Iterable<? extends CompilationUnitTree> trees = task.parse();
// Fresh task per invocation: parsing state is per-task. null DiagnosticListener
// and null Writer route diagnostics to System.err; we don't expect any on the
// reference fixture (hand-written Java 25). For selfhost (synthesized parser
// source), syntactic diagnostics may appear but parse() still returns a tree.
JavacTask task = (JavacTask) compiler.getTask(
java.io.Writer.nullWriter(),
null,
diagnostic -> {},
null,
null,
sources
);
Iterable<? extends CompilationUnitTree> trees = task.parse();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-core/src/jmh/java/org/pragmatica/peg/v6/perf/JavacParseOnlyBenchmark.java`
around lines 99 - 104, The benchmark currently passes null DiagnosticListener
and Writer to compiler.getTask, which routes diagnostics to System.err and can
add I/O overhead; instead supply an explicit no-op DiagnosticListener and a
null-printing Writer to suppress stderr: replace the null listener and writer
arguments in the call to compiler.getTask(...) (the place where JavacTask task =
(JavacTask) compiler.getTask(...) is created) with a DiagnosticListener
implementation that does nothing and a PrintWriter/Writer that discards output
(e.g., wraps OutputStream.nullOutputStream or similar), so task.parse() cannot
write to System.err during benchmarking.

Comment on lines +145 to +150
// Generous bounds — JIT warmup variance allowed. Spec target is 600ms warm; we
// budget 1500ms cold for safety. Cache hit should be sub-millisecond.
long coldMs = coldNanos / 1_000_000L;
long warmMicros = warmNanos / 1_000L;
assertTrue(coldMs < 5_000L, () -> "cold latency " + coldMs + "ms exceeds 5000ms ceiling");
assertTrue(warmMicros < 5_000L, () -> "cache hit took " + warmMicros + "µs (>5000µs)");

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 | ⚡ Quick win

Drop the hard wall-clock ceilings from this test.

These assertions depend on runner load, JIT warmup, and the CI machine, so they can fail unrelated changes even when caching is correct. Keep the cache-hit assertion here and move latency thresholds to a benchmark or an opt-in perf gate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@peglib-core/src/test/java/org/pragmatica/peg/v6/PegParserTest.java` around
lines 145 - 150, The test currently enforces hard wall-clock ceilings via the
coldMs and warmMicros assertions; drop those absolute time ceilings. Remove the
coldMs assertTrue entirely and change the warmMicros assertion in PegParserTest
to a relative cache validation (e.g., assertTrue(warmMicros < coldMs, () ->
"cache hit not faster: warm " + warmMicros + "µs vs cold " + coldMs + "ms")),
and move any fixed latency thresholds into a separate benchmark or opt-in perf
gate instead.

Comment on lines +81 to +121
private static void appendWhitespace(List<Doc> parts, String ws) {
if (ws.isEmpty()) {
return;
}
var run = new StringBuilder();
for (var i = 0; i < ws.length(); i++) {
var c = ws.charAt(i);
if (c == '\n') {
if (!run.isEmpty()) {
parts.add(text(run.toString()));
run.setLength(0);
}
parts.add(new Doc.HardLine());
} else {
run.append(c);
}
}
if (!run.isEmpty()) {
parts.add(text(run.toString()));
}
}

private static void appendLineComment(List<Doc> parts, String raw) {
var stripped = raw.endsWith("\n") ? raw.substring(0, raw.length() - 1) : raw;
parts.add(text(stripped));
parts.add(new Doc.HardLine());
}

private static void appendBlockComment(List<Doc> parts, String raw) {
if (raw.indexOf('\n') < 0) {
parts.add(text(raw));
return;
}
var lines = raw.split("\n", -1);
for (var i = 0; i < lines.length; i++) {
if (i > 0) {
parts.add(new Doc.HardLine());
}
parts.add(text(lines[i]));
}
}

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 | ⚡ Quick win

CRLF handling leaks raw \r into formatted output

Whitespace/comment rendering only treats \n as a line break. For CRLF/CR inputs, carriage returns can be emitted as literal text and skew output.

Normalize line endings before token-to-Doc conversion
 private static void appendWhitespace(List<Doc> parts, String ws) {
+    ws = ws.replace("\r\n", "\n").replace('\r', '\n');
     if (ws.isEmpty()) {
         return;
     }
@@
 private static void appendLineComment(List<Doc> parts, String raw) {
+    raw = raw.replace("\r\n", "\n").replace('\r', '\n');
     var stripped = raw.endsWith("\n") ? raw.substring(0, raw.length() - 1) : raw;
     parts.add(text(stripped));
     parts.add(new Doc.HardLine());
 }
@@
 private static void appendBlockComment(List<Doc> parts, String raw) {
+    raw = raw.replace("\r\n", "\n").replace('\r', '\n');
     if (raw.indexOf('\n') < 0) {
         parts.add(text(raw));
         return;
     }

Also applies to: 123-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-formatter/src/main/java/org/pragmatica/peg/formatter/v6/V6TriviaPolicy.java`
around lines 81 - 121, The code emits literal '\r' characters because
appendWhitespace, appendLineComment, and appendBlockComment only treat '\n' as
line breaks; normalize line endings at the start of each of these methods by
removing or converting CR characters (e.g., replace "\r\n" with "\n" and then
replace remaining '\r' or just strip '\r') so subsequent logic sees only '\n'
line breaks; update appendWhitespace to operate on the normalized ws string and
appendLineComment/appendBlockComment to use a normalized raw before trimming,
splitting, or checking for '\n'.

Comment on lines +78 to +100
if (allUpToDate(lexerTarget, parserTarget, visitorTarget)) {
getLog().info("peglib:generate-v6 skipped (up-to-date): "
+ lexerTarget.getFileName() + ", "
+ parserTarget.getFileName() + ", "
+ visitorTarget.getFileName());
return;
}
var generated = readGrammar(grammarFile.toPath()).flatMap(this::buildAll);
if (generated instanceof Result.Failure<?> failure) {
throw new MojoFailureException(failure.cause().message());
}
var bundle = generated.unwrap();
var write = writeAll(bundle, lexerTarget, parserTarget, visitorTarget);
if (write instanceof Result.Failure<?> failure) {
throw new MojoExecutionException(failure.cause().message());
}
for (var w : bundle.lexer().warnings()) {
getLog().warn("peglib:generate-v6 lexer warning: " + w);
}
getLog().info("peglib:generate-v6 wrote "
+ lexerTarget.getFileName() + ", "
+ parserTarget.getFileName() + ", "
+ visitorTarget.getFileName());

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 | ⚡ Quick win

Register outputDirectory as a source root on every run.

This mojo writes files during generate-sources, but it never calls MavenProject.addCompileSourceRoot(...). Maven will not compile an arbitrary outputDirectory just because generation ran, and the up-to-date early return currently skips any chance to register it. Add the source root before the early return.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-maven-plugin/src/main/java/org/pragmatica/peg/maven/GenerateV6Mojo.java`
around lines 78 - 100, The mojo must always register the generated source
directory as a compile source root even when up-to-date: call
MavenProject.addCompileSourceRoot(outputDirectory.getPath()) (or
project.addCompileSourceRoot(...)) before the early return that follows the
allUpToDate(lexerTarget, parserTarget, visitorTarget) check in GenerateV6Mojo so
the directory is registered on every run; keep the existing early-return
behavior but ensure outputDirectory is added first.

Comment on lines +88 to +106
if (handleCommand(line.trim())) {
return;
}
}
}

private void banner() {
out.println("peglib v6 playground REPL (grammar: " + grammarPath.toAbsolutePath() + ")");
out.println("type ':help' for commands, ':quit' to exit.");
}

/** Handle a single input line. Returns {@code true} iff the REPL should exit. */
boolean handleCommand(String line) throws IOException {
if (line.startsWith(":")) {
return handleMetaCommand(line);
}
loadGrammarIfChanged();
runParse(line);
return false;

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 | ⚡ Quick win

REPL currently trims parse input, changing grammar-visible text

Line 88 trims every line before parsing, so non-command input loses leading/trailing whitespace. That can change parse outcomes and diagnostics.

Preserve raw input for parsing; trim only for command detection
-            if (handleCommand(line.trim())) {
+            if (handleCommand(line)) {
                 return;
             }
@@
     boolean handleCommand(String line) throws IOException {
-        if (line.startsWith(":")) {
-            return handleMetaCommand(line);
+        String cmdLine = line.stripLeading();
+        if (cmdLine.startsWith(":")) {
+            return handleMetaCommand(cmdLine.trim());
         }
         loadGrammarIfChanged();
         runParse(line);
         return false;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@peglib-playground/src/main/java/org/pragmatica/peg/playground/v6/PlaygroundReplV6.java`
around lines 88 - 106, The REPL currently trims the input before handling which
strips significant whitespace for parsing; change the flow so the raw line is
preserved for parsing and only use a trimmed version to detect commands: stop
trimming at the call site and update handleCommand(String line) to compute a
local trimmed variable (e.g., trimmed = line.trim()), use
trimmed.startsWith(":") to dispatch to handleMetaCommand(trimmed) but pass the
original line into loadGrammarIfChanged() / runParse(line) so parsing and
diagnostics see the raw input; keep method names handleCommand,
handleMetaCommand, loadGrammarIfChanged, and runParse to locate where to change.

Comment thread peglib-runtime/pom.xml
Comment on lines +35 to +38
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where assertj version is managed/pinned in the repo.
fd -i pom.xml | xargs -I{} sh -c '
  echo "== {} ==";
  rg -n -C2 "assertj-core|assertj.version|dependencyManagement" "{}"
'

Repository: siy/java-peglib

Length of output: 2137


Upgrade assertj-core to version 3.27.7 or later to patch GHSA-rqfh-9r24-8c9r (XXE vulnerability in XML assertions).

The repo uses AssertJ 3.26.0, which is vulnerable. Update the assertj.version property in the root pom.xml (line 52) from 3.26.0 to 3.27.7+. This will fix the dependency across all modules that inherit from the parent dependencyManagement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@peglib-runtime/pom.xml` around lines 35 - 38, Update the AssertJ dependency
version used by the project by changing the assertj.version property in the root
POM from 3.26.0 to 3.27.7 (or later) so the dependencyManagement and all modules
pick up the patched release; ensure the assertj-core artifact
(org.assertj:assertj-core) in the pom inherits this updated property and run a
build to verify no version conflicts.

@siy siy merged commit 94ef675 into main May 11, 2026
2 checks passed
@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