Skip to content

mruby-regexp: match \Z under the backtracking engine - #7257

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-bt-eotnl
Aug 18, 2026
Merged

mruby-regexp: match \Z under the backtracking engine#7257
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-bt-eotnl

Conversation

@takumin

@takumin takumin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

\Z compiles to RE_EOTNL, and only the Pike VM executed it. bt_match()
had no case for the opcode, so it fell to default: and failed, and \Z
never matched once anything in the pattern routed it to the backtracking
engine, whether or not the subject ended in a newline:

"a"   =~ /a\Za*?/           # CRuby: 0,      mruby: nil
"a\n" =~ /a\Z.*?/           # CRuby: 0,      mruby: nil
"a\n" =~ /(a)\1*\Z/         # CRuby: 0,      mruby: nil
"ab\n".sub(/b\Z(?=)/, "X")  # CRuby: "aX\n", mruby: "ab\n"
"a\n" =~ /a\Z/              # 0 in both, the Pike VM has the case

A lazy quantifier, a backreference, or a lookaround is enough to get there,
and with #7256 an atomic group. It has been so since the backtracking engine
landed in 23b2d24, and no test in the gem used \Z, so nothing noticed.
RE_EOTNL was the one opcode the Pike VM handled that bt_match() did not;
the ones the other way round are the backreference and the lookarounds, which
only the backtracker runs.

The fix

One case beside RE_EOT in bt_match(), testing what add_thread() tests:
the string end, or the position just before a newline that is the last byte.

The test exercises \Z under both engines, with the lazy quantifier as the
shortest way onto the backtracker, and pins the two positions it must not
match: before a newline that is not the last byte, and before any other byte.

Testing

build_config/ci/gcc-clang.rb and build_config/gcc-asan.rb, run per build so
the counts are attributable:

build tests OK KO skip
full-debug 2349 2343 0 6
bintest 2349 2335 0 14
cxx_abi 2349 2335 0 14
byte-string 2279 2228 0 51
ascii-case 2346 2332 0 14
gcc-asan 2349 2343 0 6

The binary tests pass 122 of 123 under ci/gcc-clang and 84 of 85 under
gcc-asan, one skip each. The only warning any build prints is the
-Wreturn-type line gcc-asan gives for src/vm.c:4073 at -O0, and master
prints the same; this branch does not touch src/vm.c.

Against a bt_match() without the case, the test block turns red:

Fail: Regexp - \Z matches before a trailing newline under both engines (mrbgems: mruby-regexp)

Size

Summed .text over libmruby.a, full-core at -O3, each build from an
empty build directory:

master this branch delta
libmruby.a .text 1290173 1290285 +112
re_exec.o .text 16242 16354 +112

The whole of it is the new case in bt_match(). mruby-regexp is not in the
default gembox, so build_config/default.rb does not move.

Environment

Details
OS Ubuntu 24.04.4 LTS
Kernel Linux 7.0.0-29-generic x86_64
C compiler gcc 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1)
binutils GNU ld 2.47.20260726
CRuby (oracle for the examples) ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM

The size rows use a build config of their own, so that full-core at -O3
carries no test or debug options:

MRuby::Build.new('size') do |conf|
  conf.toolchain
  conf.gembox 'full-core'
end

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

# size, the -O3 full-core rows
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_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/size/include" -o "build/size/mrbgems/mruby-regexp/src/re_exec.o" "mrbgems/mruby-regexp/src/re_exec.c"

# 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_exec.o" "mrbgems/mruby-regexp/src/re_exec.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_exec.o" "mrbgems/mruby-regexp/src/re_exec.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_exec.o" "mrbgems/mruby-regexp/src/re_exec.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_exec.o" "mrbgems/mruby-regexp/src/re_exec.c"

# ci/gcc-clang ascii-case
gcc -MMD -c -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_ASCII_CASE -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-case/include" -o "build/ascii-case/mrbgems/mruby-regexp/src/re_exec.o" "mrbgems/mruby-regexp/src/re_exec.c"

# gcc-asan
gcc -MMD -c -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -fsanitize=address,undefined -g3 -O0 -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/gcc-asan/include" -o "build/gcc-asan/mrbgems/mruby-regexp/src/re_exec.o" "mrbgems/mruby-regexp/src/re_exec.c"

Summary by CodeRabbit

  • Bug Fixes
    • Corrected \Z regular-expression matching so it recognizes both the true end of a string and the position immediately before a single trailing newline.
    • Prevented incorrect matches when strings contain multiple trailing newlines or unmatched suffixes.
    • Improved behavior with lazy quantifiers and substitutions using lookaheads.
  • Tests
    • Added regression coverage across supported regular-expression engines.

`\Z` compiles to `RE_EOTNL`, and only the Pike VM executed it. `bt_match()`
had no case for the opcode, so it fell to `default:` and failed, and `\Z`
never matched once anything in the pattern routed it to the backtracking
engine, whether or not the subject ended in a newline:

```ruby
"a"   =~ /a\Za*?/           # CRuby: 0,      mruby: nil
"a\n" =~ /a\Z.*?/           # CRuby: 0,      mruby: nil
"a\n" =~ /(a)\1*\Z/         # CRuby: 0,      mruby: nil
"ab\n".sub(/b\Z(?=)/, "X")  # CRuby: "aX\n", mruby: "ab\n"
"a\n" =~ /a\Z/              # 0 in both, the Pike VM has the case
```

A lazy quantifier, a backreference, or a lookaround is enough to get there.
It has been so since the backtracking engine landed in 23b2d24, and no
test in the gem used `\Z`, so nothing noticed. `RE_EOTNL` was the one opcode
the Pike VM handled that `bt_match()` did not; the ones the other way round
are the backreference and the lookarounds, which only the backtracker runs.

The new case sits beside `RE_EOT` and tests what `add_thread()` tests: the
string end, or the position just before a newline that is the last byte. The
test exercises `\Z` under both engines, with the lazy quantifier as the
shortest way onto the backtracker.
@coderabbitai

coderabbitai Bot commented Aug 18, 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: f65e6b39-dc2d-4c2d-b0f5-d96e3b17d9ea

📥 Commits

Reviewing files that changed from the base of the PR and between 8bd0566 and d461aba.

📒 Files selected for processing (2)
  • mrbgems/mruby-regexp/src/re_exec.c
  • mrbgems/mruby-regexp/test/regexp_syntax.rb

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


📝 Walkthrough

Walkthrough

The backtracking regexp engine now supports RE_EOTNL before one final newline. Tests cover \Z matching, rejection cases, lazy quantifiers, substitution, and both regexp engines.

Changes

Regexp end-of-text assertions

Layer / File(s) Summary
RE_EOTNL handling and regression coverage
mrbgems/mruby-regexp/src/re_exec.c, mrbgems/mruby-regexp/test/regexp_syntax.rb
The backtracking engine accepts the subject end or the position before one trailing newline. Tests cover matching, rejection cases, lazy quantifiers, substitution, and both regexp engines.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to d461a

This change restores expected \Z matching for patterns using the backtracking engine with focused regression coverage; no actionable merge-blocking risk remains beyond normal checks and review.

🚥 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 identifies the mruby-regexp component and the fix for \Z matching in the backtracking engine.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 c6a3fcb into mruby:master Aug 18, 2026
21 checks passed
@takumin
takumin deleted the regexp-bt-eotnl branch August 18, 2026 08:24
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