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
60 changes: 44 additions & 16 deletions mrbgems/mruby-regexp/mrblib/string_regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -515,28 +515,49 @@ def index(*args)
md && md.begin(0)
end

# The last match that starts at or before `limit`, or nil, with the match
# globals left describing it. `limit` is a byte offset when `bytes` is
# true and a character offset otherwise, which is the whole of the
# difference between `byterindex` and `rindex`.
# 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 character past the match start and
# not at the match end, which is what keeps overlapping matches in view:
# 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.
def __regexp_rsearch(pattern, limit, bytes)
#
# 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
while (md = Regexp.__search(pattern, self, pos))
break if (bytes ? md.__byte_begin(0) : md.begin(0)) > limit
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 = md.begin(0) + 1
pos = start + 1
end
# The loop leaves behind whatever its last search published: the clear a
# failed one does, or a match past `limit` that is not the answer. Both
# have to give way to what the search found.
# 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
Expand Down Expand Up @@ -568,7 +589,14 @@ def rindex(*args)
pos = len
end
end
md = __regexp_rsearch(args[0], pos, false)
# 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
# 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.
byte_pos = pos == len ? self.bytesize : self[0, pos].bytesize
md = __regexp_rsearch(args[0], byte_pos)
md && md.begin(0)
end

Expand Down Expand Up @@ -614,7 +642,7 @@ def byterindex(*args)
pos = len
end
end
md = __regexp_rsearch(args[0], pos, true)
md = __regexp_rsearch(args[0], pos)
md && md.__byte_begin(0)
end

Expand All @@ -636,7 +664,7 @@ 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.length, false)
md = __regexp_rsearch(sep, 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
48 changes: 30 additions & 18 deletions mrbgems/mruby-regexp/src/regexp.c
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,12 @@ set_match_globals(mrb_state *mrb, mrb_value obj, mrb_value str, int *captures, i
mrb_gv_set(mrb, last_match_syms[LAST_PAREN], last_paren);
}

/* Create MatchData from captures */
/* Create MatchData from captures. `publish` says whether the match becomes the
one the match globals describe; a caller that will publish a match of its
own choosing passes FALSE and leaves them where they were. */
static mrb_value
create_matchdata(mrb_state *mrb, mrb_value regexp, mrb_value str, int *captures, int ncap)
create_matchdata(mrb_state *mrb, mrb_value regexp, mrb_value str, int *captures, int ncap,
mrb_bool publish)
{
/* Snapshot the subject: MatchData reports the string as it was at match
time, so later in-place changes to it must not be visible here. */
Expand All @@ -347,7 +350,7 @@ create_matchdata(mrb_state *mrb, mrb_value regexp, mrb_value str, int *captures,
mrb_iv_set(mrb, obj, mrb_intern_lit(mrb, "source"), str);
mrb_iv_set(mrb, obj, mrb_intern_lit(mrb, "regexp"), regexp);

set_match_globals(mrb, obj, str, captures, md->num_captures);
if (publish) set_match_globals(mrb, obj, str, captures, md->num_captures);

return obj;
}
Expand All @@ -363,9 +366,11 @@ match_operand(mrb_state *mrb, mrb_value obj)

/* Internal: execute match and create MatchData.
Returns MatchData on match, nil on no match.
Sets $~ and $1-$9 globals. */
Sets $~ and $1-$9 globals, unless `publish` says the caller owns them: a
search that publishes nothing clears nothing either, so the globals come
out of it exactly as they went in. */
static mrb_value
exec_match(mrb_state *mrb, mrb_value self, mrb_value str, mrb_int pos)
exec_match(mrb_state *mrb, mrb_value self, mrb_value str, mrb_int pos, mrb_bool publish)
{
mrb_regexp_pattern *pat = DATA_GET_PTR(mrb, self, &regexp_type, mrb_regexp_pattern);
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
Expand All @@ -378,10 +383,10 @@ exec_match(mrb_state *mrb, mrb_value self, mrb_value str, mrb_int pos)

if (ncap == 0) {
mrb_free(mrb, captures);
clear_match_globals(mrb);
if (publish) clear_match_globals(mrb);
return mrb_nil_value();
}
mrb_value md = create_matchdata(mrb, self, str, captures, cap_size);
mrb_value md = create_matchdata(mrb, self, str, captures, cap_size, publish);
mrb_free(mrb, captures);
return md;
}
Expand Down Expand Up @@ -410,7 +415,7 @@ regexp_match(mrb_state *mrb, mrb_value self)
}

re_check_encoding(mrb, str);
md = exec_match(mrb, self, str, pos);
md = exec_match(mrb, self, str, pos, TRUE);
if (!mrb_nil_p(md) && !mrb_nil_p(block)) {
return mrb_yield(mrb, block, md);
}
Expand Down Expand Up @@ -465,11 +470,11 @@ regexp_s_search(mrb_state *mrb, mrb_value klass)
return mrb_nil_value();
}
if (!checked) re_check_encoding(mrb, str);
return exec_match(mrb, re, str, pos);
return exec_match(mrb, re, str, pos, TRUE);
}

/*
* Regexp.__byte_search(re, str, pos = 0, checked = false)
* Regexp.__byte_search(re, str, pos = 0, checked = false, publish = true)
*
* Internal: the byte-offset search the mrblib loops of `gsub`, `split` and
* `byteindex` drive themselves. No position normalization, because the
Expand All @@ -478,6 +483,12 @@ regexp_s_search(mrb_state *mrb, mrb_value klass)
* the check reads the flag core left on it after the first turn. `checked`
* carries the same meaning as in `__search`: `gsub` sets it for a block over
* a quoted String pattern.
*
* `publish` says whether the match becomes the one the match globals describe.
* A loop that walks past a match on the way to the one it wants clears it:
* `rindex` and its family pass FALSE and publish the match they settled on
* with `MatchData#__set_globals`. `gsub` and `scan` cannot, since the block
* they call reads the globals of the match it was handed.
*/
static mrb_value
regexp_s_byte_search(mrb_state *mrb, mrb_value klass)
Expand All @@ -486,11 +497,12 @@ regexp_s_byte_search(mrb_state *mrb, mrb_value klass)
mrb_int pos = 0;

mrb_bool checked = FALSE;
mrb_bool publish = TRUE;

mrb_get_args(mrb, "oS|ib", &re, &str, &pos, &checked);
mrb_get_args(mrb, "oS|ibb", &re, &str, &pos, &checked, &publish);
check_regexp_arg(mrb, re);
if (!checked) re_check_encoding(mrb, str);
return exec_match(mrb, re, str, pos);
return exec_match(mrb, re, str, pos, publish);
}

/* Internal: the search of `match?`, run with a NULL capture buffer so that
Expand Down Expand Up @@ -557,7 +569,7 @@ regexp_match_op(mrb_state *mrb, mrb_value self)
str = match_operand(mrb, str);
re_check_encoding(mrb, str);

mrb_value md = exec_match(mrb, self, str, 0);
mrb_value md = exec_match(mrb, self, str, 0, TRUE);
if (mrb_nil_p(md)) return mrb_nil_value();

mrb_match_data *m = DATA_GET_PTR(mrb, md, &matchdata_type, mrb_match_data);
Expand All @@ -581,7 +593,7 @@ regexp_case_match(mrb_state *mrb, mrb_value self)
if (!pat) return mrb_false_value();
re_check_encoding(mrb, str);

md = exec_match(mrb, self, str, 0);
md = exec_match(mrb, self, str, 0, TRUE);
return mrb_bool_value(!mrb_nil_p(md));
}

Expand Down Expand Up @@ -1239,7 +1251,7 @@ regexp_s_gsub_str(mrb_state *mrb, mrb_value klass)

/* set $~ from last match */
if (last_ncap > 0) {
create_matchdata(mrb, re, str, last_captures, last_ncap);
create_matchdata(mrb, re, str, last_captures, last_ncap, TRUE);
}
else {
clear_match_globals(mrb);
Expand Down Expand Up @@ -1302,7 +1314,7 @@ regexp_s_sub_str(mrb_state *mrb, mrb_value klass)
mrb_str_cat(mrb, result, s + captures[1], slen - captures[1]);
}

create_matchdata(mrb, re, str, captures, cap_size);
create_matchdata(mrb, re, str, captures, cap_size, TRUE);
mrb_free(mrb, captures);
re_mark_spliced(result, str, replacement, TRUE);
return result;
Expand Down Expand Up @@ -1382,7 +1394,7 @@ regexp_s_scan(mrb_state *mrb, mrb_value klass)
mrb_free(mrb, captures);

if (last_ncap > 0) {
create_matchdata(mrb, re, str, last_captures, last_ncap);
create_matchdata(mrb, re, str, last_captures, last_ncap, TRUE);
}
else {
clear_match_globals(mrb);
Expand Down Expand Up @@ -1438,7 +1450,7 @@ mrb_mruby_regexp_gem_init(mrb_state *mrb)
mrb_define_class_method(mrb, re, "__check_encoding", regexp_check_encoding, MRB_ARGS_REQ(1));
mrb_define_class_method(mrb, re, "__check_pattern", regexp_check_pattern, MRB_ARGS_REQ(1));
mrb_define_class_method(mrb, re, "__search", regexp_s_search, MRB_ARGS_ARG(2, 2));
mrb_define_class_method(mrb, re, "__byte_search", regexp_s_byte_search, MRB_ARGS_ARG(2, 2));
mrb_define_class_method(mrb, re, "__byte_search", regexp_s_byte_search, MRB_ARGS_ARG(2, 3));
mrb_define_class_method(mrb, re, "__search_p", regexp_s_search_p, MRB_ARGS_ARG(2, 1));

/* Instance methods */
Expand Down
73 changes: 73 additions & 0 deletions mrbgems/mruby-regexp/test/string_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,40 @@ def re.is_a?(klass); false; end
assert_nil "hello".rindex(/l/, -10)
end

assert("String#rindex bounds the match start in characters") do
# The position `rindex` takes is a character offset and the one
# `byterindex` takes is a byte offset. On a single-byte subject the two
# name the same place, so only a multibyte one says which is being read.
skip unless __ENCODING__ == "UTF-8"
str = "あいうあいう" # 6 characters, 18 bytes

assert_equal 4, str.rindex(/い/)
assert_equal 12, str.byterindex(/い/)

# 1 as a character offset is the second character, which the first `い` is;
# as a byte offset it is inside the first character and reaches no match at
# all
assert_equal 1, str.rindex(/い/, 1)
assert_nil str.rindex(/い/, 0)
assert_equal 3, str.byterindex(/い/, 3)

# 4 as a character offset reaches the second `い`, where the same number of
# bytes is still short of the first
assert_equal 4, str.rindex(/い/, 4)
assert_equal 1, str.rindex(/い/, 3)

# a negative position counts back in the same space it counts forward in
assert_equal 4, str.rindex(/い/, -1)
assert_equal 1, str.rindex(/い/, -3)

# overlapping matches stay in view across a multibyte character, where a
# walk that resumed at the match end would answer 0
assert_equal 1, "あああ".rindex(/ああ/)
assert_equal 3, "あああ".byterindex(/ああ/)
assert_equal ["あ", "ああ", ""], "あああ".rpartition(/ああ/)
assert_equal ["あい", "うあ", "いう"], str.rpartition(/うあ/)
end

assert("String#index and String#rindex with regexp set the match globals") do
assert_equal 1, "abc".index(/(b)/)
assert_equal "b", $1
Expand Down Expand Up @@ -544,6 +578,45 @@ def fake.match(str, pos = 0); raise "must not be called"; end
assert_nil $~
end

assert("a backward search publishes every global of the match it settled on") do
# `$~` and `$1` are what the tests above read back. The rest of what a match
# leaves behind is published by the same act and is asserted here, since the
# walk these three share passes matches on the way to the one it answers
# with and none of those may be what is left standing.
assert_equal 4, "abcabc".rindex(/(b)(c)/)
assert_equal "bc", $&
assert_equal "abca", $`
assert_equal "", $'
assert_equal "b", $1
assert_equal "c", $2
assert_equal "c", $+
assert_equal "abca", $~.pre_match
assert_equal "", $~.post_match

assert_equal 4, "abcabc".byterindex(/(b)(c)/)
assert_equal "bc", $&
assert_equal "abca", $`

assert_equal ["abca", "bc", ""], "abcabc".rpartition(/(b)(c)/)
assert_equal "bc", $&
assert_equal "abca", $`
assert_equal "c", $+

# a group that did not take part leaves nil behind, and `$+` reaches past it
assert_equal 1, "abc".rindex(/(b)(z)?/)
assert_nil $2
assert_equal "b", $+

# and a search that finds nothing clears all of them, not `$~` alone
"zzz" =~ /(z)/
assert_nil "abc".rindex(/x/)
assert_nil $&
assert_nil $`
assert_nil $'
assert_nil $1
assert_nil $+
Comment thread
coderabbitai[bot] marked this conversation as resolved.
end

assert("String#partition and String#rpartition delegate every non-regexp argument") do
assert_equal ["he", "ll", "o"], "hello".partition("ll")
assert_equal ["hello", "", ""], "hello".partition("z")
Expand Down
Loading