Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Implementation of Module#prepend #1848

Closed
wants to merge 13 commits into from

4 participants

@LTe
LTe commented

Ruby 2.0.0

module M; end

class C
  include M
end

C.ancestors # => [C, M, Object, Kernel, BasicObject] 

class C1
  prepend M
end

C1.ancestors # => [M, C1, Object, Kernel, BasicObject] 

This is only partial implementation of this feature. I don't know how change superclass chain. I found in Rubinius code. In this case we need create root for IncludedModule

  void test_object_class_with_superclass_chain() {
    Module* mod = Module::create(state);
    Class* cls = Class::create(state, G(object));
    Object* obj = state->new_object<Object>(cls);

    /* This should be functionally correct but not actually the
     * way a superclass chain is implemented. However, it doesn't
     * require that we create a root for IncludedModule.
     */
    Module* m = cls->superclass();
    cls->superclass(state, mod);
    mod->superclass(state, m);

    TS_ASSERT_EQUALS(cls, obj->class_object(state));
  }

In MRI implementation each RClass have origin field. On start origin points to itself. But after prepend interpreter allocate memory for new class and create another.

origin = class_alloc(T_ICLASS, klass);
RCLASS_SUPER(origin) = RCLASS_SUPER(klass);
RCLASS_SUPER(klass) = origin;
RCLASS_ORIGIN(klass) = origin;
RCLASS_M_TBL(origin) = RCLASS_M_TBL(klass);
RCLASS_M_TBL(klass) = 0;

So klass is still on top but have new origin. Little tricky for me :)

Someone can help me with Module#prepend implementation? Maybe Rubinius internals are not ready for prepend. Any advice appreciated.

@travisbot

This pull request fails (merged e12da472 into df43311).

@travisbot

This pull request fails (merged e641e34e into df43311).

@travisbot

This pull request fails (merged 3524617 into df43311).

@brixen
Owner

Your specs need a proper version guard for 2.0 so they don't fail on 1.8 and 1.9.

I don't think we need prepend in kernel/alpha.rb as I don't expect us to be using it in Rubinius itself, so it doesn't need to be available to load the kernel.

The rest I haven't looked into yet.

@travisbot

This pull request fails (merged d6d2f3eb into df43311).

@travisbot

This pull request fails (merged 36d282f into d47f143).

@travisbot

This pull request fails (merged 648a7e8 into d47f143).

@travisbot

This pull request fails (merged 49e09d8 into d47f143).

@dbussink dbussink referenced this pull request from a commit
@dbussink dbussink Implement Module#prepend
This sets up prepending. What it does for subclassing is making sure the
subclass points to the origin class, which could be an included module.
This is done so that method lookup is still simple.

First this was attempted by walking back into the origin during lookup,
but this was causing issues with infinite loops. This approach turns out
to be much simpler in it's implementation.

Implementation is also very loosely based on some experimentation done
in #1848

Fixes #1848
9a8e2df
@dbussink dbussink closed this pull request from a commit
@dbussink dbussink Implement Module#prepend
This sets up prepending. What it does for subclassing is making sure the
subclass points to the origin class, which could be an included module.
This is done so that method lookup is still simple.

First this was attempted by walking back into the origin during lookup,
but this was causing issues with infinite loops. This approach turns out
to be much simpler in it's implementation.

Implementation is also very loosely based on some experimentation done
in #1848

Fixes #1848
9a8e2df
@dbussink dbussink closed this in 9a8e2df
@LTe

@dbussink very nice implementation of Module#prepend. I used my specs to validate your solution

module ModuleSpecs
  if RUBY_VERSION > "1.9.3"
    module PrependModules
      module M0
        def m1; [:M0] end
      end
      module M1
        def m1; [:M1, *super] end
      end
      module M2
        def m1; [:M2, *super] end
      end
      M3 = Module.new do
        def m1; [:M3, *super] end
      end
      module M4
        def m1; [:M4, *super] end
      end
      class C
        def m1; end
      end
      class C0 < C
        include M0
        prepend M1
        def m1; [:C0, *super] end
      end
      class C1 < C0
        prepend M2, M3
        include M4
        def m1; [:C1, *super] end
      end
    end

    module ModuleToPrepend
      def m
        result = super
        [:m, result]
      end
    end

    class ClassToPrepend
      prepend ModuleToPrepend
      def m
        :c
      end
    end
  end
end

class Object
  def labeled_module(name, &block)
    Module.new do
      singleton_class.class_eval {define_method(:to_s) {name}}
      class_eval(&block) if block
    end
  end

  def labeled_class(name, superclass = Object, &block)
    Class.new(superclass) do
      singleton_class.class_eval {define_method(:to_s) {name}}
      class_eval(&block) if block
    end
  end
end

ruby_version_is "2.0" do
  describe "Module#prepend" do
    it "prepends modules in proper sequence" do
      obj = ModuleSpecs::PrependModules::C0.new
      obj.m1.should == [:M1,:C0,:M0]

      obj = ModuleSpecs::PrependModules::C1.new
      obj.m1.should == [:M2,:M3,:C1,:M4,:M1,:C0,:M0]
    end

    it "returns proper prepend module ancestors" do
      m0 = labeled_module("m0") {def x; [:m0, *super] end}
      m1 = labeled_module("m1") {def x; [:m1, *super] end; prepend m0}
      m2 = labeled_module("m2") {def x; [:m2, *super] end; prepend m1}
      c0 = labeled_class("c0") {def x; [:c0] end}
      c1 = labeled_class("c1") {def x; [:c1] end; prepend m2}
      c2 = labeled_class("c2", c0) {def x; [:c2, *super] end; include m2}

      m1.ancestors.should == [m0, m1]

      c1.ancestors[0, 4].should == [m0, m1, m2, c1]
      m2.ancestors.should == [m0, m1, m2]
      c1.new.x.should == [:m0, :m1, :m2, :c1]
      c2.ancestors[0, 5].should == [c2, m0, m1, m2, c0]
      c2.new.x.should == [:c2, :m0, :m1, :m2, :c0]
    end

    it "updates ancestors after prepend" do
      m   = Module.new
      m1  = Module.new
      c   = Class.new { prepend m }
      c1  = Class.new(c)

      c1.ancestors.should include(m)
      c1.ancestors.should_not include(m1)

      c.send(:prepend, m1)
      c1.ancestors.should include(m1)
    end
  end
end

In result

1)
Module#prepend prepends modules in proper sequence ERROR
SystemStackError: SystemStackError
                                   ModuleSpecs::PrependModules::C0#m1 at spec/ruby/core/module/fixtures/classes.rb:433 (6192 times)
  ModuleSpecs::PrependModules::M1(ModuleSpecs::PrependModules::C0)#m1 at spec/ruby/core/module/fixtures/classes.rb:416
                                             { } in Object#__script__ at spec/ruby/core/module/prepend_spec.rb:24
                                    BasicObject(Object)#instance_eval at kernel/common/eval19.rb:45
                                        { } in Enumerable(Array)#all? at kernel/common/enumerable.rb:102
                                                           Array#each at kernel/bootstrap/array.rb:68
                                               Enumerable(Array)#all? at kernel/common/enumerable.rb:102
                                                Integer(Fixnum)#times at kernel/common/integer.rb:83
                                                           Array#each at kernel/bootstrap/array.rb:68
                                             { } in Object#__script__ at spec/ruby/core/module/prepend_spec.rb:21
                                                    Object#__script__ at spec/ruby/core/module/prepend_spec.rb:20
                                                          Kernel.load at kernel/common/kernel.rb:588
                                    BasicObject(Object)#instance_eval at kernel/common/eval19.rb:45
                                                           Array#each at kernel/bootstrap/array.rb:68
                                     Rubinius::CodeLoader#load_script at kernel/delta/codeloader.rb:68
                                     Rubinius::CodeLoader.load_script at kernel/delta/codeloader.rb:119
                                              Rubinius::Loader#script at kernel/loader.rb:645
                                                Rubinius::Loader#main at kernel/loader.rb:844

2)
Module#prepend returns proper prepend module ancestors FAILED
Expected [m2, m0, m1, c1]
 to equal [m0, m1, m2, c1]

           { } in Object#__script__ at spec/ruby/core/module/prepend_spec.rb:40
  BasicObject(Object)#instance_eval at kernel/common/eval19.rb:45
      { } in Enumerable(Array)#all? at kernel/common/enumerable.rb:102
                         Array#each at kernel/bootstrap/array.rb:68
             Enumerable(Array)#all? at kernel/common/enumerable.rb:102
              Integer(Fixnum)#times at kernel/common/integer.rb:83
                         Array#each at kernel/bootstrap/array.rb:68
           { } in Object#__script__ at spec/ruby/core/module/prepend_spec.rb:21
                  Object#__script__ at spec/ruby/core/module/prepend_spec.rb:20
                        Kernel.load at kernel/common/kernel.rb:588
  BasicObject(Object)#instance_eval at kernel/common/eval19.rb:45
                         Array#each at kernel/bootstrap/array.rb:68
   Rubinius::CodeLoader#load_script at kernel/delta/codeloader.rb:68
   Rubinius::CodeLoader.load_script at kernel/delta/codeloader.rb:119
            Rubinius::Loader#script at kernel/loader.rb:645
              Rubinius::Loader#main at kernel/loader.rb:844

3)
Module#prepend updates ancestors after prepend FAILED
Expected [#<Class:0x2438>, #<Module:0x243c>, #<Class:0x2440>, Object, PP::ObjectMixin, Kernel, BasicObject]
to include #<Module:0x2444>
           { } in Object#__script__ at spec/ruby/core/module/prepend_spec.rb:57
  BasicObject(Object)#instance_eval at kernel/common/eval19.rb:45
      { } in Enumerable(Array)#all? at kernel/common/enumerable.rb:102
                         Array#each at kernel/bootstrap/array.rb:68
             Enumerable(Array)#all? at kernel/common/enumerable.rb:102
              Integer(Fixnum)#times at kernel/common/integer.rb:83
                         Array#each at kernel/bootstrap/array.rb:68
           { } in Object#__script__ at spec/ruby/core/module/prepend_spec.rb:21
                  Object#__script__ at spec/ruby/core/module/prepend_spec.rb:20
                        Kernel.load at kernel/common/kernel.rb:588
  BasicObject(Object)#instance_eval at kernel/common/eval19.rb:45
                         Array#each at kernel/bootstrap/array.rb:68
   Rubinius::CodeLoader#load_script at kernel/delta/codeloader.rb:68
   Rubinius::CodeLoader.load_script at kernel/delta/codeloader.rb:119
            Rubinius::Loader#script at kernel/loader.rb:645
              Rubinius::Loader#main at kernel/loader.rb:844

Finished in 0.032570 seconds

1 file, 3 examples, 5 expectations, 2 failures, 1 error
@dbussink
Owner

Can you open an issue for that / open a pull request with the additional specs in it? If it's just a commit comment, we're going to forget about it.

@dbussink
Owner

Probably better to start with a new pull request / issue than trying to rework this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 3, 2012
  1. @LTe

    Add simple spec for ruby prepend

    LTe authored
  2. @LTe
Commits on Aug 9, 2012
  1. @LTe

    Add helpers for labeled modules

    LTe authored
  2. @LTe
  3. @LTe

    Update specs for Module#prepend

    LTe authored
    Use ruby guard. Test only for ruby => 2.0
Commits on Aug 21, 2012
  1. @LTe
  2. @LTe

    Update attach_before method.

    LTe authored
    Method attach_before should remember last parent. Without this we can
    use prepend only once.
  3. @LTe
  4. @LTe
  5. @LTe

    Don't prepend module twice

    LTe authored
  6. @LTe

    Add empty #prepended method

    LTe authored
    Developer can implement that method and put the logic in method
  7. @LTe

    Update guard to 2.0

    LTe authored
  8. @LTe
This page is out of date. Refresh to see the latest.
View
22 kernel/alpha.rb
@@ -469,6 +469,14 @@ def direct_superclass
@superclass
end
+ def superclass_root
+ @superclass_root
+ end
+
+ def superclass_root=(other)
+ @superclass_root = other
+ end
+
# :internal:
#
# Perform actual work for including a Module in this one.
@@ -700,6 +708,20 @@ def attach_to(cls)
# :internal:
#
+ # Inject self before class
+ #
+ def attach_before(cls)
+ if old_parent = cls.superclass_root
+ @superclass = old_parent
+ else
+ @superclass = cls
+ end
+
+ cls.superclass_root = self
+ end
+
+ # :internal:
+ #
# Name of the included Module.
#
def name
View
6 kernel/common/module19.rb
@@ -155,4 +155,10 @@ def public_constant(*names)
raise NameError, "#{unknown_constants.size > 1 ? 'Constants' : 'Constant'} #{unknown_constants.map{|e| "#{name}::#{e}"}.join(', ')} undefined"
end
end
+
+ # Hook method called on Module when another Module is .prepend'd into it.
+ #
+ # Override for module-specific behaviour.
+ #
+ def prepended(mod); end
end
View
17 kernel/common/type.rb
@@ -85,12 +85,19 @@ def self.coerce_to_comparison(a, b)
end
def self.each_ancestor(mod)
- unless object_kind_of?(mod, Class) and singleton_class_object(mod)
- yield mod
- end
+ sup = mod
+ last_seen = nil
- sup = mod.direct_superclass()
while sup
+ if root = sup.superclass_root
+ if last_seen == sup
+ sup = last_seen
+ else
+ last_seen = sup
+ sup = root
+ end
+ end
+
if object_kind_of?(sup, IncludedModule)
yield sup.module
elsif object_kind_of?(sup, Class)
@@ -98,7 +105,7 @@ def self.each_ancestor(mod)
else
yield sup
end
- sup = sup.direct_superclass()
+ sup = sup.direct_superclass
end
end
View
69 kernel/delta/module.rb
@@ -85,6 +85,75 @@ def include(*modules)
self
end
+ def prepend(*modules)
+ modules.reverse_each do |mod|
+ if !mod.kind_of?(Module) or mod.kind_of?(Class)
+ raise TypeError, "wrong argument type #{mod.class} (expected Module)"
+ end
+
+ Rubinius.privately do
+ mod.prepend_features self
+ end
+
+ Rubinius.privately do
+ mod.prepended self
+ end
+ end
+ end
+
+ def prepend_features(klass)
+ unless klass.kind_of? Module
+ raise TypeError, "invalid argument class #{klass.class}, expected Module"
+ end
+
+ mod = self
+ changed = false
+
+ while mod
+
+ # Check for a cyclic prepend
+ if mod == klass
+ raise ArgumentError, "cyclic include detected"
+ end
+
+ add = true
+ k = klass.superclass_root ? klass.superclass_root : klass
+
+ while k
+ if k.kind_of? Rubinius::IncludedModule
+ if k == mod
+ add = false
+ break
+ end
+ end
+
+ k = k.direct_superclass
+ end
+
+ if add
+ if mod.kind_of? Rubinius::IncludedModule
+ original_mod = mod.module
+ else
+ original_mod = mod
+ end
+
+ Rubinius::IncludedModule.new(original_mod).attach_before klass
+
+ changed = true
+ end
+
+ mod = mod.direct_superclass
+ end
+
+ if changed
+ method_table.each do |meth, obj, vis|
+ Rubinius::VM.reset_method_cache meth
+ end
+ end
+
+ return self
+ end
+
# Add all constants, instance methods and module variables
# of this Module and all Modules that this one includes to +klass+
#
View
1  mspec/lib/mspec/helpers.rb
@@ -13,6 +13,7 @@
require 'mspec/helpers/io'
require 'mspec/helpers/language_version'
require 'mspec/helpers/mock_to_path'
+require 'mspec/helpers/module'
require 'mspec/helpers/numeric'
require 'mspec/helpers/ruby_exe'
require 'mspec/helpers/scratch'
View
15 mspec/lib/mspec/helpers/module.rb
@@ -0,0 +1,15 @@
+class Object
+ def labeled_module(name, &block)
+ Module.new do
+ singleton_class.class_eval {define_method(:to_s) {name}}
+ class_eval(&block) if block
+ end
+ end
+
+ def labeled_class(name, superclass = Object, &block)
+ Class.new(superclass) do
+ singleton_class.class_eval {define_method(:to_s) {name}}
+ class_eval(&block) if block
+ end
+ end
+end
View
10 spec/ruby/core/module/ancestors_spec.rb
@@ -17,4 +17,14 @@ class << ModuleSpecs::Child; self; end.ancestors.should include(ModuleSpecs::Int
it "has 1 entry per module or class" do
ModuleSpecs::Parent.ancestors.should == ModuleSpecs::Parent.ancestors.uniq
end
+
+ ruby_version_is "2.0" do
+ ModuleSpecs::PrependModules::C1.ancestors.should include(
+ ModuleSpecs::PrependModules::M2, ModuleSpecs::PrependModules::M3,
+ ModuleSpecs::PrependModules::C1, ModuleSpecs::PrependModules::M4,
+ ModuleSpecs::PrependModules::M1, ModuleSpecs::PrependModules::C0,
+ ModuleSpecs::PrependModules::M0, ModuleSpecs::PrependModules::C,
+ Object, Kernel, BasicObject
+ )
+ end
end
View
47 spec/ruby/core/module/fixtures/classes.rb
@@ -380,6 +380,53 @@ def extend_object(obj)
private :extend_object
end
end
+
+ if RUBY_VERSION > "1.9.3"
+ module PrependModules
+ module M0
+ def m1; [:M0] end
+ end
+ module M1
+ def m1; [:M1, *super] end
+ end
+ module M2
+ def m1; [:M2, *super] end
+ end
+ M3 = Module.new do
+ def m1; [:M3, *super] end
+ end
+ module M4
+ def m1; [:M4, *super] end
+ end
+ class C
+ def m1; end
+ end
+ class C0 < C
+ include M0
+ prepend M1
+ def m1; [:C0, *super] end
+ end
+ class C1 < C0
+ prepend M2, M3
+ include M4
+ def m1; [:C1, *super] end
+ end
+ end
+
+ module ModuleToPrepend
+ def m
+ result = super
+ [:m, result]
+ end
+ end
+
+ class ClassToPrepend
+ prepend ModuleToPrepend
+ def m
+ :c
+ end
+ end
+ end
end
class Object
View
94 spec/ruby/core/module/prepend_spec.rb
@@ -0,0 +1,94 @@
+require File.expand_path('../../../spec_helper', __FILE__)
+require File.expand_path('../fixtures/classes', __FILE__)
+
+ruby_version_is "2.0" do
+ describe "Module#prepend" do
+ it "prepends module do class" do
+ ModuleSpecs::ClassToPrepend.new.m.should == [:m, :c]
+ end
+
+ it "prepends modules in proper sequence" do
+ obj = ModuleSpecs::PrependModules::C0.new
+ obj.m1.should == [:M1,:C0,:M0]
+
+ obj = ModuleSpecs::PrependModules::C1.new
+ obj.m1.should == [:M2,:M3,:C1,:M4,:M1,:C0,:M0]
+ end
+
+ it "does not prepend twice one module" do
+ m = Module.new
+
+ lambda {
+ @klass = Class.new do
+ 2.times { prepend m }
+ end
+ }.should_not raise_error(ArgumentError)
+
+ @klass.ancestors.count(m).should == 1
+ end
+
+ it "prepends instance methods" do
+ Object.instance_methods.should == Class.new {prepend Module.new}.instance_methods
+ end
+
+ it "prepends singleton methods" do
+ o = Object.new
+ o.singleton_class.class_eval {prepend Module.new}
+ o.singleton_methods.should == []
+ end
+
+ it "raises NameError in case of remove method" do
+ lambda {
+ Class.new do
+ prepend Module.new {def foo; end}
+ remove_method(:foo)
+ end
+ }.should raise_error(NameError)
+ end
+
+ it "returns proper class ancestors" do
+ m = labeled_module("m")
+ c = labeled_class("c") {prepend m}
+ c.ancestors[0, 2].should == [m, c]
+
+ c2 = labeled_class("c2", c)
+ anc = c2.ancestors
+ anc[0..anc.index(Object)].should == [c2, m, c, Object]
+ end
+
+ it "returns proper prepend module ancestors" do
+ m0 = labeled_module("m0") {def x; [:m0, *super] end}
+ m1 = labeled_module("m1") {def x; [:m1, *super] end; prepend m0}
+ m2 = labeled_module("m2") {def x; [:m2, *super] end; prepend m1}
+ c0 = labeled_class("c0") {def x; [:c0] end}
+ c1 = labeled_class("c1") {def x; [:c1] end; prepend m2}
+ c2 = labeled_class("c2", c0) {def x; [:c2, *super] end; include m2}
+
+ m1.ancestors.should == [m0, m1]
+
+ c1.ancestors[0, 4].should == [m0, m1, m2, c1]
+ m2.ancestors.should == [m0, m1, m2]
+ c1.new.x.should == [:m0, :m1, :m2, :c1]
+ c2.ancestors[0, 5].should == [c2, m0, m1, m2, c0]
+ c2.new.x.should == [:c2, :m0, :m1, :m2, :c0]
+ end
+
+ it "prepends instance methods" do
+ Class.new{ prepend Module.new; def m1; end }.instance_methods(false).should == [:m1]
+ Class.new(Class.new{def m2;end}){ prepend Module.new; def m1; end }.instance_methods(false).should == [:m1]
+ end
+
+ it "updates ancestors after prepend" do
+ m = Module.new
+ m1 = Module.new
+ c = Class.new { prepend m }
+ c1 = Class.new(c)
+
+ c1.ancestors.should include(m)
+ c1.ancestors.should_not include(m1)
+
+ c.send(:prepend, m1)
+ c1.ancestors.should include(m1)
+ end
+ end
+end
View
1  vm/builtin/module.cpp
@@ -50,6 +50,7 @@ namespace rubinius {
void Module::setup(STATE) {
constant_table(state, LookupTable::create(state));
method_table(state, MethodTable::create(state));
+ superclass_root(state, nil<IncludedModule>());
}
void Module::setup(STATE, std::string name, Module* under) {
View
3  vm/builtin/module.hpp
@@ -8,6 +8,7 @@
namespace rubinius {
class LookupTable;
class MethodTable;
+ class IncludedModule;
class Module : public Object {
public:
@@ -19,6 +20,7 @@ namespace rubinius {
LookupTable* constant_table_; // slot
Module* superclass_; // slot
Array* seen_ivars_; // slot
+ IncludedModule* superclass_root_; // slot
public:
/* accessors */
@@ -28,6 +30,7 @@ namespace rubinius {
attr_accessor(constant_table, LookupTable);
attr_accessor(superclass, Module);
attr_accessor(seen_ivars, Array);
+ attr_accessor(superclass_root, IncludedModule)
LookupTable* constants() {
return constant_table();
Something went wrong with that request. Please try again.