[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()