diff --git a/internal/vm/refinement.go b/internal/vm/refinement.go index 7082bc03..a1f771d0 100644 --- a/internal/vm/refinement.go +++ b/internal/vm/refinement.go @@ -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 [#]. + 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 { @@ -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 { @@ -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 { @@ -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 @@ -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. diff --git a/internal/vm/wave31_refinement_internal_test.go b/internal/vm/wave31_refinement_internal_test.go new file mode 100644 index 00000000..40a2a891 --- /dev/null +++ b/internal/vm/wave31_refinement_internal_test.go @@ -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) + } +}