Skip to content

mruby-regexp: apply /i to the backreference comparison - #7046

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-backref-ignorecase
Aug 9, 2026
Merged

mruby-regexp: apply /i to the backreference comparison#7046
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-backref-ignorecase

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

A backreference ignored the /i flag: the RE_BACKREF case of bt_match()
compared the captured text with memcmp() unconditionally, so \1 stayed
case-sensitive even when the rest of the pattern was folded.

/(a)\1/i.match?("aA")           # CRuby: true, mruby: false
/(?<n>a)\k<n>/i.match?("aA")    # CRuby: true, mruby: false
/(a)(?i)\1/.match?("aA")        # CRuby: true, mruby: false
/(a)(?i:\1)/.match?("aA")       # CRuby: true, mruby: false

Fix

The case sensitivity is now decided at compile time, per instruction, the
same way every other ignorecase decision in this engine is taken.
compile_atom() records c->flags & RE_FLAG_IGNORECASE in the offset
field of the emitted RE_BACKREF (unused for this opcode), and bt_match()
selects an ASCII case-insensitive comparison when the bit is set.

Reading pat->flags from the matcher would have been a smaller change but
the wrong one. pat->flags is the whole-pattern option set, whereas (?i)
and (?i:...) are a compile-time mechanism on the compiler's own c->flags
that is never written back to the pattern. That form would fold
/(?-i:(a)\1)/i, which must not fold, and would leave /(a)(?i)\1/
unfolded, which must.

Reusing offset needs no change to re_inst. It is safe for this opcode:
insert_inst() and emit_atom_copy() rewrite offsets only for RE_JMP,
RE_SPLIT and RE_SPLITNG, and compute_fixed_len() rejects RE_BACKREF
through its default: arm.

Only bt_match() had to change. A pattern with a backreference always sets
has_backref, so mrb_re_exec() dispatches it to the backtracking engine;
the Pike VM has no RE_BACKREF case at all. literal_exec() is unreachable
here because is_literal requires !has_backref.

Case folding stops at ASCII, consistent with the existing /i handling for
literals and character classes. Extending /i to non-ASCII is a separate
limitation and is left untouched.

Tests

mrbgems/mruby-regexp/test/regexp.rb

  • a new Regexp - backreference under /i assertion for \1
  • /i cases added to Regexp - named backreference \k
  • the inline forms /(a)(?i)\1/, /(a)(?i:\1)/ and the /(?-i:(a)\1)/i
    control added to Regexp - inline options (?i) / (?i:...)

All new assertions were run against CRuby 4.0.6 and produce identical
results. rake test passes: 1968 total, 0 KO.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed case-insensitive matching for numeric and named regular-expression backreferences.
    • Inline and scoped case-insensitivity options now apply correctly to backreferences.
    • Scoped case-sensitive settings correctly override an outer case-insensitive option.
    • Exact matching behavior remains unchanged when case-insensitivity is not enabled.
  • Tests

    • Added regression coverage for toggled, scoped, numeric, and named backreference matching.

The `RE_BACKREF` case of `bt_match()` compared the captured text with
`memcmp()` unconditionally, so a backreference stayed case-sensitive even
when the rest of the pattern was folded.

```ruby
/(a)\1/i.match?("aA")           # CRuby: true, mruby: false
/(?<n>a)\k<n>/i.match?("aA")    # CRuby: true, mruby: false
/(a)(?i)\1/.match?("aA")        # CRuby: true, mruby: false
```

Decide the case sensitivity at compile time, per instruction, like every
other ignorecase decision in this engine. `compile_atom()` now records
`c->flags & RE_FLAG_IGNORECASE` in the otherwise unused `offset` field of
the emitted `RE_BACKREF`, and `bt_match()` picks an ASCII case-insensitive
comparison when it is set.

Reading `pat->flags` in the matcher instead would have been wrong for
inline options: `pat->flags` holds the whole-pattern option set, while
`(?i)` and `(?i:...)` live only in the compiler's `c->flags` and are never
written back. That form would fold `/(?-i:(a)\1)/i`, which must not fold,
and would leave `/(a)(?i)\1/` unfolded, which must.

Reusing `offset` is safe for this opcode: `insert_inst()` and
`emit_atom_copy()` relocate offsets only for `RE_JMP`, `RE_SPLIT` and
`RE_SPLITNG`, and `compute_fixed_len()` rejects `RE_BACKREF` through its
`default:` arm. Only `bt_match()` executes the opcode, since a pattern
with a backreference always sets `has_backref` and is dispatched to the
backtracking engine, so the Pike VM needs no change.

Folding stops at ASCII, matching the existing `/i` behavior for literals
and character classes.
@takumin
takumin requested a review from matz as a code owner August 9, 2026 14:14
@coderabbitai

coderabbitai Bot commented Aug 9, 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: d828b74e-5da4-40db-971b-001c32d51c7a

📥 Commits

Reviewing files that changed from the base of the PR and between 9360b3f and f9adb30.

📒 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.rb

📝 Walkthrough

Walkthrough

This change enables ASCII case-insensitive matching for numeric and named backreferences. The compiler records the active ignore-case state in RE_BACKREF instructions, and the executor applies case-insensitive comparison when requested. Regression tests cover inline and scoped options.

Changes

Case-insensitive backreferences

Layer / File(s) Summary
Compile backreference flags
mrbgems/mruby-regexp/include/re_internal.h, mrbgems/mruby-regexp/src/re_compile.c
RE_BACKREF documents a as the capture group and offset as the ignore-case flag. Numeric and named backreferences emit the active flag.
Execute case-insensitive matching
mrbgems/mruby-regexp/src/re_exec.c
The executor adds ASCII-insensitive byte comparison and uses it when the instruction requests ignore-case. Exact comparison remains unchanged otherwise.
Validate backreference options
mrbgems/mruby-regexp/test/regexp.rb
Tests cover inline toggles, scoped options, numeric and named backreferences, and nonmatching captures.

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

Possibly related PRs

  • mruby/mruby#7019: Both changes modify numeric backreference handling and related tests, but address different behavior.

Suggested labels: mrbgems

Suggested reviewers: matz, nattzn

🚥 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 and concisely describes applying the /i ignorecase option to backreference comparisons.
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 f699bea into mruby:master Aug 9, 2026
21 checks passed
@takumin
takumin deleted the regexp-backref-ignorecase branch August 9, 2026 14:45
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