mruby-regexp: share the pattern-skipping rules between the two prescans - #7165
Conversation
preprocess_pattern() and has_named_group() each walk the pattern before
the parser does, and each has to agree with the parser on where an escape
ends and where a character class ends, so that a '(' hidden behind one is
not read as a group opener. Both carried their own copy of those rules.
The copies had already drifted. preprocess_pattern() treats `\u{...}` as
a single escape, because the free-spacing pass would otherwise remove the
spaces separating the list's codepoints and join `\u{61 62}` into the one
codepoint `\u{6162}`; has_named_group() stepped over the two bytes of
`\u` and then read the brace group as ordinary pattern syntax.
That drift is observable. It cannot change whether a pattern compiles: a
well-formed list holds nothing but hex digits and separators, so it can
hide neither a class opener nor a "(?<", and every pattern that exposes
the difference is malformed and rejected on both sides. What it changes
is which error is reported. dont_capture is decided before parsing
starts, while the list is validated during it, so a "(?<" mistaken for a
declaration switches on the demotion that rejects a numbered
backreference earlier in the pattern, before the parser ever reaches the
malformed list:
/\1\u{(?<a>/ was: numbered backref/call is not allowed. (use name)
now: invalid Unicode list, which is what CRuby reports
The message moves in both directions across the affected patterns, and
on balance toward CRuby rather than away from it.
Move the rules into skip_uninterpreted(), which steps over whichever of
them begins at the current byte and hands the class state back to its
caller. preprocess_pattern() copies the span it returns verbatim, which
leaves that walk a byte-for-byte no-op; has_named_group() jumps over the
span and so gains the `\u{...}` rule it had been missing.
The new test rows are read by both walks at once: each puts a "(?<" and
a plain group behind a construct the rules have to cross, so a rule lost
from the free-spacing pass strips a space it should have kept, and the
same rule lost from the named-group scan demotes the plain group. A last
row pins the diagnostic above, the one behaviour this commit changes.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe regexp compiler now shares scanning logic between ChangesRegexp scanner synchronization
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This change consolidates regexp pattern-skipping logic and adds focused coverage; no actionable merge-blocking risk remains beyond normal checks and review. Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Two scans read a regexp pattern before the parser does:
preprocess_pattern()removes(?#...)comment groups, and under/xalso whitespace and#line comments;has_named_group()answers whether the pattern declares a(?<name>...)group, which is what letscompile_atom()demote a plain(...)written before the declaration that demotes it.Both are hunting for a
(?opener, so both have to agree with the parser about when a(is really an opener and not a byte hiding behind an escape or inside a character class. Each carried its own copy of those rules, and the copies had drifted apart.Three rules matched: the escape pair, the class from
[through](with^and a leading literal]handled ascompile_charclass()handles them), and the POSIX bracket, whose]must not close the class. The fourth did not.preprocess_pattern()treats\u{...}as one escape and copies the brace group whole, because the free-spacing pass would otherwise strip the spaces separating the list's codepoints and collapse\u{61 62}into the single codepoint\u{6162}.has_named_group()skipped\uas a plain two-byte escape and then walked into the braces.This is observable, in diagnostics
The divergence cannot change whether a pattern compiles: a well-formed
\u{...}list contains only hex digits and separator whitespace, so it can hide neither a class opener nor a(?<, and every pattern that exposes the difference is malformed and rejected either way. But it changes which error is reported.dont_captureis computed fromhas_named_group()before parsing begins, while the Unicode list is validated during parsing, so a(?<mistaken for a declaration switches on the demotion that rejects a numbered backreference earlier in the pattern, before the parser ever reaches the malformed list:Measured by differential testing between a build of this branch and a build of its base, over patterns assembled from the tokens that exercise these rules (
\u{,},[,],^,(?<,(,),a, space,\1,[:alpha:]), every sequence of one to four of them, each also with a\1prefix, 43,356 distinct patterns:Every one of those patterns was already an error; no valid pattern is affected.
The free-spacing pass is untouched
preprocess_pattern()now copies verbatim the span the shared helper returns, which is by construction the same span the inline code copied. Differentially: over 6,990 distinct patterns built the same way underRegexp::EXTENDEDfrom a token set extended with61 62,#and a newline, and matching every pattern that compiled against twelve subjects, no row where either side compiled differs at all. The 39 rows that do differ are error-message changes of the kind above.The change
This moves the rules into one helper,
skip_uninterpreted(), which steps over whichever construct starts at the current byte and reports the class state back through anin_classflag the caller owns.preprocess_pattern()copies the returned span verbatim;has_named_group()jumps over it, gaining the\u{...}rule it had been missing.has_comment_group()is deliberately left alone. It is a third walk, but a naivememchrprefilter that visits every(and so returns TRUE iff the bytes(?#occur anywhere; its only possible error is a false positive costing one unnecessarypreprocess_pattern()call. It is not a copy of these rules, and folding it in would only make the fast path slower.Tests
The new rows are read by both walks at once: each places a
(?<and a following plain group behind a construct the rules must cross. A rule dropped from the free-spacing pass strips a space it should have kept inside a class; the same rule dropped from the named-group scan turns the bracketed(?<into a phantom named group and demotes the plain group. A final row pins the diagnostic above, which is the one behaviour this change deliberately alters.rake -m test: mrbtestTotal: 2076, OK: 2047, KO: 0, Crash: 0, Warning: 0, Skip: 29(2075 without this branch, so the block is the +1); bintestTotal: 105, OK: 105, KO: 0. Withre_compile.calone reverted and the tests in place, that block fails, so it does test the change.Summary by CodeRabbit
Bug Fixes
/x) handling so spaces and patterns are interpreted consistently.Tests