[js_types] Add Helpers to work with `JSUndefined` and `JSNull`.
Before we can reify `JSUndefined` and `JSNull` we need to give users a way to detect these values. Once existing users have migrated to these new helpers, then we can start boxing `JSUndefined` and `JSNull` on Dart2Wasm.
There are a few steps here:
1) Land these helpers, but for now they will just preserve the existing semantics.
2) Deploy JS types to Flutter and `package:test`, i.e. everywhere Dart2Wasm's JS interop is currently being used, and use these helpers instead of `null` checks.
3) Switch the semantics of the helpers to stop conflating on all Web backends, while simultaneously boxing `JSNull` and `JSUndefined` on Dart2Wasm.
CoreLibraryReviewExempt: Refactoring web only libraries + some changes to Wasm's internal libraries.
Change-Id: Idb50b28b3087438751557ffd28505c7b536bf78b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/282481
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Joshua Litt <joshualitt@google.com>
diff --git a/pkg/dart2wasm/lib/js_runtime_generator.dart b/pkg/dart2wasm/lib/js_runtime_generator.dart
index 91bcce6..e377a15 100644
--- a/pkg/dart2wasm/lib/js_runtime_generator.dart
+++ b/pkg/dart2wasm/lib/js_runtime_generator.dart
@@ -433,7 +433,7 @@
Expression expression;
VariableGet v = VariableGet(positionalParameters[i]);
if (_isStaticInteropType(callbackParameterType) && boxExternRef) {
- expression = _invokeOneArg(_jsValueBoxTarget, v);
+ expression = _createJSValue(v);
} else {
expression = AsExpression(
_invokeOneArg(_dartifyRawTarget, v), callbackParameterType);
@@ -630,6 +630,9 @@
type));
}
+ Expression _createJSValue(Expression value) =>
+ ConstructorInvocation(_jsValueConstructor, Arguments([value]));
+
/// Lowers an invocation of `<Function>.toJS` to:
///
/// JSValue(jsWrapperFunction(<Function>))
@@ -639,16 +642,11 @@
_createFunctionTrampoline(node, type, boxExternRef: true);
Procedure jsWrapperFunction =
_getJSWrapperFunction(type, functionTrampolineName, node.fileUri);
- return ConstructorInvocation(
- _jsValueConstructor,
+ return _createJSValue(StaticInvocation(
+ jsWrapperFunction,
Arguments([
- StaticInvocation(
- jsWrapperFunction,
- Arguments([
- StaticInvocation(
- _jsObjectFromDartObjectTarget, Arguments([argument]))
- ]))
- ]));
+ StaticInvocation(_jsObjectFromDartObjectTarget, Arguments([argument]))
+ ])));
}
InstanceInvocation _invokeMethod(
@@ -711,6 +709,9 @@
} else {
Expression expression;
if (_isStaticInteropType(returnType)) {
+ // TODO(joshualitt): Expose boxed `JSNull` and `JSUndefined` to Dart
+ // code after migrating existing users of js interop on Dart2Wasm.
+ // expression = _createJSValue(invocation);
expression = _invokeOneArg(_jsValueBoxTarget, invocation);
} else {
expression = AsExpression(
diff --git a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
index 87f8dd7..47e7240 100644
--- a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
+++ b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
@@ -2,12 +2,23 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'dart:_foreign_helper' show JS;
import 'dart:_internal' show patch;
import 'dart:_js_types';
-import 'dart:js';
+import 'dart:js_util';
import 'dart:js_util';
import 'dart:typed_data';
+/// Helper for working with the [JSAny?] top type in a backend agnostic way.
+/// TODO(joshualitt): Remove conflation of null and undefined after migration.
+extension NullableUndefineableJSAnyExtension on JSAny? {
+ @patch
+ bool get isUndefined => this == null || typeofEquals(this, 'undefined');
+
+ @patch
+ bool get isNull => this == null || JS('bool', '# === null', this);
+}
+
/// [JSExportedDartFunction] <-> [Function]
extension JSExportedDartFunctionToFunction on JSExportedDartFunction {
@patch
diff --git a/sdk/lib/_internal/wasm/lib/js_helper.dart b/sdk/lib/_internal/wasm/lib/js_helper.dart
index 3684e96..295904c 100644
--- a/sdk/lib/_internal/wasm/lib/js_helper.dart
+++ b/sdk/lib/_internal/wasm/lib/js_helper.dart
@@ -20,21 +20,24 @@
// TODO(joshualitt): In many places we use `WasmExternRef?` when the ref can't
// be null, we should use `WasmExternRef` in those cases.
-/// [JSValue] is just a box [WasmExternRef]. For now, it is the single box for
+/// [JSValue] is just a box [WasmExternRef?]. For now, it is the single box for
/// all JS types, but in time we may want to make each JS type a unique box
/// type.
class JSValue {
- final WasmExternRef _ref;
+ final WasmExternRef? _ref;
JSValue(this._ref);
- // Currently we always explicitly box JS ref's in [JSValue] objects. In the
- // future, we will want to leave these values unboxed when possible, even when
- // they are nullable.
+ // This is currently only used in js_util.
+ // TODO(joshualitt): Investigate migrating `js_util` to js types. It should be
+ // non-breaking for js backends, and a tractable migration for wasm backends.
static JSValue? box(WasmExternRef? ref) =>
isDartNull(ref) ? null : JSValue(ref!);
- static WasmExternRef? unbox(JSValue? v) => v == null ? null : v._ref;
+ // We need to handle the case of a nullable [JSValue] to match the semantics
+ // of the JS backends.
+ static WasmExternRef? unbox(JSValue? v) =>
+ v == null ? WasmExternRef.nullRef : v._ref;
@override
bool operator ==(Object that) =>
@@ -56,7 +59,7 @@
String toString() => stringify(_ref);
// Overrides to avoid using [ObjectToJS].
- WasmExternRef toExternRef() => _ref;
+ WasmExternRef? toExternRef() => _ref;
JSValue toJS() => this;
}
@@ -79,7 +82,8 @@
WasmExternRef toExternRef() => jsObjectFromDartObject(this);
}
-// For now both `null` and `undefined` in JS map to `null` in Dart.
+// For `dartify` and `jsify`, we match the conflation of `JSUndefined`, `JSNull`
+// and `null`.
bool isDartNull(WasmExternRef? ref) => ref.isNull || isJSUndefined(ref);
// Extensions for [JSArray] and [JSObject].
@@ -373,10 +377,7 @@
bool isWasmGCStruct(WasmExternRef? ref) => ref.internalize()?.isObject ?? false;
Object? dartifyRaw(WasmExternRef? ref) {
- if (ref.isNull) {
- return null;
- } else if (isJSUndefined(ref)) {
- // TODO(joshualitt): Introduce a `JSUndefined` type.
+ if (ref.isNull || isJSUndefined(ref)) {
return null;
} else if (isJSBoolean(ref)) {
return toDartBool(ref);
@@ -413,7 +414,7 @@
} else if (isWasmGCStruct(ref)) {
return jsObjectToDartObject(ref);
} else {
- return JSValue.box(ref);
+ return JSValue(ref);
}
}
@@ -455,7 +456,7 @@
ByteBuffer toDartByteBuffer(WasmExternRef? ref) =>
toDartByteData(callConstructorVarArgsRaw(
- getConstructorString('DataView'), [JSValue.box(ref)].toExternRef()))
+ getConstructorString('DataView'), [JSValue(ref)].toExternRef()))
.buffer;
ByteData toDartByteData(WasmExternRef? ref) {
@@ -503,7 +504,7 @@
List<JSAny?> toDartListJSAny(WasmExternRef? ref) => List<JSAny?>.generate(
objectLength(ref).round(),
- (int n) => JSValue.box(objectReadIndex(ref, n.toDouble())) as JSAny?);
+ (int n) => JSValue(objectReadIndex(ref, n.toDouble())) as JSAny?);
List<Object?> toDartList(WasmExternRef? ref) => List<Object?>.generate(
objectLength(ref).round(),
@@ -524,7 +525,7 @@
getPropertyRaw(globalThisRaw(), name.toExternRef());
/// Equivalent to `Object.keys(object)`.
-JSArray objectKeys(JSObject object) => JSValue.box(callMethodVarArgsRaw(
+JSArray objectKeys(JSObject object) => JSValue(callMethodVarArgsRaw(
getConstructorRaw('Object'),
'keys'.toExternRef(),
[object].toExternRef())!) as JSArray;
diff --git a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart
index 56731fc..5038360 100644
--- a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart
@@ -5,6 +5,7 @@
import 'dart:_internal' show patch;
import 'dart:_js_helper';
import 'dart:async' show Completer;
+import 'dart:js_interop';
import 'dart:js_util' show NullRejectionException;
import 'dart:typed_data';
import 'dart:wasm';
@@ -12,8 +13,20 @@
/// Some helpers for working with JS types internally. If we implement the JS
/// types as inline classes then these should go away.
/// TODO(joshualitt): Find a way to get rid of the explicit casts.
-WasmExternRef _ref<T>(T o) => (o as JSValue).toExternRef();
-T _box<T>(WasmExternRef? ref) => JSValue.box(ref) as T;
+WasmExternRef? _ref<T>(T o) => (o as JSValue).toExternRef();
+T _box<T>(WasmExternRef? ref) => JSValue(ref) as T;
+
+/// Helper for working with the [JSAny?] top type in a backend agnostic way.
+extension NullableUndefineableJSAnyExtension on JSAny? {
+ // TODO(joshualitt): To support incremental migration of existing users to
+ // reified `JSUndefined` and `JSNull`, we have to handle the case where
+ // `this == null`. However, after migration we can remove these checks.
+ @patch
+ bool get isUndefined => this == null || isJSUndefined(_ref<JSAny>(this!));
+
+ @patch
+ bool get isNull => this == null || _ref<JSAny>(this!).isNull;
+}
/// [JSExportedDartFunction] <-> [Function]
extension JSExportedDartFunctionToFunction on JSExportedDartFunction {
diff --git a/sdk/lib/_internal/wasm/lib/js_util_patch.dart b/sdk/lib/_internal/wasm/lib/js_util_patch.dart
index df4010a..5761e1c 100644
--- a/sdk/lib/_internal/wasm/lib/js_util_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/js_util_patch.dart
@@ -39,7 +39,7 @@
o is Float64List ||
o is ByteBuffer ||
o is ByteData) {
- return JSValue.box(jsifyRaw(o));
+ return JSValue(jsifyRaw(o));
}
if (o is Map) {
@@ -60,7 +60,7 @@
return convertedIterable;
} else {
// None of the objects left will require recursive conversions.
- return JSValue.box(jsifyRaw(o));
+ return JSValue(jsifyRaw(o));
}
}
@@ -143,6 +143,10 @@
@patch
bool lessThanOrEqual<T>(Object? first, Object? second) => throw 'unimplemented';
+@patch
+bool typeofEquals<T>(Object? o, String type) =>
+ JS<bool>('(o, t) => typeof o === t', jsifyRaw(o), jsifyRaw(type));
+
typedef _PromiseSuccessFunc = void Function(Object? value);
typedef _PromiseFailureFunc = void Function(Object? error);
@@ -205,8 +209,9 @@
return o;
}
- WasmExternRef ref = o.toExternRef();
- if (isJSBoolean(ref) ||
+ WasmExternRef? ref = o.toExternRef();
+ if (ref.isNull ||
+ isJSBoolean(ref) ||
isJSNumber(ref) ||
isJSString(ref) ||
isJSUndefined(ref) ||
diff --git a/sdk/lib/_internal/wasm/lib/regexp_helper.dart b/sdk/lib/_internal/wasm/lib/regexp_helper.dart
index 43326fb..45085e4 100644
--- a/sdk/lib/_internal/wasm/lib/regexp_helper.dart
+++ b/sdk/lib/_internal/wasm/lib/regexp_helper.dart
@@ -114,8 +114,8 @@
RegExpMatch? firstMatch(String string) {
JSNativeMatch? m = _nativeRegExp.exec(string);
- if (m == null) return null;
- return new _MatchImplementation(this, m);
+ if (m.isUndefinedOrNull) return null;
+ return new _MatchImplementation(this, m!);
}
bool hasMatch(String string) {
@@ -139,18 +139,18 @@
JSNativeRegExp regexp = _nativeGlobalVersion;
regexp.lastIndex = start;
JSNativeMatch? match = regexp.exec(string);
- if (match == null) return null;
- return new _MatchImplementation(this, match);
+ if (match.isUndefinedOrNull) return null;
+ return new _MatchImplementation(this, match!);
}
RegExpMatch? _execAnchored(String string, int start) {
JSNativeRegExp regexp = _nativeAnchoredVersion;
regexp.lastIndex = start;
JSNativeMatch? match = regexp.exec(string);
- if (match == null) return null;
+ if (match.isUndefinedOrNull) return null;
// If the last capture group participated, the original regexp did not
// match at the start position.
- if (match.pop() != null) return null;
+ if (match!.pop() != null) return null;
return new _MatchImplementation(this, match);
}
@@ -200,8 +200,8 @@
String? namedGroup(String name) {
JSObject? groups = _match.groups;
- if (groups != null) {
- Object? result = groups[name];
+ if (groups.isDefinedAndNotNull) {
+ Object? result = groups![name];
if (result != null ||
hasPropertyRaw(
(groups as JSValue).toExternRef(), name.toExternRef())) {
@@ -213,8 +213,8 @@
Iterable<String> get groupNames {
JSObject? groups = _match.groups;
- if (groups != null) {
- return JSArrayIterableAdapter<String>(objectKeys(groups));
+ if (groups.isDefinedAndNotNull) {
+ return JSArrayIterableAdapter<String>(objectKeys(groups!));
}
return Iterable.empty();
}
diff --git a/sdk/lib/js_interop/js_interop.dart b/sdk/lib/js_interop/js_interop.dart
index a85b15f..264334d 100644
--- a/sdk/lib/js_interop/js_interop.dart
+++ b/sdk/lib/js_interop/js_interop.dart
@@ -93,7 +93,21 @@
/// The type of JS strings, [JSString] <: [JSAny].
typedef JSString = js_types.JSString;
-/// TODO(joshualitt): Figure out how we want to handle JSUndefined and JSNull.
+/// `JSUndefined` and `JSNull` are actual reified types on some backends, but
+/// not others. Instead, users should use nullable types for any type that could
+/// contain `JSUndefined` or `JSNull`. However, instead of trying to determine
+/// the nullability of a JS type in Dart, i.e. using `?`, `!`, `!= null` or `==
+/// null`, users should use the provided helpers below to determine if it is
+/// safe to downcast a potentially `JSNullable` or `JSUndefineable` object to a
+/// defined and non-null JS type.
+/// TODO(joshualitt): Investigate whether or not it will be possible to reify
+/// `JSUndefined` and `JSNull` on all backends.
+extension NullableUndefineableJSAnyExtension on JSAny? {
+ external bool get isUndefined;
+ external bool get isNull;
+ bool get isUndefinedOrNull => isUndefined || isNull;
+ bool get isDefinedAndNotNull => !isUndefinedOrNull;
+}
/// The type of `JSUndefined` when returned from functions. Unlike pure JS,
/// no actual object will be returned.
diff --git a/tests/lib/js/static_interop_test/js_types_test.dart b/tests/lib/js/static_interop_test/js_types_test.dart
index cb77cc9..9c27359 100644
--- a/tests/lib/js/static_interop_test/js_types_test.dart
+++ b/tests/lib/js/static_interop_test/js_types_test.dart
@@ -88,6 +88,15 @@
@JS()
external JSString str;
+@JS()
+external JSAny? nullAny;
+
+@JS()
+external JSAny? undefinedAny;
+
+@JS()
+external JSAny? definedNonNullAny;
+
class DartObject {
String get foo => 'bar';
}
@@ -100,6 +109,9 @@
globalThis.fun = function(a, b) {
return globalThis.edf(a, b);
}
+ globalThis.nullAny = null;
+ globalThis.undefinedAny = undefined;
+ globalThis.definedNonNullAny = {};
''');
// [JSObject]
@@ -218,6 +230,25 @@
expect(str is JSString, true);
String dartStr = str.toDart;
expect(dartStr, 'foo');
+
+ // null and undefined can flow into `JSAny?`.
+ // TODO(joshualitt): Fix tests when `JSNull` and `JSUndefined` are no longer
+ // conflated.
+ expect(nullAny.isNull, true);
+ //expect(nullAny.isUndefined, false);
+ expect(nullAny.isUndefined, true);
+ expect(nullAny.isDefinedAndNotNull, false);
+ expect(typeofEquals(nullAny, 'object'), true);
+ //expect(undefinedAny.isNull, false);
+ expect(undefinedAny.isNull, true);
+ expect(undefinedAny.isUndefined, true);
+ expect(undefinedAny.isDefinedAndNotNull, false);
+ //expect(typeofEquals(undefinedAny, 'undefined'), true);
+ //expect(typeofEquals(undefinedAny, 'object'), true);
+ expect(definedNonNullAny.isNull, false);
+ expect(definedNonNullAny.isUndefined, false);
+ expect(definedNonNullAny.isDefinedAndNotNull, true);
+ expect(typeofEquals(definedNonNullAny, 'object'), true);
}
@JS()