[dart2wasm] Use default value sentinel when member body unreachable

This issue was partially fixed by:
https://dart-review.googlesource.com/c/sdk/+/428541

However, this only looked at the case of a member in an abstract class
that itself was made abstract if it was unused. TFA does not make the
member abstract if the enclosing class is not abstract:
pkg/vm/lib/transformations/type_flow/transformer.dart#L2289

Instead we can use the unreachable metadata to identify these
unreachable body members (that have default values cleared):
pkg/vm/lib/transformations/type_flow/transformer.dart#L736

Attached test fails prior to this fix.

Bug: https://github.com/dart-lang/sdk/issues/62273
Change-Id: I1ccb818ec41bbe805ee828be0b96ba23e73beae4
Tested: Introduced unit test.
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/469280
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Nate Biggs <natebiggs@google.com>
This commit is contained in:
Nate Biggs
2026-01-06 21:24:51 -08:00
committed by Commit Queue
parent f4ad484867
commit 600f8a9601
6 changed files with 70 additions and 8 deletions
+26 -6
View File
@@ -7,6 +7,7 @@ import 'dart:math' show min;
import 'package:kernel/ast.dart';
import 'package:vm/metadata/procedure_attributes.dart';
import 'package:vm/metadata/table_selector.dart';
import 'package:vm/metadata/unreachable.dart';
import 'package:wasm_builder/wasm_builder.dart' as w;
import 'class_info.dart';
@@ -131,7 +132,11 @@ class SelectorInfo {
final references = source.readList(source.readReference);
final paramInfo = _parameterInfoFromReferences(
references, useSentinelForOptionalParameters);
source.component
.metadata[UnreachableNodeMetadataRepository.repositoryTag]
as UnreachableNodeMetadataRepository,
references,
useSentinelForOptionalParameters);
return SelectorInfo._(dispatchTable, id, name, callCount,
isSetter: isSetter)
..useMultipleEntryPoints = useMultipleEntryPoints
@@ -747,7 +752,9 @@ class DispatchTable {
selector.useMultipleEntryPoints = false;
selector._useSentinelForOptionalParameters = true;
selector.paramInfo = _parameterInfoFromReferences(
selector._references, selector._useSentinelForOptionalParameters);
translator.unreachableMetadata,
selector._references,
selector._useSentinelForOptionalParameters);
} else {
// Will be initialized in the `selectorTargets.forEach()` below.
}
@@ -814,7 +821,9 @@ class DispatchTable {
selector._useSentinelForOptionalParameters = true;
}
selector.paramInfo = _parameterInfoFromReferences(
selector._references, selector._useSentinelForOptionalParameters);
translator.unreachableMetadata,
selector._references,
selector._useSentinelForOptionalParameters);
// Split up [ranges] into those that are statically dispatched to and
// those are used via dispatch table.
@@ -1076,15 +1085,26 @@ bool _isUsedViaDispatchTableCall(SelectorInfo selector) {
}
ParameterInfo _parameterInfoFromReferences(
List<Reference> references, bool useDefaultValueSentinel) {
UnreachableNodeMetadataRepository unreachableMetadata,
List<Reference> references,
bool useDefaultValueSentinel) {
final unreachableNodeMapping = unreachableMetadata.mapping;
// We know all target implementations (closed world) if all of them use
// the same default value for optionals, we can make the caller pass it.
final first = references.first;
final firstMember = first.asMember;
final paramInfo = ParameterInfo.fromMember(
first, useDefaultValueSentinel || first.asMember.isAbstract);
first,
useDefaultValueSentinel ||
firstMember.isAbstract ||
unreachableNodeMapping[firstMember] != null);
for (final target in references.skip(1)) {
final targetMember = target.asMember;
paramInfo.merge(ParameterInfo.fromMember(
target, useDefaultValueSentinel || target.asMember.isAbstract));
target,
useDefaultValueSentinel ||
targetMember.isAbstract ||
unreachableNodeMapping[targetMember] != null));
}
return paramInfo;
}
+2 -1
View File
@@ -178,11 +178,12 @@ class DataSerializer {
}
class DataDeserializer {
final Component component;
final _BinaryDataSource _source;
final _IdToEntityMapper _mapper;
final List<String> _stringIndexer = [];
DataDeserializer(Uint8List bytes, Component component)
DataDeserializer(Uint8List bytes, this.component)
: _source = _BinaryDataSource(bytes),
_mapper = _IdToEntityMapper(component);
+4
View File
@@ -15,6 +15,7 @@ import 'package:vm/metadata/direct_call.dart';
import 'package:vm/metadata/inferred_type.dart';
import 'package:vm/metadata/procedure_attributes.dart';
import 'package:vm/metadata/unboxing_info.dart';
import 'package:vm/metadata/unreachable.dart';
import 'package:wasm_builder/wasm_builder.dart' as w;
import 'class_info.dart';
@@ -184,6 +185,9 @@ class Translator with KernelNodes {
(component.metadata[ProcedureAttributesMetadataRepository.repositoryTag]
as ProcedureAttributesMetadataRepository)
.mapping;
late final UnreachableNodeMetadataRepository unreachableMetadata =
component.metadata[UnreachableNodeMetadataRepository.repositoryTag]
as UnreachableNodeMetadataRepository;
// Other parts of the global compiler state.
@override
+2
View File
@@ -16,6 +16,7 @@ import 'package:vm/metadata/procedure_attributes.dart'
show ProcedureAttributesMetadataRepository;
import 'package:vm/metadata/table_selector.dart'
show TableSelectorMetadataRepository;
import 'package:vm/metadata/unreachable.dart';
final bool compilerAssertsEnabled = (() {
bool compilerAsserts = false;
@@ -104,6 +105,7 @@ String intToMinString(int i) {
Component createEmptyComponent() {
return Component()
..addMetadataRepository(UnreachableNodeMetadataRepository())
..addMetadataRepository(ProcedureAttributesMetadataRepository())
..addMetadataRepository(TableSelectorMetadataRepository())
..addMetadataRepository(DirectCallMetadataRepository())
+3 -1
View File
@@ -19,8 +19,10 @@ class UnreachableNode {
/// Repository for [UnreachableNode].
class UnreachableNodeMetadataRepository
extends MetadataRepository<UnreachableNode> {
static const repositoryTag = 'vm.unreachable.metadata';
@override
final String tag = 'vm.unreachable.metadata';
final String tag = repositoryTag;
@override
final Map<TreeNode, UnreachableNode> mapping = <TreeNode, UnreachableNode>{};
+33
View File
@@ -0,0 +1,33 @@
// 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.
class A {
void foo({String s = 'hi'}) {
print(s);
}
}
class B extends A {
@override
void foo({String s = 'there'}) {
print(s);
}
}
A getA(int x) {
if (x < 10) {
return B();
} else {
// A is instantiated but the type cannot flow into a 'foo' call so A.foo's
// body is not reachable.
throw 'here';
return A();
}
}
void main() {
getA(9).foo();
// Ensure the default value is not inlined into the function.
getA(8).foo(s: 'wow');
}