[dart2wasm] Handle is/as checks that only require nullability check in backend.

Up to Patchset 4 reverts commit 36fb02decad14ae79e6e410fa46340cd9502cada

Remaining Patchsets implement specialized support in is/as check
implementation to handle cases where only a null check is required. The
most common case in iterators:

  class <...>Iterator<T> {
    T? _current;

    T get current => _current as T;
  }

This ensures we're never emitting the general subtype checking code as
this only requres checking whether the object is non-`null` or whether
the destination type is declared nullable.

The asymmetry between `is` and `as` checks comes due to the fact that
the `as` checks have to (for the slow case) materialize a type object
for a nice error message.


Issue https://github.com/dart-lang/sdk/issues/55516

Change-Id: Ie4a845816ec4eda37a5a2e78cac0aeda0e411abd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/381482
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart
index 94d3fb2..e1d17b9 100644
--- a/pkg/dart2wasm/lib/code_generator.dart
+++ b/pkg/dart2wasm/lib/code_generator.dart
@@ -2834,8 +2834,8 @@
         ? translator.topInfo.nullableType
         : translator.topInfo.nonNullableType;
     wrap(node.operand, boxedOperandType);
-    return types.emitAsCheck(
-        this, node.type, operandType, boxedOperandType, node.location);
+    return types.emitAsCheck(this, node.isCovarianceCheck, node.type,
+        operandType, boxedOperandType, node.location);
   }
 
   @override
@@ -2952,7 +2952,7 @@
     if (translator.options.minify) {
       // We don't need to include the name in the error message, so we can use
       // the optimized `as` checks.
-      types.emitAsCheck(this, testedAgainstType,
+      types.emitAsCheck(this, false, testedAgainstType,
           translator.coreTypes.objectNullableRawType, argumentType);
       b.drop();
     } else {
diff --git a/pkg/dart2wasm/lib/kernel_nodes.dart b/pkg/dart2wasm/lib/kernel_nodes.dart
index 91d52bb..31178f4 100644
--- a/pkg/dart2wasm/lib/kernel_nodes.dart
+++ b/pkg/dart2wasm/lib/kernel_nodes.dart
@@ -312,6 +312,8 @@
       index.getProcedure("dart:core", '_Closure', "_getClosureRuntimeType");
   late final Procedure getMasqueradedRuntimeType =
       index.getTopLevelProcedure("dart:core", "_getMasqueradedRuntimeType");
+  late final Procedure isNullabilityCheck =
+      index.getTopLevelProcedure("dart:core", "_isNullabilityCheck");
   late final Procedure isSubtype =
       index.getTopLevelProcedure("dart:core", "_isSubtype");
   late final Procedure isInterfaceSubtype =
diff --git a/pkg/dart2wasm/lib/transformers.dart b/pkg/dart2wasm/lib/transformers.dart
index 9956bc5..ef049a0 100644
--- a/pkg/dart2wasm/lib/transformers.dart
+++ b/pkg/dart2wasm/lib/transformers.dart
@@ -8,8 +8,6 @@
 import 'package:kernel/core_types.dart';
 import 'package:kernel/type_algebra.dart';
 import 'package:kernel/type_environment.dart';
-import 'package:vm/modular/transformations/type_casts_optimizer.dart'
-    as typeCastsOptimizer show transformAsExpression;
 
 import 'list_factory_specializer.dart';
 
@@ -727,12 +725,6 @@
     node.transformChildren(this);
     return node.receiver;
   }
-
-  @override
-  TreeNode visitAsExpression(AsExpression node) {
-    node.transformChildren(this);
-    return typeCastsOptimizer.transformAsExpression(node, typeContext);
-  }
 }
 
 class _AsyncStarFrame {
diff --git a/pkg/dart2wasm/lib/types.dart b/pkg/dart2wasm/lib/types.dart
index 0a88619..93fe46c 100644
--- a/pkg/dart2wasm/lib/types.dart
+++ b/pkg/dart2wasm/lib/types.dart
@@ -387,9 +387,11 @@
       operandTemp = b.addLocal(translator.topInfo.nullableType);
       b.local_tee(operandTemp);
     }
+    final checkOnlyNullAssignability =
+        _requiresOnlyNullAssignabilityCheck(operandType, testedAgainstType);
     final (typeToCheck, :checkArguments) =
         _canUseTypeCheckHelper(testedAgainstType, operandType);
-    if (typeToCheck != null) {
+    if (!checkOnlyNullAssignability && typeToCheck != null) {
       if (checkArguments) {
         for (final typeArgument in typeToCheck.typeArguments) {
           makeType(codeGen, typeArgument);
@@ -403,24 +405,35 @@
         final typeClassInfo =
             translator.classInfo[testedAgainstType.classNode]!;
         final typeArguments = testedAgainstType.typeArguments;
-        b.i32_const(encodedNullability(testedAgainstType));
-        b.i32_const(typeClassInfo.classId);
-        if (typeArguments.isEmpty) {
-          codeGen.call(translator.isInterfaceSubtype0.reference);
-        } else if (typeArguments.length == 1) {
-          makeType(codeGen, typeArguments[0]);
-          codeGen.call(translator.isInterfaceSubtype1.reference);
-        } else if (typeArguments.length == 2) {
-          makeType(codeGen, typeArguments[0]);
-          makeType(codeGen, typeArguments[1]);
-          codeGen.call(translator.isInterfaceSubtype2.reference);
+        if (checkOnlyNullAssignability) {
+          b.i32_const(encodedNullability(testedAgainstType));
+          codeGen.call(translator.isNullabilityCheck.reference);
         } else {
-          _makeTypeArray(codeGen, typeArguments);
-          codeGen.call(translator.isInterfaceSubtype.reference);
+          b.i32_const(encodedNullability(testedAgainstType));
+          b.i32_const(typeClassInfo.classId);
+          if (typeArguments.isEmpty) {
+            codeGen.call(translator.isInterfaceSubtype0.reference);
+          } else if (typeArguments.length == 1) {
+            makeType(codeGen, typeArguments[0]);
+            codeGen.call(translator.isInterfaceSubtype1.reference);
+          } else if (typeArguments.length == 2) {
+            makeType(codeGen, typeArguments[0]);
+            makeType(codeGen, typeArguments[1]);
+            codeGen.call(translator.isInterfaceSubtype2.reference);
+          } else {
+            _makeTypeArray(codeGen, typeArguments);
+            codeGen.call(translator.isInterfaceSubtype.reference);
+          }
         }
       } else {
         makeType(codeGen, testedAgainstType);
-        codeGen.call(translator.isSubtype.reference);
+        if (checkOnlyNullAssignability) {
+          b.struct_get(typeClassInfo.struct,
+              translator.fieldIndex[translator.typeIsDeclaredNullableField]!);
+          codeGen.call(translator.isNullabilityCheck.reference);
+        } else {
+          codeGen.call(translator.isSubtype.reference);
+        }
       }
     }
     if (translator.options.verifyTypeChecks) {
@@ -438,14 +451,22 @@
     }
   }
 
-  w.ValueType emitAsCheck(AstCodeGenerator codeGen, DartType testedAgainstType,
-      DartType operandType, w.RefType boxedOperandType,
+  w.ValueType emitAsCheck(
+      AstCodeGenerator codeGen,
+      bool isCovarianceCheck,
+      DartType testedAgainstType,
+      DartType operandType,
+      w.RefType boxedOperandType,
       [Location? location]) {
     final b = codeGen.b;
 
+    // Keep casts inserted by the CFE to ensure soundness of covariant types.
+    final checkOnlyNullAssignability = !isCovarianceCheck &&
+        _requiresOnlyNullAssignabilityCheck(operandType, testedAgainstType);
+
     final (typeToCheck, :checkArguments) =
         _canUseTypeCheckHelper(testedAgainstType, operandType);
-    if (typeToCheck != null) {
+    if (!checkOnlyNullAssignability && typeToCheck != null) {
       if (checkArguments) {
         for (final typeArgument in typeToCheck.typeArguments) {
           makeType(codeGen, typeArgument);
@@ -460,6 +481,7 @@
     b.local_tee(operand);
 
     late List<w.ValueType> outputsToDrop;
+    b.i32_const(encodedBool(checkOnlyNullAssignability));
     if (testedAgainstType is InterfaceType &&
         classForType(testedAgainstType) == translator.interfaceTypeClass) {
       final typeClassInfo = translator.classInfo[testedAgainstType.classNode]!;
@@ -518,6 +540,17 @@
     return (null, checkArguments: false);
   }
 
+  bool _requiresOnlyNullAssignabilityCheck(
+      DartType operandType, DartType testedAgainstType) {
+    // We may only need to check that either of these hold:
+    //   * value is non-null
+    //   * value is null and the type is nullable.
+    return translator.typeEnvironment.isSubtypeOf(
+        operandType,
+        testedAgainstType.withDeclaredNullability(Nullability.nonNullable),
+        type_env.SubtypeCheckMode.withNullabilities);
+  }
+
   bool _staticTypesEnsureTypeArgumentsMatch(
       InterfaceType testedAgainstType, InterfaceType operandType) {
     assert(testedAgainstType.typeArguments.isNotEmpty);
@@ -640,6 +673,8 @@
 int encodedNullability(DartType type) =>
     type.declaredNullability == Nullability.nullable ? 1 : 0;
 
+int encodedBool(bool value) => value ? 1 : 0;
+
 class IsCheckerCallTarget extends CallTarget {
   final Translator translator;
 
diff --git a/pkg/front_end/testcases/dart2wasm/inference_update_2/issue52452.dart.strong.transformed.expect b/pkg/front_end/testcases/dart2wasm/inference_update_2/issue52452.dart.strong.transformed.expect
index 52ea82d..9324da8 100644
--- a/pkg/front_end/testcases/dart2wasm/inference_update_2/issue52452.dart.strong.transformed.expect
+++ b/pkg/front_end/testcases/dart2wasm/inference_update_2/issue52452.dart.strong.transformed.expect
@@ -128,13 +128,13 @@
 }
 static method testNoConflictWithNoSuchMethodForwarderIfImplementedInMixin1(final self::C c) → void {
   if(!(c.{self::C::_f3}{core::int?} == null)) {
-    final core::int x = let core::int? #t3 = c.{self::C::_f3}{core::int?} in #t3 == null ?{core::int} #t3 as{Unchecked} core::int : #t3{core::int};
+    final core::int x = c.{self::C::_f3}{core::int?} as{Unchecked} core::int;
     self::acceptsInt(x);
   }
 }
 static method testNoConflictWithNoSuchMethodForwarderIfImplementedInMixin2(final self::C c) → void {
   if(!(c.{self::C::_f4}{core::int?} == null)) {
-    final core::int x = let core::int? #t4 = c.{self::C::_f4}{core::int?} in #t4 == null ?{core::int} #t4 as{Unchecked} core::int : #t4{core::int};
+    final core::int x = c.{self::C::_f4}{core::int?} as{Unchecked} core::int;
     self::acceptsInt(x);
   }
 }
diff --git a/sdk/lib/_internal/wasm/lib/type.dart b/sdk/lib/_internal/wasm/lib/type.dart
index 2bd1bba..838c9eb 100644
--- a/sdk/lib/_internal/wasm/lib/type.dart
+++ b/sdk/lib/_internal/wasm/lib/type.dart
@@ -1261,10 +1261,17 @@
 
 @pragma("wasm:entry-point")
 @pragma("wasm:prefer-inline")
+bool _isNullabilityCheck(Object? o, bool isDeclaredNullable) {
+  if (o == null) return isDeclaredNullable;
+  return true;
+}
+
+@pragma("wasm:entry-point")
+@pragma("wasm:prefer-inline")
 bool _isInterfaceSubtype(Object? o, bool isDeclaredNullable, WasmI32 tId,
     WasmArray<_Type> typeArguments) {
+  if (o == null) return isDeclaredNullable;
   final t = _InterfaceType(tId, isDeclaredNullable, typeArguments);
-  if (o == null) return t.isDeclaredNullable;
   return t._checkInstance(o);
 }
 
@@ -1300,44 +1307,65 @@
 
 @pragma("wasm:entry-point")
 @pragma("wasm:prefer-inline")
-void _asSubtype(Object? o, _Type t) {
-  if (!_isSubtype(o, t)) {
+void _asSubtype(Object? o, bool onlyNullabilityCheck, _Type t) {
+  final bool success =
+      (onlyNullabilityCheck && _isNullabilityCheck(o, t.isDeclaredNullable)) ||
+          _isSubtype(o, t);
+  if (!success) {
     _TypeError._throwAsCheckError(o, t);
   }
 }
 
 @pragma("wasm:entry-point")
 @pragma("wasm:prefer-inline")
-void _asInterfaceSubtype(Object? o, bool isDeclaredNullable, WasmI32 tId,
-    WasmArray<_Type> typeArguments) {
-  if (!_isInterfaceSubtype(o, isDeclaredNullable, tId, typeArguments)) {
+void _asInterfaceSubtype(Object? o, bool onlyNullabilityCheck,
+    bool isDeclaredNullable, WasmI32 tId, WasmArray<_Type> typeArguments) {
+  final bool success =
+      (onlyNullabilityCheck && _isNullabilityCheck(o, isDeclaredNullable)) ||
+          _isInterfaceSubtype(o, isDeclaredNullable, tId, typeArguments);
+  if (!success) {
     _throwInterfaceTypeAsCheckError(o, isDeclaredNullable, tId, typeArguments);
   }
 }
 
 @pragma("wasm:entry-point")
 @pragma("wasm:prefer-inline")
-void _asInterfaceSubtype0(Object? o, bool isDeclaredNullable, WasmI32 tId) {
-  if (!_isInterfaceSubtype0(o, isDeclaredNullable, tId)) {
+void _asInterfaceSubtype0(Object? o, bool onlyNullabilityCheck,
+    bool isDeclaredNullable, WasmI32 tId) {
+  final bool success =
+      (onlyNullabilityCheck && _isNullabilityCheck(o, isDeclaredNullable)) ||
+          _isInterfaceSubtype0(o, isDeclaredNullable, tId);
+  if (!success) {
     _throwInterfaceTypeAsCheckError0(o, isDeclaredNullable, tId);
   }
 }
 
 @pragma("wasm:entry-point")
 @pragma("wasm:prefer-inline")
-void _asInterfaceSubtype1(
-    Object? o, bool isDeclaredNullable, WasmI32 tId, _Type typeArgument0) {
-  if (!_isInterfaceSubtype1(o, isDeclaredNullable, tId, typeArgument0)) {
+void _asInterfaceSubtype1(Object? o, bool onlyNullabilityCheck,
+    bool isDeclaredNullable, WasmI32 tId, _Type typeArgument0) {
+  final bool success =
+      (onlyNullabilityCheck && _isNullabilityCheck(o, isDeclaredNullable)) ||
+          _isInterfaceSubtype1(o, isDeclaredNullable, tId, typeArgument0);
+  if (!success) {
     _throwInterfaceTypeAsCheckError1(o, isDeclaredNullable, tId, typeArgument0);
   }
 }
 
 @pragma("wasm:entry-point")
 @pragma("wasm:prefer-inline")
-void _asInterfaceSubtype2(Object? o, bool isDeclaredNullable, WasmI32 tId,
-    _Type typeArgument0, _Type typeArgument1) {
-  if (!_isInterfaceSubtype2(
-      o, isDeclaredNullable, tId, typeArgument0, typeArgument1)) {
+void _asInterfaceSubtype2(
+    Object? o,
+    bool onlyNullabilityCheck,
+    bool isDeclaredNullable,
+    WasmI32 tId,
+    _Type typeArgument0,
+    _Type typeArgument1) {
+  final bool success =
+      (onlyNullabilityCheck && _isNullabilityCheck(o, isDeclaredNullable)) ||
+          _isInterfaceSubtype2(
+              o, isDeclaredNullable, tId, typeArgument0, typeArgument1);
+  if (!success) {
     _throwInterfaceTypeAsCheckError2(
         o, isDeclaredNullable, tId, typeArgument0, typeArgument1);
   }