-
-
Notifications
You must be signed in to change notification settings - Fork 925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RuntimeError: can't modify frozen Hash when trying to run Lotus::View tests #2988
Comments
Here's the traceback from the
This line corresponds to the |
Ok, I've finally reduced it! It appears we're always putting class variables assigned in module methods on the lexically-enclosing module, rather than on the actual self module they are called against. module A
def cvar
@@cvar = 1
end
end
class B
include A.dup
end
B.new.cvar
p A.class_variables # [:@@cvar] on JRuby, [] on MRI This behaves incorrectly as far back as 1.6.8, so this doesn't appear to be a new bug by any means. |
👍 |
Well this turns out to be a really big issue. It appears that in MRI, when you dup a module or class, it also duplicates all methods and physically rewrites the new copies to behave as though they were lexically defined within the dupe. That allows it to scope these class vars properly, since class vars scope similar to constants. As @enebo puts it, we'd consider it "highly undesirable" to have to clone entire method bodies to emulate this behavior, but I'm having trouble thinking of an alternative solution. Note that this cloning likely also affects other lexically-scoped behaviors like refinements. Maybe @subbuss or @chrisseaton have ideas on how we can solve this. |
I would ask this question: since methods are ruby objects and can have state, is there any piece of method object state that cannot be adequately represented across the dup-ed uses without duplicating them? |
@subbuss A key example is call sites for refined methods, which will need the proper (new) lexical enclosure. The class variable stuff could probably get away with just having a different StaticScope to use with the same IR. |
There is one small piece of good news out of this, I suppose: cref appears to be appropriate for locating where the class vars should go. We're just using the wrong cref, since the dup'ed module's methods think they're still in the original. |
Hmm, we may not need to clone call sites after all. Here's an example that I'd expect to use the new refinement module Refined
refine(String) { def bar; puts :bar; end }
end
module Refine2
refine(String) { def foo; puts :quux; end }
end
class A
using Refined
def foo; 'str'.bar; end
end
A.new.foo # => bar
B = A.dup
class B
using Refine2
end
B.new.foo # => also bar, but I expected quux Of course it's possible they do clone the refined site but they carry the original lexical container along... |
Ok...so the key logic happens in rb_vm_rewrite_cref_stack. This function is called after the method (and its instruction sequence) have been cloned, and it does the following: While recursing up the cref stack...
I believe this can be summarized like this: When cloning a module, all instructions are duplicated, and the cref for the entire sequence must be copied:
This is hard to explain, but I guess it's basically fixing up all crefs up to the module we're cloning so that they eventually terminate on the new module. If they're already scoped to a different module, they're mostly left alone. Once we find our source module's cref in the stack, we modify that one to point at the new module and we're done. I believe that MRI does get call site cloning as part of this process, since I think their inline cache is just a slot on the call instruction. This needs to be confirmed, however, as it could affect performance of call sites in dup'ed modules. |
Phew .. I need to digest all this. At first glance, it seems like all the cloning is just a side effect of MRI maintaining state in the generated code. Not sure if that applies to how JRuby runtime / callsite caching works. But, that is just an off-the-cuff remark without a whole lot of thought behind it. But, however, a related thought is that this need not be a blocker for 9k RC1 since this is not a new issue? |
@subbuss @enebo I believe yes. This has apparently never worked right in JRuby. It will mean that our RC can't run Lotus (without modifications to Lotus) but it would never have worked on JRuby, and it will never work on Rubinius either as it stands. When we do fix it, I still believe we can get away with having the same instructions using cloned and modified StaticScope. |
I think so too .. as long as you don't maintain scope-specific state in the call-site caches. |
I think refinements MUST rewrite some state and it is lexical in nature so it makes sense to fiddle with static/dynamicscope, but I am less sure cvars are lexical. While doing this may be necessary to appropriately support refinements we may be able to make an algorithm which works for only crefs. Does anyone have a lexical example of cvars? I used findInstanceMethodContainer and it largely only failed in cases where there was a singleton scenario. We know from our current cvar code we need to cope with singletons so perhaps we just need to make a findXXXer which copes properly with singletons? With having said all that the idea of rewriting instructions does have the potential to eliminate a lot of logic at runtime. |
haha...I just realized findInstanceMethod is in terms of lexical constructs, but I am still not sure you cannot derive cvars from self + inheritance chain... |
ok I convinced myself that for cvardecl at least it must be lexical: module Foo
def a
puts "a self = #{self}"
@@a = 1
end
def b
puts "b self = #{self}"
class << self
puts "b singleton self = #{self}"
@@b = 2
end
end
end
module Bar
def a
puts "a self = #{self}"
@@a = 1
end
def b
puts "b self = #{self}"
class << self
puts "b singleton self = #{self}"
@@b = 2
end
end
end
class A
include Foo
end
C = Bar.dup
class B
include C
end
a = A.new
a.a
a.b
puts "A: #{A.class_variables(false)}"
puts "Foo: #{ Foo.class_variables(false)}" # on A
b = B.new
b.a
b.b
puts "B: #{B.class_variables(false)}"
puts "C: #{C.class_variables(false)}"
puts "Bar: #{Bar.class_variables(false)}" with output:
So self is in terms of what you would think self would be but cvars need to go to their lexical equivalent BUT the dup completely hoses it all up. |
Any news around this "issue"? |
We are aware of what is wrong and MRI made this behavior so long ago that we can no longer complain about this behavior but fixing it is far from trivial and we have been pondering the best way to do that. |
@enebo Thanks for the explanation! |
In face of JRuby issue to a particular MRI behavior involving lexical escope of `cvar`s of duped classes, this commit changes the approach by using instance vars without duping classes. Note: Even though it's a JRuby issue jruby/jruby#2988 (comment) I decided to change Lotus implementation because of the complexity of fixing it on JRuby side (second their core maintainers). Ref. hanami/hanami#307
Partial fix for #3834 The main issue here is that we frequently return this for DynamicMethod#dup, rather than return a new instance, and then this gets modified by code expecting it to be something different. This breaks reflective code because many reflective methods will expect the methods in a module's method table to say their implementationClass is that module. The case in #3834 broke because the accessor methods in Struct have this "fake" dup logic, so when the struct class itself was duplicated, the methods got modified implementationClass = clone and seemed to "disappear" from the original. This is only a partial fix because it just fixes those accessors in Struct. The proper fix would be to make sure DynamicMethod#dup returns a real new object for all DynamicMethod (except "dead" ones like UndefinedMethod). This may also fix (or make easier to fix) issues like #2988, where a dup'ed module's methods do not reflect their new implementationClass. Specs pending.
I don't want to speculate what an exact issue is here (Rubinus fails on that too). So a question here is why there is no issue under MRI though.
How to reproduce:
The text was updated successfully, but these errors were encountered: