Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions mrbgems/mruby-regexp/include/re_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,11 @@ int mrb_re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int start,
int *captures, int captures_size, mrb_bool binary);

/* Execute a match backward: the last match that starts at or before `limit`.
Answers as mrb_re_exec() does, and clears the capture buffer itself before
each of the searches it makes, having to make more than one. */
int mrb_re_rexec(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int limit,
int *captures, int captures_size, mrb_bool binary);

#endif /* MRB_RE_INTERNAL_H */
66 changes: 9 additions & 57 deletions mrbgems/mruby-regexp/mrblib/string_regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
#
# With the type established, each override reaches the engine through class
# methods that take the pattern as an argument (`Regexp.__search`,
# `__byte_search`, `__search_p`, `__sub_str`, `__gsub_str`, `__scan`), so
# `__byte_search`, `__byte_rsearch`, `__search_p`, `__sub_str`, `__gsub_str`,
# `__scan`), so
# nothing rewritten on the pattern instance is consulted on the way: the C
# side searches, and the loops and blocks stay here. The MatchData those
# searches answer is built in C too, so what the overrides read from it
Expand Down Expand Up @@ -444,53 +445,6 @@ def index(*args)
md && md.begin(0)
end

# The last match that starts at or before `limit`, a byte offset, or nil,
# with the match globals left describing it.
#
# The engine searches forward only, so this walks the subject from the
# start and keeps the last match that qualifies: linear in the number of
# positions a match starts at, where the backward search CRuby hands to
# Onig is not. Each step resumes one byte past the match start and not at
# the match end, which is what keeps overlapping matches in view:
# `"aaa".rindex(/aa/)` is 1, where resuming at the end would answer 0.
#
# The walk is in byte space so that its length is what it looks like. A
# character offset is not a place the subject can be read from: every one
# handed to `Regexp.__search` is counted out from the start of the subject
# again, and every one read back off a match with `begin` is counted the
# same way, so a walk that speaks characters pays for the whole subject on
# every turn and takes quadratic time on a multibyte one. Nothing is given
# up by leaving them: a byte inside a character is not a position a match
# can start at, and the engine steps over one on its own rather than seed a
# match attempt there, so `+ 1` reaches the next character by itself.
#
# `Regexp.__byte_search` does not range check its position, so the walk
# stops itself at the end of the subject. An empty match there is the one
# that reaches it: it leaves `pos` one past the last byte.
#
# The walk publishes none of what it passes over. A search that publishes
# cuts the whole subject into the pre match and post match globals, and
# every match here but the last is one this method already means to
# replace, so publishing them costs the subject once per match and leaves
# behind nothing anything reads. The answer is published below instead,
# which is where it was published from before.
def __regexp_rsearch(pattern, limit)
found = nil
pos = 0
size = self.bytesize
while pos <= size && (md = Regexp.__byte_search(pattern, self, pos, false, false))
start = md.__byte_begin(0)
break if start > limit
found = md
pos = start + 1
end
# The globals still describe whatever they described before the call, the
# walk having said nothing to them, so the answer is published here and a
# search that found none clears them itself.
found ? found.__set_globals : Regexp.__search(pattern, nil)
found
end

# Regexp-aware `rindex`. Falls back to the C-defined `rindex` (aliased as
# `__rindex` above) for every other argument form.
def rindex(*args)
Expand Down Expand Up @@ -518,14 +472,12 @@ def rindex(*args)
pos = len
end
end
# The walk reads the subject by byte, so the character position it is to
# stop at has to be read as one here. A position at the end of the
# The search reads the subject by byte, so the character position it is
# to stop at has to be read as one here. A position at the end of the
# subject is the end of its bytes and needs no reading, which is the form
# `rindex` is called in when it is called with one argument at all; only
# a position named by the caller is measured, once, where the walk would
# otherwise have measured one on every turn.
# `rindex` is called in when it is called with one argument at all.
byte_pos = pos == len ? self.bytesize : self[0, pos].bytesize
md = __regexp_rsearch(args[0], byte_pos)
md = Regexp.__byte_rsearch(args[0], self, byte_pos)
md && md.begin(0)
end

Expand Down Expand Up @@ -576,7 +528,7 @@ def byterindex(*args)
# As in `byteindex` above, and after the same clamp: a position past the
# end of the subject has already been read as its end, which is a boundary.
Regexp.__check_byte_pos(self, pos)
md = __regexp_rsearch(args[0], pos)
md = Regexp.__byte_rsearch(args[0], self, pos)
md && md.__byte_begin(0)
end

Expand All @@ -597,8 +549,8 @@ def partition(sep)
def rpartition(sep)
return __rpartition(sep) unless Regexp === sep
# The last match anywhere in the subject, so the limit is its end and the
# walk below never stops early.
md = __regexp_rsearch(sep, self.bytesize)
# search below never stops early.
md = Regexp.__byte_rsearch(sep, self, self.bytesize)
# No match puts the whole subject in the tail, which is the row this
# method is most often got wrong on.
return ["", "", self.byteslice(0, self.bytesize)] unless md
Expand Down
123 changes: 111 additions & 12 deletions mrbgems/mruby-regexp/src/re_exec.c
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,10 @@ add_thread(pike_state *s, re_threadlist *list,

static int
pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int start,
const char *str, mrb_int len, mrb_int start, mrb_int start_limit,
int *captures, int captures_size, mrb_bool binary)
{
const char *start_cap = str + start_limit;
const char *sp = str + start;
const char *str_end = str + len;
int ncap = pat->num_captures * 2;
Expand Down Expand Up @@ -415,7 +416,10 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
curr.count = next.count = 0;

for (; sp <= str_end; sp++) {
if (!s.matched) {
/* Past the last position a match may start at, only the threads already
running can still answer; once they are gone nothing can. */
if (!s.matched && sp > start_cap && curr.count == 0) break;
if (!s.matched && sp <= start_cap) {
/* Skip ahead when no active threads */
if (curr.count == 0) {
if (pat->prefix_len > 0) {
Expand All @@ -427,6 +431,7 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
while (sp < str_end && !FIRST_BYTE_OK(pat, (uint8_t)*sp)) sp++;
if (sp > str_end) break;
}
if (sp > start_cap) break;
}
/* Don't seed a new match attempt inside a character. Its interior is
not a char boundary, and starting a thread there mis-decodes the
Expand Down Expand Up @@ -804,16 +809,17 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end,

static int
backtrack_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int start,
const char *str, mrb_int len, mrb_int start, mrb_int start_limit,
int *captures, int captures_size, mrb_bool binary)
{
const char *start_cap = str + start_limit;
const char *str_end = str + len;
int ncap = pat->num_captures * 2;
if (ncap == 0) ncap = 2;

int *caps = (int*)mrb_malloc(mrb, sizeof(int) * ncap);

for (const char *sp = str + start; sp <= str_end; sp++) {
for (const char *sp = str + start; sp <= str_end && sp <= start_cap; sp++) {
/* Skip ahead using literal prefix or first-byte bitmap */
if (pat->prefix_len > 0) {
const char *skip = skip_to_prefix(pat, sp, str_end);
Expand All @@ -824,6 +830,7 @@ backtrack_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
while (sp < str_end && !FIRST_BYTE_OK(pat, (uint8_t)*sp)) sp++;
if (sp > str_end) break;
}
if (sp > start_cap) break;
if (!binary && sp < str_end && mrb_re_char_interior_p(str, sp, str_end)) {
continue;
}
Expand All @@ -846,16 +853,17 @@ backtrack_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
/* Fast path for pure literal patterns: use memchr+memcmp, no NFA needed */
static int
literal_exec(const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int start,
const char *str, mrb_int len, mrb_int start, mrb_int start_limit,
int *captures, int captures_size, mrb_bool binary)
{
const char *start_cap = str + start_limit;
const char *sp = str + start;
const char *str_end = str + len;
int plen = pat->prefix_len;

while (sp + plen <= str_end) {
while (sp + plen <= str_end && sp <= start_cap) {
const char *found = (const char*)memchr(sp, pat->prefix[0], str_end - sp);
if (!found || found + plen > str_end) return 0;
if (!found || found + plen > str_end || found > start_cap) return 0;
if (!binary && mrb_re_char_interior_p(str, found, str_end)) {
sp = found + 1; /* not a char boundary, same rule as the other engines */
continue;
Expand All @@ -879,17 +887,108 @@ literal_exec(const mrb_regexp_pattern *pat,
return 0;
}

/* The search the three engines make, with the last position a match may
start at named. The bound is on where a match may begin and not on how
far the subject is read: a match that begins at `start_limit` runs to
wherever it ends, which is why this is a separate argument rather than a
shorter `len`. Shortening the subject would answer a different question
-- `$` and `\z` would assert at the cut, and a match reaching past it
would be lost. */
static int
exec_range(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int start, mrb_int start_limit,
int *captures, int captures_size, mrb_bool binary)
{
if (pat->is_literal) {
return literal_exec(pat, str, len, start, start_limit, captures, captures_size, binary);
}
if (pat->has_backref || pat->needs_backtrack) {
return backtrack_exec(mrb, pat, str, len, start, start_limit, captures, captures_size, binary);
}
return pike_vm(mrb, pat, str, len, start, start_limit, captures, captures_size, binary);
}

/* Public entry point */
int
mrb_re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int start,
int *captures, int captures_size, mrb_bool binary)
{
if (pat->is_literal) {
return literal_exec(pat, str, len, start, captures, captures_size, binary);
/* The end of the subject is the last position anything can start at, so
the forward search is the unbounded case of the above. */
return exec_range(mrb, pat, str, len, start, len, captures, captures_size, binary);
}

/* How far the backward probe below may read before it stops being the cheap
question. A search bounded to a window still reads from where the window
starts to the end of the subject, so the bound is on that span and not on
the window's own width: a narrow window at the end of a long subject is
cheap, the same window asked about a position far from the end is the
whole search over again. */
#define RE_RSEARCH_PROBE_SPAN 256

/* The last match that starts at or before `limit`, which is what `rindex`,
`byterindex` and `rpartition` ask for.

A forward walk answers this by stepping every match from the front and
keeping the last, which costs a search per match: on a subject a pattern
matches everywhere that is a search per position, and where one search is
itself linear in the subject -- a greedy `/a+b?/` and the like -- the walk
is quadratic in it.

The last match is usually near the end, so ask about the end first: widen
a window there until a match starts inside it. A window that catches one
costs the window rather than the subject, and the walk that follows has
only the window to cross. Widening stops at the span above rather than at
the front, so a subject with no match near the end falls through to the
single forward search this cost before, instead of paying for the
widening as well. */
int
mrb_re_rexec(mrb_state *mrb, const mrb_regexp_pattern *pat,
const char *str, mrb_int len, mrb_int limit,
int *captures, int captures_size, mrb_bool binary)
{
if (limit > len) limit = len;
if (limit < 0) return 0;

int last[RE_MAX_CAPTURES * 2];
int last_n = 0;

for (mrb_int k = 1; ; k *= 2) {
mrb_int lo = limit - k + 1;
if (lo < 0) lo = 0;
if (len - lo > RE_RSEARCH_PROBE_SPAN) break;
memset(captures, -1, sizeof(int) * captures_size);
last_n = exec_range(mrb, pat, str, len, lo, limit, captures, captures_size, binary);
if (last_n) break;
/* The window had grown to the whole range, so there is no match to find
and the search below would only ask again. */
if (lo == 0) return 0;
}
if (pat->has_backref || pat->needs_backtrack) {
return backtrack_exec(mrb, pat, str, len, start, captures, captures_size, binary);

if (last_n == 0) {
memset(captures, -1, sizeof(int) * captures_size);
last_n = exec_range(mrb, pat, str, len, 0, limit, captures, captures_size, binary);
if (last_n == 0) return 0;
}

/* Whichever range answered gave its leftmost match; the last one is found
by walking forward from it. Each step resumes one byte past the match
start and not at the match end, which is what keeps overlapping matches
in view: `"aaa"` against `/aa/` answers 1, where resuming at the end
would answer 0. A byte inside a character is not a position a match can
start at, and the engine steps over one rather than seed an attempt
there, so `+ 1` reaches the next character by itself. */
memcpy(last, captures, sizeof(int) * captures_size);
mrb_int pos = captures[0] + 1;
while (pos <= limit) {
memset(captures, -1, sizeof(int) * captures_size);
int n = exec_range(mrb, pat, str, len, pos, limit, captures, captures_size, binary);
if (n == 0) break;
last_n = n;
memcpy(last, captures, sizeof(int) * captures_size);
pos = captures[0] + 1;
}
return pike_vm(mrb, pat, str, len, start, captures, captures_size, binary);
memcpy(captures, last, sizeof(int) * captures_size);
return last_n;
}
Loading
Loading