mruby-regexp: fix non-greedy quantifiers (*?, +?, ??)

non-greedy patterns now correctly match the shortest possible
string. patterns with non-greedy quantifiers are dispatched to
the backtracking engine which naturally handles non-greedy
semantics.

the Pike VM continues to be used for purely greedy patterns
(O(n*m) guarantee).

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-21 12:44:58 +09:00
parent 23b2d24cf5
commit 8d92379d7c
4 changed files with 15 additions and 6 deletions
@@ -56,6 +56,7 @@ typedef struct mrb_regexp_pattern {
uint16_t num_captures; /* number of capture groups (including group 0) */
uint32_t flags;
mrb_bool has_backref; /* true if pattern uses \1-\9 */
mrb_bool has_nongreedy; /* true if pattern uses *?, +?, ?? */
} mrb_regexp_pattern;
/* Regexp flags */
+10 -2
View File
@@ -26,6 +26,7 @@ typedef struct {
uint16_t num_captures;
uint32_t flags;
mrb_bool has_backref;
mrb_bool has_nongreedy;
} re_compiler;
static void compile_alt(re_compiler *c); /* forward */
@@ -427,7 +428,10 @@ compile_quantified(re_compiler *c)
if (ch == '*' || ch == '+' || ch == '?') {
next_char(c);
mrb_bool nongreedy = (peek(c) == '?');
if (nongreedy) next_char(c);
if (nongreedy) {
next_char(c);
c->has_nongreedy = TRUE;
}
if (ch == '*') {
@@ -458,7 +462,10 @@ compile_quantified(re_compiler *c)
return; /* not a quantifier */
}
mrb_bool nongreedy = (peek(c) == '?');
if (nongreedy) next_char(c);
if (nongreedy) {
next_char(c);
c->has_nongreedy = TRUE;
}
/* For {n,m}: repeat atom min times, then optional (max-min) times */
uint32_t atom_end = c->code_len;
@@ -595,6 +602,7 @@ re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags)
pat->num_captures = c.num_captures;
pat->flags = flags;
pat->has_backref = c.has_backref;
pat->has_nongreedy = c.has_nongreedy;
return pat;
}
+1 -1
View File
@@ -426,7 +426,7 @@ re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int start,
int *captures, int captures_size)
{
if (pat->has_backref) {
if (pat->has_backref || pat->has_nongreedy) {
return backtrack_exec(mrb, pat, str, len, start, captures, captures_size);
}
return pike_vm(mrb, pat, str, len, start, captures, captures_size);
+3 -3
View File
@@ -178,9 +178,9 @@ assert("Regexp - nested captures") do
end
assert("Regexp - non-greedy quantifiers") do
# TODO: non-greedy a+? returns "aaa" instead of "a" (needs priority fix)
assert_equal "aaa", /a+?/.match("aaa")[0]
# non-greedy a*? test skipped: needs further debugging
assert_equal "a", /a+?/.match("aaa")[0]
assert_equal "", /a*?/.match("aaa")[0]
end
assert("Regexp - word boundary") do