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
100 changes: 99 additions & 1 deletion internal/vm/refinement.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,50 @@ func (vm *VM) registerRefinements() {
// Refinement#import_methods (see refinement_import.go).
vm.installRefinementImport()

// MRI UNDEFINES the three module hooks on Refinement, so a refinement is
// never asked to graft itself into a class: Init_eval calls
// rb_undef_method(rb_cRefinement, …) for append_features, prepend_features
// and extend_object (ruby/ruby v3_4_0 eval.c:2140-2142). An `undefined`
// entry rather than a deletion is what keeps the name out of
// Refinement.private_instance_methods(true) while Module keeps its own — the
// same shape bootstrap already uses to undefine them on Class.
for _, n := range []string{"append_features", "prepend_features", "extend_object"} {
vm.cRefinement.methods[n] = &Method{name: n, owner: vm.cRefinement, undefined: true}
}

// Module#include / Module#prepend refuse outright when the RECEIVER is a
// refinement: rb_mod_include raises TypeError "Refinement#include has been
// removed" and rb_mod_prepend "Refinement#prepend has been removed" before
// they even look at their arguments (ruby/ruby v3_4_0 eval.c:1212 and
// eval.c:1266). Refinement overriding Module's methods is where that
// receiver check lands for a Refinement receiver.
for _, n := range []string{"include", "prepend"} {
msg := "Refinement#" + n + " has been removed"
vm.cRefinement.define(n, func(_ *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value {
raise("TypeError", "%s", msg)
return object.NilV
})
}

// Refinement#ancestors stops AT the refined class. A refinement's superclass
// is the class it refines (see refinementFor), which is what lets `alias`,
// #instance_methods and #method_defined? inside a refine block reach the
// refined class's methods — but MRI does not report that link in #ancestors:
// rb_mod_ancestors breaks out of the walk when it reaches the refined class
// (ruby/ruby v3_4_0 class.c:1584-1597), so `refine(Array) { ancestors }`
// answers just [#<refinement:Array@…>].
vm.cRefinement.define("ancestors", func(vm *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
r := self.(*RClass)
arr := object.NewArray()
for _, k := range vm.ancestors(r) {
if k == r.refinedClass {
break
}
arr.Elems = append(arr.Elems, k)
}
return arr
})

// Module#refinements returns this module's own refinement modules (not those
// of included modules).
vm.cModule.define("refinements", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
Expand All @@ -80,6 +124,7 @@ func (vm *VM) registerRefinements() {
}
scope := vm.refinementScope(self)
vm.anyRefinements = true
vm.adoptRefinementCref(scope)
// Record in call order, de-duplicating so a repeated using is a no-op for
// used_modules ordering while still being harmless.
for _, m := range scope.usedModules {
Expand All @@ -95,6 +140,22 @@ func (vm *VM) registerRefinements() {
vm.cObject.methods["using"] = &Method{name: "using", native: usingFn, owner: vm.cObject, vis: visPrivate}
vm.cModule.define("using", usingFn)

// refine and using are PRIVATE instance methods of Module — Init_eval uses
// rb_define_private_method for both (ruby/ruby v3_4_0 eval.c:2130-2131) — so
// they are callable only with an implicit receiver, from inside the body
// they affect. Witnessed on 4.0.5:
// `Module.private_instance_methods(true).include?(:refine)` is true.
for _, n := range []string{"refine", "using"} {
vm.cModule.methods[n].vis = visPrivate
}
// Class UNDEFINES refine (rb_undef_method(rb_cClass, "refine"), eval.c:2137):
// a refinement is held by a module, never by a class, and MRI answers
// NoMethodError rather than scoping one to a class body. An `undefined`
// entry (as for Class#module_function) is what keeps the name out of
// Class.private_instance_methods(true) while Module keeps its own.
vm.cClass.methods["refine"] = &Method{
name: "refine", owner: vm.cClass, vis: visPrivate, undefined: true}

// Module#used_modules lists the modules activated by `using` reachable from
// this scope. Called as an instance method, self is the scope module.
vm.cModule.define("used_modules", func(vm *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
Expand All @@ -119,7 +180,17 @@ func (holder *RClass) refinementFor(target *RClass) *RClass {
if r, ok := holder.refinements[target]; ok {
return r
}
r := newClass("", nil)
// The refinement's SUPERCLASS is the class it refines: MRI's rb_mod_refine
// does `RCLASS_SET_SUPER(refinement, refinement_superclass(klass))`
// (ruby/ruby v3_4_0 eval.c:1497-1500). That link is what makes the refined
// class's methods reachable from inside the refine block, so
// `refine(Array) { alias :orig_count :count }` finds Array#count and
// `refine(Array) { instance_methods }` answers Array's list. It does NOT
// widen dispatch: refinedMethod resolves through lookupOwnOrIncluded, which
// walks own + included modules and never the superclass, so a refinement
// still only answers for methods actually written in it. #ancestors hides
// the link, exactly as MRI's does (see the Refinement#ancestors override).
r := newClass("", target)
r.isModule = true
r.isRefinement = true
r.refinedClass = target
Expand All @@ -142,6 +213,33 @@ func (vm *VM) refinementScope(self object.Value) *RClass {
return vm.cObject
}

// adoptRefinementCref points the CALLING frame's cref entry at the scope that
// just recorded a `using`, so a native called from that frame reads the scope
// the activation landed on.
//
// MRI's `using` does not merely record the module: it REPLACES the calling
// frame's cref with a duplicate and hangs the refinements off that duplicate —
// rb_vm_cref_replace_with_duplicated_cref (ruby/ruby v3_4_0 vm.c:1931) →
// vm_cref_replace_with_duplicated_cref (vm_insnhelper.c:912), called from both
// mod_using (eval.c:1547) and top_using (eval.c:1890). Every later reader —
// rb_mod_s_used_refinements, rb_method_entry_with_refinements — then finds them
// by walking that frame's cref chain.
//
// rbgo keeps the activation on the scope OBJECT (refinementScope) rather than
// on a per-frame cref record, and a frame's cref entry is its TEXTUAL lexical
// scope, which is not the same class inside a Module.new / Class.new /
// class_eval body: there the block's cref is where the block was written while
// `using` records on the body's definee. Pointing the entry at the scope is the
// narrowest equivalent of MRI's replacement — the entry is popped with the
// frame, so it activates nothing outside the body that called `using`, and for
// a class/module body or the top level (where the two already agree) it is a
// no-op.
func (vm *VM) adoptRefinementCref(scope *RClass) {
if n := len(vm.frameCrefs); n > 0 && vm.frameCrefs[n-1] != scope {
vm.frameCrefs[n-1] = scope
}
}

// inMethodScope reports whether the innermost interpreted frame (the caller of a
// native such as `using`) is a method body rather than a class/module/top-level
// body or block — those push a non-empty frame name, methods a non-empty one.
Expand Down
209 changes: 209 additions & 0 deletions internal/vm/wave31_refinement_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

import "testing"

// TestRefinementModuleHooksAreUndefined pins the three module hooks MRI
// UNDEFINES on Refinement (ruby/ruby v3_4_0 eval.c:2140-2142), which is what
// keeps Refinement.private_instance_methods(true) free of them while Module
// still carries its own.
func TestRefinementModuleHooksAreUndefined(t *testing.T) {
got := runSrc(t, `
p Refinement.private_instance_methods(true).include?(:append_features)
p Refinement.private_instance_methods(true).include?(:prepend_features)
p Refinement.private_instance_methods(true).include?(:extend_object)
p Module.private_instance_methods(true).include?(:append_features)
`)
want := "false\nfalse\nfalse\ntrue"
if got != want {
t.Errorf("Refinement hook visibility =\n%s\nwant\n%s", got, want)
}
}

// TestRefinementIncludePrependRemoved covers the two TypeError branches added
// for a refinement RECEIVER: rb_mod_include / rb_mod_prepend raise before
// looking at their arguments (ruby/ruby v3_4_0 eval.c:1212 and eval.c:1266).
func TestRefinementIncludePrependRemoved(t *testing.T) {
cases := []struct{ name, src, want string }{
{
"include",
`Module.new { refine(String) { include Module.new } }`,
"TypeError: Refinement#include has been removed",
},
{
"prepend",
`Module.new { refine(String) { prepend Module.new } }`,
"TypeError: Refinement#prepend has been removed",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := refErr(t, c.src); got != c.want {
t.Errorf("%s: got %q, want %q", c.src, got, c.want)
}
})
}
}

// TestRefinementSuperIsTheRefinedClass covers the superclass link rb_mod_refine
// installs (ruby/ruby v3_4_0 eval.c:1497-1500): the refined class's methods are
// reachable from inside the refine block, so `alias`, #instance_methods and
// #method_defined? answer as MRI's do.
func TestRefinementSuperIsTheRefinedClass(t *testing.T) {
got := runSrc(t, `
Module.new do
refine Array do
p method_defined?(:size)
p instance_methods(false)
p instance_method(:size).owner == Array
alias :aliased_size :size
end
end
`)
want := "true\n[]\ntrue"
if got != want {
t.Errorf("refine block reflection =\n%s\nwant\n%s", got, want)
}
}

// TestRefinementAncestorsStopsAtRefinedClass covers both arms of the
// Refinement#ancestors walk: the refinement itself is reported, and the walk
// BREAKS at the refined class instead of continuing into it — which is how
// MRI's rb_mod_ancestors hides the superclass link (v3_4_0 class.c:1584-1597).
func TestRefinementAncestorsStopsAtRefinedClass(t *testing.T) {
got := runSrc(t, `
Module.new do
refine Array do
a = ancestors
p a.size
p a.first.equal?(self)
p a.include?(Array)
end
end
`)
want := "1\ntrue\nfalse"
if got != want {
t.Errorf("Refinement#ancestors =\n%s\nwant\n%s", got, want)
}
}

// TestRefinementSuperDoesNotWidenDispatch is the guard on the superclass link:
// a refinement that reaches the refined class for REFLECTION must not start
// answering sends for methods it does not define. refinedMethod resolves
// through lookupOwnOrIncluded, which never walks the superclass, so an
// unrefined name still reaches the receiver's own definition — and a class the
// refinement does not name is untouched.
func TestRefinementSuperDoesNotWidenDispatch(t *testing.T) {
got := runSrc(t, `
k = Class.new do
def foo; "plain foo"; end
def bar; "plain bar"; end
end
r = Module.new do
refine k do
def foo; "refined foo"; end
end
end
using r
o = k.new
p o.foo
p o.bar
p o.frozen?
p "str".size
`)
want := `"refined foo"
"plain bar"
false
3`
if got != want {
t.Errorf("dispatch under a refinement =\n%s\nwant\n%s", got, want)
}
}

// TestUsingAdoptsTheFrameCref covers both arms of adoptRefinementCref. Inside a
// Module.new body the frame's cref is the block's TEXTUAL scope while `using`
// records on the body's definee, so the entry is repointed and
// Module.used_refinements — a native that reads the caller's cref — sees the
// activation. At the top level the two already agree, so nothing is written and
// the same read answers from the unchanged entry.
func TestUsingAdoptsTheFrameCref(t *testing.T) {
got := runSrc(t, `
r = Module.new do
refine Integer do
def foo; "foo"; end
end
end

# Module.new body: scope and frame cref differ (the write arm).
Module.new do
p Module.used_refinements.size
using r
p Module.used_refinements.size
p Module.used_refinements.first.target
end

# Top level: the two already agree (the no-write arm).
p Module.used_refinements.size
using r
p Module.used_refinements.size
`)
want := "0\n1\nInteger\n0\n1"
if got != want {
t.Errorf("used_refinements around using =\n%s\nwant\n%s", got, want)
}
}

// TestUsingActivationIsPoppedWithTheFrame is the narrowness guard on the cref
// adoption: the entry adoptRefinementCref writes is popped when the body that
// called `using` returns, so a LATER Module.new body — one that never called
// `using` — reads no refinement at all.
func TestUsingActivationIsPoppedWithTheFrame(t *testing.T) {
got := runSrc(t, `
r = Module.new do
refine Integer do
def foo; "foo"; end
end
end
Module.new do
using r
p Module.used_refinements.size
end
Module.new do
p Module.used_refinements.size
end
`)
want := "1\n0"
if got != want {
t.Errorf("used_refinements after the using body returned =\n%s\nwant\n%s", got, want)
}
}

// TestRefineAndUsingAreModulePrivate pins the declaration MRI gives them:
// rb_define_private_method for Module#refine and Module#using
// (ruby/ruby v3_4_0 eval.c:2130-2131), so both are callable only with an
// implicit receiver — and rb_undef_method(rb_cClass, "refine") (eval.c:2137),
// so a class cannot hold a refinement at all.
func TestRefineAndUsingAreModulePrivate(t *testing.T) {
got := runSrc(t, `
p Module.private_instance_methods(true).include?(:refine)
p Module.private_instance_methods(true).include?(:using)
p Class.private_instance_methods(true).include?(:refine)
begin
Module.new.public_send(:refine, String) { }
rescue NoMethodError => e
p :refine_is_private
end
begin
Class.new.send(:refine, String) { }
rescue NoMethodError => e
p :class_refine_undefined
end
`)
want := "true\ntrue\nfalse\n:refine_is_private\n:class_refine_undefined"
if got != want {
t.Errorf("refine/using declarations =\n%s\nwant\n%s", got, want)
}
}
Loading