diff --git a/pkg/dart2wasm/lib/types.dart b/pkg/dart2wasm/lib/types.dart index fe1b32ea494..035cf921e05 100644 --- a/pkg/dart2wasm/lib/types.dart +++ b/pkg/dart2wasm/lib/types.dart @@ -6,6 +6,7 @@ import 'dart:math' show max; import 'package:kernel/ast.dart'; import 'package:kernel/core_types.dart'; +import 'package:kernel/src/bounds_checks.dart' show calculateBounds; import 'package:kernel/type_environment.dart' as type_env; import 'package:wasm_builder/wasm_builder.dart' as w; @@ -534,6 +535,12 @@ class Types { ]) { final b = codeGen.b; + // If this is a covariance check, we cannot trust the static operand type + // arguments, so we rewrite it to a safe version. + operandType = isCovarianceCheck + ? _safeCovarianceOperandType(operandType) + : operandType; + // Keep casts inserted by the CFE to ensure soundness of covariant types. final checkOnlyNullAssignability = !isCovarianceCheck && @@ -595,6 +602,41 @@ class Types { return operand.type; } + /// Safely rewrites the static [operandType] of a covariance check to a + /// version that is safe to trust for optimizations, while preserving + /// class structure and nullability. + /// + /// During covariance checks (inserted by the CFE for covariant overrides), + /// we cannot trust the static type arguments of the operand because the + /// static type of the operand might be narrower than its actual runtime type + /// (i.e. the runtime value might be a supertype of the static type), + /// which would violate soundness if we optimized based on the static type arguments. + /// + /// To ensure soundness while still allowing class-check optimizations: + /// - If [operandType] is an [InterfaceType], we keep the class node and + /// nullability, but rewrite all its type arguments to their upper bounds + /// using [calculateBounds]. + /// - Otherwise, we fall back to [Object?] or [Object] depending on + /// whether [operandType] is potentially nullable. + DartType _safeCovarianceOperandType(DartType operandType) { + if (operandType is InterfaceType) { + if (operandType.classNode.typeParameters.isEmpty) { + return operandType; + } + return InterfaceType( + operandType.classNode, + operandType.nullability, + calculateBounds( + operandType.classNode.typeParameters, + translator.coreTypes.objectClass, + ), + ); + } + return operandType.isPotentiallyNullable + ? translator.coreTypes.objectNullableRawType + : translator.coreTypes.objectNonNullableRawType; + } + bool _requiresOnlyNullAssignabilityCheck( DartType operandType, DartType testedAgainstType, diff --git a/pkg/dart2wasm/test/ir_tests/covariance_check.dart b/pkg/dart2wasm/test/ir_tests/covariance_check.dart new file mode 100644 index 00000000000..c3526e9310b --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/covariance_check.dart @@ -0,0 +1,28 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// 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. + +// functionFilter=covarianceCheckMain +// functionFilter=as Callable +// typeFilter=NoMatch +// globalFilter=NoMatch +// compilerOption=-O0 + +class Callable {} + +class Fields { + final Callable contravariantUse = + Callable(); +} + +void main() { + covarianceCheckMain(); +} + +void covarianceCheckMain() { + Fields fields = Fields(); + // This getter access statically returns Callable, + // but at runtime returns Callable. + // This triggers a covariance check (AsExpression) on the return value. + fields.contravariantUse; +} diff --git a/pkg/dart2wasm/test/ir_tests/covariance_check.wat b/pkg/dart2wasm/test/ir_tests/covariance_check.wat new file mode 100644 index 00000000000..fb4c41a0794 --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/covariance_check.wat @@ -0,0 +1,106 @@ +(module $module0 + (type $#Top <...>) + (type $Array<_Type> <...>) + (type $Callable <...>) + (type $Fields <...>) + (type $_FunctionType <...>) + (type $_InterfaceType <...>) + (type $_Type <...>) + (global $_FunctionType (ref $_FunctionType) <...>) + (global $_InterfaceType (ref $_InterfaceType) <...>) + (func $" as Callable" (param $var0 (ref $#Top)) (param $var1 (ref $_Type)) (result (ref $Callable)) + (local $var2 (ref $#Top)) + (local $var3 (ref $_Type)) + (local $var4 i32) + (local $var5 (ref $#Top)) + (local $var6 i32) + (local $var7 i32) + (local $var8 (ref $Array<_Type>)) + block $label0 + local.get $var0 + local.get $var1 + local.set $var3 + local.set $var2 + block $label1 (result i32) + block $label2 (result i32) + local.get $var2 + local.set $var5 + block $label3 (result i32) + block $label4 (result i32) + local.get $var5 + struct.get $#Top $field0 + local.set $var7 + block $label5 (result i32) + local.get $var7 + i32.const 106 + i32.eq + if + i32.const 1 + br $label5 + end + i32.const 0 + br $label5 + end $label5 + br $label4 + end $label4 + br $label3 + end $label3 + local.set $var4 + block $label6 + local.get $var4 + i32.const 1 + i32.eq + br_if $label6 + i32.const 0 + br $label2 + end $label6 + local.get $var2 + call $Object._getTypeArguments + local.set $var8 + local.get $var8 + i32.const 0 + array.get $Array<_Type> + local.get $var3 + call $_isTypeSubtype + local.set $var4 + block $label7 + local.get $var4 + i32.const 1 + i32.eq + br_if $label7 + i32.const 0 + br $label2 + end $label7 + i32.const 1 + br $label2 + end $label2 + br $label1 + end $label1 + br_if $label0 + local.get $var0 + i32.const 0 + i32.const 106 + local.get $var1 + call $"_throwInterfaceTypeAsCheckError1 " + unreachable + end $label0 + local.get $var0 + ref.cast $Callable + return + ) + (func $_throwInterfaceTypeAsCheckError1 (param $o (ref null $#Top)) (param $isDeclaredNullable i32) (param $tId i32) (param $typeArgument0 (ref $_Type)) <...>) + (func $Fields (param $var0 (ref $_Type)) (result (ref $Fields)) <...>) + (func $Object._getTypeArguments (param $object (ref $#Top)) (result (ref $Array<_Type>)) <...>) + (func $_isTypeSubtype (param $s (ref $_Type)) (param $t (ref $_Type)) (result i32) <...>) + (func $covarianceCheckMain + (local $fields (ref $Fields)) + global.get $_InterfaceType + call $Fields + local.set $fields + local.get $fields + struct.get $Fields $contravariantUse + global.get $_FunctionType + call $" as Callable" + drop + ) +) \ No newline at end of file