Skip to content

Commit ce568cd

Browse files
committed
[GR-30035] Adapt JavaScript to use new hashes interop APIs.
2 parents fd58307 + ed5148f commit ce568cd

31 files changed

+910
-316
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The main focus is on user-observable behavior of the engine.
1111
* Experimental option `js.array-sort-inherited` was removed. Values visible through holes in array(-like) object are always sorted according to the latest version of ECMAScript specification.
1212
* Updated ICU4J library to version 68.2.
1313
* Implemented the [Atomics.waitAsync](https://github.com/tc39/proposal-atomics-wait-async) proposal. It is available in ECMAScript 2022 mode (`--js.ecmascript-version=2022`).
14+
* Implemented hash map interop support. Allows foreign hash maps to be iterated using `for in/of` loops, `new Map(hash)`, `Array.from(hash)`, etc. If the `--js.foreign-hash-properties` option is enabled (default), foreign hash maps can also be accessed using `hash[key]`, `hash.key`, and used in `{...hash}`. If the `--js.foreign-object-prototype` option is enabled, foreign hash maps also have `Map.prototype` methods.
1415

1516
## Version 21.0.0
1617
* ECMAScript 2021 mode/features enabled by default.

graal-js/src/com.oracle.truffle.js.test.sdk/src/com/oracle/truffle/js/test/sdk/tck/JavaScriptTCKLanguageProvider.java

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,10 @@ public Collection<? extends Snippet> createStatements(final Context context) {
273273
// for of
274274
res.add(createStatement(context, "for-of", "for (let v of {1});",
275275
TypeDescriptor.NULL,
276-
JavaScriptVerifier.foreignOrHasIteratorVerifier(context, null),
277-
TypeDescriptor.union(
278-
TypeDescriptor.STRING,
279-
TypeDescriptor.OBJECT,
280-
TypeDescriptor.ARRAY,
281-
TypeDescriptor.ITERABLE)));
276+
JavaScriptVerifier.hasIteratorVerifier(null),
277+
TypeDescriptor.ANY));
278+
// Using ANY because of GR-30278. Should be: union(STRING, ARRAY, ITERABLE, HASH).
279+
282280
// with
283281
res.add(createStatement(context, "with", "with({1}) undefined",
284282
TypeDescriptor.NULL,
@@ -538,26 +536,21 @@ public void accept(SnippetRun snippetRun) throws PolyglotException {
538536
}
539537

540538
/**
541-
* Creates a {@link ResultVerifier} ignoring errors caused by missing iterator method. Use
542-
* this verifier in case the operator accepts arbitrary foreign Objects for iteration but
543-
* requires iterator for JSObject.
539+
* Creates a {@link ResultVerifier} ignoring errors caused by non-iterable objects. Use this
540+
* verifier in case the operator formally accepts arbitrary types but requires objects to
541+
* provide an {@link Value#hasIterator() iterator}.
544542
*
545543
* @param next the next {@link ResultVerifier} to be called, null for last one
544+
*
546545
* @return the {@link ResultVerifier}
547546
*/
548-
static ResultVerifier foreignOrHasIteratorVerifier(final Context context, ResultVerifier next) {
547+
static ResultVerifier hasIteratorVerifier(ResultVerifier next) {
549548
return new JavaScriptVerifier(next) {
550549
@Override
551550
public void accept(SnippetRun snippetRun) throws PolyglotException {
552551
if (snippetRun.getException() != null) {
553552
final Value param = snippetRun.getParameters().get(0);
554-
final boolean jsObject = context.eval(ID, "Object").isMetaInstance(param);
555-
boolean hasIterator = false;
556-
try {
557-
hasIterator = !context.eval(ID, "(function(a) {return a[Symbol.iterator];})").execute(param).isNull();
558-
} catch (Exception e) {
559-
}
560-
if (jsObject && !hasIterator) {
553+
if (!param.hasIterator() && !param.hasArrayElements() && !param.hasHashEntries() && !param.isString()) {
561554
// Expected for not iterable
562555
return;
563556
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
6+
*/
7+
8+
load('../assert.js');
9+
10+
const HashMap = Java.type("java.util.LinkedHashMap");
11+
12+
let obj = {};
13+
14+
let foreignMap = new HashMap();
15+
foreignMap.put("key", "value");
16+
foreignMap.put(obj, 42);
17+
foreignMap.put(13.37, 3.14);
18+
19+
assertArrayEquals(["key", obj, 13.37], forIn(foreignMap));
20+
assertArrayEquals([["key", "value"], [obj, 42], [13.37, 3.14]], forOf(foreignMap));
21+
assertArrayEquals([["key", "value"], [obj, 42], [13.37, 3.14]], Array.from(foreignMap));
22+
23+
// keys() returns member keys (only)!
24+
let keys = Object.keys(foreignMap);
25+
assertFalse(keys.includes("key"));
26+
27+
let jsMap = new Map(foreignMap);
28+
assertSame("value", jsMap.get("key"));
29+
assertSame(42, jsMap.get(obj));
30+
assertSame(3.14, jsMap.get(13.37));
31+
32+
let objFromMap = Object.fromEntries(foreignMap);
33+
assertSame("value", objFromMap.key);
34+
assertSame(42, objFromMap[obj]); // key is '[object Object]'
35+
assertSame(3.14, objFromMap[13.37]);
36+
37+
38+
function forIn(iterable) {
39+
let result = [];
40+
for (let i in iterable) {
41+
result.push(i);
42+
}
43+
return result;
44+
}
45+
46+
function forOf(iterable) {
47+
let result = [];
48+
for (let i of iterable) {
49+
result.push(i);
50+
}
51+
return result;
52+
}
53+
54+
function assertArrayEquals(expected, actual) {
55+
if (!Array.isArray(actual)) {
56+
throw new Error(`Not an array: ${actual}`);
57+
}
58+
if (expected.length != actual.length) {
59+
throw new Error(`Expected length: ${expected.length}, actual length: ${actual.length}`);
60+
}
61+
62+
for (let i = 0; i < expected.length; i++) {
63+
let e = expected[i];
64+
let a = actual[i];
65+
if (Array.isArray(e)) {
66+
assertArrayEquals(e, a);
67+
} else {
68+
assertSame(e, a);
69+
}
70+
}
71+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
6+
*/
7+
8+
load('../assert.js');
9+
10+
const HashMap = Java.type("java.util.LinkedHashMap");
11+
12+
let obj = {};
13+
14+
let foreignMap = new HashMap();
15+
foreignMap.put("key", "value");
16+
foreignMap.put(obj, 42);
17+
foreignMap.put(13.37, 3.14);
18+
19+
assertSame("value", foreignMap["key"]);
20+
assertSame("value", foreignMap.key);
21+
assertSame(42, foreignMap[obj]);
22+
assertSame(3.14, foreignMap[13.37]);
23+
assertSame(undefined, foreignMap['unknown']);
24+
assertSame(undefined, foreignMap.unknown);
25+
26+
foreignMap["key2"] = "value2"
27+
foreignMap.key3 = "value3"
28+
assertSame("value2", foreignMap.key2);
29+
assertSame("value3", foreignMap["key" + "3"]);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
6+
*/
7+
8+
/*
9+
* Tests foreign map method vs. hash entry precedence.
10+
*
11+
* @option foreign-object-prototype
12+
*/
13+
14+
load('../assert.js');
15+
16+
const HashMap = Java.type("java.util.LinkedHashMap");
17+
18+
let doNotCallMe = () => {fail("should not be called");};
19+
let callMeMaybe = () => 'ok';
20+
21+
let foreignMap = new HashMap();
22+
foreignMap.put("size", 42);
23+
foreignMap.put("clear", doNotCallMe);
24+
foreignMap.put("callMeMaybe", callMeMaybe);
25+
foreignMap.put("set", doNotCallMe);
26+
foreignMap.put("notCallable", 42);
27+
28+
// invocation favors methods over hash entries.
29+
assertSame(42, foreignMap.size);
30+
assertSame(5, foreignMap.size());
31+
32+
// invocation favors foreign object prototype methods over hash entries, too.
33+
foreignMap.set("set", callMeMaybe);
34+
assertSame(callMeMaybe, foreignMap.set);
35+
36+
assertThrows(() => foreignMap.doesNotExist(), TypeError);
37+
assertThrows(() => foreignMap.notCallable(), TypeError);
38+
39+
// if the hash entry is callable and there's no member with the same name, we should be able to invoke it.
40+
assertSame('ok', foreignMap.callMeMaybe());
41+
42+
assertSame(doNotCallMe, foreignMap.clear);
43+
foreignMap.clear();
44+
assertSame(0, foreignMap.size());
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
6+
*/
7+
8+
/*
9+
* Tests Map.prototype methods on foreign maps.
10+
*
11+
* @option foreign-object-prototype
12+
*/
13+
14+
load('../assert.js');
15+
16+
const HashMap = Java.type("java.util.LinkedHashMap");
17+
18+
let obj = {};
19+
let unknownKey = {};
20+
21+
let foreignMap = new HashMap();
22+
foreignMap.put("key", "value");
23+
24+
foreignMap.set(obj, 42);
25+
foreignMap.set(13.37, 43);
26+
foreignMap.set("removeMe", "please");
27+
28+
assertSame(4, foreignMap.size());
29+
30+
assertSame(42, foreignMap.get(obj));
31+
assertTrue(undefined == foreignMap.get(unknownKey));
32+
assertTrue(foreignMap.has("removeMe"));
33+
assertTrue(foreignMap.delete("removeMe"));
34+
assertFalse(foreignMap.has("removeMe"));
35+
assertFalse(foreignMap.delete("removeMe"));
36+
37+
assertArrayEquals([["key", "value"], [obj, 42], [13.37, 43]], forOf(foreignMap.entries()));
38+
assertArrayEquals(["key", obj, 13.37], forOf(foreignMap.keys()));
39+
assertArrayEquals(["value", 42, 43], forOf(foreignMap.values()));
40+
41+
assertSame(42, Map.prototype.get.call(foreignMap, obj));
42+
assertSame(undefined, Map.prototype.get.call(foreignMap, unknownKey));
43+
assertSame(foreignMap, Map.prototype.set.call(foreignMap, obj, 42));
44+
assertSame(true, Map.prototype.has.call(foreignMap, obj));
45+
assertSame(false, Map.prototype.has.call(foreignMap, unknownKey));
46+
47+
for (let prototypeCall of [true, false]) {
48+
let size = 0;
49+
let entries = [];
50+
let callback = (k, v) => {
51+
size++;
52+
entries.push([k, v]);
53+
}
54+
if (prototypeCall) {
55+
Map.prototype.forEach.call(foreignMap, callback);
56+
} else {
57+
foreignMap.forEach(callback);
58+
}
59+
assertSame(3, size);
60+
assertArrayEquals([["key", "value"], [obj, 42], [13.37, 43]], forOf(foreignMap.entries()));
61+
}
62+
63+
Map.prototype.clear.call(foreignMap);
64+
assertSame(0, foreignMap.size());
65+
66+
function forOf(iterable) {
67+
let result = [];
68+
for (let i of iterable) {
69+
result.push(i);
70+
}
71+
return result;
72+
}
73+
74+
function assertArrayEquals(expected, actual) {
75+
if (!Array.isArray(actual)) {
76+
throw new Error(`Not an array: ${actual}`);
77+
}
78+
if (expected.length != actual.length) {
79+
throw new Error(`Expected length: ${expected.length}, actual length: ${actual.length}`);
80+
}
81+
82+
for (let i = 0; i < expected.length; i++) {
83+
let e = expected[i];
84+
let a = actual[i];
85+
if (Array.isArray(e)) {
86+
assertArrayEquals(e, a);
87+
} else {
88+
assertSame(e, a);
89+
}
90+
}
91+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
6+
*/
7+
8+
load('../assert.js');
9+
10+
const HashMap = Java.type("java.util.LinkedHashMap");
11+
12+
let foreignMap = new HashMap();
13+
foreignMap.put("key1", "value1");
14+
foreignMap.put("key2", "value2");
15+
foreignMap.put("key3", "value3");
16+
foreignMap.put(13.37, 3.14);
17+
18+
let spread = {...foreignMap};
19+
assertSame("value1", spread.key1);
20+
assertSame("value2", spread.key2);
21+
assertSame("value3", spread.key3);
22+
23+
let {key1, ...rest} = foreignMap;
24+
assertSame(undefined, rest.key1);
25+
assertSame("value2", rest.key2);
26+
assertSame("value3", rest.key3);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
6+
*/
7+
8+
/*
9+
* @option foreign-hash-properties=false
10+
*/
11+
12+
load('../assert.js');
13+
14+
const HashMap = Java.type("java.util.LinkedHashMap");
15+
16+
let foreignMap = new HashMap();
17+
foreignMap.put("key1", "value1");
18+
foreignMap.put("key2", "value2");
19+
20+
assertSame(undefined, foreignMap.key1);
21+
assertSame(undefined, foreignMap["key2"]);

graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/interop/ForeignBoxedObjectTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -71,8 +71,10 @@ public void testForeignBoxedString() {
7171
context.getBindings("js").putMember("obj", ForeignBoxedObject.createNew("foo"));
7272
assertTrue(context.eval(ID, "typeof Object(obj) === 'object'").asBoolean());
7373
assertTrue(context.eval(ID, "typeof obj.includes === 'function'").asBoolean());
74+
assertTrue(context.eval(ID, "typeof obj['includes'] === 'function'").asBoolean());
7475
assertEquals("foo", context.eval(ID, "obj.toString()").asString());
7576
assertEquals("foo", context.eval(ID, "obj.valueOf()").asString());
77+
assertEquals("foo", context.eval(ID, "obj['valueOf']()").asString());
7678
assertTrue(context.eval(ID, "obj.includes('o')").asBoolean());
7779
}
7880
}
@@ -83,6 +85,7 @@ public void testForeignBoxedNumber() {
8385
context.getBindings("js").putMember("obj", ForeignBoxedObject.createNew(42));
8486
assertTrue(context.eval(ID, "typeof Object(obj) === 'object'").asBoolean());
8587
assertTrue(context.eval(ID, "typeof obj.valueOf === 'function'").asBoolean());
88+
assertTrue(context.eval(ID, "typeof obj['valueOf'] === 'function'").asBoolean());
8689
assertEquals("4.2e+1", context.eval(ID, "obj.toExponential()").asString());
8790
assertEquals("4e+1", context.eval(ID, "obj.toExponential(0)").asString());
8891
assertEquals("42.00", context.eval(ID, "obj.toFixed(2)").asString());
@@ -91,6 +94,7 @@ public void testForeignBoxedNumber() {
9194
assertEquals("42.0", context.eval(ID, "obj.toPrecision(3)").asString());
9295
assertEquals("42", context.eval(ID, "obj.toString()").asString());
9396
assertEquals(42, context.eval(ID, "obj.valueOf()").asInt());
97+
assertEquals(42, context.eval(ID, "obj['valueOf']()").asInt());
9498
}
9599
}
96100

@@ -100,7 +104,9 @@ public void testForeignBoxedBoolean() {
100104
context.getBindings("js").putMember("obj", ForeignBoxedObject.createNew(true));
101105
assertTrue(context.eval(ID, "typeof Object(obj) === 'object'").asBoolean());
102106
assertTrue(context.eval(ID, "typeof obj.valueOf === 'function'").asBoolean());
107+
assertTrue(context.eval(ID, "typeof obj['valueOf'] === 'function'").asBoolean());
103108
assertTrue(context.eval(ID, "obj.valueOf()").asBoolean());
109+
assertTrue(context.eval(ID, "obj['valueOf']()").asBoolean());
104110
assertEquals("true", context.eval(ID, "obj.toString()").asString());
105111
}
106112
}

0 commit comments

Comments
 (0)