[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<void Function(num)>).

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<void
Function(int)>) 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 <kustermann@google.com>
Auto-Submit: Kevin Moore <kevmoo@google.com>
Commit-Queue: Kevin Moore <kevmoo@google.com>
This commit is contained in:
Kevin Moore
2026-06-07 21:51:07 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent a21f54e771
commit 23a289417d
3 changed files with 176 additions and 0 deletions
+42
View File
@@ -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,
@@ -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<U> {}
class Fields<T> {
final Callable<void Function(T)> contravariantUse =
Callable<void Function(T)>();
}
void main() {
covarianceCheckMain();
}
void covarianceCheckMain() {
Fields<num> fields = Fields<int>();
// This getter access statically returns Callable<void Function(num)>,
// but at runtime returns Callable<void Function(int)>.
// This triggers a covariance check (AsExpression) on the return value.
fields.contravariantUse;
}
@@ -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 $"<obj> as Callable<T0>" (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 <noInline>"
unreachable
end $label0
local.get $var0
ref.cast $Callable
return
)
(func $_throwInterfaceTypeAsCheckError1 <noInline> (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 $"<obj> as Callable<T0>"
drop
)
)