Skip to content

Commit e9e3973

Browse files
moste00eregon
andcommitted
Implement the Data class from Ruby 3.2
Co-authored-by: Benoit Daloze <benoit.daloze@oracle.com>
1 parent 16e1906 commit e9e3973

File tree

10 files changed

+267
-40
lines changed

10 files changed

+267
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Compatibility:
2323
* Display "unhandled exception" as the message for `RuntimeError` instances with an empty message (#3255, @nirvdrum).
2424
* Set `RbConfig::CONFIG['configure_args']` for openssl and libyaml (#3170, #3303, @eregon).
2525
* Support `Socket.sockaddr_in(port, Socket::INADDR_ANY)` (#3361, @mtortonesi).
26+
* Implement the `Data` class from Ruby 3.2 (#3039, @moste00, @eregon).
2627

2728
Performance:
2829

lib/mri/pp.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ def pretty_print_cycle(q) # :nodoc:
419419
class Data # :nodoc:
420420
def pretty_print(q) # :nodoc:
421421
q.group(1, sprintf("#<data %s", PP.mcall(self, Kernel, :class).name), '>') {
422-
q.seplist(PP.mcall(self, Data, :members), lambda { q.text "," }) {|member|
422+
q.seplist(PP.mcall(self, Kernel, :class).members, lambda { q.text "," }) {|member|
423423
q.breakable
424424
q.text member.to_s
425425
q.text '='

spec/tags/core/data/define_tags.txt

Lines changed: 0 additions & 5 deletions
This file was deleted.

spec/tags/core/data/initialize_tags.txt

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/main/java/org/truffleruby/cext/CExtNodes.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,16 @@ int sourceLine() {
11881188

11891189
}
11901190

1191+
@CoreMethod(names = "rb_is_local_id", onSingleton = true, required = 1)
1192+
public abstract static class IsLocalIdNode extends CoreMethodArrayArgumentsNode {
1193+
1194+
@Specialization
1195+
boolean isLocalId(RubySymbol symbol) {
1196+
return symbol.getType() == IdentifierType.LOCAL;
1197+
}
1198+
1199+
}
1200+
11911201
@CoreMethod(names = "rb_is_instance_id", onSingleton = true, required = 1)
11921202
public abstract static class IsInstanceIdNode extends CoreMethodArrayArgumentsNode {
11931203

src/main/java/org/truffleruby/core/CoreLibrary.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1048,9 +1048,10 @@ public boolean isTruffleBootMainMethod(SharedMethodInfo info) {
10481048
"/core/truffle/polyglot.rb",
10491049
"/core/truffle/polyglot_methods.rb",
10501050
"/core/posix.rb",
1051+
"/core/data.rb",
1052+
"/core/truffle/queue_operations.rb",
10511053
"/core/main.rb",
10521054
"/core/post.rb",
1053-
"/core/truffle/queue_operations.rb",
10541055
POST_BOOT_FILE
10551056
};
10561057

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# frozen_string_literal: true
2+
3+
module Truffle
4+
module DataOperations
5+
def self.unknown_keywords_message(given, object)
6+
unknowns = given - Primitive.class(object)::CLASS_MEMBERS
7+
s = 's' if unknowns.size > 1
8+
"unknown keyword#{s}: #{unknowns.map(&:inspect).join ', '}"
9+
end
10+
11+
def self.missing_keywords_message(given, object)
12+
missing = Primitive.class(object)::CLASS_MEMBERS - given
13+
s = 's' if missing.size > 1
14+
"missing keyword#{s}: #{missing.map(&:inspect).join ', '}"
15+
end
16+
end
17+
end
18+
19+
class Data
20+
# The entire API of Data is this single class method
21+
def self.define(*class_members, &block)
22+
members_hash = {}
23+
class_members.each do |m|
24+
member = Truffle::Type.symbol_or_string_to_symbol(m)
25+
26+
raise ArgumentError, "invalid data member: #{member}" if member.end_with?('=')
27+
raise ArgumentError, "duplicate member: #{member}" if members_hash[member]
28+
members_hash[member] = true
29+
end
30+
31+
members = members_hash.keys
32+
members.freeze
33+
members_hash.freeze
34+
35+
klass = Class.new self do
36+
const_set :CLASS_MEMBERS, members
37+
const_set :CLASS_MEMBERS_HASH, members_hash
38+
39+
def self.members
40+
self::CLASS_MEMBERS.dup
41+
end
42+
43+
class << self
44+
define_method(:__allocate__, BasicObject.method(:__allocate__))
45+
46+
undef_method :define
47+
end
48+
49+
def self.new(*args, **kwargs)
50+
if !args.empty? and !kwargs.empty?
51+
raise ArgumentError, "wrong number of arguments (given #{args.size + 1}, expected 0)"
52+
end
53+
54+
instance = allocate
55+
56+
if !kwargs.empty?
57+
instance.send(:initialize, **kwargs)
58+
else
59+
if args.size > self::CLASS_MEMBERS.size
60+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..#{self::CLASS_MEMBERS.size})"
61+
end
62+
63+
kwargs_for_initialize = {}
64+
args.each_with_index do |arg, i|
65+
kwargs_for_initialize[self::CLASS_MEMBERS[i]] = arg
66+
end
67+
68+
instance.send(:initialize, **kwargs_for_initialize)
69+
end
70+
71+
instance
72+
end
73+
singleton_class.alias_method :[], :new
74+
75+
# As an exception, these instance methods are directly defined on the returned class, like in CRuby.
76+
# The reason is CRuby does not use an extra module so it needs to define these instance methods on the returned class.
77+
members.each do |member|
78+
define_method(member) { Primitive.object_hidden_var_get(self, member) }
79+
end
80+
end
81+
82+
# Instance methods are defined in an included module, so it is possible, e.g.,
83+
# to redefine #initialize in the returned Data subclass and use super() to use the #initialize just below.
84+
# CRuby defines these directly on Data, but that is suboptimal for performance.
85+
# We want to have a specialized copy of these methods for each Data subclass.
86+
instance_methods_module = Module.new do
87+
def initialize(**kwargs)
88+
members_hash = Primitive.class(self)::CLASS_MEMBERS_HASH
89+
kwargs.each do |member, value|
90+
if members_hash.include?(member)
91+
Primitive.object_hidden_var_set(self, member, value)
92+
else
93+
raise ArgumentError, Truffle::DataOperations.unknown_keywords_message(kwargs.keys, self)
94+
end
95+
end
96+
97+
if kwargs.size < members_hash.size
98+
raise ArgumentError, Truffle::DataOperations.missing_keywords_message(kwargs.keys, self)
99+
end
100+
Primitive.freeze(self)
101+
end
102+
103+
def initialize_copy(other)
104+
Primitive.class(other)::CLASS_MEMBERS.each do |member|
105+
Primitive.object_hidden_var_set self, member, Primitive.object_hidden_var_get(other, member)
106+
end
107+
Primitive.freeze(self)
108+
self
109+
end
110+
111+
def members
112+
Primitive.class(self).members
113+
end
114+
115+
def to_h(&block)
116+
h = {}
117+
Primitive.class(self)::CLASS_MEMBERS.each do |member|
118+
h[member] = Primitive.object_hidden_var_get(self, member)
119+
end
120+
h.to_h(&block)
121+
end
122+
123+
def with(**changes)
124+
return self if changes.empty?
125+
126+
h = to_h
127+
changes.each_pair do |key, value|
128+
if h.include?(key)
129+
h[key] = value
130+
else
131+
raise ArgumentError, Truffle::DataOperations.unknown_keywords_message(changes.keys, self)
132+
end
133+
end
134+
Primitive.class(self).new(**h)
135+
end
136+
137+
def inspect
138+
klass = Primitive.class(self)
139+
class_name = Primitive.module_anonymous?(klass) ? '' : "#{Primitive.module_name(klass)} "
140+
members_and_values = to_h.map do |member, value|
141+
if Truffle::CExt.rb_is_local_id(member) or Truffle::CExt.rb_is_const_id(member)
142+
"#{member}=#{value.inspect}"
143+
else
144+
"#{member.inspect}=#{value.inspect}"
145+
end
146+
end
147+
"#<data #{class_name}#{members_and_values.join(', ')}>"
148+
end
149+
alias_method :to_s, :inspect
150+
151+
def deconstruct
152+
Primitive.class(self)::CLASS_MEMBERS.map do |member|
153+
Primitive.object_hidden_var_get(self, member)
154+
end
155+
end
156+
157+
def deconstruct_keys(keys)
158+
return to_h if Primitive.nil?(keys)
159+
raise TypeError, "wrong argument type #{Primitive.class(keys)} (expected Array or nil)" unless Primitive.is_a?(keys, Array)
160+
161+
members_hash = Primitive.class(self)::CLASS_MEMBERS_HASH
162+
return {} if members_hash.size < keys.size
163+
164+
h = {}
165+
keys.each do |requested_key|
166+
case requested_key
167+
when Symbol
168+
symbolized_key = requested_key
169+
when String
170+
symbolized_key = requested_key.to_sym
171+
end
172+
173+
if members_hash.include?(symbolized_key)
174+
h[requested_key] = Primitive.object_hidden_var_get(self, symbolized_key)
175+
else
176+
return h
177+
end
178+
end
179+
h
180+
end
181+
182+
def ==(other)
183+
return true if Primitive.equal?(self, other)
184+
return false unless Primitive.class(self) == Primitive.class(other)
185+
186+
Truffle::ThreadOperations.detect_pair_recursion self, other do
187+
return self.deconstruct == other.deconstruct
188+
end
189+
190+
# Subtle: if we are here, we are recursing and haven't found any difference, so:
191+
true
192+
end
193+
194+
def eql?(other)
195+
return true if Primitive.equal?(self, other)
196+
return false unless Primitive.class(self) == Primitive.class(other)
197+
198+
Truffle::ThreadOperations.detect_pair_recursion self, other do
199+
return self.deconstruct.eql?(other.deconstruct)
200+
end
201+
202+
# Subtle: if we are here, we are recursing and haven't found any difference, so:
203+
true
204+
end
205+
206+
def hash
207+
klass = Primitive.class(self)
208+
members = klass::CLASS_MEMBERS
209+
210+
val = Primitive.vm_hash_start(klass.hash)
211+
val = Primitive.vm_hash_update(val, members.size)
212+
213+
return val if Truffle::ThreadOperations.detect_outermost_recursion self do
214+
members.each do |member|
215+
member_hash = Primitive.object_hidden_var_get(self, member).hash
216+
val = Primitive.vm_hash_update(val, member_hash)
217+
end
218+
end
219+
220+
Primitive.vm_hash_end(val)
221+
end
222+
end
223+
224+
klass.include instance_methods_module
225+
226+
klass.module_eval(&block) if block
227+
228+
klass
229+
end
230+
231+
class << self
232+
undef_method :new
233+
end
234+
235+
def self.__allocate__
236+
raise TypeError, "allocator undefined for #{self}"
237+
end
238+
end

src/main/ruby/truffleruby/core/struct.rb

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,7 @@ def self.new(klass_name, *attrs, keyword_init: nil, &block)
4848
end
4949
end
5050

51-
attrs = attrs.map do |a|
52-
case a
53-
when Symbol
54-
a
55-
when String
56-
sym = a.to_sym
57-
unless Primitive.is_a?(sym, Symbol)
58-
raise TypeError, "#to_sym didn't return a symbol"
59-
end
60-
sym
61-
else
62-
raise TypeError, "#{a.inspect} is not a symbol"
63-
end
64-
end
51+
attrs = attrs.map { |a| Truffle::Type.symbol_or_string_to_symbol(a) }
6552

6653
duplicates = attrs.uniq!
6754
if duplicates
@@ -354,6 +341,7 @@ def to_a
354341
def deconstruct_keys(keys)
355342
return to_h if Primitive.nil?(keys)
356343
raise TypeError, "wrong argument type #{Primitive.class(keys)} (expected Array or nil)" unless Primitive.is_a?(keys, Array)
344+
357345
return {} if self.length < keys.length
358346

359347
h = {}

src/main/ruby/truffleruby/core/type.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,19 @@ def self.coerce_to_symbol(obj)
449449
end
450450
end
451451

452+
def self.symbol_or_string_to_symbol(obj)
453+
case obj
454+
when Symbol
455+
obj
456+
when String
457+
sym = obj.to_sym
458+
raise TypeError, "#to_sym didn't return a symbol" unless Primitive.is_a?(sym, Symbol)
459+
sym
460+
else
461+
raise TypeError, "#{obj.inspect} is not a symbol"
462+
end
463+
end
464+
452465
# Equivalent of num_exact in MRI's time.c, used by Time methods.
453466
def self.coerce_to_exact_num(obj)
454467
if Primitive.is_a? obj, Integer

test/mri/excludes/TestData.rb

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)