[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:
committed by
dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
a21f54e771
commit
23a289417d
@@ -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
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user