Skip to content

Commit 1a40956

Browse files
author
Nicolas Laurent
committed
[GR-18163] Allow immediate and frozen key/values in WeakMap (#2267)
PullRequest: truffleruby/2463
2 parents dfc60c6 + e839f3f commit 1a40956

File tree

11 files changed

+132
-70
lines changed

11 files changed

+132
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Compatibility:
6969
* Implemented `Kernel#raise` `cause` parameter.
7070
* Improved compatibility of `Signal.trap` and `Kernel#trap` (#2287, @chrisseaton).
7171
* Implemented `GC.stat(:total_allocated_objects)` as `0` (#2292, @chrisseaton).
72+
* `ObjectSpace::WeakMap` now supports immediate and frozen values as both keys and values (#2267).
7273

7374
Performance:
7475

spec/tags/core/objectspace/weakmap/element_set_tags.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

spec/truffle/objectspace/weakmap_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,20 @@
7171
map.each_value { |v| a << v }
7272
a.should == ["X"]
7373
end
74+
75+
it "supports constant objects" do
76+
map = ObjectSpace::WeakMap.new
77+
map[1] = 2
78+
Primitive.gc_force
79+
map[1].should == 2
80+
end
81+
82+
it "supports frozen objects" do
83+
map = ObjectSpace::WeakMap.new
84+
x = "x".freeze
85+
y = "y".freeze
86+
map[x] = y
87+
Primitive.gc_force
88+
map[x].should == y
89+
end
7490
end

src/main/java/org/truffleruby/core/basicobject/BasicObjectNodes.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,20 @@ protected boolean equal(VirtualFrame frame, Object a, Object b) {
117117
/** This node is not trivial because primitives must be compared by value and never by identity. Also, this node
118118
* must consider (byte) n and (short) n and (int) n and (long) n equal, as well as (float) n and (double) n. So even
119119
* if a and b have different classes they might still be equal if they are primitives. */
120+
@GenerateUncached
121+
@GenerateNodeFactory
120122
@CoreMethod(names = { "equal?", "==" }, required = 1)
121-
public abstract static class ReferenceEqualNode extends CoreMethodArrayArgumentsNode {
123+
@NodeChild(value = "arguments", type = RubyNode[].class)
124+
public abstract static class ReferenceEqualNode extends RubySourceNode {
122125

123126
public static ReferenceEqualNode create() {
124127
return ReferenceEqualNodeFactory.create(null);
125128
}
126129

130+
public static ReferenceEqualNode getUncached() {
131+
return ReferenceEqualNodeFactory.getUncached();
132+
}
133+
127134
public abstract boolean executeReferenceEqual(Object a, Object b);
128135

129136
@Specialization

src/main/java/org/truffleruby/core/cast/ToRubyIntegerNode.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,23 @@
99
*/
1010
package org.truffleruby.core.cast;
1111

12+
import com.oracle.truffle.api.dsl.CachedContext;
13+
import com.oracle.truffle.api.dsl.GenerateUncached;
14+
import org.truffleruby.RubyContext;
15+
import org.truffleruby.RubyLanguage;
1216
import org.truffleruby.core.numeric.RubyBignum;
13-
import org.truffleruby.language.RubyContextSourceNode;
14-
import org.truffleruby.language.RubyNode;
17+
import org.truffleruby.language.RubyBaseNode;
1518
import org.truffleruby.language.dispatch.DispatchNode;
1619

1720
import com.oracle.truffle.api.dsl.Cached;
18-
import com.oracle.truffle.api.dsl.NodeChild;
1921
import com.oracle.truffle.api.dsl.Specialization;
2022

2123
/** See {@link ToIntNode} for a comparison of different integer conversion nodes. */
22-
@NodeChild(value = "child", type = RubyNode.class)
23-
public abstract class ToRubyIntegerNode extends RubyContextSourceNode {
24+
@GenerateUncached
25+
public abstract class ToRubyIntegerNode extends RubyBaseNode {
2426

2527
public static ToRubyIntegerNode create() {
26-
return ToRubyIntegerNodeGen.create(null);
27-
}
28-
29-
public static ToRubyIntegerNode create(RubyNode child) {
30-
return ToRubyIntegerNodeGen.create(child);
28+
return ToRubyIntegerNodeGen.create();
3129
}
3230

3331
public abstract Object execute(Object object);
@@ -49,7 +47,8 @@ protected RubyBignum coerceRubyBignum(RubyBignum value) {
4947

5048
@Specialization(guards = "!isRubyInteger(object)")
5149
protected Object coerceObject(Object object,
50+
@CachedContext(RubyLanguage.class) RubyContext context,
5251
@Cached DispatchNode toIntNode) {
53-
return toIntNode.call(getContext().getCoreLibrary().truffleTypeModule, "rb_to_int_fallback", object);
52+
return toIntNode.call(context.getCoreLibrary().truffleTypeModule, "rb_to_int_fallback", object);
5453
}
5554
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved. This
3+
* code is released under a tri EPL/GPL/LGPL license. You can use it,
4+
* redistribute it and/or modify it under the terms of the:
5+
*
6+
* Eclipse Public License version 2.0, or
7+
* GNU General Public License version 2, or
8+
* GNU Lesser General Public License version 2.1.
9+
*/
10+
package org.truffleruby.core.hash;
11+
12+
import org.truffleruby.core.basicobject.BasicObjectNodes.ReferenceEqualNode;
13+
import org.truffleruby.core.hash.HashingNodes.ToHashByIdentity;
14+
15+
/** Wraps a value so that it will compared and hashed according to Ruby identity semantics. These semantics differ from
16+
* Java semantics notably for primitives (e.g. all the NaN are different according to Ruby, but Java compares them
17+
* equal). */
18+
public final class CompareByRubyIdentityWrapper {
19+
20+
public final Object value;
21+
22+
public CompareByRubyIdentityWrapper(Object value) {
23+
this.value = value;
24+
}
25+
26+
@Override
27+
public int hashCode() {
28+
return ToHashByIdentity.getUncached().execute(value);
29+
}
30+
31+
@Override
32+
public boolean equals(Object obj) {
33+
return obj instanceof CompareByRubyIdentityWrapper &&
34+
ReferenceEqualNode
35+
.getUncached()
36+
.executeReferenceEqual(value, ((CompareByRubyIdentityWrapper) obj).value);
37+
}
38+
}

src/main/java/org/truffleruby/core/hash/HashingNodes.java

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,22 @@
1313
import com.oracle.truffle.api.dsl.Cached;
1414
import com.oracle.truffle.api.dsl.CachedContext;
1515
import com.oracle.truffle.api.dsl.Fallback;
16+
import com.oracle.truffle.api.dsl.GenerateUncached;
1617
import com.oracle.truffle.api.dsl.Specialization;
1718
import org.truffleruby.RubyContext;
1819
import org.truffleruby.RubyLanguage;
1920
import org.truffleruby.core.basicobject.BasicObjectNodes.ObjectIDNode;
2021
import org.truffleruby.core.cast.ToRubyIntegerNode;
22+
import org.truffleruby.core.hash.HashingNodesFactory.HashCastResultNodeGen;
23+
import org.truffleruby.core.hash.HashingNodesFactory.ToHashByIdentityNodeGen;
2124
import org.truffleruby.core.numeric.BigIntegerOps;
2225
import org.truffleruby.core.numeric.RubyBignum;
2326
import org.truffleruby.core.string.RubyString;
2427
import org.truffleruby.core.string.StringNodes;
2528
import org.truffleruby.core.symbol.RubySymbol;
2629
import org.truffleruby.core.symbol.SymbolNodes;
2730
import org.truffleruby.core.string.ImmutableRubyString;
31+
import org.truffleruby.language.RubyBaseNode;
2832
import org.truffleruby.language.RubyContextNode;
2933
import org.truffleruby.language.dispatch.DispatchNode;
3034

@@ -137,7 +141,16 @@ private int castResult(Object value) {
137141

138142
}
139143

140-
public abstract static class ToHashByIdentity extends RubyContextNode {
144+
@GenerateUncached
145+
public abstract static class ToHashByIdentity extends RubyBaseNode {
146+
147+
public static ToHashByIdentity create() {
148+
return ToHashByIdentityNodeGen.create();
149+
}
150+
151+
public static ToHashByIdentity getUncached() {
152+
return ToHashByIdentityNodeGen.getUncached();
153+
}
141154

142155
public abstract int execute(Object key);
143156

@@ -147,19 +160,15 @@ protected int toHashByIdentity(Object hashed,
147160
@Cached HashCastResultNode hashCastResultNode) {
148161
return hashCastResultNode.execute(objectIDNode.execute(hashed));
149162
}
150-
151163
}
152164

153-
154-
public abstract static class HashCastResultNode extends RubyContextNode {
155-
156-
@Child private ToRubyIntegerNode toRubyInteger;
157-
@Child private HashCastResultNode hashCastResultNode;
165+
@GenerateUncached
166+
public abstract static class HashCastResultNode extends RubyBaseNode {
158167

159168
public abstract int execute(Object key);
160169

161170
public static HashCastResultNode create() {
162-
return HashingNodesFactory.HashCastResultNodeGen.create();
171+
return HashCastResultNodeGen.create();
163172
}
164173

165174
@Specialization
@@ -177,24 +186,11 @@ protected int castBignum(RubyBignum hashed) {
177186
return BigIntegerOps.hashCode(hashed);
178187
}
179188

180-
@Fallback
181-
protected int castOther(Object hashed) {
182-
if (toRubyInteger == null) {
183-
CompilerDirectives.transferToInterpreterAndInvalidate();
184-
toRubyInteger = insert(ToRubyIntegerNode.create());
185-
}
186-
final Object coercedHashedObject = toRubyInteger.execute(hashed);
187-
return castCoerced(coercedHashedObject);
189+
@Specialization(guards = "!isRubyInteger(hashed)")
190+
protected int castOther(Object hashed,
191+
@Cached ToRubyIntegerNode toRubyInteger,
192+
@Cached HashCastResultNode hashCastResult) {
193+
return hashCastResult.execute(toRubyInteger.execute(hashed));
188194
}
189-
190-
private int castCoerced(Object coerced) {
191-
if (hashCastResultNode == null) {
192-
CompilerDirectives.transferToInterpreterAndInvalidate();
193-
hashCastResultNode = insert(HashCastResultNode.create());
194-
}
195-
return hashCastResultNode.execute(coerced);
196-
}
197-
198195
}
199-
200196
}

src/main/java/org/truffleruby/core/objectspace/WeakMapNodes.java

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
import org.truffleruby.builtins.Primitive;
1616
import org.truffleruby.builtins.UnaryCoreMethodNode;
1717
import org.truffleruby.builtins.YieldingCoreMethodNode;
18-
import org.truffleruby.collections.WeakValueCache;
18+
import org.truffleruby.collections.WeakValueCache.WeakMapEntry;
1919
import org.truffleruby.core.array.RubyArray;
20+
import org.truffleruby.core.hash.CompareByRubyIdentityWrapper;
2021
import org.truffleruby.core.klass.RubyClass;
2122
import org.truffleruby.core.proc.RubyProc;
2223
import org.truffleruby.language.Nil;
@@ -27,6 +28,8 @@
2728
import com.oracle.truffle.api.dsl.Specialization;
2829
import org.truffleruby.language.objects.AllocationTracing;
2930

31+
import java.util.Collection;
32+
3033
/** Note that WeakMap uses identity comparison semantics. See top comment in src/main/ruby/truffleruby/core/weakmap.rb
3134
* for more information. */
3235
@CoreModule(value = "ObjectSpace::WeakMap", isClass = true)
@@ -57,7 +60,7 @@ public abstract static class MemberNode extends CoreMethodArrayArgumentsNode {
5760

5861
@Specialization
5962
protected boolean isMember(RubyWeakMap map, Object key) {
60-
return map.storage.get(key) != null;
63+
return map.storage.get(new CompareByRubyIdentityWrapper(key)) != null;
6164
}
6265
}
6366

@@ -66,7 +69,7 @@ public abstract static class GetIndexNode extends CoreMethodArrayArgumentsNode {
6669

6770
@Specialization
6871
protected Object get(RubyWeakMap map, Object key) {
69-
Object value = map.storage.get(key);
72+
Object value = map.storage.get(new CompareByRubyIdentityWrapper(key));
7073
return value == null ? nil : value;
7174
}
7275
}
@@ -76,7 +79,7 @@ public abstract static class SetIndexNode extends CoreMethodArrayArgumentsNode {
7679

7780
@Specialization
7881
protected Object set(RubyWeakMap map, Object key, Object value) {
79-
map.storage.put(key, value);
82+
map.storage.put(new CompareByRubyIdentityWrapper(key), value);
8083
return value;
8184
}
8285
}
@@ -144,8 +147,8 @@ protected RubyWeakMap each(RubyWeakMap map, Nil block) {
144147
@Specialization
145148
protected RubyWeakMap each(RubyWeakMap map, RubyProc block) {
146149

147-
for (WeakValueCache.WeakMapEntry<?, ?> e : entries(map.storage)) {
148-
yield(block, e.getKey(), e.getValue());
150+
for (MapEntry entry : entries(map.storage)) {
151+
yield(block, entry.key, entry.value);
149152
}
150153

151154
return map;
@@ -154,17 +157,39 @@ protected RubyWeakMap each(RubyWeakMap map, RubyProc block) {
154157

155158
@TruffleBoundary
156159
private static Object[] keys(WeakMapStorage storage) {
157-
return storage.keys().toArray();
160+
final Collection<CompareByRubyIdentityWrapper> keyWrappers = storage.keys();
161+
final Object[] keys = new Object[keyWrappers.size()];
162+
int i = 0;
163+
for (CompareByRubyIdentityWrapper keyWrapper : keyWrappers) {
164+
keys[i++] = keyWrapper.value;
165+
}
166+
return keys;
158167
}
159168

160169
@TruffleBoundary
161170
private static Object[] values(WeakMapStorage storage) {
162171
return storage.values().toArray();
163172
}
164173

174+
private static class MapEntry {
175+
final Object key;
176+
final Object value;
177+
178+
private MapEntry(Object key, Object value) {
179+
this.key = key;
180+
this.value = value;
181+
}
182+
}
183+
165184
@TruffleBoundary
166-
private static WeakValueCache.WeakMapEntry<?, ?>[] entries(WeakMapStorage storage) {
167-
return storage.entries().toArray(new WeakValueCache.WeakMapEntry<?, ?>[0]);
185+
private static MapEntry[] entries(WeakMapStorage storage) {
186+
final Collection<WeakMapEntry<CompareByRubyIdentityWrapper, Object>> wrappedEntries = storage.entries();
187+
final MapEntry[] entries = new MapEntry[wrappedEntries.size()];
188+
int i = 0;
189+
for (WeakMapEntry<CompareByRubyIdentityWrapper, Object> wrappedEntry : wrappedEntries) {
190+
entries[i++] = new MapEntry(wrappedEntry.getKey().value, wrappedEntry.getValue());
191+
}
192+
return entries;
168193
}
169194

170195
private static RubyWeakMap eachNoBlockProvided(YieldingCoreMethodNode node, RubyWeakMap map) {

src/main/java/org/truffleruby/core/objectspace/WeakMapStorage.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package org.truffleruby.core.objectspace;
1111

1212
import org.truffleruby.collections.WeakValueCache;
13+
import org.truffleruby.core.hash.CompareByRubyIdentityWrapper;
1314

14-
public final class WeakMapStorage extends WeakValueCache<Object, Object> {
15+
public final class WeakMapStorage extends WeakValueCache<CompareByRubyIdentityWrapper, Object> {
1516
}

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

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,11 @@
99
# GNU Lesser General Public License version 2.1.
1010

1111
module ObjectSpace
12-
# WeakMap uses identity comparison semantics. The implementation assumes that the Java representation of objects
13-
# do compare (equals() and hashCode()) using object identity. This is the case for instances of RubyDynamicObject.
14-
#
15-
# However, results are unspecified if used with instances of TruffleObject that override equals() and
16-
# hashCode().
17-
#
18-
# Note that, following MRI, we disallow immediate (primitive) values and frozen objects as keys or value of
19-
# WeakMap. We could easily transcend this limitation as we do not modify objects like MRI does (it sets a finalizer).
20-
# However, keeping the limitation enables the above assumption of identity comparison.
12+
# WeakMap uses Ruby identity semantics to compare and hash keys.
2113
class WeakMap
2214
include Enumerable
2315

24-
private def check_key_or_value(kv, type)
25-
if Primitive.immediate_value?(kv)
26-
raise ArgumentError, "WeakMap #{type} can't be an instance of #{kv.class}"
27-
elsif Truffle::KernelOperations.value_frozen?(kv)
28-
raise FrozenError, "WeakMap #{type} can't be a frozen object"
29-
end
30-
end
31-
3216
def []=(key, value)
33-
check_key_or_value(key, 'key')
34-
check_key_or_value(value, 'value')
3517
Primitive.weakmap_aset(self, key, value)
3618
end
3719

0 commit comments

Comments
 (0)