Skip to content

mruby-regexp: number atomic groups so a possessive repeat cuts as its own - #7276

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-possessive-cut-depth
Aug 19, 2026
Merged

mruby-regexp: number atomic groups so a possessive repeat cuts as its own#7276
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-possessive-cut-depth

Conversation

@takumin

@takumin takumin commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

A possessive repeat (a*+, a?+, a++) wraps an atomic group around the
code it repeats after that code is emitted, and it takes the group's depth
from the count of groups open around it, which is the count the groups inside
that code took theirs from. So the wrapper and a group inside it share a
depth, and a failure after the wrapper, which is meant to fail the wrapper
without opening the repeat to being skipped, is read by the inner group as
its own cut: the inner group fails, the ? or * skips the repeat, and the
text after the wrapper is tried once more where CRuby has already failed:

/(?>a)?+a/ =~ "a"          # CRuby: nil, mruby: 0
/(?>a)*+a/ =~ "aa"         # CRuby: nil, mruby: 0
/(?:(?>a)b?)?+a/ =~ "a"    # CRuby: nil, mruby: 0
/(?>(?:(?>a))?)a/ =~ "a"   # nil in both: the group form nests its depths

The written-out group nests because its depth is taken before its body is
compiled; the wrapper is the one construct that is put around code already
emitted, so a depth is not what tells it apart from what it holds.

The fix

The two instructions of an atomic group carry the count of groups numbered
before it, one more for each (?>...) the pattern opens and for each
possessive repeat, instead of the nesting depth. A cut is still keyed by that
number (BT_CUT), and it is unique in the pattern, so a wrapper and any group
inside it never share one. Every numbered group emits two instructions, so the
number fits the field as the depth did. emit_atom_copy() copies a group's
number with the group, and the copies are sequential, never one inside the
other, so they cut independently as before.

Size

.text of bin/mruby, build_config/ci/gcc-clang.rb, each side from a clean
build directory. re_compile.o is the only object that changes; re_exec.o, whose
change is a comment and the name of a macro parameter, keeps its size.

build master this PR delta
bintest 1,281,398 1,281,318 -80
ascii-ctype 1,269,286 1,269,158 -128
byte-string 1,251,142 1,251,094 -48
cxx_abi 1,306,873 1,306,841 -32
full-debug (-O0) 1,881,126 1,881,094 -32

Verification

The tests go in regexp_syntax.rb, in the atomic group block: the examples
above, a possessive repeat around a possessive repeat, a repeat that matches
with the text after it, and a failure inside the repeat before its end, which
still fails only the inner group and lets the ? skip the repeat
(/(?:(?>a)b)?+c/ =~ "ac" is 1 in both). Four of the assertions fail on
master.

Differential against CRuby 4.0.6, the harness of #7269 with a possessive
form added to the quantifiers it draws, master and this PR against the same
cases, compared as MatchData#to_a. 10,000 random patterns over the default
features (seed 2), 3,081 of them with a possessive quantifier: 8,406 compared
(master and this PR both refuse 1,592, an empty group under a quantifier,
which is #7275), 8,294 the same everywhere, 111 differing from CRuby on both
sides, 1 that master answers differently and this PR as CRuby does
(/(?:(?<!aa)(?:b|)??(?>a{2}?))?+\B/ on "baba"), 0 that only this PR
answers differently. A second 10,000 (seed 3) drawn from atomic groups,
possessive and lazy quantifiers, intervals, empty-matching atoms, classes and
backreferences: 8,247 compared, 5 that master answers differently and this PR
as CRuby does, every one a possessive repeat around an atomic group, 11 that
both differ on (none with a possessive repeat around a group), 0 only this PR.

rake test, build_config/ci/gcc-clang.rb, no compiler warning:

build total KO crash
full-debug 2,365 0 0
bintest 2,365 0 0
bintest (bintest suite) 123 0 0
cxx_abi 2,365 0 0
byte-string 2,294 0 0
ascii-ctype 2,361 0 0

The default configuration: 2,140 total, 0 KO, 0 crash, plus its 112 bintests.

Environment

Details
OS Ubuntu 24.04, Linux 7.0.0 x86_64, AMD Ryzen 9 5950X
gcc 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1)
binutils 2.47
CRuby 4.0.6, for the differential

Compile lines for mrbgems/mruby-regexp/src/re_compile.c in the builds
quoted above, paths shortened:

# ci/gcc-clang full-debug
gcc -MMD -c -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -g3 -O0 -DMRB_GC_STRESS -DMRB_USE_DEBUG_HOOK -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_DEBUG -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -I"include" -I"mrbgems/mruby-regexp/include" -I"build/full-debug/include" -o "build/full-debug/mrbgems/mruby-regexp/src/re_compile.o" "mrbgems/mruby-regexp/src/re_compile.c"

# ci/gcc-clang bintest
gcc -MMD -c -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_GC_FIXED_ARENA -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -DMRB_USE_DEBUG_HOOK -I"include" -I"mrbgems/mruby-regexp/include" -I"build/bintest/include" -o "build/bintest/mrbgems/mruby-regexp/src/re_compile.o" "mrbgems/mruby-regexp/src/re_compile.c"

# ci/gcc-clang cxx_abi
gcc -MMD -c -g -O3 -Wall -Wundef -Wwrite-strings -x c++ -std=gnu++03 -DMRB_GC_FIXED_ARENA -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_USE_CXX_EXCEPTION -DMRB_USE_CXX_ABI -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -I"include" -I"mrbgems/mruby-regexp/include" -I"build/cxx_abi/include" -o "build/cxx_abi/mrbgems/mruby-regexp/src/re_compile.o" "mrbgems/mruby-regexp/src/re_compile.c"

# ci/gcc-clang byte-string
gcc -MMD -c -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -I"include" -I"mrbgems/mruby-regexp/include" -I"build/byte-string/include" -o "build/byte-string/mrbgems/mruby-regexp/src/re_compile.o" "mrbgems/mruby-regexp/src/re_compile.c"

# ci/gcc-clang ascii-ctype
gcc -MMD -c -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_ASCII_CTYPE -DMRBGEM_MRUBY_REGEXP_VERSION=0.0.0 -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -I"include" -I"mrbgems/mruby-regexp/include" -I"build/ascii-ctype/include" -o "build/ascii-ctype/mrbgems/mruby-regexp/src/re_compile.o" "mrbgems/mruby-regexp/src/re_compile.c"

Summary by CodeRabbit

  • Bug Fixes

    • Improved regular expression handling for possessive repeats and atomic groups.
    • Prevented backtracking from incorrectly reopening protected patterns after a match failure.
    • Preserved valid matches involving nested atomic groups and possessive quantifiers.
  • Tests

    • Added coverage for optional, repeated, and grouped possessive patterns, including nested failure scenarios.

… own

A possessive repeat wraps an atomic group around the code it repeats,
after that code is emitted, and it took the group's depth from the
count of groups open around it, the same count the groups inside that
code took theirs from. So the wrapper and a group inside it shared a
depth, and a failure after the wrapper, meant to fail the wrapper
without opening the repeat to being skipped, was read by the inner
group as its own cut: the inner group failed, the repeat was skipped,
and the text after the wrapper was tried again where CRuby has already
failed:

```ruby
/(?>a)?+a/ =~ "a"          # CRuby: nil, mruby: 0
/(?>a)*+a/ =~ "aa"         # CRuby: nil, mruby: 0
/(?:(?>a)b?)?+a/ =~ "a"    # CRuby: nil, mruby: 0
/(?>(?:(?>a))?)a/ =~ "a"   # nil in both: the group form nests its depths
```

The two instructions of an atomic group carry the count of groups
numbered before it now, one more for each group the pattern opens and
for each possessive repeat, instead of the nesting depth. A cut is
still keyed by that number, and it is unique in the pattern, so a
wrapper and any group inside it never share one; every numbered group
emits two instructions, so the number fits the field as the depth did.
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 40bfb147-780b-4b0c-beec-e34047711f1d

📥 Commits

Reviewing files that changed from the base of the PR and between e9f21a4 and a2d11dd.

📒 Files selected for processing (4)
  • mrbgems/mruby-regexp/include/re_internal.h
  • mrbgems/mruby-regexp/src/re_compile.c
  • mrbgems/mruby-regexp/src/re_exec.c
  • mrbgems/mruby-regexp/test/regexp_syntax.rb

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The regexp compiler now assigns unique numbers to atomic groups and possessive repeats. The bytecode documentation and executor terminology use cut identifiers. Regression tests cover possessive-repeat backtracking and nested atomic-group failures.

Changes

Regexp atomic cut numbering

Layer / File(s) Summary
Unique cut numbering and bytecode emission
mrbgems/mruby-regexp/include/re_internal.h, mrbgems/mruby-regexp/src/re_compile.c
The bytecode contract and compiler use monotonically increasing cut numbers for atomic groups and possessive repeats instead of nesting depth.
Executor terminology and regression coverage
mrbgems/mruby-regexp/src/re_exec.c, mrbgems/mruby-regexp/test/regexp_syntax.rb
Executor documentation and BT_CUT use cut identifiers. Tests cover possessive-repeat atomicity and nested atomic-group failures.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to a2d11

This focused regexp change is merge-ready after normal checks and review; no actionable merge-blocking risk remains.

Possibly related PRs

  • mruby/mruby#7256: Introduces the atomic-group bytecode and compiler logic refined here.
  • mruby/mruby#7273: Modifies related regexp atomic-cut handling and backtracking behavior.
  • mruby/mruby#7269: Modifies related mruby-regexp atomic handling and backtracking tests.

Suggested reviewers: matz, nattzn

Sequence Diagram(s)

sequenceDiagram
  participant RegexpCompiler
  participant RegexpBytecode
  participant RegexpExecutor
  participant SyntaxTests
  RegexpCompiler->>RegexpBytecode: Emit atomic boundaries with unique cut numbers
  RegexpCompiler->>RegexpBytecode: Emit possessive-repeat boundaries with unique cut numbers
  RegexpBytecode->>RegexpExecutor: Provide numbered atomic boundaries
  SyntaxTests->>RegexpExecutor: Validate possessive atomicity and nested failures
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: assigning separate numbers to atomic groups and possessive repeats.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

@matz
matz merged commit 47bdaf4 into mruby:master Aug 19, 2026
21 checks passed
@takumin
takumin deleted the regexp-possessive-cut-depth branch August 19, 2026 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants