mruby-regexp: share one class per codepoint for /i literals - #7262
Conversation
Under /i a literal with case counterparts compiles to a character class
holding it and them, and every occurrence made a class of its own. A class
id is a `uint8_t`, so a pattern holds 256 at most, and a phrase of a few
hundred letters ran out of them and was refused:
```ruby
Regexp.new("д" * 300, Regexp::IGNORECASE) =~ "Д" * 300
# CRuby: 0
# mruby: RegexpError (too many character classes: /ддд.../)
Regexp.new("a" * 300, Regexp::IGNORECASE) =~ "A" * 300
# CRuby: 0
# mruby: RegexpError (too many character classes: /aaa.../)
```
The class such a literal needs holds the codepoint and its case
counterparts and nothing else: neither the flags in force nor the pattern
around it reach it, and nothing writes to it once `emit_char()` or
`emit_cp_folded()` has returned. What it holds is a function of the
codepoint, so the second occurrence of a codepoint can name the class the
first one made rather than make another.
The compiler now records, by class id, the codepoint each /i literal class
stands for, and `literal_class()` looks a codepoint up there before adding a
class. The record sits in the compiler's frame and dies with it. Zero marks
a class made by anything else, which U+0000 cannot be confused with, since
it has no case. Both literal paths, the ASCII letter in `emit_char()` and
the folded codepoint in `emit_cp_folded()`, fill the class only when it is
new.
The cap now counts distinct codepoints: a pattern that folds more than 256
different ones is still refused, which CRuby does not do. Bracket classes
and the shorthands keep taking a class per occurrence.
|
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 (3)
Included review availability: Your plan includes up to 8 reviews per rolling hour; 5 remain after this review. 📝 WalkthroughWalkthroughRegexp compilation now reuses case-insensitive literal character classes for ASCII and Unicode codepoints. The compiler enforces a 256-class limit. Regression tests cover repeated literals, scoped ChangesCase-insensitive regexp compilation
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This change shares case-insensitive literal classes by codepoint, addressing excessive class allocation while preserving the existing cap behavior; no actionable merge-blocking risk remains after normal checks and review. Possibly related PRs
Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant RegexpCompiler
participant literal_class
participant CharacterClassTable
RegexpCompiler->>literal_class: lookup literal codepoint
literal_class->>CharacterClassTable: search cached class
CharacterClassTable-->>literal_class: existing class or no match
literal_class->>CharacterClassTable: allocate class when absent
literal_class-->>RegexpCompiler: class ID and reuse status
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
Under /i a literal with case counterparts compiles to a character class holding it and them, and every occurrence made a class of its own. A class id is a
uint8_t, so a pattern holds 256 at most, and a phrase of a few hundred letters ran out of them and was refused. CRuby compiles and matches it.The class such a literal needs holds the codepoint and its case counterparts and nothing else: neither the flags in force nor the pattern around it reach it, and nothing writes to it once
emit_char()oremit_cp_folded()has returned. What it holds is a function of the codepoint, so the second occurrence of a codepoint can name the class the first one made rather than make another.Fix
The compiler records, by class id, the codepoint each /i literal class stands for (
literal_cp[RE_MAX_CLASSES]inre_compiler), andliteral_class()looks a codepoint up there before adding a class. The record sits in the compiler's frame and dies with it; zero marks a class made by anything else, which U+0000 cannot be confused with, since it has no case. Both literal paths, the ASCII letter inemit_char()and the folded codepoint inemit_cp_folded(), fill the class only when it is new.add_class()is unchanged: the slot is still zero cleared beforenum_classescounts it.The cap now counts distinct codepoints. A pattern that folds more than 256 different ones is still refused, which CRuby does not do; that is out of scope here and the test pins the present meaning of the cap. Bracket classes (
[k]) and the shorthands (\d) keep taking a class per occurrence, so"\\d" * 300still hits the cap; that is a separate defect of the same shape and not touched here.Testing
mrbgems/mruby-regexp/test/unicode_case.rb(runs whereRE_UNICODE_CASEis on):"д" * 300under /i compiles and matches both cases;"дД" * 150,"д" * 256 + "a" * 256, and a\u{434}escape mixed with the spelled character all compile; the same codepoint outside(?i:...)still matches its own case alone; 281 distinct cased codepoints (Cyrillic, Latin-1, Greek, Armenian) are still refused, 207 of them compile and match their upcase.mrbgems/mruby-regexp/test/regexp_syntax.rb(every build):"a" * 300and"aA" * 150under /i compile and match;(?i:a)amatches"Aa"and not"AA".RegexpError: too many character classesand pass at the tip.Full suite green at every commit (single commit).
rake -m test)Environment
Machine, toolchain, and the compile line of every build
Actual compile line of
src/string.cin eachbuild_config/ci/gcc-clang.rbbuild (-MMD -c,-I, and-odropped).full-debugis-O0becauseenable_debugappends-g3 -O0after the toolchain's-g -O3;cxx_abicompiles C as C++ withgcc -x c++ -std=gnu++03, g++ only links.Summary by CodeRabbit
/ipatterns.