Skip to content

Override freeze-related behaviour in RubyRange for compatibility with Ruby 3.0 #2570

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

Merged
merged 1 commit into from
Jan 14, 2022

Conversation

MattAlp
Copy link
Contributor

@MattAlp MattAlp commented Jan 11, 2022

Update
As stated in the discussion below, this PR is now an optimization + compatibility change that modifies the behaviour of RubyRange and how it is used.


While working on #2453 and updating Range's behaviour to comply with 3.0, I discovered that RubyRange inherits from RubyDynamicObject, which exports freeze and isFrozen methods that dynamically set the frozen? property on the object.

image

This proposed change implements the same pattern as seen in RubyString, which exposes a frozen parameter in the constructor and overrides the freeze and isFrozen methods to explicitly work on the boolean frozen field. By doing so, the field is always present on the object and can default to true, rather than needing to be instantiated as a dynamic property. This also allows us to set the property at the appropriate call sites (i.e. to false when creating an object in Ruby that inherits from Range).

@nirvdrum
Copy link
Collaborator

Is this to handle freeze state in Range subclasses better? I think that's why RubyString has a local field like that.

@MattAlp MattAlp force-pushed the mattalp/optimize-rubyrange branch from 1550895 to dc37253 Compare January 11, 2022 19:44
@chrisseaton
Copy link
Collaborator

Extra context is that because RubyRange does not inherit from RubyBasicObject, it does not have any field slots. So the frozen field goes into an extension array, which is indirect and wasteful. Adding this field in here like this, like RubyString does avoids that extension array being used.

This is preliminary work to fixing the frozen behaviour of Range.

Note that another conversation to have is that Range, String etc also store their object IDs in extension arrays, for the same reason. We should probably fix that?

@eregon
Copy link
Member

eregon commented Jan 12, 2022

Ah, yes, not all Range are frozen, only instances of Range but not instances of subclasses of Ranges: https://bugs.ruby-lang.org/issues/15504#note-19
(Otherwise we'd just always return true for isFrozen on RubyRange and call it a day)

I think this is something to fix in CRuby at some point, it's a weird exception to immutable classes (https://gist.github.com/eregon/bce555fa9e9133ed27fbfc1deb181573).

@eregon
Copy link
Member

eregon commented Jan 12, 2022

Note that another conversation to have is that Range, String etc also store their object IDs in extension arrays, for the same reason. We should probably fix that?

So we'd have long objectID in RubyDynamicObject? The trade-off is higher memory footprint for all Ruby objects even if they never need the object_id. I think few objects actually need an object_id, but it would be good to have some data on that if we do such a change.
If the object_id computations come mostly from Kernel#inspect we might be able to instead use the Java identityHashCode for that purpose, which is somewhat what CRuby does IIRC (#inspect hexa identity code != object_id).

@chrisseaton
Copy link
Collaborator

So we'd have long objectID in RubyDynamicObject?

No, in RubyString, RubyRange, etc. Maybe a new base class for objects that don't expect lots of instance variables, but might need frozen and object_id?

@MattAlp
Copy link
Contributor Author

MattAlp commented Jan 12, 2022

Benoit and I briefly chatted in the Graal Slack. I'm going to go ahead and update this PR with additional changes that bring in the .frozen? compatibility change, including the call site changes, untag, and new spec.

@eregon
Copy link
Member

eregon commented Jan 12, 2022

No, in RubyString, RubyRange, etc. Maybe a new base class for objects that don't expect lots of instance variables, but might need frozen and object_id?

Yeah, that could work. It'd still increase the footprint for all instances of these. I'm not sure which builtin classes are likely to be frozen (but not always frozen) and/or object_id, would be good to have some data on that.
For now I don't see a strong need to share the trivial logic between RubyString and RubyRange.

@MattAlp MattAlp force-pushed the mattalp/optimize-rubyrange branch 2 times, most recently from 1f8f02d to 4da431a Compare January 12, 2022 19:59
@MattAlp MattAlp changed the title Override freeze-related behaviour in RubyRange Override freeze-related behaviour in RubyRange for compatibility with Ruby 3.0 Jan 12, 2022
Copy link
Collaborator

@chrisseaton chrisseaton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to fix the spec.

@MattAlp MattAlp force-pushed the mattalp/optimize-rubyrange branch from 2f07b6d to 94cfbac Compare January 13, 2022 16:46
@@ -223,7 +224,8 @@ protected RubyObjectRange dup(RubyObjectRange range) {
getLanguage().objectRangeShape,
range.excludedEnd,
range.begin,
range.end);
range.end,
range.frozen);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.dup should actually create an unfrozen copy:

irb(main):001:0> RUBY_VERSION
=> "3.0.2"
irb(main):002:0> (1..2).frozen?
=> true
irb(main):003:0> (1..2).dup.frozen?
=> false

Could you add specs for that (if there isn't already) and fix the behavior?
For the int and long ranges, we need to create non-frozen variants as well then (or create RubyObjectRange, but that seems not advantageous).

@MattAlp MattAlp force-pushed the mattalp/optimize-rubyrange branch from 94cfbac to 50115c4 Compare January 13, 2022 20:49
Copy link
Member

@eregon eregon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, thank you for the PR!


it "is not frozen if duplicated" do
(42..).dup.should_not.frozen?
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should move to spec/ruby/core/range/dup_spec.rb, I'll do that.

@eregon eregon added the in-ci The PR is being tested in CI. Do not push new commits. label Jan 14, 2022
@eregon eregon self-assigned this Jan 14, 2022
@eregon eregon added this to the 22.1.0 milestone Jan 14, 2022
@eregon
Copy link
Member

eregon commented Jan 14, 2022

I just remembered, there is a follow-up optimization related to this, range literals can be embedded in the AST:

$ ruby -ve '2.times { p (1..2).object_id }'  
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
60
60

However this is a problem for AST sharing (sharing the same AST for multiple execution contexts, which have different Ruby classes, etc), because RubyRange objects are currently not context-independent, they point to a given RubyClass (private RubyClass metaClass; in RubyDynamicObject).
So to solve this we would need RubyIntRange/RubyLongRange to no longer inherit from RubyRange (we'd also just remove RubyRange), and instead inherit from ImmutableRubyObject.
Then in the translator when building a literal Range with int or long literals for both begin & end (non-beginless & non-endless) we could create a RubyIntRange/RubyLongRange directly in the translator (in org.truffleruby.parser.BodyTranslator#visitDotNode) and use an ObjectLiteralNode to embed the range instance in the AST.
For the cases we need to create a non-frozen range with int/long begin/end we'd go to ObjectLiteralNode then (e.g., for Range#dup).

@MattAlp Would you like to try that (separate PR of course) or you would prefer if somebody else works on that? It is a bit more involved than the change here but I think also not very difficult either.

=> moved to #2622

graalvmbot pushed a commit that referenced this pull request Jan 14, 2022
…bility with Ruby 3.0 (#2570)

PullRequest: truffleruby/3120
@graalvmbot graalvmbot merged commit 50115c4 into oracle:master Jan 14, 2022
@MattAlp
Copy link
Contributor Author

MattAlp commented Jan 18, 2022

Sure, I'll take it @eregon

@chrisseaton chrisseaton deleted the mattalp/optimize-rubyrange branch December 1, 2022 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in-ci The PR is being tested in CI. Do not push new commits. oca-signed performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants