Skip to content

mruby-regexp: scope the inline x option the way i and m are scoped - #7252

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-inline-extended-mode
Aug 18, 2026
Merged

mruby-regexp: scope the inline x option the way i and m are scoped#7252
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-inline-extended-mode

Conversation

@takumin

@takumin takumin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Regexp#to_s spells an extended pattern as (?x-mi:...), and the parser
refused that form. So an extended Regexp could not recompile from its own
printed form and could not be interpolated into another pattern:

re = Regexp.new("a b", Regexp::EXTENDED)
re.to_s                    # both: "(?x-mi:a b)"
Regexp.new(re.to_s)        # CRuby: /(?x-mi:a b)/
                           # mruby: RegexpError, inline extended mode (?x) is not supported
/#{re}c d/                 # same

Where x has to be resolved

Free-spacing is applied by preprocess_pattern() before the parser runs: it
copies the pattern into a buffer with the whitespace and # comments dropped.
That is why parse_inline_flags() could not honour the letter where it reads
i and m: by the time the parser reaches a (?x, the whitespace it governs
is either already gone or already kept.

So the pass now tracks the option itself. It keeps one bit per open group,
pushed at every ( it interprets and popped at the matching ), and reads the
letters of each (?imx-imx) and (?imx-imx: it passes over: the toggle form
switches x for the rest of the enclosing group, the scoped form for its own
body. That is the scope the parser already gives i and m, and it is what
Onigmo does with the whitespace under (?x). parse_inline_flags() then
carries x as a bit like the other two letters, and nothing in the parser reads
it.

/(?x)a b/ =~ "ab"                # 0
/(?x:a b)c d/ =~ "abc d"         # 0
/(a(?x)b c)d e/ =~ "abcd e"      # 0, the toggle ends with its group
/(?x)a\ b/ =~ "a b"              # 0, an escape keeps its whitespace
/(?x)[a b]/ =~ " "               # 0, so does a class

Two consequences follow.

(?-x:...) inside a pattern that is itself extended brings the whitespace back
for its scope. Before, the letter was accepted so that Regexp#to_s output
would recompile, and then dropped:

Regexp.new("(?-x:a b)", Regexp::EXTENDED).match?("a b")
# CRuby: true, mruby before: false, now: true

A # comment or a run of whitespace ends where the group that turned x on
ends. CRuby is itself split on this: Onigmo restores the whitespace at the
group's ), so /((?x)a) b/ matches "a b" there as here, but the pre-pass in
re.c (unescape_nonascii0()) that strips # comments sets its extended flag
for the rest of the pattern on a toggle, so /((?x)a)#c\nb/ matches "ab" in
CRuby and does not here. This pass follows the group for both.

The pre-check that lets a plain pattern skip the pass and its allocation now
also fires on an option group that turns x on, so a pattern without one costs
what it cost. The scope stack lives behind the rewritten pattern in the same
allocation, one bit per byte of source, since a group opener is one byte.

The README's limitation entry goes, and the option groups join the list of
supported syntax.

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.

build master this PR delta
bintest 1,279,862 1,280,870 +1,008
ascii-case 1,267,590 1,268,614 +1,024
byte-string 1,247,798 1,248,790 +992
cxx_abi 1,305,017 1,305,897 +880
full-debug (-O0) 1,876,566 1,877,478 +912

Verification

The tests are in the two files that already cover Regexp#to_s and the inline
options. The to_s block gains the round trip that motivated this: the printed
(?x-mi:a b) reads back as free-spacing, matches "ab" and not "a b", and
interpolates. The inline-options block replaces the two assert_raise that
pinned the refusal with the toggle and scoped forms in plain, named,
non-capturing and lookahead groups, x beside i, (?-x) inside (?x),
free-spacing following the scope through comments, (?# groups, escapes and
classes, a comment swallowing a ) on its line as it does in CRuby, and -x
bringing whitespace back inside an extended pattern.

Differential against CRuby 4.0.6. 57 hand-written patterns covering the
forms above and the malformed spellings, each under four flag sets and against
26 subjects: identical, except (?-) and (?-x-x), which Onigmo accepts and
this gem refuses on master as after. Then 6,000 random patterns over two seeds,
well nested from plain, named, lookaround and option groups, toggles, escapes,
classes and runs of whitespace, each under 0 and EXTENDED against 30
subjects: 12,000 lines, output byte-identical. Alternation is left out of the
generator, since the scope a toggle takes across | differs from Onigmo for
i and m already. A third run of 3,000 patterns that adds # comments to the
alphabet differs in 2 lines of 6,000, both a comment after the ) of the group
that turned x on, the re.c behaviour described above.

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

build total KO crash
full-debug 2,348 0 0
bintest 2,348 0 0
bintest (bintest suite) 123 0 0
cxx_abi 2,348 0 0
byte-string 2,278 0 0
ascii-case 2,345 0 0

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

Environment

Details
OS Ubuntu 24.04, Linux x86_64
gcc 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1)
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 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 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 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-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_compile.o" "mrbgems/mruby-regexp/src/re_compile.c"

Summary by CodeRabbit

  • New Features

    • Regular expressions now support inline extended-mode options, including enabling, disabling, and scoping free-spacing behavior within groups.
    • Nested groups, comments, whitespace, character classes, escapes, lookaheads, and combined options are handled consistently.
  • Bug Fixes

    • Extended-mode settings are correctly restored after scoped groups and preserved when patterns are converted or interpolated.
  • Documentation

    • Updated syntax documentation to describe inline and scoped option groups.
  • Tests

    • Added coverage for matching behavior, nesting, round-tripping, and interactions with extended regular expressions.

`Regexp#to_s` spells an extended pattern as `(?x-mi:...)`, and the parser
refused that form, so an extended Regexp could not recompile from its own
printed form and could not be interpolated into another pattern:

```ruby
Regexp.new(/a b/x.to_s)   # CRuby: /(?x-mi:a b)/
                          # mruby: RegexpError, inline extended mode (?x) is not supported
```

Free-spacing is applied by `preprocess_pattern()` before the parser runs,
which is why the letter could not be honoured where the parser reads it: by
then the whitespace it governs is either gone or kept. So the pass now
tracks the option itself. It keeps one bit per open group, pushed at every
`(` it interprets and popped at the matching `)`, and reads the letters of
each `(?imx-imx)` and `(?imx-imx:` it passes: the toggle form switches x
for the rest of the enclosing group, the scoped form for its own body. That
is the scope the parser already gives i and m, and it is what Onigmo does
with the whitespace under `(?x)`. `parse_inline_flags()` then carries x
like the other two letters, and nothing in the parser reads the bit.

Two consequences follow. `(?-x:...)` inside a pattern that is itself
extended brings the whitespace back for its scope, where before the letter
was accepted and dropped. And a `#` comment or a run of whitespace ends
where the group that turned x on ends; CRuby's own pre-pass in `re.c`
strips `#` comments after a `(?x)` toggle to the end of the pattern while
Onigmo restores the whitespace at the group's `)`, and this pass follows
the group for both.

The pre-check that lets a plain pattern skip the pass and its allocation
now also fires on an option group that turns x on. The scope stack lives
behind the rewritten pattern in the same allocation, one bit per byte of
source, since a group opener is one byte.

The README's limitation entry goes, and the option groups join the list of
supported syntax.
@takumin
takumin requested a review from matz as a code owner August 18, 2026 02:53
@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: 3191e7b6-5944-4642-b44b-40fe94320ced

📥 Commits

Reviewing files that changed from the base of the PR and between 364d6dd and fdc9449.

📒 Files selected for processing (4)
  • mrbgems/mruby-regexp/README.md
  • mrbgems/mruby-regexp/src/re_compile.c
  • mrbgems/mruby-regexp/test/regexp.rb
  • mrbgems/mruby-regexp/test/regexp_syntax.rb

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


📝 Walkthrough

Walkthrough

The regexp compiler now supports inline and scoped x options. Preprocessing tracks nested extended-mode scopes, strips applicable whitespace and comments, and restores prior modes. Tests and documentation cover the new syntax and round-trip behavior.

Changes

Inline extended-mode regexp support

Layer / File(s) Summary
Inline x parsing and scope-aware preprocessing
mrbgems/mruby-regexp/src/re_compile.c
The compiler accepts inline x options, processes nested toggle and scoped groups, restores enclosing modes, and preprocesses affected patterns.
Syntax coverage and documentation
mrbgems/mruby-regexp/test/regexp_syntax.rb, mrbgems/mruby-regexp/test/regexp.rb, mrbgems/mruby-regexp/README.md
Tests cover scoping, nesting, whitespace, comments, escapes, character classes, and round-trip reconstruction. Documentation describes inline option groups and removes the previous limitation.

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

Merge Risk: ⚪ Minimal · up to fdc94

The PR adds scoped extended-mode handling for regular expressions and documents broad validation, with no actionable merge-blocking risk remaining beyond normal checks and review.

Possibly related PRs

  • mruby/mruby#7062: Both changes modify inline regexp option handling and related round-trip tests.
  • mruby/mruby#7055: Both changes modify extended-mode preprocessing for comment and whitespace handling.
  • mruby/mruby#7031: Both changes modify regexp preprocessing and compiler state.

Suggested reviewers: matz

Sequence Diagram(s)

sequenceDiagram
  participant Pattern
  participant preprocess_pattern
  participant RegexpParser
  Pattern->>preprocess_pattern: provide inline x groups
  preprocess_pattern->>preprocess_pattern: apply nested scope and strip whitespace/comments
  preprocess_pattern->>RegexpParser: pass rewritten pattern with option syntax
  RegexpParser-->>Pattern: produce compiled regexp
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: scoping the inline x option like the i and m options.
✨ 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 a181044 into mruby:master Aug 18, 2026
21 checks passed
@takumin
takumin deleted the regexp-inline-extended-mode branch August 18, 2026 03:55
This was referenced Aug 18, 2026
takumin added a commit to takumin/mruby that referenced this pull request Aug 19, 2026
`preprocess_pattern()` removed the whitespace of `/x` from the source before the parser read it, so whatever the removal brought together was what the parser read. CRuby does this in the other layer: its tokenizer skips whitespace only where it fetches a token, and reads each token, an escape, a group name, an interval `{n,m}`, a `(?` opener, with the whitespace inside it in place. Removing the bytes instead means every kind of token needs a rule of its own to keep the two apart, the `(?:)` written between a digit escape and a digit (mruby#7268) being one, and the tokens without one still read differently:

```ruby
Regexp.new("( ?i)A", Regexp::EXTENDED) =~ "a"
# CRuby: RegexpError (target of repeat operator is not specified), mruby: 0
Regexp.new("a{1, 2}", Regexp::EXTENDED) =~ "aa"
# CRuby: nil (`{1, 2}` is not an interval), mruby: 0
Regexp.new("(?<a b>x)", Regexp::EXTENDED).names
# CRuby: ["a b"], mruby: ["ab"]
Regexp.new("(?<ab>x)\\k <ab>", Regexp::EXTENDED) =~ "xk<ab>"
# CRuby: 0 (`\k` is the letter and `<ab>` a literal), mruby: nil (it read `\k<ab>`)
Regexp.new("a\vb", Regexp::EXTENDED) =~ "ab"
# CRuby: nil (a vertical tab is not free-spacing whitespace), mruby: 0
```

The parser now skips the whitespace where the tokenizer does. `RE_FLAG_EXTENDED` was already carried in `c->flags` and scoped by the save and restore every group does for `(?i)` and `(?m)`; `skip_extended_space()` reads it at the two places a token can end: at the top of `compile_seq()`, before each atom and before the `|` or `)` that ends the sequence, and in `compile_quantified()` between the atom and its quantifier. Nowhere else, so `{1, 2}` is not an interval, `( ?` is a group and then a `?`, a group name keeps its blanks, and a numeric escape reads what stands after it: `\x6 1` is `\x06` and `1` because the hex digits stop at the space, with nothing written between them. The bytes skipped are the five Onigmo skips; the vertical tab the pass also removed is a literal under `/x` in CRuby and now here.

The pass keeps what CRuby's own pre-pass (`re.c`) removes before its tokenizer runs, `(?#...)` groups under any flags and `#` comments under `/x`, with the scope stack from mruby#7252 saying where `/x` is on. Both stay in the pass rather than moving to the parser because a removed comment does join the bytes on either side of it in CRuby, `\1(?#c)0` and `\1#c`, a newline and `0` both being `\10`; the tests from mruby#7268 that pin this are unchanged. The `(?:)` insertion goes with its `esc_end` and `blank_out` bookkeeping, and with it the reason the rewrite buffer was twice the source. `has_rewritten_group()`, which decided whether the pass runs at all, is replaced by a `memchr()` for `#`: both things the pass removes are spelled with one, and neither the `/x` flag nor a `(?x)` needs the pass any more.

One pattern that compiled on master is refused now. `a* ?` under `/x` was read as the non-greedy `a*?` once the blank was gone; CRuby reads a `?` a blank away from the `*` as a repeat of the repeat, `(?:a*)?`, and this engine refuses a repeat of a repeat wherever it is written, `a**` included, so it refuses this one too rather than give it a meaning CRuby does not.
takumin added a commit to takumin/mruby that referenced this pull request Aug 19, 2026
`preprocess_pattern()` removed the whitespace of `/x` from the source before the parser read it, so whatever the removal brought together was what the parser read. CRuby does this in the other layer: its tokenizer skips whitespace only where it fetches a token, and reads each token, an escape, a group name, an interval `{n,m}`, a `(?` opener, with the whitespace inside it in place. Removing the bytes instead means every kind of token needs a rule of its own to keep the two apart, the `(?:)` written between a digit escape and a digit (mruby#7268) being one, and the tokens without one still read differently:

```ruby
Regexp.new("( ?i)A", Regexp::EXTENDED) =~ "a"
Regexp.new("a{1, 2}", Regexp::EXTENDED) =~ "aa"
Regexp.new("(?<a b>x)", Regexp::EXTENDED).names
Regexp.new("(?<ab>x)\\k <ab>", Regexp::EXTENDED) =~ "xk<ab>"
Regexp.new("a\vb", Regexp::EXTENDED) =~ "ab"
```

The parser now skips the whitespace where the tokenizer does. `RE_FLAG_EXTENDED` was already carried in `c->flags` and scoped by the save and restore every group does for `(?i)` and `(?m)`; `skip_extended_space()` reads it at the two places a token can end: at the top of `compile_seq()`, before each atom and before the `|` or `)` that ends the sequence, and in `compile_quantified()` between the atom and its quantifier. Nowhere else, so `{1, 2}` is not an interval, `( ?` is a group and then a `?`, a group name keeps its blanks, and a numeric escape reads what stands after it: `\x6 1` is `\x06` and `1` because the hex digits stop at the space, with nothing written between them. The bytes skipped are the five Onigmo skips; the vertical tab the pass also removed is a literal under `/x` in CRuby and now here.

The pass keeps what CRuby's own pre-pass (`re.c`) removes before its tokenizer runs, `(?#...)` groups under any flags and `#` comments under `/x`, with the scope stack from mruby#7252 saying where `/x` is on. Both stay in the pass rather than moving to the parser because a removed comment does join the bytes on either side of it in CRuby, `\1(?#c)0` and `\1#c`, a newline and `0` both being `\10`; the tests from mruby#7268 that pin this are unchanged. The `(?:)` insertion goes with its `esc_end` and `blank_out` bookkeeping, and with it the reason the rewrite buffer was twice the source. `has_rewritten_group()`, which decided whether the pass runs at all, is replaced by a `memchr()` for `#`: both things the pass removes are spelled with one, and neither the `/x` flag nor a `(?x)` needs the pass any more.

One shape of pattern that compiled on master is refused now. `a* ?` under `/x` was read as the non-greedy `a*?` once the blank was gone, and `a{2} ?` the same way; CRuby reads a `?` a blank away from the quantifier as a repeat of the repeat, `(?:a*)?`, and this engine refuses a repeat of a repeat wherever it is written, `a**` included, so it refuses these too rather than give them a meaning CRuby does not.
takumin added a commit to takumin/mruby that referenced this pull request Aug 19, 2026
`preprocess_pattern()` removed the whitespace of `/x` from the source before the parser read it, so whatever the removal brought together was what the parser read. CRuby does this in the other layer: its tokenizer skips whitespace only where it fetches a token, and reads each token, an escape, a group name, an interval `{n,m}`, a `(?` opener, with the whitespace inside it in place. Removing the bytes instead means every kind of token needs a rule of its own to keep the two apart, the `(?:)` written between a digit escape and a digit (mruby#7268) being one, and the tokens without one still read differently:

```ruby
Regexp.new("( ?i)A", Regexp::EXTENDED) =~ "a"
Regexp.new("a{1, 2}", Regexp::EXTENDED) =~ "aa"
Regexp.new("(?<a b>x)", Regexp::EXTENDED).names
Regexp.new("(?<ab>x)\\k <ab>", Regexp::EXTENDED) =~ "xk<ab>"
Regexp.new("a\vb", Regexp::EXTENDED) =~ "ab"
```

The parser now skips the whitespace where the tokenizer does. `RE_FLAG_EXTENDED` was already carried in `c->flags` and scoped by the save and restore every group does for `(?i)` and `(?m)`; `skip_extended_space()` reads it at the two places a token can end: at the top of `compile_seq()`, before each atom and before the `|` or `)` that ends the sequence, and in `compile_quantified()` between the atom and its quantifier. Nowhere else, so `{1, 2}` is not an interval, `( ?` is a group and then a `?`, a group name keeps its blanks, and a numeric escape reads what stands after it: `\x6 1` is `\x06` and `1` because the hex digits stop at the space, with nothing written between them. The bytes skipped are the five Onigmo skips; the vertical tab the pass also removed is a literal under `/x` in CRuby and now here.

The pass keeps what CRuby's own pre-pass (`re.c`) removes before its tokenizer runs, `(?#...)` groups under any flags and `#` comments under `/x`, with the scope stack from mruby#7252 saying where `/x` is on. Both stay in the pass rather than moving to the parser because a removed comment does join the bytes on either side of it in CRuby, `\1(?#c)0` and `\1#c`, a newline and `0` both being `\10`; the tests from mruby#7268 that pin this are unchanged. The `(?:)` insertion goes with its `esc_end` and `blank_out` bookkeeping, and with it the reason the rewrite buffer was twice the source. `has_rewritten_group()`, which decided whether the pass runs at all, is replaced by a `memchr()` for `#`: both things the pass removes are spelled with one, and neither the `/x` flag nor a `(?x)` needs the pass any more.

One shape of pattern that compiled on master is refused now. `a* ?` under `/x` was read as the non-greedy `a*?` once the blank was gone, and `a{2} ?` the same way; CRuby reads a `?` a blank away from the quantifier as a repeat of the repeat, `(?:a*)?`, and this engine refuses a repeat of a repeat wherever it is written, `a**` included, so it refuses these too rather than give them a meaning CRuby does not.
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