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
15 changes: 15 additions & 0 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -2143,6 +2143,21 @@ vm_op_getidx0(mrb_state *mrb, uint32_t a, uint16_t b, mrb_sym *midp)
}
return VM_NEXT;
}
else if (tt == MRB_TT_STRING) {
/* optimize only for String class; subclasses/singleton may override [] */
if (mrb_obj_ptr(recv)->c != mrb->string_class) goto getidx0_fallback;
{
/* mrb_str_aref() allocates, and an inline opcode never runs the cfunc
epilogue that would shrink the arena, so save and restore it here.
Unlike the Hash branch above, `ci` needs no refresh: this call cannot
run Ruby code and so cannot move the stack. */
int ai = mrb_gc_arena_save(mrb);
mrb_value val = mrb_str_aref(mrb, recv, mrb_fixnum_value(0), mrb_undef_value());
regs[a] = val;
mrb_gc_arena_restore(mrb, ai);
}
return VM_NEXT;
}
getidx0_fallback:
regs[a] = recv;
SET_FIXNUM_VALUE(regs[a+1], 0);
Expand Down
12 changes: 12 additions & 0 deletions test/t/gc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,18 @@
assert_operator GC.stat[:live] - base, :<, 5000
end

assert('OP_GETIDX0 does not retain a String result in the GC arena') do
s = "hello"
GC.start
base = GC.stat[:live]
i = 0
while i < 20000
s[0]
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
end

assert('OP_GETIDX0 does not retain a Hash default in the GC arena') do
h = Hash.new { Object.new }
GC.start
Expand Down
28 changes: 28 additions & 0 deletions test/t/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,34 @@
assert_equal 'xyz', k2
end

assert('String#[] redefined on String itself is bypassed by the index opcodes') do
# `OP_GETIDX` answers `s[1]` from C, and `OP_GETIDX0` answers `s[0]` the same
# way, whenever the receiver's class is exactly `String`. Both therefore
# bypass a redefinition installed on `String` itself, the same way the Array
# and Hash branches of those opcodes do. A subclass receiver fails the class
# guard and keeps reaching the redefinition.
String.class_eval do
alias_method :__aref_before_test, :[]
def [](*args)
:overridden
end
end
begin
s = 'hello'
sub = Class.new(String).new('hello')
assert_equal 'h', s[0]
assert_equal 'e', s[1]
assert_equal :overridden, sub[0]
ensure
String.class_eval do
alias_method :[], :__aref_before_test
# `remove_method` comes from mruby-metaprog, which the core test build
# does not have; the saved alias is harmless where it is missing.
remove_method :__aref_before_test if respond_to?(:remove_method, true)
end
end
end

assert('String#[]=') do
# length of args is 1
a = 'abc'
Expand Down
Loading