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
25 changes: 25 additions & 0 deletions include/mruby.h
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,19 @@ struct mrb_jmpbuf;

typedef void (*mrb_atexit_func)(mrb_state*);

/**
* Slots of `mrb_state.idx_class`, one per builtin the inline index opcodes
* (`OP_GETIDX`, `OP_GETIDX0`, `OP_SETIDX`) reimplement in C.
*/
enum mrb_idx_op_slot {
MRB_IDX_OP_ARY_AREF, /* Array#[] */
MRB_IDX_OP_HASH_AREF, /* Hash#[] */
MRB_IDX_OP_STR_AREF, /* String#[] */
MRB_IDX_OP_ARY_ASET, /* Array#[]= */
MRB_IDX_OP_HASH_ASET, /* Hash#[]= */
MRB_IDX_OP_SLOT_COUNT
};

#ifdef MRB_USE_TASK_SCHEDULER
struct mrb_task;

Expand Down Expand Up @@ -396,6 +409,18 @@ struct mrb_state {
#endif
uint16_t atexit_stack_len;

/* The inline index opcodes answer `[]` and `[]=` from C for a receiver whose
class is exactly Array, Hash or String, which would bypass a redefinition
installed on those classes themselves. Each slot holds the core class
while the name still resolves to the builtin recorded in `idx_builtin`,
and NULL once it does not, so the class-pointer test the opcodes already
perform rejects a redefined operator at no extra cost. NULL is safe as
the disabled value because no live object has a NULL class pointer.
Armed by mrb_idx_op_init(), rechecked by mrb_idx_op_update(). Placed at
the end of the struct so that adding them moves no existing field. */
struct RClass *idx_class[MRB_IDX_OP_SLOT_COUNT];
mrb_method_t idx_builtin[MRB_IDX_OP_SLOT_COUNT];

#ifdef MRB_USE_TASK_SCHEDULER
mrb_task_state task; /* Task scheduler state */
#endif
Expand Down
4 changes: 4 additions & 0 deletions include/mruby/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ size_t mrb_class_mt_memsize(mrb_state*, struct RClass*);
mrb_value mrb_obj_extend(mrb_state*, mrb_value obj);
#endif

/* inline index opcode guards (class.c); see `idx_class` in `struct mrb_state` */
void mrb_idx_op_init(mrb_state *mrb);
void mrb_idx_op_update(mrb_state *mrb, mrb_sym mid);

mrb_value mrb_obj_equal_m(mrb_state *mrb, mrb_value);

/* debug */
Expand Down
24 changes: 12 additions & 12 deletions mrbgems/mruby-regexp/mrblib/string_regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -388,14 +388,15 @@ def split(pattern = nil, *args)
# (aliased as `__aref` above) for every other argument form, and handles a
# regexp here.
#
# `vm_op_getidx()` answers `str[Integer]`, `str[String]` and `str[Range]`
# from C without consulting the method table, so those three keep bypassing
# this override. They are exactly the forms it would have handed back to
# `__aref` unchanged, so they cost nothing and behave as before, while a
# regexp index leaves the opcode through its fallback and arrives here as an
# ordinary send. `str[i, len]` and every `slice` call are not opcode
# receivers and do arrive here, paying a Ruby frame on their way to
# `__aref`.
# Defining this disables the String branch of `vm_op_getidx()` and
# `vm_op_getidx0()` for the whole VM: those opcodes answer `str[Integer]`,
# `str[String]` and `str[Range]` from C only while `String#[]` is the builtin
# they reimplement, and installing this method makes it no longer so. Every
# `str[i]` in the program therefore arrives here, pays a Ruby frame and two
# splat arrays, and reaches `__aref` with the same result as before. That is
# the price of the regexp forms being reachable at all; the alternative,
# letting the opcode keep answering, is the redefinition being silently
# ignored for those three argument types.
def [](*args)
# Before any argument inspection, so that the non-regexp forms keep the
# arity check `mrb_get_args()` does. With no arguments at all `args[0]`
Expand Down Expand Up @@ -432,10 +433,9 @@ def [](*args)
# regexp here.
#
# `vm_op_setidx()` optimizes Array and Hash only and sends `[]=` for every
# other receiver, so unlike the read side there is no opcode keeping the
# ordinary `str[i] = repl` off this override: it pays a Ruby frame on its
# way to `__aset`. That is why the delegation guard is a single
# `Regexp ===`, before any other work.
# 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
Expand Down
101 changes: 99 additions & 2 deletions src/class.c
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,10 @@ mrb_define_method_raw(mrb_state *mrb, struct RClass *c, mrb_sym mid, mrb_method_
}
}
mt_put(mrb, h, mid, flags, ptr);
if (!mrb->bootstrapping) mc_clear_by_id(mrb, mid);
if (!mrb->bootstrapping) {
mc_clear_by_id(mrb, mid);
mrb_idx_op_update(mrb, mid);
}
if (modfunc) {
/* module_function scope: also define a public method on the singleton
class, so the module method (M.foo) mirrors the private instance one */
Expand Down Expand Up @@ -2106,7 +2109,12 @@ include_module_at(mrb_state *mrb, struct RClass *c, struct RClass *ins_pos, stru
skip:
m = m->super;
}
if (!mrb->bootstrapping) mrb_method_cache_clear(mrb);
if (!mrb->bootstrapping) {
mrb_method_cache_clear(mrb);
/* An included or prepended module can carry both operators, and it is not
one method name that changed, so recheck every slot. */
mrb_idx_op_update(mrb, 0);
}
return 0;
}

Expand Down Expand Up @@ -2486,6 +2494,7 @@ mrb_mod_visibility(mrb_state *mrb, mrb_value mod, int vis)
}
mt_put(mrb, h, mid, m.flags, ptr);
mc_clear_by_id(mrb, mid);
mrb_idx_op_update(mrb, mid);
}
}
}
Expand Down Expand Up @@ -2798,6 +2807,93 @@ mc_clear_by_id(mrb_state *mrb, mrb_sym id)
}
#endif // MRB_NO_METHOD_CACHE

/*
* Guards for the inline index opcodes.
*
* `OP_GETIDX`, `OP_GETIDX0` and `OP_SETIDX` implement `[]` and `[]=` for an
* Array, Hash or String receiver in C, without a method lookup. They may only
* do so while those classes still carry the builtin the opcode reimplements,
* so each (class, operator) pair keeps a slot in `mrb->idx_class` that holds
* the class while that is true and NULL once it is not. The opcodes compare
* the receiver's class against the slot instead of against the core class, so
* a disarmed slot sends the operator like any other method and the check costs
* nothing while nothing is redefined.
*
* Validity is the resolved method itself, not merely "was `[]` assigned to":
* a slot is armed while `mid` resolves, from the core class, to exactly the
* `mrb_method_t` recorded at startup. That covers `def`, `alias_method`,
* `undef_method`, `remove_method`, visibility changes and `prepend` without
* enumerating them, re-arms when an override is aliased back away, and stays
* armed when an unrelated module is included.
*/

static struct RClass*
idx_op_class(mrb_state *mrb, int slot)
{
switch (slot) {
case MRB_IDX_OP_ARY_AREF: case MRB_IDX_OP_ARY_ASET: return mrb->array_class;
case MRB_IDX_OP_HASH_AREF: case MRB_IDX_OP_HASH_ASET: return mrb->hash_class;
default: return mrb->string_class;
}
}

static mrb_sym
idx_op_mid(int slot)
{
return slot < MRB_IDX_OP_ARY_ASET ? MRB_OPSYM(aref) : MRB_OPSYM(aset);
}

static void
idx_op_refresh(mrb_state *mrb, int slot)
{
/* A slot that startup never armed (the builtin was already gone, or the
state failed to initialize) stays off; there is nothing to compare to. */
if (mrb->idx_builtin[slot].as.func == NULL) return;

struct RClass *c = idx_op_class(mrb, slot);
struct RClass *base = c;
mrb_method_t m = mrb_vm_find_method(mrb, c, &c, idx_op_mid(slot));
mrb->idx_class[slot] =
(m.flags == mrb->idx_builtin[slot].flags &&
m.as.func == mrb->idx_builtin[slot].as.func) ? base : NULL;
}

/* Records the builtin `[]` / `[]=` of each core class and arms its slot.
Called once, after core initialization has installed them. */
void
mrb_idx_op_init(mrb_state *mrb)
{
for (int slot = 0; slot < MRB_IDX_OP_SLOT_COUNT; slot++) {
struct RClass *c = idx_op_class(mrb, slot);
struct RClass *base = c;
mrb_method_t m = mrb_vm_find_method(mrb, c, &c, idx_op_mid(slot));
/* Only a C function is the builtin an index opcode reimplements. Anything
else means the operator was already replaced before the state was handed
out, and the opcode must not answer for it. */
if (MRB_METHOD_UNDEF_P(m) || !MRB_METHOD_FUNC_P(m)) continue;
mrb->idx_builtin[slot] = m;
mrb->idx_class[slot] = base;
}
}

/* Rechecks the slots that `mid` can affect. Call after any change to a method
table that could change what `[]` or `[]=` resolves to; pass 0 for `mid` when
the change is not tied to one name, as module inclusion is not. */
void
mrb_idx_op_update(mrb_state *mrb, mrb_sym mid)
{
if (mrb->bootstrapping) return;
if (mid == 0 || mid == MRB_OPSYM(aref)) {
idx_op_refresh(mrb, MRB_IDX_OP_ARY_AREF);
idx_op_refresh(mrb, MRB_IDX_OP_HASH_AREF);
idx_op_refresh(mrb, MRB_IDX_OP_STR_AREF);
}
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);
}
}

mrb_method_t
mrb_vm_find_method(mrb_state *mrb, struct RClass *c, struct RClass **cp, mrb_sym mid)
{
Expand Down Expand Up @@ -3780,6 +3876,7 @@ mrb_remove_method(mrb_state *mrb, struct RClass *c0, mrb_sym mid)
mrb_name_error(mrb, mid, "method '%n' not defined in %C", mid, c);
}
mc_clear_by_id(mrb, mid);
mrb_idx_op_update(mrb, mid);
if (c0->tt == MRB_TT_SCLASS) {
mrb_sym cb = MRB_SYM(singleton_method_removed);
mrb_value recv = mrb_iv_get(mrb, mrb_obj_value(c0), MRB_SYM(__attached__));
Expand Down
3 changes: 3 additions & 0 deletions src/state.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ mrb_open_core(void)

mrb_method_cache_clear(mrb);
mrb->bootstrapping = FALSE;
/* After bootstrapping, so that a core `[]` replaced from mrblib is recorded
as replaced rather than as the builtin. */
mrb_idx_op_init(mrb);

return mrb;
}
Expand Down
30 changes: 16 additions & 14 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -2043,8 +2043,10 @@ vm_op_getidx(mrb_state *mrb, uint32_t a, mrb_sym *midp)
/* Array case is most common - check first with branch hint */
if (mrb_likely(tt == MRB_TT_ARRAY)) {
struct RArray *ary = mrb_ary_ptr(va);
/* optimize only for Array class; subclasses/singleton may override [] */
if (mrb_unlikely(ary->c != mrb->array_class)) goto getidx_fallback;
/* optimize only for Array itself; a subclass or singleton may override [],
and mrb->idx_class[] is NULL while Array#[] is overridden, so the same
comparison rejects that too */
if (mrb_unlikely(ary->c != mrb->idx_class[MRB_IDX_OP_ARY_AREF])) goto getidx_fallback;
if (mrb_likely(mrb_integer_p(vb))) {
mrb_int idx = mrb_integer(vb);
mrb_int len;
Expand Down Expand Up @@ -2075,8 +2077,8 @@ vm_op_getidx(mrb_state *mrb, uint32_t a, mrb_sym *midp)
goto getidx_fallback;
}
else if (tt == MRB_TT_HASH) {
/* optimize only for Hash class; subclasses/singleton may override [] */
if (mrb_obj_ptr(va)->c != mrb->hash_class) goto getidx_fallback;
/* optimize only for Hash itself; see the Array branch above */
if (mrb_obj_ptr(va)->c != mrb->idx_class[MRB_IDX_OP_HASH_AREF]) goto getidx_fallback;
int ai = mrb_gc_arena_save(mrb);
va = mrb_hash_get(mrb, va, vb);
ci = mrb->c->ci;
Expand All @@ -2085,8 +2087,8 @@ vm_op_getidx(mrb_state *mrb, uint32_t a, 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(va)->c != mrb->string_class) goto getidx_fallback;
/* optimize only for String itself; see the Array branch above */
if (mrb_obj_ptr(va)->c != mrb->idx_class[MRB_IDX_OP_STR_AREF]) goto getidx_fallback;
switch (mrb_type(vb)) {
case MRB_TT_INTEGER:
case MRB_TT_STRING:
Expand Down Expand Up @@ -2116,7 +2118,7 @@ vm_op_getidx0(mrb_state *mrb, uint32_t a, uint16_t b, mrb_sym *midp)

if (mrb_likely(tt == MRB_TT_ARRAY)) {
struct RArray *ary = mrb_ary_ptr(recv);
if (mrb_unlikely(ary->c != mrb->array_class)) goto getidx0_fallback;
if (mrb_unlikely(ary->c != mrb->idx_class[MRB_IDX_OP_ARY_AREF])) goto getidx0_fallback;
#ifndef MRB_ARY_NO_EMBED
if (ARY_EMBED_P(ary)) {
regs[a] = ARY_EMBED_LEN(ary) > 0 ? ary->as.ary[0] : mrb_nil_value();
Expand All @@ -2129,7 +2131,7 @@ vm_op_getidx0(mrb_state *mrb, uint32_t a, uint16_t b, mrb_sym *midp)
return VM_NEXT;
}
else if (tt == MRB_TT_HASH) {
if (mrb_obj_ptr(recv)->c != mrb->hash_class) goto getidx0_fallback;
if (mrb_obj_ptr(recv)->c != mrb->idx_class[MRB_IDX_OP_HASH_AREF]) goto getidx0_fallback;
{
/* same as the Hash branch of vm_op_getidx(): mrb_hash_get() can run a
default proc and move the stack, so take the result first and store
Expand All @@ -2144,8 +2146,8 @@ 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;
/* optimize only for String itself; see vm_op_getidx() */
if (mrb_obj_ptr(recv)->c != mrb->idx_class[MRB_IDX_OP_STR_AREF]) 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.
Expand All @@ -2172,16 +2174,16 @@ vm_op_setidx(mrb_state *mrb, uint32_t a, mrb_sym *midp)
mrb_value va = regs[a], vb = regs[a+1], vc = regs[a+2];
switch (mrb_type(va)) {
case MRB_TT_ARRAY:
/* optimize only for Array class; subclasses/singleton may override []= */
if (mrb_obj_ptr(va)->c != mrb->array_class) goto setidx_fallback;
/* optimize only for Array itself; see vm_op_getidx() */
if (mrb_obj_ptr(va)->c != mrb->idx_class[MRB_IDX_OP_ARY_ASET]) goto setidx_fallback;
if (!mrb_integer_p(vb)) goto setidx_fallback;
mrb_ary_set(mrb, va, mrb_integer(vb), vc);
ci = mrb->c->ci;
regs[a] = vc;
return VM_NEXT;
case MRB_TT_HASH:
/* optimize only for Hash class; subclasses/singleton may override []= */
if (mrb_obj_ptr(va)->c != mrb->hash_class) goto setidx_fallback;
/* optimize only for Hash itself; see vm_op_getidx() */
if (mrb_obj_ptr(va)->c != mrb->idx_class[MRB_IDX_OP_HASH_ASET]) goto setidx_fallback;
{
int ai = mrb_gc_arena_save(mrb);
mrb_hash_set(mrb, va, vb, vc);
Expand Down
65 changes: 65 additions & 0 deletions test/t/array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,71 @@ class SubArray < Array
assert_equal("b", a[1.1])
end

assert('Array#[] redefined on Array itself reaches the redefinition') do
# `OP_GETIDX` answers `a[1]` from C and `OP_GETIDX0` answers `a[0]` the same
# way whenever the receiver's class is exactly `Array`, which they may only
# do while `Array#[]` is still the builtin they reimplement. Both test the
# receiver against `mrb->idx_class[]`, which the method table drops the
# moment `Array#[]` is replaced, so a redefinition installed on `Array`
# itself is honored as in CRuby. The results are read before the operator is
# put back because the assertions themselves index arrays.
Array.class_eval do
alias_method :__aref_before_test, :[]
def [](*args)
:overridden
end
end
begin
a = [7, 8]
sub = Class.new(Array).new
got0 = a[0]
got1 = a[1]
got_sub = sub[0]
ensure
Array.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
assert_equal :overridden, got0
assert_equal :overridden, got1
assert_equal :overridden, got_sub
# Aliasing the original implementation back re-arms the opcodes.
assert_equal 7, [7, 8][0]
assert_equal 8, [7, 8][1]
end

assert('Array#[]= redefined on Array itself reaches the redefinition') do
# `OP_SETIDX` answers `a[0] = 9` from C on the same terms; see the `[]` test
# above. A redefinition that stores nothing makes the difference visible in
# the receiver as well as in the return value.
Array.class_eval do
alias_method :__aset_before_test, :[]=
def []=(*args)
$aset_redefinition_args = args
end
end
begin
a = [7, 8]
a[0] = 9
seen = $aset_redefinition_args
untouched = a
ensure
Array.class_eval do
alias_method :[]=, :__aset_before_test
remove_method :__aset_before_test if respond_to?(:remove_method, true)
end
$aset_redefinition_args = nil
end
assert_equal [0, 9], seen
assert_equal [7, 8], untouched
a = [7, 8]
a[0] = 9
assert_equal [9, 8], a
end

assert('Array#[]=', '15.2.12.5.5') do
a = Array.new
assert_raise(ArgumentError) do
Expand Down
Loading
Loading