[
https://issues.apache.org/jira/browse/GROOVY-12398?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18114113#comment-18114113
]
ASF GitHub Bot commented on GROOVY-12398:
-----------------------------------------
paulk-asert commented on PR #2921:
URL: https://github.com/apache/groovy/pull/2921#issuecomment-5630235433
Initial AI read (my initial leaning is master only to start with and
potentially backport to 6 after some settling time):
> ## Assessment: PR 2921 (GROOVY-12398, lexer/parser performance)
>
> **Verdict: this is ~95% mechanically equivalent refactoring, but not 100%
— there are three genuine behavior deltas, two of which are bug fixes and one
of which is a deliberate parsing-strategy change. My recommendation: merge to
master, but don't pull it into the 6.0 RC line.**
>
> I read the full diff and hand-verified each change against the current
grammar. What's in it:
>
> **Verified mechanically equivalent (no behavior change):**
> - The number-literal fragment flattening (`Digits`, `HexDigits`,
`OctalDigits`, `BinaryDigits`, `ExponentPart`, `OctalEscape`, the `Dot`/`Zero`
fragment inlining) — I compared each old fragment chain against its new
character class and they match exactly. All references to the removed fragments
are replaced in the diff.
> - `isFollowedByWhiteSpaces` and `isFollowedByJavaLetterInGString`
regex-to-char-test rewrites — equivalent, including the subtle cases: Java's
ASCII-only `\s` (so `\u00A0` still counts as non-space), lone low surrogates,
and EOF (the old code reached `isJavaIdentifierPart(-1)` which happens to
return false; the new code guards it explicitly).
> - The ASCII identifier split (`[A-Z]` vs `[a-z$_]`) — the classes are
disjoint and their union equals the old ASCII `JavaLetter`, and the
BMP-non-ASCII alternatives reproduce the old predicate logic exactly.
>
> **Behavior deltas:**
>
> 1. **Supplementary-plane uppercase identifiers reclassify** from
`Identifier` to `CapitalizedIdentifier`. The old `CapitalizedIdentifier`
predicate tested `isUpperCase(_input.LA(-1))`, which for a surrogate pair sees
only the low surrogate — always false. The new rule tests the real code point.
So an identifier starting with, say, Deseret `𐐀` or mathematical `𝔄` (U+1D504)
now lexes as `CapitalizedIdentifier`. I checked the parser: the bare
`Identifier` token appears only inside the `identifier` rule (which accepts
both), so nothing that parses today breaks — but `className` accepts *only*
`CapitalizedIdentifier`, so something like `𝔄 x = 1` that today parses as a
command expression could now parse as a typed variable declaration.
Realistically this touches no real-world code, and the new behavior is arguably
the correct one, but it is technically a parse change, not pure performance.
>
> 2. **The `elementValues` predicate gate** (`@Foo(a = 1)` no longer
explored as an assignment expression). This one I checked hard because it's the
scariest on paper: the new `ELEMENT_VALUE_PAIR_NAME_TYPES` BitSet is *exactly*
the token set of `identifier ∪ keywords` (all 60 tokens including `MODULE`,
which is in `identifier` but not `keywords` — Daniel got that right). And since
the alternative order already made `elementValuePairs` win any ambiguity, and
any `name = expr` input viable as a single element value is also viable as a
pair (element values are a superset that includes expressions), the gate cannot
reject anything that parses today — worst case is a different error message on
already-invalid input. Safe.
>
> 3. **Invalid-octal error offset fix**: the old `invalidDigitCount` field
was never reset between tokens, so a second invalid octal in the same file got
a wrong caret position. The new `getText().length()` form is stateless and
correct. Error-reporting change only, and a fix.
>
> **Other observations:** CI is fully green across the entire matrix (all
LTS JDKs, three OSes, lint gate, CodeQL, SonarCloud). The PR adds decent
targeted tests for exactly the risky spots (identifier split incl. Unicode,
flattened literals incl. `07` invalid-octal boundary, annotation forms incl.
`@Foo(class = 1)` and `@Foo(a + 1)`). The ANTLR dependency is
`me.sunlan:antlr4` — Daniel's own fork — so prediction-time predicate semantics
for the new gate are under the author's control. The claimed 1.6× is
parse-only; end-to-end compile improvement will be a modest fraction of that,
though it does chip at the Groovy 6 vs 5.0.6 parse-phase gap we've catalogued.
>
> **Recommendation given RC phase:** By your own criterion this doesn't
qualify as "strictly performance" — items 1 and 3 change observable behavior in
edge cases, and grammar/ATN rewrites are inherently the highest-blast-radius
category of perf change (a lexer DFA subtlety that the test suite doesn't cover
would ship as a parse regression). None of the deltas endanger realistic code,
so I'd merge it to master for the next train without hesitation. For 6.0 in RC,
I'd defer: the win is parse-time only, and the cost of being wrong during RC
outweighs it. If you want it in 6.0 anyway, the minimum bar I'd suggest is one
more RC cycle after it lands plus a compile sweep of a large corpus (e.g. the
usual grails/spock reproducer set) comparing ASTs before/after.
> Improve lexer and parser performance
> ------------------------------------
>
> Key: GROOVY-12398
> URL: https://issues.apache.org/jira/browse/GROOVY-12398
> Project: Groovy
> Issue Type: Improvement
> Reporter: Daniel Sun
> Priority: Major
>
> The Groovy ANTLR4 grammar already uses left-recursive expressions, which is
> the main idea of the [optimized Java
> grammar|https://github.com/antlr/grammars-v4/tree/master/java/java]. Several
> remaining lexer and parser costs still match patterns that grammar was
> written to avoid.
> h3. Problem
> * Number literals are assembled from a chain of one-character fragments
> ({{Digit}}, {{Zero}}, {{NonZeroDigit}}, ...). The invalid-octal alternative
> also counts digits in a mid-rule action loop, which prevents DFA compilation
> of {{IntegerLiteral}}.
> * Every identifier, including ASCII names such as {{Foo}} and {{foo}},
> evaluates {{Character.isUpperCase}} to choose {{CapitalizedIdentifier}} vs
> {{Identifier}}.
> * GString and comment predicates ({{isFollowedByJavaLetterInGString}},
> {{isFollowedByWhiteSpaces}}) allocate a one-character {{String}} and run
> {{Pattern.matcher}} on each lookahead.
> * {{elementValues}} is ambiguous with assignment: {{@Foo(a = 1)}} can be
> named pairs or an assignment expression, so {{AdaptivePredict}} explores both
> (see [antlr/grammars-v4#215|https://github.com/antlr/grammars-v4/issues/215]).
> h3. Approach
> Keep token types and language semantics unchanged.
> * Flatten number and float fragments to character classes, as in the
> optimized Java lexer. Validate invalid octal once at the end of the
> alternative.
> * Split ASCII identifiers on a predicate-free DFA path: uppercase {{A-Z}}
> becomes {{CapitalizedIdentifier}}; {{a-z}}, dollar and underscore become
> {{Identifier}}. Unicode still uses predicates.
> * Replace the regex-based predicates with direct character tests.
> * Gate {{elementValue}} with {{!isIdentifierAssign(_input)}}, the same idea
> as Java's {{IsNotIdentifierAssign}}.
> h3. Result
> Local parse-only measurement (SLL then LL) of {{src/main/groovy}} +
> {{src/test-resources/core}} (245 files, approximately 570k characters):
> ||Run||Best||Mean||
> |before|0.231s|0.401s|
> |after|0.146s|0.169s|
> Best run is about 1.6x faster. Mean improves more because prediction is more
> stable after DFA warmup.
> Related: GROOVY-10454, GROOVY-9588
--
This message was sent by Atlassian Jira
(v8.20.10#820010)