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
1 change: 1 addition & 0 deletions include/mruby.h
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ enum mrb_idx_op_slot {
MRB_IDX_OP_STR_AREF, /* String#[] */
MRB_IDX_OP_ARY_ASET, /* Array#[]= */
MRB_IDX_OP_HASH_ASET, /* Hash#[]= */
MRB_IDX_OP_STR_ASET, /* String#[]= */
MRB_IDX_OP_SLOT_COUNT
};

Expand Down
1 change: 1 addition & 0 deletions include/mruby/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ mrb_value mrb_str_inspect(mrb_state *mrb, mrb_value str);
mrb_bool mrb_str_beg_len(mrb_int str_len, mrb_int *begp, mrb_int *lenp);
mrb_value mrb_str_byte_subseq(mrb_state *mrb, mrb_value str, mrb_int beg, mrb_int len);
mrb_value mrb_str_aref(mrb_state *mrb, mrb_value str, mrb_value idx, mrb_value len);
void mrb_str_aset(mrb_state *mrb, mrb_value str, mrb_value idx, mrb_value len, mrb_value replace);
mrb_bool mrb_strcasecmp_p(const char *s1, mrb_int len1, const char *s2, mrb_int len2);
#define MRB_STR_CASECMP_P(str, lit) \
mrb_strcasecmp_p(RSTRING_PTR(str), RSTRING_LEN(str), lit, sizeof(lit"")-1)
Expand Down
73 changes: 12 additions & 61 deletions mrbgems/mruby-regexp/mrblib/string_regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
# is no Ruby-side helper for a subclass to redefine; an accepted String is
# compiled or quoted into a Regexp here before anything is searched. `split`
# leaves a nil or String pattern to the built-in it aliased and uses the same
# check to reject everything that is not a Regexp. `[]`, `[]=` and `slice!`
# read the real class with `Regexp === pattern` and leave anything else to the
# built-in method they aliased. `=~` rejects a String, which would recurse
# check to reject everything that is not a Regexp. `slice!` reads the real
# class with `Regexp === pattern` and leaves anything else to the built-in
# method it aliased; `[]` and `[]=` are `str_aref()` and `str_aset()` in
# src/regexp.c, where the same test is the argument's own type. `=~` rejects a String, which would recurse
# back into this method, and hands anything that is not a Regexp to the
# argument's own `=~`, as CRuby does.
#
Expand Down Expand Up @@ -36,10 +37,10 @@ class String
# back to the core implementation.
alias __split split

# The write side of the same pair, overridden at the end of this file too.
# `[]=` has a single method table entry, and `slice!` comes from
# mruby-string-ext, which this gem depends on, so it needs its own capture.
alias __aset []=
# `slice!` comes from mruby-string-ext, which this gem depends on, and is
# overridden at the end of this file. The core `[]=` it also reaches is
# captured as `__aset` in src/regexp.c, before the override defined there
# takes the name.
alias __slice_bang slice!

# The four search methods of src/string.c whose regexp form is overridden at
Expand Down Expand Up @@ -379,60 +380,10 @@ def split(pattern = nil, *args)
result
end

# The regexp-aware `[]` and `slice` are `str_aref()` in src/regexp.c, where
# the indexes the core method answers reach it without a Ruby frame in
# between. Everything below stays here, where a block or a loop needs the
# VM anyway.

# Regexp-aware element assignment. Falls back to the C-defined `[]=`
# (aliased as `__aset` above) for every other argument form, and handles a
# regexp here.
#
# `vm_op_setidx()` optimizes Array and Hash only and sends `[]=` for every
# other receiver, so the ordinary `str[i] = repl` has always arrived here and
# paid a Ruby frame on its way to `__aset`. That is why the delegation guard
# is a single `Regexp ===`, before any other work.
def []=(*args)
return __aset(*args) unless Regexp === args[0]
unless args.length == 2 || args.length == 3
raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 2..3)"
end
# A full search and not `match?`, so that the match globals are published
# here including the clearing a failed match does. CRuby searches before
# it checks the receiver for modification, which makes the order
# observable: a frozen receiver still leaves the match behind, and a
# pattern that does not match raises IndexError rather than FrozenError.
# Letting the mutation below be what raises reproduces both.
md = Regexp.__search(args[0], self)
raise IndexError, "regexp not matched" unless md
group = args.length > 2 ? args[1] : 0
if Integer === group
# An index out of range is an error here, not a missing group, and
# CRuby reports it before normalizing a negative one, so the message
# names the index as given, and group 0 is out of the negative end's
# reach. `MatchData#begin` has its own wording for this and rejects
# every negative index, so the check cannot be left to it.
size = md.size
if group >= size || -group >= size
raise IndexError, "index #{group} out of regexp"
end
group += size if group < 0
end
# A String or Symbol reaches `MatchData#begin` as it stands: it resolves
# the name to its group and raises the IndexError CRuby raises for a name
# that resolves to none, with the same message.
beg = md.begin(group)
# A group that exists but did not take part in the match has nothing to
# replace. CRuby names the group's number even when the argument was a
# name; the number is not reachable from Ruby, so the message repeats the
# argument as it was given.
raise IndexError, "regexp group #{group} not matched" unless beg
# `begin` and `end` report character offsets, which is the space the
# two-integer form of `[]=` works in, so a multibyte subject needs no
# further conversion. The replacement is handed over unchecked: the type
# check belongs to the core method, as it does for `sub`.
__aset(beg, md.end(group) - beg, args[-1])
end
# The regexp-aware `[]`, `slice` and `[]=` are `str_aref()` and `str_aset()`
# in src/regexp.c, where the indexes the core methods answer reach them
# without a Ruby frame in between. Everything below stays here, where a
# block or a loop needs the VM anyway.

# Regexp-aware `slice!`. Falls back to the C-defined `slice!` (aliased as
# `__slice_bang` above) for every other argument form.
Expand Down
100 changes: 100 additions & 0 deletions mrbgems/mruby-regexp/src/regexp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,97 @@ str_aref(mrb_state *mrb, mrb_value str)
return md_aref(mrb, md, argc == 1 ? mrb_fixnum_value(0) : a2);
}

/*
* String#[]=(index, replace) / String#[]=(index, length, replace)
* String#[]=(regexp, replace) / String#[]=(regexp, capture, replace)
*
* The write side of `str_aref()` above, and C for the same reason: the core
* `[]=` answers every argument form but a Regexp, so an override written in
* Ruby made every `str[i] = x` in the program pay a Ruby frame and the two
* splat arrays a `*args` method builds on its way back to the core one.
*
* The arguments are read raw rather than with the core method's `"oo|S!"`,
* because the regexp form has to search before it looks at the replacement:
* CRuby's `rb_str_subpat_set()` raises IndexError for a pattern that did not
* match whatever the replacement is, and `$~` is left describing the failure.
* The delegation below therefore repeats what `"oo|S!"` does, in its order:
* the replacement's type is checked before the argument count, which is what
* makes `str[1, 2, 3] = "X"` a TypeError rather than an ArgumentError.
*/
static mrb_value
str_aset(mrb_state *mrb, mrb_value str)
{
const mrb_value *argv;
mrb_int argc;
mrb_get_args(mrb, "*", &argv, &argc);

/* The real type, not `argv[0].is_a?`; see str_aref() above. */
if (argc < 1 || mrb_type(argv[0]) != MRB_TT_CDATA ||
!mrb_obj_is_kind_of(mrb, argv[0], mrb_class_get_id(mrb, MRB_SYM(Regexp)))) {
if (argc >= 3 && !mrb_nil_p(argv[2])) mrb_ensure_string_type(mrb, argv[2]);
if (argc < 2 || argc > 3) mrb_argnum_error(mrb, argc, 2, 3);
mrb_value replace = argv[argc-1];
mrb_str_aset(mrb, str, argv[0], argc == 2 ? mrb_undef_value() : argv[1], replace);
return replace;
}

if (argc < 2 || argc > 3) mrb_argnum_error(mrb, argc, 2, 3);
/* Read out of `argv`, which points into the VM stack, before anything else
runs. */
mrb_value pattern = argv[0];
mrb_value group = argc > 2 ? argv[1] : mrb_fixnum_value(0);
mrb_value replace = argv[argc-1];

/* A full search and not a match test, so that the match globals are
published here including the clearing a failed match does. CRuby searches
before it checks the receiver for modification, which makes the order
observable: a frozen receiver still leaves the match behind, and a pattern
that does not match raises IndexError rather than FrozenError. Letting the
store below be what raises reproduces both. */
mrb_value match = re_search(mrb, pattern, str, 0, FALSE);
if (mrb_nil_p(match)) mrb_raise(mrb, E_INDEX_ERROR, "regexp not matched");
mrb_match_data *md = DATA_GET_PTR(mrb, match, &matchdata_type, mrb_match_data);

mrb_int idx;
if (mrb_obj_is_kind_of(mrb, group, mrb->integer_class)) {
/* An index out of range is an error here, not a missing group, and CRuby
reports it before normalizing a negative one, so the message names the
index as given. Group 0 is out of the negative end's reach. An index
that does not even fit an `mrb_int` reaches no group either. */
mrb_int size = md->num_captures;
if (!mrb_integer_p(group) ||
mrb_integer(group) >= size || mrb_integer(group) <= -size) {
mrb_raisef(mrb, E_INDEX_ERROR, "index %v out of regexp", group);
}
idx = mrb_integer(group);
if (idx < 0) idx += size;
group = mrb_int_value(mrb, idx);
}
else {
/* A String or Symbol resolves to the group it names, and everything else
is read as an index, both the way `MatchData#begin` reads its argument:
a name that resolves to no group raises the IndexError CRuby raises for
it, with the same message. */
idx = matchdata_group_arg(mrb, md, group);
}

/* A group that exists but did not take part in the match has nothing to
replace. CRuby names the group's number even when the argument was a
name; the number is not reachable from Ruby, so the message repeats the
argument as it was given. */
int beg = md->captures[idx * 2];
if (beg < 0) mrb_raisef(mrb, E_INDEX_ERROR, "regexp group %v not matched", group);

/* Character offsets, which is the space the two-integer form of `[]=` works
in, so a multibyte subject needs no further conversion. The replacement is
handed over unchecked: the type check belongs to the core method, as it
does for `sub`. */
mrb_int cbeg = re_byte_to_char(mrb, md->source, beg);
mrb_int clen = re_byte_to_char(mrb, md->source, md->captures[idx * 2 + 1]) - cbeg;
mrb_str_aset(mrb, str, mrb_int_value(mrb, cbeg), mrb_int_value(mrb, clen), replace);
return replace;
}

/* --- Gem init --- */

void
Expand Down Expand Up @@ -1582,6 +1673,15 @@ mrb_mruby_regexp_gem_init(mrb_state *mrb)
go; test/string_index.rb asks both ways round and would catch it. */
mrb_idx_op_rearm(mrb, MRB_IDX_OP_STR_AREF);

/* `String#[]=` the same way, and on the same terms: `str_aset()` answers an
Integer, String or Range index through the same `mrb_str_aset()` the
opcode calls. `slice!` stays in mrblib and reaches the core method under
`__aset`, captured here rather than in mrblib so that it is the core one
and not the override defined next. */
mrb_alias_method(mrb, mrb->string_class, MRB_SYM(__aset), MRB_OPSYM(aset));
mrb_define_method(mrb, mrb->string_class, "[]=", str_aset, MRB_ARGS_ANY());
mrb_idx_op_rearm(mrb, MRB_IDX_OP_STR_ASET);

/* MatchData class */
struct RClass *md = mrb_define_class(mrb, "MatchData", mrb->object_class);
MRB_SET_INSTANCE_TT(md, MRB_TT_CDATA);
Expand Down
59 changes: 59 additions & 0 deletions mrbgems/mruby-regexp/test/string_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -872,3 +872,62 @@ def r.__scan(*args); "PWNED"; end
[0..2, 1...4, -2..-1].each { |r| assert_equal u.slice(r), u[r], "u[#{r.inspect}]" }
end
end

assert("String#[]= answers the same through the opcode and through a send") do
# `s[x] = repl` is answered by `OP_SETIDX` in C, without a method lookup,
# while `s.[]=(x, repl)` reaches `str_aset()` itself. The two are only
# allowed to be different code while they cannot be told apart, which is the
# promise `mrb_idx_op_rearm()` takes from this gem for `[]=` as it does for
# `[]`: `str_aset()` takes the name to reach a Regexp index, and re-arms the
# opcode because it hands every other argument form to the same
# `mrb_str_aset()` the opcode calls. Widening it to another argument type
# the opcode answers would not show up in `s[x] = repl` at all, so ask both
# ways, and compare what each store left behind rather than the replacement
# both forms answer with.
[0, 1, 5, -1, -11, 10].each do |i|
a = "hello world"
b = "hello world"
a[i] = "X"
b.[]=(i, "X")
assert_equal b, a, "s[#{i}] = 'X'"
end
["h", "lo w", "hello world", "", "d"].each do |sub|
a = "hello world"
b = "hello world"
a[sub] = "X"
b.[]=(sub, "X")
assert_equal b, a, "s[#{sub.inspect}] = 'X'"
end
[0..3, 1...3, -3..-1, 0..-1, 5..99, 3..1].each do |r|
a = "hello world"
b = "hello world"
a[r] = "X"
b.[]=(r, "X")
assert_equal b, a, "s[#{r.inspect}] = 'X'"
end
# The forms neither answers from the opcode: a Regexp index reaches
# `str_aset()` through the send the opcode falls back to, and an index that
# matches nothing raises the same error either way.
a = "hello world"
b = "hello world"
a[/o.w/] = "X"
b.[]=(/o.w/, "X")
assert_equal b, a
assert_raise(IndexError) { "hello world"[99] = "X" }
assert_raise(IndexError) { "hello world".[]=(99, "X") }
assert_raise(IndexError) { "hello world"["zz"] = "X" }
assert_raise(IndexError) { "hello world".[]=("zz", "X") }
# A receiver whose characters are not one byte each.
if "あ".length == 1
a = "こんにちは"
b = "こんにちは"
a[1] = "X"
b.[]=(1, "X")
assert_equal b, a
a = "こんにちは"
b = "こんにちは"
a[1..3] = "X"
b.[]=(1..3, "X")
assert_equal b, a
end
end
1 change: 1 addition & 0 deletions src/class.c
Original file line number Diff line number Diff line change
Expand Up @@ -2919,6 +2919,7 @@ mrb_idx_op_update(mrb_state *mrb, mrb_sym mid)
if (mid == 0 || mid == MRB_OPSYM(aset)) {
idx_op_refresh(mrb, MRB_IDX_OP_ARY_ASET);
idx_op_refresh(mrb, MRB_IDX_OP_HASH_ASET);
idx_op_refresh(mrb, MRB_IDX_OP_STR_ASET);
}
}

Expand Down
12 changes: 11 additions & 1 deletion src/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -2009,7 +2009,17 @@ str_escape(mrb_state *mrb, mrb_value str, mrb_bool inspect)
return result;
}

static void
/*
* @param mrb The mruby state.
* @param str The receiver, modified in place.
* @param idx The index or range, read as `mrb_str_aref()` reads it.
* @param alen An optional length (if `idx` is an integer), or undef.
* @param replace The replacement, which has to be a String already: anything
* else raises TypeError, before the range is looked at.
*
* Implements string element assignment (e.g. `str[idx] = replace`).
*/
void
mrb_str_aset(mrb_state *mrb, mrb_value str, mrb_value idx, mrb_value alen, mrb_value replace)
{
mrb_int beg, len, charlen;
Expand Down
27 changes: 27 additions & 0 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -2192,6 +2192,33 @@ vm_op_setidx(mrb_state *mrb, uint32_t a, mrb_sym *midp)
mrb_gc_arena_restore(mrb, ai);
}
return VM_NEXT;
case MRB_TT_STRING:
/* optimize only for String itself; see vm_op_getidx() */
if (mrb_obj_ptr(va)->c != mrb->idx_class[MRB_IDX_OP_STR_ASET]) goto setidx_fallback;
/* A replacement that is not a String is a TypeError rather than a store,
and the method is where it is raised: leaving it to the send keeps the
`String#[]=` frame the backtrace has always shown for it. */
if (!mrb_string_p(vc)) goto setidx_fallback;
switch (mrb_type(vb)) {
case MRB_TT_INTEGER:
case MRB_TT_STRING:
case MRB_TT_RANGE:
{
/* mrb_str_aset() allocates, and an inline opcode never runs the cfunc
epilogue that would shrink the arena, so save and restore it here.
As in the String branch of vm_op_getidx(), `ci` needs no refresh:
none of the three index types reaches a conversion that runs Ruby
code, so the call cannot move the stack. */
int ai = mrb_gc_arena_save(mrb);
mrb_str_aset(mrb, va, vb, mrb_undef_value(), vc);
regs[a] = vc;
mrb_gc_arena_restore(mrb, ai);
}
return VM_NEXT;
default:
break;
}
goto setidx_fallback;
default:
setidx_fallback:
SET_NIL_VALUE(regs[a+3]);
Expand Down
Loading
Loading