Skip to content

Commit 7882084

Browse files
mkustermannCommit Queue
authored andcommitted
[dart2wasm] Correctly deal with abstract members in selector building
When we create a `Selector` we compute various information based on all members belonging to that selector. Before this CL this was split up: Some things like `ParameterInfo` was calculated&updated while discovering members, others was calculated after all members were discovered. We now make all fields whose value depends on knowing all members `late final` and initialize those fields after having found all members of a selector. When we compute the `ParameterInfo` of the selector we take special care: If there's non-abstract implementations and the selector is not overridable by dynamic modules then we create the `ParameterInfo` by looking only at the non-abstract members => This increases precision as we ignore the types and default values of optional parameters for abstract members If there's no implementations of the selector it may still be referred to by instance invocation AST nodes (which are unreachable). The current code generator still needs a `Selector.signature` in order to evaluate arguments for such nodes. => We calculate `ParameterInfo` based on the abstract member and use `defaultSentinel` for optional parameters. If a selector can have implementations in dynamic modules then we cannot determine whether all implementations have the same default value of optional parameters. => We calculate `ParameterInfo` using `defaultSentinel` value for all optional parameters. Ensuring the caller doesn't pass them but the callee will default to it's default value if it wasn't passed by caller. See added test case for when we (before this CL) may use unreachable members in the param info / signature calculation and how that could lead to a compiler bug (before this CL) due to a missing `VariableDeclaration.initializer` TEST=web/wasm/unreachable_selector_implementation_test Change-Id: I3628177f8e93c5d24ff0eb0ff8fee6121a4a08d1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/428541 Reviewed-by: Nate Biggs <[email protected]> Commit-Queue: Martin Kustermann <[email protected]>
1 parent 5ed14b2 commit 7882084

File tree

4 files changed

+152
-50
lines changed

4 files changed

+152
-50
lines changed

pkg/dart2wasm/lib/dispatch_table.dart

Lines changed: 106 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class SelectorInfo {
3939
final int callCount;
4040

4141
/// Least upper bound of [ParameterInfo]s of all targets.
42-
final ParameterInfo paramInfo;
42+
late final ParameterInfo paramInfo;
4343

4444
/// Is this an implicit or explicit setter?
4545
final bool isSetter;
@@ -48,10 +48,14 @@ class SelectorInfo {
4848
///
4949
/// We create multiple entry points when any implementation of this selector
5050
/// performs type checks on the passed arguments.
51-
bool useMultipleEntryPoints = false;
51+
late bool useMultipleEntryPoints;
5252

53-
bool isDynamicSubmoduleOverridable = false;
54-
bool isDynamicSubmoduleCallable = false;
53+
late bool isDynamicSubmoduleOverridable;
54+
late bool isDynamicSubmoduleCallable;
55+
56+
/// Whether the computation of [paramInfo] should enforce usage of sentinels
57+
/// for optional parameters.
58+
late bool _useSentinelForOptionalParameters;
5559

5660
/// Wasm function type for the selector.
5761
///
@@ -65,6 +69,7 @@ class SelectorInfo {
6569
SelectorTargets? _unchecked;
6670
SelectorTargets? _normal;
6771

72+
/// The set of references we use to calculate the [paramInfo].
6873
final List<Reference> _references = [];
6974

7075
SelectorTargets targets({required bool unchecked}) {
@@ -80,8 +85,7 @@ class SelectorInfo {
8085
this.dispatchTable,
8186
this.id,
8287
this.name,
83-
this.callCount,
84-
this.paramInfo, {
88+
this.callCount, {
8589
required this.isSetter,
8690
});
8791

@@ -94,6 +98,7 @@ class SelectorInfo {
9498
useMultipleEntryPoints,
9599
isDynamicSubmoduleOverridable,
96100
isDynamicSubmoduleCallable,
101+
_useSentinelForOptionalParameters,
97102
]);
98103
sink.writeNullable(_checked, (targets) => targets.serialize(sink));
99104
sink.writeNullable(_unchecked, (targets) => targets.serialize(sink));
@@ -111,6 +116,7 @@ class SelectorInfo {
111116
useMultipleEntryPoints,
112117
isDynamicSubmoduleOverridable,
113118
isDynamicSubmoduleCallable,
119+
useSentinelForOptionalParameters,
114120
] = source.readBoolList();
115121
final checked =
116122
source.readNullable(() => SelectorTargets.deserialize(source));
@@ -120,22 +126,19 @@ class SelectorInfo {
120126
source.readNullable(() => SelectorTargets.deserialize(source));
121127
final references = source.readList(source.readReference);
122128

123-
final paramInfo = references.skip(1).fold(
124-
ParameterInfo.fromMember(references.first),
125-
(p, e) => p..merge(ParameterInfo.fromMember(e)));
126-
return SelectorInfo._(dispatchTable, id, name, callCount, paramInfo,
129+
final paramInfo = _parameterInfoFromReferences(
130+
references, useSentinelForOptionalParameters);
131+
return SelectorInfo._(dispatchTable, id, name, callCount,
127132
isSetter: isSetter)
128133
..useMultipleEntryPoints = useMultipleEntryPoints
129134
..isDynamicSubmoduleCallable = isDynamicSubmoduleCallable
130135
..isDynamicSubmoduleOverridable = isDynamicSubmoduleOverridable
136+
.._useSentinelForOptionalParameters = useSentinelForOptionalParameters
131137
.._checked = checked
132138
.._unchecked = unchecked
133-
.._normal = normal;
134-
}
135-
136-
void _addImplementationReference(Reference reference) {
137-
_references.add(reference);
138-
paramInfo.merge(ParameterInfo.fromMember(reference));
139+
.._normal = normal
140+
.._references.addAll(references)
141+
..paramInfo = paramInfo;
139142
}
140143

141144
String entryPointName(bool unchecked) {
@@ -473,9 +476,6 @@ class DispatchTable {
473476
metadata.methodOrSetterCalledDynamically ||
474477
member.name.text == "call");
475478

476-
final isDynamicSubmoduleOverridable =
477-
member.isDynamicSubmoduleOverridable(translator.coreTypes);
478-
479479
// The compiler will generate calls to `noSuchMethod` in the dynamic
480480
// invocation forwarders. So we ensure that the call count is positive.
481481
final isNoSuchMethod = member == translator.objectNoSuchMethod;
@@ -484,20 +484,9 @@ class DispatchTable {
484484
final selector = _selectorInfo.putIfAbsent(
485485
selectorId,
486486
() => SelectorInfo._(this, selectorId, member.name.text, callCount,
487-
ParameterInfo.fromMember(target),
488487
isSetter: isSetter));
489488
assert(selector.isSetter == isSetter);
490-
final useMultipleEntryPoints = !member.isAbstract &&
491-
!member.isExternal &&
492-
!target.isGetter &&
493-
!target.isTearOffReference &&
494-
translator.needToCheckTypesFor(member);
495-
selector.useMultipleEntryPoints |= useMultipleEntryPoints;
496-
selector.isDynamicSubmoduleOverridable |= isDynamicSubmoduleOverridable;
497-
selector.isDynamicSubmoduleCallable |=
498-
member.isDynamicSubmoduleCallable(translator.coreTypes);
499-
500-
selector._addImplementationReference(target);
489+
selector._references.add(target);
501490

502491
if (calledDynamically) {
503492
if (isGetter) {
@@ -639,6 +628,30 @@ class DispatchTable {
639628
}
640629
}
641630

631+
_selectorInfo.forEach((_, selector) {
632+
bool isDynamicSubmoduleCallable = false;
633+
bool isDynamicSubmoduleOverridable = false;
634+
for (final target in selector._references) {
635+
final member = target.asMember;
636+
isDynamicSubmoduleOverridable |=
637+
member.isDynamicSubmoduleOverridable(translator.coreTypes);
638+
isDynamicSubmoduleCallable |=
639+
member.isDynamicSubmoduleCallable(translator.coreTypes);
640+
}
641+
selector.isDynamicSubmoduleOverridable = isDynamicSubmoduleOverridable;
642+
selector.isDynamicSubmoduleCallable = isDynamicSubmoduleCallable;
643+
644+
if (!selectorTargets.containsKey(selector)) {
645+
// There are no concrete implementations for the given [selector].
646+
selector._normal = SelectorTargets([], []);
647+
selector.useMultipleEntryPoints = false;
648+
selector._useSentinelForOptionalParameters = true;
649+
selector.paramInfo = _parameterInfoFromReferences(
650+
selector._references, selector._useSentinelForOptionalParameters);
651+
} else {
652+
// Will be initialized in the `selectorTargets.forEach()` below.
653+
}
654+
});
642655
selectorTargets
643656
.forEach((SelectorInfo selector, Map<int, Reference> targets) {
644657
final List<({Range range, Reference target})> ranges = targets.entries
@@ -664,6 +677,47 @@ class DispatchTable {
664677
}
665678
ranges.length = writeIndex + 1;
666679

680+
bool useMultipleEntryPoints = false;
681+
final implementationReferences = <Reference>[];
682+
for (final targetRange in ranges) {
683+
final target = targetRange.target;
684+
final member = target.asMember;
685+
assert(!member.isAbstract);
686+
687+
// Compute [useMultipleEntryPoints]
688+
if (!member.isExternal &&
689+
!target.isGetter &&
690+
!target.isTearOffReference &&
691+
translator.needToCheckTypesFor(member)) {
692+
useMultipleEntryPoints = true;
693+
}
694+
implementationReferences.add(target);
695+
}
696+
selector.useMultipleEntryPoints = useMultipleEntryPoints;
697+
698+
if (!selector.isDynamicSubmoduleOverridable &&
699+
implementationReferences.isNotEmpty) {
700+
// We have global knowledge of all targets of the selector. We can use
701+
// this global knowledge to compute the [ParameterInfo].
702+
selector._references.clear();
703+
selector._references.addAll(implementationReferences);
704+
selector._useSentinelForOptionalParameters = false;
705+
} else {
706+
// Case 1) We may have no targets for the selector, but there may
707+
// still be calls to it (see e.g. https://dartbug.com/60733).
708+
//
709+
// Case 2) We may have 3rd party implementations of the selector
710+
// in a dynamic module with unknown default values for optionals.
711+
//
712+
// => We make caller pass sentinel if the optional parameter is not
713+
// provided.
714+
selector._useSentinelForOptionalParameters = true;
715+
}
716+
selector.paramInfo = _parameterInfoFromReferences(
717+
selector._references, selector._useSentinelForOptionalParameters);
718+
719+
// Split up [ranges] into those that are statically dispatched to and
720+
// those are used via dispatch table.
667721
final staticDispatchRanges = selector.isDynamicSubmoduleOverridable
668722
? const <({Range range, Reference target})>[]
669723
: (translator.options.polymorphicSpecialization || ranges.length == 1)
@@ -752,20 +806,6 @@ class DispatchTable {
752806
}
753807
}
754808

755-
_selectorInfo.forEach((_, selector) {
756-
if (!selectorTargets.containsKey(selector)) {
757-
// There are no concrete implementations for the given [selector].
758-
// But there may be an abstract interface target which is targed by a
759-
// call. In this case the call should be unreachable.
760-
if (selector.useMultipleEntryPoints) {
761-
selector._checked = SelectorTargets(const [], const []);
762-
selector._unchecked = SelectorTargets(const [], const []);
763-
} else {
764-
selector._normal = SelectorTargets(const [], const []);
765-
}
766-
}
767-
});
768-
769809
_initializeWasmTable();
770810
}
771811

@@ -872,14 +912,35 @@ bool _isUsedViaDispatchTableCall(SelectorInfo selector) {
872912

873913
final targets = selector.targets(unchecked: false);
874914

915+
// If there's 0 or 1 target than the call sites will either emit an
916+
// unreachable or a direct call, so no call site goes via dispatch table, so
917+
// we don't need a row in the table.
875918
if (targets.targetRanges.length <= 1) return false;
919+
920+
// If all targets are dispatched to via static polymorphic dispatchers,
921+
// then no callsite will emit a dispatch table call, so we don't need a row in
922+
// the table.
876923
if (targets.staticDispatchRanges.length == targets.targetRanges.length) {
877924
return false;
878925
}
879926

880927
return true;
881928
}
882929

930+
ParameterInfo _parameterInfoFromReferences(
931+
List<Reference> references, bool useDefaultValueSentinel) {
932+
// We know all target implementations (closed world) if all of them use
933+
// the same default value for optionals, we can make the caller pass it.
934+
final first = references.first;
935+
final paramInfo = ParameterInfo.fromMember(
936+
first, useDefaultValueSentinel || first.asMember.isAbstract);
937+
for (final target in references.skip(1)) {
938+
paramInfo.merge(ParameterInfo.fromMember(
939+
target, useDefaultValueSentinel || target.asMember.isAbstract));
940+
}
941+
return paramInfo;
942+
}
943+
883944
/// Build a row-displacement table based on fitting the [rows].
884945
///
885946
/// The returned list is the resulting row displacement table with `null`

pkg/dart2wasm/lib/param_info.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ class ParameterInfo {
5151
ParameterInfo._(this.takesContextOrReceiver, this.typeParamCount,
5252
this.positional, this.named);
5353

54-
factory ParameterInfo.fromMember(Reference target) {
54+
factory ParameterInfo.fromMember(
55+
Reference target, bool useDefaultValueSentinel) {
5556
final member = target.asMember; // Constructor, Field, or Procedure
57+
assert(!member.isAbstract || useDefaultValueSentinel);
5658
final function = member.function;
5759

5860
if (target.isTearOffReference) {
@@ -74,12 +76,18 @@ class ParameterInfo {
7476
List.generate(function.positionalParameters.length, (i) {
7577
// A required parameter has no default value.
7678
if (i < function.requiredParameterCount) return null;
77-
return _defaultValue(function.positionalParameters[i]);
79+
if (useDefaultValueSentinel) return defaultValueSentinel;
80+
return _defaultValue(function.positionalParameters[i])!;
7881
});
7982

8083
final named = {
8184
for (VariableDeclaration param in function.namedParameters)
82-
param.name!: _defaultValue(param)
85+
if (param.isRequired)
86+
param.name!: null
87+
else
88+
param.name!: useDefaultValueSentinel
89+
? defaultValueSentinel
90+
: _defaultValue(param)!,
8391
};
8492

8593
return ParameterInfo._(

pkg/dart2wasm/lib/translator.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,8 +1374,8 @@ class Translator with KernelNodes {
13741374
return selector.paramInfo;
13751375
}
13761376
}
1377-
return staticParamInfo.putIfAbsent(
1378-
target, () => ParameterInfo.fromMember(target));
1377+
return staticParamInfo.putIfAbsent(target,
1378+
() => ParameterInfo.fromMember(target, target.asMember.isAbstract));
13791379
}
13801380

13811381
w.ValueType preciseThisFor(Member member, {bool nullable = false}) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
void main() {
6+
final l = <Base>[Sub1(), Sub2()];
7+
for (final x in l) print(x.foo());
8+
print(Sub3);
9+
}
10+
11+
abstract class Base {
12+
String foo();
13+
}
14+
15+
class Sub1 extends Base {
16+
String foo() => 'Sub1.foo()';
17+
}
18+
19+
class Sub2 extends Base {
20+
String foo() => 'Sub2.foo()';
21+
}
22+
23+
// This class is never allocated, TFA will make it abstract.
24+
class Sub3 extends Base {
25+
// This member is not callable (since the class is never allocated) but the
26+
// entrypoint annotation keps it alive. TFA will mark it as abstract and
27+
// set the `VariableDeclaration.initializer` of `a` to `null`.
28+
//
29+
// We should not take this member into account when creatin dispatch table
30+
// selector parameter info or signature.
31+
@pragma('wasm:entry-point')
32+
String foo({int a = 4}) => 'Sub3.foo($a)';
33+
}

0 commit comments

Comments
 (0)