[dyn_modules] dynamically-callable pragmas in annotator and TFA.

This change introduces the `dyn-module:dynamically-callable` and
`dyn-module:implicitly-dynamically-callable` entrypoint pragmas. It
changes the annotator to attach these pragmas to kernel nodes based
on the dynamic-interface and updates TFA to treat these nodes as
reachable and not elegible for tree-shaking.

On a later change, the pragma will be read by the VM to verify that
dynamic modules can only call dynamically members that were exposed
as dynamically callable in the dynamic interface..

TEST=pkg/vm/test/

Bug: b/448095881
Change-Id: I4a8dbe0614e23d8b921e83323eacf1a1b1171ab9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/495660
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Sigmund Cherem <sigmund@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
This commit is contained in:
Sigmund Cherem
2026-04-16 09:49:11 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent d75661c6d8
commit 14517e3735
15 changed files with 437 additions and 11 deletions
+11 -3
View File
@@ -10,16 +10,23 @@ import 'package:vm/transformations/pragma.dart';
import 'collector.dart';
bool _isDefault(ParsedEntryPointPragma pragma) =>
pragma.type == PragmaEntryPointType.Default ||
pragma.type == PragmaEntryPointType.DynamicallyCallable;
bool _allowsCall(ParsedEntryPointPragma pragma) =>
pragma.type == PragmaEntryPointType.Default ||
pragma.type == PragmaEntryPointType.DynamicallyCallable ||
pragma.type == PragmaEntryPointType.CallOnly;
bool _allowsGet(ParsedEntryPointPragma pragma) =>
pragma.type == PragmaEntryPointType.Default ||
pragma.type == PragmaEntryPointType.DynamicallyCallable ||
pragma.type == PragmaEntryPointType.GetterOnly;
bool _allowsSet(ParsedEntryPointPragma pragma) =>
pragma.type == PragmaEntryPointType.Default ||
pragma.type == PragmaEntryPointType.DynamicallyCallable ||
pragma.type == PragmaEntryPointType.SetterOnly;
EntryPointShimCollector visitLibrary(
@@ -69,13 +76,14 @@ class EntryPointShimVisitor extends RecursiveVisitor {
(p) =>
p.type != PragmaEntryPointType.Extendable &&
p.type != PragmaEntryPointType.ImplicitlyExtendable &&
p.type != PragmaEntryPointType.CanBeOverridden,
p.type != PragmaEntryPointType.CanBeOverridden &&
p.type != PragmaEntryPointType.CanBeUsedAsType,
);
if (pragmas.isEmpty) return null;
var pragma = pragmas.first;
if (pragma.type != PragmaEntryPointType.Default) {
if (!_isDefault(pragma)) {
for (final p in pragmas.skip(1)) {
if (p.type == PragmaEntryPointType.Default) {
if (_isDefault(p)) {
pragma = p;
break;
}
@@ -16,6 +16,8 @@ import 'pragma.dart'
kDynModuleCanBeUsedAsTypePragmaName,
kDynModuleImplicitlyCallablePragmaName,
kDynModuleImplicitlyExtendablePragmaName,
kDynModuleDynamicallyCallablePragmaName,
kDynModuleImplicitlyDynamicallyCallablePragmaName,
kDynModuleCanBeOverriddenImplicitlyPragmaName;
void annotateComponent(
@@ -129,6 +131,29 @@ void annotateComponent(
annotateInstanceMembers: false,
logger: logger,
);
logger?.setActiveSection('dynamically-callable');
final dynamicallyCallableAnnotator = annotateNodes(
spec.dynamicallyCallable,
kDynModuleDynamicallyCallablePragmaName,
baseUri,
coreTypes,
annotateClasses: false,
annotateFinalClasses: false,
annotateExtensionTypes: false,
annotateStaticMembers: false,
annotateInstanceMembers: true,
logger: logger,
);
final dynamicallyCallableImplicitUsesAnnotator = _ImplicitUsesAnnotator(
pragmaConstant(
coreTypes,
kDynModuleImplicitlyDynamicallyCallablePragmaName,
),
dynamicallyCallableAnnotator.annotatedClasses,
dynamicallyCallableAnnotator.annotatedMembers,
);
dynamicallyCallableImplicitUsesAnnotator.annotateDispatchTargets(component);
}
InstanceConstant pragmaConstant(CoreTypes coreTypes, String pragmaName) {
+10
View File
@@ -36,6 +36,10 @@ const kDynModuleCallablePragmaName = "dyn-module:callable";
const kDynModuleImplicitlyCallablePragmaName = "dyn-module:implicitly-callable";
const kDynModuleCanBeUsedAsTypePragmaName = "dyn-module:can-be-used-as-type";
const kDynModuleEntryPointPragmaName = "dyn-module:entry-point";
const kDynModuleDynamicallyCallablePragmaName =
"dyn-module:dynamically-callable";
const kDynModuleImplicitlyDynamicallyCallablePragmaName =
"dyn-module:implicitly-dynamically-callable";
abstract class ParsedPragma {}
@@ -48,6 +52,7 @@ enum PragmaEntryPointType {
SetterOnly,
CallOnly,
CanBeUsedAsType,
DynamicallyCallable,
}
enum PragmaRecognizedType { AsmIntrinsic, GraphIntrinsic, Other }
@@ -253,6 +258,11 @@ class ConstantPragmaAnnotationParser implements PragmaAnnotationParser {
case kDynModuleCallablePragmaName:
case kDynModuleImplicitlyCallablePragmaName:
return getEntryPointTypeFromOptions(options, pragmaName);
case kDynModuleDynamicallyCallablePragmaName:
case kDynModuleImplicitlyDynamicallyCallablePragmaName:
return const ParsedEntryPointPragma(
PragmaEntryPointType.DynamicallyCallable,
);
case kDynModuleEntryPointPragmaName:
return const ParsedDynModuleEntryPointPragma();
case kVmSharedPragmaName:
@@ -626,7 +626,10 @@ final class _DispatchableInvocation extends _Invocation {
if (selector.callKind != CallKind.PropertyGet) {
if (selector is DynamicSelector) {
typeFlowAnalysis._methodsAndSettersCalledDynamically.add(target);
typeFlowAnalysis.recordMemberCalledDynamically(
target,
isGetter: false,
);
} else if (selector is VirtualSelector) {
typeFlowAnalysis._calledViaThis.add(target);
} else {
@@ -634,7 +637,10 @@ final class _DispatchableInvocation extends _Invocation {
}
} else {
if (selector is DynamicSelector) {
typeFlowAnalysis._gettersCalledDynamically.add(target);
typeFlowAnalysis.recordMemberCalledDynamically(
target,
isGetter: true,
);
}
}
@@ -907,11 +913,10 @@ final class _DispatchableInvocation extends _Invocation {
// the mismatch in the number or names of arguments,
// it still participates in the dynamic lookup.
// So mark it as called dynamically so its signature is preserved.
if (selector.callKind != CallKind.PropertyGet) {
typeFlowAnalysis._methodsAndSettersCalledDynamically.add(target);
} else {
typeFlowAnalysis._gettersCalledDynamically.add(target);
}
typeFlowAnalysis.recordMemberCalledDynamically(
target,
isGetter: selector.callKind == CallKind.PropertyGet,
);
}
_ReceiverTypeBuilder _getReceiverTypeBuilder(
@@ -2333,6 +2338,15 @@ class TypeFlowAnalysis
_tearOffTaken.add(target);
}
@override
void recordMemberCalledDynamically(Member target, {required bool isGetter}) {
if (isGetter) {
_gettersCalledDynamically.add(target);
} else {
_methodsAndSettersCalledDynamically.add(target);
}
}
@override
Procedure getClosureCallMethod(Closure closure) =>
_callMethodByClosure[closure] ??= _createCallMethod(closure);
@@ -41,6 +41,9 @@ abstract class EntryPointsListener {
/// Record the fact that given member is torn off.
void recordTearOff(Member target) {}
/// Record the fact that given member is called dynamically.
void recordMemberCalledDynamically(Member target, {required bool isGetter});
/// Artificial call method corresponding to the given [closure].
Procedure getClosureCallMethod(Closure closure);
@@ -202,6 +205,22 @@ class PragmaEntryPointsVisitor extends RecursiveVisitor {
}
}
break;
case PragmaEntryPointType.DynamicallyCallable:
if (proc.isGetter) {
addSelector(CallKind.PropertyGet);
entryPoints.recordMemberCalledDynamically(proc, isGetter: true);
} else if (proc.isSetter) {
addSelector(CallKind.PropertySet);
entryPoints.recordMemberCalledDynamically(proc, isGetter: false);
} else {
addSelector(CallKind.Method);
entryPoints.recordMemberCalledDynamically(proc, isGetter: false);
if (!proc.isFactory) {
addSelector(CallKind.PropertyGet);
entryPoints.recordMemberCalledDynamically(proc, isGetter: true);
}
}
break;
case PragmaEntryPointType.Extendable:
case PragmaEntryPointType.ImplicitlyExtendable:
throw "Error: only class can be extendable";
@@ -287,6 +306,14 @@ class PragmaEntryPointsVisitor extends RecursiveVisitor {
addSelector(CallKind.PropertySet);
}
break;
case PragmaEntryPointType.DynamicallyCallable:
addSelector(CallKind.PropertyGet);
entryPoints.recordMemberCalledDynamically(field, isGetter: true);
if (field.hasSetter) {
addSelector(CallKind.PropertySet);
entryPoints.recordMemberCalledDynamically(field, isGetter: false);
}
break;
case PragmaEntryPointType.CallOnly:
throw "Error: 'call' is not a valid entry-point pragma annotation "
"argument for the field $field.\n$_referenceToDocumentation";
@@ -624,6 +624,9 @@ class _EntryPointsListenerImpl implements EntryPointsListener {
@override
void addDynamicallyExtendableClass(Class c) {}
@override
void recordMemberCalledDynamically(Member target, {required bool isGetter}) {}
@override
Field getRecordPositionalField(RecordShape shape, int pos) =>
throw 'Unsupported operation';
@@ -72,6 +72,9 @@ class FakeEntryPointsListener implements EntryPointsListener {
@override
void recordTearOff(Member target) {}
@override
void recordMemberCalledDynamically(Member target, {required bool isGetter}) {}
@override
Procedure getClosureCallMethod(Closure closure) => closure.createCallMethod();
File diff suppressed because one or more lines are too long
@@ -33,3 +33,22 @@ can-be-overridden:
class: 'D'
member: 'build'
- library: 'lib4.dart'
dynamically-callable:
- library: 'lib6.dart'
class: ['A', 'B', 'D']
- library: 'lib6.dart'
class: E
member: dcMethod5
- library: 'lib6.dart'
class: E
member: 'get:dcGetter3'
- library: 'lib6.dart'
class: E
member: 'set:dcSetter3'
- library: 'lib6.dart'
class: E
member: dcField7
- library: 'lib6.dart'
class: E
member: dcField8
@@ -0,0 +1,62 @@
// 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 dcMethod1() {}
void dcMethod2(String x, int y) {}
int get dcGetter1 => 1;
void set dcSetter1(int value) {}
static void staticMethod1() {}
A();
factory A.factory() => A();
}
class A2 extends A {
@override
void dcMethod1() {
print('1');
}
}
class A3 implements A {
@override
void dcMethod1() {
print('1');
}
@override
void dcMethod2(String x, int y) {}
@override
int get dcGetter1 => 1;
@override
void set dcSetter1(int value) {}
}
class B {
final int dcField1 = 1;
int dcField2 = 1;
void dcMethod3() {}
void dcMethod4(String x, int y) {}
int get dcGetter2 => 1;
void set dcSetter2(int value) {}
}
class C {
int _dcPrivateMethod1() => 1;
}
mixin D on C {
int method1() => (this as dynamic)._dcPrivateMethod1();
}
class E {
void dcMethod5() {}
int get dcGetter3 => 1;
void set dcSetter3(int value) {}
final int dcField7 = 1;
int dcField8 = 1;
int dcField9 = 1; // unexposed
}
@@ -0,0 +1,106 @@
library;
import self as self;
import "dart:core" as core;
class A extends core::Object {
constructor •() → self::A
: super core::Object::•()
;
@#C3
method dcMethod1() → void {}
@#C3
method dcMethod2(core::String x, core::int y) → void {}
@#C3
get dcGetter1() → core::int
return 1;
@#C3
set dcSetter1(core::int value) → void {}
static method staticMethod1() → void {}
static factory factory() → self::A
return new self::A::•();
}
class A2 extends self::A {
synthetic constructor •() → self::A2
: super self::A::•()
;
@#C4
@#C6
method dcMethod1() → void {
core::print("1");
}
}
class A3 extends core::Object implements self::A {
synthetic constructor •() → self::A3
: super core::Object::•()
;
@#C4
@#C6
method dcMethod1() → void {
core::print("1");
}
@#C4
@#C6
method dcMethod2(core::String x, core::int y) → void {}
@#C4
@#C6
get dcGetter1() → core::int
return 1;
@#C4
@#C6
set dcSetter1(core::int value) → void {}
}
class B extends core::Object {
@#C3
final field core::int dcField1 = 1;
@#C3
field core::int dcField2 = 1;
synthetic constructor •() → self::B
: super core::Object::•()
;
@#C3
method dcMethod3() → void {}
@#C3
method dcMethod4(core::String x, core::int y) → void {}
@#C3
get dcGetter2() → core::int
return 1;
@#C3
set dcSetter2(core::int value) → void {}
}
class C extends core::Object {
synthetic constructor •() → self::C
: super core::Object::•()
;
method _dcPrivateMethod1() → core::int
return 1;
}
abstract class D extends self::C /*isMixinDeclaration*/ {
@#C3
method method1() → core::int
return (this as dynamic){dynamic}._dcPrivateMethod1() as{TypeError,ForDynamic} core::int;
}
class E extends core::Object {
@#C3
final field core::int dcField7 = 1;
@#C3
field core::int dcField8 = 1;
field core::int dcField9 = 1;
synthetic constructor •() → self::E
: super core::Object::•()
;
@#C3
method dcMethod5() → void {}
@#C3
get dcGetter3() → core::int
return 1;
@#C3
set dcSetter3(core::int value) → void {}
}
constants {
#C1 = "dyn-module:dynamically-callable"
#C2 = null
#C3 = core::pragma {name:#C1, options:#C2}
#C4 = core::_Override {}
#C5 = "dyn-module:implicitly-dynamically-callable"
#C6 = core::pragma {name:#C5, options:#C2}
}
@@ -6,6 +6,7 @@ import 'lib1.dart';
import 'lib2.dart';
import 'lib3.dart';
import 'lib4.dart';
import 'lib6.dart';
import 'prefix/lib1.dart';
import 'prefix/lib2.dart';
@@ -5,6 +5,7 @@ import "file:pkg/vm/testcases/transformations/dynamic_interface_annotator/lib1.d
import "file:pkg/vm/testcases/transformations/dynamic_interface_annotator/lib2.dart";
import "file:pkg/vm/testcases/transformations/dynamic_interface_annotator/lib3.dart";
import "file:pkg/vm/testcases/transformations/dynamic_interface_annotator/lib4.dart";
import "file:pkg/vm/testcases/transformations/dynamic_interface_annotator/lib6.dart";
import "file:pkg/vm/testcases/transformations/dynamic_interface_annotator/prefix/lib1.dart";
import "file:pkg/vm/testcases/transformations/dynamic_interface_annotator/prefix/lib2.dart";
@@ -0,0 +1,78 @@
// 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.
// Test for @pragma('dyn-module:dynamically-callable').
abstract class A {
@pragma('dyn-module:dynamically-callable')
Object method1(); // Not eliminated
@pragma('dyn-module:dynamically-callable')
Object method2() => 42; // Not eliminated
Object method3() => 1; // Eliminated
}
class B extends A {
@pragma('dyn-module:implicitly-dynamically-callable')
int method1() => 42; // Not eliminated
Object method4() => 1; // Eliminated
}
class C implements A {
@pragma('dyn-module:implicitly-dynamically-callable')
int method1() => 42; // Not eliminated
@pragma('dyn-module:implicitly-dynamically-callable')
int method2() => 42; // Not eliminated
Object method3() => 2; // Eliminated
@pragma('dyn-module:dynamically-callable')
Object method5() => 42; // Not eliminated
Object method6() => 1; // Eliminated
}
abstract class D {
Object method7(); // Eliminated
}
class E implements D {
@pragma('dyn-module:dynamically-callable')
int get field1 => 0;
int method7() => 3; // Eliminated
}
abstract class F {
Object method8(); // Eliminated
}
class G implements F {
@pragma('dyn-module:implicitly-dynamically-callable')
int field1 = 0; // Not eliminated
int field2 = 0; // Eliminated
int method8() => 4; // Eliminated
@pragma('dyn-module:dynamically-callable')
int method9() => 42; // Eliminated
@pragma('dyn-module:dynamically-callable')
int get getter2 => 0; // Not eliminated
@pragma('dyn-module:dynamically-callable')
void set setter2(int v) {} // Not eliminated
int field3 = 0; // Eliminated
int get getter4 => 0; // Eliminated
void set setter4(int v) {} // Eliminated
}
main() {}
@@ -0,0 +1,69 @@
library #lib;
import self as self;
import "dart:core" as core;
abstract class A extends core::Object {
[@vm.procedure-attributes.metadata=hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:1,getterSelectorId:2]
@#C3
abstract method method1() → core::Object;
[@vm.procedure-attributes.metadata=hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3,getterSelectorId:4]
@#C3
abstract method method2() → core::Object;
}
abstract class B extends core::Object {
[@vm.procedure-attributes.metadata=hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:5,getterSelectorId:6]
@#C5
abstract method method1() → core::int;
}
abstract class C extends core::Object {
[@vm.procedure-attributes.metadata=hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:7,getterSelectorId:8]
@#C5
abstract method method1() → core::int;
[@vm.procedure-attributes.metadata=hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:9,getterSelectorId:10]
@#C5
abstract method method2() → core::int;
[@vm.procedure-attributes.metadata=hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:11,getterSelectorId:12]
@#C3
abstract method method5() → core::Object;
}
abstract class E extends core::Object {
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,getterSelectorId:13]
@#C3
abstract get field1() → core::int;
}
abstract class G extends core::Object {
[@vm.unreachable.metadata=]
[@vm.procedure-attributes.metadata=hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:14,getterSelectorId:15]
@#C5
field core::int field1;
[@vm.procedure-attributes.metadata=hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:16,getterSelectorId:17]
@#C3
abstract method method9() → core::int;
[@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,getterSelectorId:18]
@#C3
abstract get getter2() → core::int;
[@vm.procedure-attributes.metadata=getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:19]
@#C3
abstract set setter2(core::int v) → void;
}
[@vm.inferred-return-type.metadata=dart.core::Null? (value: null)]
static method main() → dynamic {}
constants {
#C1 = "dyn-module:dynamically-callable"
#C2 = null
#C3 = core::pragma {name:#C1, options:#C2}
#C4 = "dyn-module:implicitly-dynamically-callable"
#C5 = core::pragma {name:#C4, options:#C2}
}