mruby-regexp: disable first-byte skip when pattern can match empty

first_set_walk returned TRUE when it reached RE_MATCH via epsilon
transitions, but that's exactly the case where the optimization is
wrong: an empty-matchable pattern can start matching at any position,
including bytes that aren't in the computed first-byte set. The
skip-ahead loop in pike_vm then advanced past valid empty-match
positions, producing a match at the wrong offset (e.g. /a?/.match("b")
reported the empty match at index 1 instead of 0).

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-05-22 19:16:24 +09:00
parent b9b8186f00
commit d21eceb286
2 changed files with 22 additions and 2 deletions
+8 -2
View File
@@ -823,12 +823,18 @@ first_set_walk(const re_inst *code, uint32_t code_len,
case RE_ANY: case RE_ANY_NL:
return FALSE; /* any byte possible */
case RE_MATCH:
return TRUE; /* empty match; first_bytes still valid for other branches */
/* Reaching MATCH via epsilon transitions means the regex can match
zero characters at any position. Skipping bytes that aren't in the
first-byte set would skip past valid empty-match positions, so the
optimization isn't safe -- bail out and accept any starting byte. */
return FALSE;
default:
return FALSE;
}
}
return TRUE;
/* Walked off the end without hitting MATCH or a consuming op. Treat as
empty-matchable, same as RE_MATCH. */
return FALSE;
}
static mrb_bool
+14
View File
@@ -478,3 +478,17 @@ assert("Regexp - consecutive optional quantifiers (#6853)") do
assert_equal [""], /a*b*/.match("").to_a
assert_equal [""], /a?b?c?d?/.match("").to_a
end
assert("Regexp - empty-matchable patterns find earliest match position") do
# When a regex can match zero characters via epsilon transitions, the
# first-byte skip-ahead optimization is unsafe: skipping past bytes
# that aren't in the first-byte set would also skip past valid
# empty-match positions.
md = /a?/.match("b")
assert_equal "", md[0]
assert_equal 0, md.begin(0)
md = /a?b?/.match("c")
assert_equal "", md[0]
assert_equal 0, md.begin(0)
end