From 23a289417dc762d093a47457ecc6208462f0e53a Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Sun, 7 Jun 2026 21:51:07 -0700 Subject: [PATCH] [dart2wasm] Fix unsound type-argument check optimization on covariance checks When casting a getter return value that requires a covariance check, the CFE inserts an AsExpression where both the operand type and tested-against type are statically identical (the instantiated member return type, e.g. Callable). The types.dart optimizer previously assumed that because the static types matched, the runtime type arguments must also match, and optimized away the type-argument checks. However, in a covariance check, the dynamic value returned is a supertype (e.g. Callable) due to class parameter covariance. This change safely rewrites the static operand type of a covariance check by preserving the InterfaceType structure but using calculateBounds to rewrite its type arguments to their upper bounds (falling back to Object? or Object for non-interface types). This allows us to keep class-check optimizations active while remaining sound. TEST=tests/language/covariant/callable_class_field_getter_test.dart Fixes https://github.com/dart-lang/sdk/issues/53091 Change-Id: Ia64ea90b1bad2f7c1dab81cc3385103507b97b3e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/508425 Reviewed-by: Martin Kustermann Auto-Submit: Kevin Moore Commit-Queue: Kevin Moore --- pkg/dart2wasm/lib/types.dart | 42 +++++++ .../test/ir_tests/covariance_check.dart | 28 +++++ .../test/ir_tests/covariance_check.wat | 106 ++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 pkg/dart2wasm/test/ir_tests/covariance_check.dart create mode 100644 pkg/dart2wasm/test/ir_tests/covariance_check.wat 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