Skip to content

Commit 4e99b93

Browse files
committed
[GR-39067] ForeignObjectPrototype enabled by default.
PullRequest: js/2481
2 parents ed1eee6 + 94c7e0e commit 4e99b93

File tree

10 files changed

+337
-23
lines changed

10 files changed

+337
-23
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ See [version roadmap](https://www.graalvm.org/release-notes/version-roadmap/) fo
77

88
## Version 22.2.0
99
* GraalVM JavaScript is now an installable component of GraalVM. It can be installed with `gu install js`.
10+
* Enabled option `js.foreign-object-prototype` by default. Polyglot Interop objects now get a fitting JavaScript prototype assigned unless explicitly turned off using this flag.
11+
* Added intermediate prototype for foreign objects to simplify adapting functionality.
12+
* Removed deprecated experimental option `experimental-foreign-object-prototype`.
1013
* Removed experimental option `commonjs-global-properties`. The same functionality can be achieved in user code with a direct call to `require()` after context creation.
1114
* Added an experimental option `--js.zone-rules-based-time-zones` that allows to use timezone-related data from `ZoneRulesProvider` (instead of ICU4J data files).
1215
* Temporal objects can be converted to compatible Java objects when possible, using the `Value` API's methods like `asDate()`.
1316

17+
1418
## Version 22.1.0
1519
* Updated Node.js to version 16.14.2.
1620
* Graal.js now requires Java 11+ and no longer supports Java 8.

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

Lines changed: 238 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2019, 2022, 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
@@ -46,6 +46,7 @@
4646
import static org.junit.Assert.assertFalse;
4747
import static org.junit.Assert.assertTrue;
4848

49+
import java.time.Instant;
4950
import java.util.Arrays;
5051
import java.util.List;
5152

@@ -54,8 +55,13 @@
5455
import org.graalvm.polyglot.Value;
5556
import org.graalvm.polyglot.proxy.ProxyArray;
5657
import org.graalvm.polyglot.proxy.ProxyExecutable;
58+
import org.junit.Assert;
5759
import org.junit.Test;
5860

61+
import com.oracle.truffle.api.interop.InteropLibrary;
62+
import com.oracle.truffle.api.interop.TruffleObject;
63+
import com.oracle.truffle.api.library.ExportLibrary;
64+
import com.oracle.truffle.api.library.ExportMessage;
5965
import com.oracle.truffle.js.test.JSTest;
6066

6167
public class ForeignObjectPrototypeTest {
@@ -120,4 +126,235 @@ public void testHostMethodStatic() {
120126
}
121127
}
122128

129+
@Test
130+
public void testPrototype() {
131+
testPrototypeIntl("Array", ProxyArray.fromArray("fun", "with", "proxy", "array"));
132+
testPrototypeIntl("Date", Instant.now());
133+
testPrototypeIntl("Map", new TestTruffleHash());
134+
testPrototypeIntl("String", new TestTruffleString());
135+
testPrototypeIntl("Boolean", new TestTruffleBoolean());
136+
testPrototypeIntl("Number", new TestTruffleNumber());
137+
testPrototypeIntl("Function", (ProxyExecutable) v -> true);
138+
testPrototypeIntl("Object", new Object());
139+
}
140+
141+
private static void testPrototypeIntl(String prototype, Object obj) {
142+
String code = "(obj) => { var proto = Object.getPrototypeOf(obj); \n" +
143+
" var protoProto = Object.getPrototypeOf(proto); \n" +
144+
" return protoProto === " + prototype + ".prototype; \n" +
145+
"}";
146+
try (Context context = JSTest.newContextBuilder(ID).build()) {
147+
Value result = context.eval(ID, code).execute(obj);
148+
Assert.assertTrue(result.asBoolean());
149+
}
150+
}
151+
152+
@Test
153+
public void testPrototypeDisabled() {
154+
testDisabled(ProxyArray.fromArray("fun", "with", "proxy", "array"));
155+
testDisabled((ProxyExecutable) args -> "hi");
156+
testDisabled(Instant.now());
157+
}
158+
159+
private static void testDisabled(Object array) {
160+
String code = "(obj) => { return Object.getPrototypeOf(obj) === null; }";
161+
try (Context context = JSTest.newContextBuilder(ID).option(FOREIGN_OBJECT_PROTOTYPE_NAME, "false").build()) {
162+
Value result = context.eval(ID, code).execute(array);
163+
Assert.assertTrue(result.asBoolean());
164+
}
165+
}
166+
167+
@Test
168+
public void testHostMethodHasPrecedence() {
169+
String codeGetEpochSecond = "(obj) => { return obj.getEpochSecond(); }";
170+
try (Context context = JSTest.newContextBuilder(ID).allowHostAccess(HostAccess.ALL).build()) {
171+
Instant inst = Instant.now();
172+
173+
// check getter from Java can be called
174+
long second = context.eval(ID, codeGetEpochSecond).execute(inst).asLong();
175+
Assert.assertEquals(inst.getEpochSecond(), second);
176+
177+
// provide your own getter
178+
context.eval(ID, "(obj) => { Object.getPrototypeOf(obj).getEpochSecond = () => { return 666; }; };").execute(inst);
179+
180+
// verify that still the host getter is called
181+
Instant inst2 = Instant.now();
182+
Value result = context.eval(ID, codeGetEpochSecond).execute(inst2);
183+
Assert.assertEquals(inst2.getEpochSecond(), result.asLong());
184+
}
185+
}
186+
187+
@Test
188+
public void testJSBuiltinCanBeOverwritten() {
189+
String codeGetUTCString = "(obj) => { return obj.toUTCString(); }";
190+
try (Context context = JSTest.newContextBuilder(ID).allowHostAccess(HostAccess.ALL).build()) {
191+
Instant inst = Instant.now();
192+
193+
// check JS Prototype method can be called
194+
String utcString = context.eval(ID, codeGetUTCString).execute(inst).asString();
195+
Assert.assertTrue(utcString.length() > 10);
196+
197+
// provide your own getter
198+
context.eval(ID, "(obj) => { Object.getPrototypeOf(obj).toUTCString = () => { return 'special'; }; };").execute(inst);
199+
200+
// verify that on another object this overwritten method is called
201+
Instant inst2 = Instant.now();
202+
Value result = context.eval(ID, codeGetUTCString).execute(inst2);
203+
Assert.assertEquals("special", result.asString());
204+
}
205+
}
206+
207+
@Test
208+
public void testForeignInstanceof() {
209+
testInstanceofIntl("Array", ProxyArray.fromArray("fun", "with", "proxy", "array"));
210+
testPrototypeIntl("Date", Instant.now());
211+
testPrototypeIntl("Map", new TestTruffleHash());
212+
testPrototypeIntl("String", new TestTruffleString());
213+
testPrototypeIntl("Boolean", new TestTruffleBoolean());
214+
testPrototypeIntl("Number", new TestTruffleNumber());
215+
testPrototypeIntl("Function", (ProxyExecutable) v -> true);
216+
testPrototypeIntl("Object", new Object());
217+
}
218+
219+
private static void testInstanceofIntl(String prototype, Object obj) {
220+
String code = "(obj) => { return (obj instanceof " + prototype + "); }";
221+
try (Context context = JSTest.newContextBuilder(ID).build()) {
222+
Value result = context.eval(ID, code).execute(obj);
223+
Assert.assertTrue(result.asBoolean());
224+
}
225+
}
226+
227+
@ExportLibrary(InteropLibrary.class)
228+
public static class TestTruffleHash implements TruffleObject {
229+
230+
@ExportMessage
231+
@SuppressWarnings("static-method")
232+
boolean hasHashEntries() {
233+
return true;
234+
}
235+
236+
@ExportMessage
237+
@SuppressWarnings("static-method")
238+
long getHashSize() {
239+
return 0;
240+
}
241+
242+
@ExportMessage
243+
@SuppressWarnings("static-method")
244+
Object getHashEntriesIterator() {
245+
return null;
246+
}
247+
}
248+
249+
@ExportLibrary(InteropLibrary.class)
250+
public static class TestTruffleString implements TruffleObject {
251+
@ExportMessage
252+
@SuppressWarnings("static-method")
253+
boolean isString() {
254+
return true;
255+
}
256+
257+
@ExportMessage
258+
@SuppressWarnings("static-method")
259+
String asString() {
260+
return "";
261+
}
262+
}
263+
264+
@ExportLibrary(InteropLibrary.class)
265+
public static class TestTruffleBoolean implements TruffleObject {
266+
@ExportMessage
267+
@SuppressWarnings("static-method")
268+
boolean isBoolean() {
269+
return true;
270+
}
271+
272+
@ExportMessage
273+
@SuppressWarnings("static-method")
274+
boolean asBoolean() {
275+
return true;
276+
}
277+
}
278+
279+
@ExportLibrary(InteropLibrary.class)
280+
public static class TestTruffleNumber implements TruffleObject {
281+
@ExportMessage
282+
@SuppressWarnings("static-method")
283+
boolean isNumber() {
284+
return true;
285+
}
286+
287+
@ExportMessage
288+
@SuppressWarnings("static-method")
289+
final boolean fitsInByte() {
290+
return true;
291+
}
292+
293+
@ExportMessage
294+
@SuppressWarnings("static-method")
295+
final boolean fitsInShort() {
296+
return true;
297+
}
298+
299+
@ExportMessage
300+
@SuppressWarnings("static-method")
301+
final boolean fitsInInt() {
302+
return true;
303+
}
304+
305+
@ExportMessage
306+
@SuppressWarnings("static-method")
307+
final boolean fitsInLong() {
308+
return true;
309+
}
310+
311+
@ExportMessage
312+
@SuppressWarnings("static-method")
313+
final boolean fitsInFloat() {
314+
return true;
315+
}
316+
317+
@ExportMessage
318+
@SuppressWarnings("static-method")
319+
final boolean fitsInDouble() {
320+
return true;
321+
}
322+
323+
@ExportMessage
324+
@SuppressWarnings("static-method")
325+
final byte asByte() {
326+
return (byte) 0;
327+
}
328+
329+
@ExportMessage
330+
@SuppressWarnings("static-method")
331+
final short asShort() {
332+
return (short) 0;
333+
}
334+
335+
@ExportMessage
336+
@SuppressWarnings("static-method")
337+
final int asInt() {
338+
return 0;
339+
}
340+
341+
@ExportMessage
342+
@SuppressWarnings("static-method")
343+
final long asLong() {
344+
return 0L;
345+
}
346+
347+
@ExportMessage
348+
@SuppressWarnings("static-method")
349+
final float asFloat() {
350+
return 0.0F;
351+
}
352+
353+
@ExportMessage
354+
@SuppressWarnings("static-method")
355+
final double asDouble() {
356+
return 0.0D;
357+
}
358+
359+
}
123360
}

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/ObjectFunctionBuiltins.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -299,23 +299,28 @@ public ObjectGetPrototypeOfNode(JSContext context, JSBuiltin builtin) {
299299
}
300300

301301
@Specialization(guards = "!isJSObject(object)")
302-
protected JSDynamicObject getPrototypeOfNonObject(Object object) {
302+
protected JSDynamicObject getPrototypeOfNonObject(Object object,
303+
@Cached("createBinaryProfile()") ConditionProfile isForeignProfile) {
303304
if (getContext().getEcmaScriptVersion() < 6) {
304305
if (JSRuntime.isJSPrimitive(object)) {
305306
throw Errors.createTypeErrorNotAnObject(object);
306307
} else {
307308
return Null.instance;
308309
}
309310
} else {
310-
Object tobject = toObject(object);
311-
if (JSDynamicObject.isJSDynamicObject(tobject)) {
312-
return JSObject.getPrototype((JSDynamicObject) tobject);
313-
} else {
311+
if (isForeignProfile.profile(JSRuntime.isForeignObject(object))) {
312+
if (InteropLibrary.getUncached(object).isNull(object)) {
313+
throw Errors.createTypeErrorNotAnObject(object);
314+
}
314315
if (getContext().getContextOptions().hasForeignObjectPrototype()) {
315-
return getForeignObjectPrototype(tobject);
316+
return getForeignObjectPrototype(object);
316317
} else {
317318
return Null.instance;
318319
}
320+
} else {
321+
assert JSRuntime.isJSPrimitive(object);
322+
Object tobject = toObject(object);
323+
return JSObject.getPrototype((JSDynamicObject) tobject);
319324
}
320325
}
321326
}

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/binary/InstanceofNode.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import com.oracle.truffle.js.nodes.binary.InstanceofNodeGen.OrdinaryHasInstanceNodeGen;
6969
import com.oracle.truffle.js.nodes.cast.JSToBooleanNode;
7070
import com.oracle.truffle.js.nodes.function.JSFunctionCallNode;
71+
import com.oracle.truffle.js.nodes.interop.ForeignObjectPrototypeNode;
7172
import com.oracle.truffle.js.nodes.unary.IsCallableNode;
7273
import com.oracle.truffle.js.runtime.BigInt;
7374
import com.oracle.truffle.js.runtime.Errors;
@@ -236,7 +237,18 @@ protected boolean doIsBound(Object obj, JSDynamicObject check,
236237
return instanceofNode.executeBoolean(obj, boundTargetFunction);
237238
}
238239

239-
@Specialization(guards = {"!isJSObject(left)", "isJSFunction(right)", "!isBoundFunction(right)"})
240+
@Specialization(guards = {"!isJSObject(left)", "isForeignObject(left)", "isJSFunction(right)", "!isBoundFunction(right)"})
241+
protected boolean doForeignObject(@SuppressWarnings("unused") Object left, @SuppressWarnings("unused") JSDynamicObject right,
242+
@Cached ForeignObjectPrototypeNode getForeignPrototypeNode,
243+
@Cached @Shared("getPrototype1Node") GetPrototypeNode getPrototype1Node,
244+
@Cached @Shared("invalidPrototypeBranch") BranchProfile invalidPrototypeBranch) {
245+
Object rightProto = getConstructorPrototype(right, invalidPrototypeBranch);
246+
Object foreignProto = getForeignPrototypeNode.execute(left);
247+
Object foreignProtoProto = getPrototype1Node.execute(foreignProto);
248+
return rightProto == foreignProtoProto;
249+
}
250+
251+
@Specialization(guards = {"!isJSObject(left)", "!isForeignObject(left)", "isJSFunction(right)", "!isBoundFunction(right)"})
240252
protected boolean doNotAnObject(@SuppressWarnings("unused") Object left, @SuppressWarnings("unused") JSDynamicObject right) {
241253
return false;
242254
}

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/interop/ForeignObjectPrototypeNode.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,23 @@ public JSDynamicObject doTruffleObject(Object truffleObject,
6464
@CachedLibrary("truffleObject") InteropLibrary interop) {
6565
JSRealm realm = getRealm();
6666
if (interop.hasArrayElements(truffleObject)) {
67-
return realm.getArrayPrototype();
67+
return realm.getForeignArrayPrototype();
6868
} else if (interop.isInstant(truffleObject)) {
69-
return realm.getDatePrototype();
69+
return realm.getForeignDatePrototype();
7070
} else if (interop.hasHashEntries(truffleObject)) {
71-
return realm.getMapPrototype();
71+
return realm.getForeignMapPrototype();
7272
} else if (interop.hasIterator(truffleObject)) {
7373
return realm.getForeignIterablePrototype();
7474
} else if (interop.isString(truffleObject)) {
75-
return realm.getStringPrototype();
75+
return realm.getForeignStringPrototype();
7676
} else if (interop.isNumber(truffleObject)) {
77-
return realm.getNumberPrototype();
77+
return realm.getForeignNumberPrototype();
7878
} else if (interop.isBoolean(truffleObject)) {
79-
return realm.getBooleanPrototype();
79+
return realm.getForeignBooleanPrototype();
8080
} else if (interop.isExecutable(truffleObject) || interop.isInstantiable(truffleObject)) {
81-
return realm.getFunctionPrototype();
81+
return realm.getForeignFunctionPrototype();
8282
} else {
83-
return realm.getObjectPrototype();
83+
return realm.getForeignObjectPrototype();
8484
}
8585
}
8686

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,6 +1498,10 @@ public boolean isOptionAsyncStackTraces() {
14981498
return contextOptions.isAsyncStackTraces();
14991499
}
15001500

1501+
public boolean isOptionForeignObjectPrototype() {
1502+
return contextOptions.hasForeignObjectPrototype();
1503+
}
1504+
15011505
public long getTimerResolution() {
15021506
assert !(getInitialEnvironment() != null && getInitialEnvironment().isPreInitialization()) : "Patchable option timer-resolution accessed during context pre-initialization.";
15031507
return contextOptions.getTimerResolution();

0 commit comments

Comments
 (0)