diff --git a/pkg/vm/lib/transformations/record_use/constant_collector.dart b/pkg/vm/lib/transformations/record_use/constant_collector.dart new file mode 100644 index 000000000000..23345753f7b3 --- /dev/null +++ b/pkg/vm/lib/transformations/record_use/constant_collector.dart @@ -0,0 +1,140 @@ +// Copyright (c) 2024, 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. + +import 'package:kernel/ast.dart'; +import 'package:front_end/src/kernel/record_use.dart' as recordUse; + +/// Expose only the [collect] method of a [_ConstantCollector] to outside use. +extension type ConstantCollector(_ConstantCollector _collector) { + ConstantCollector.collectWith( + Function( + ConstantExpression context, + InstanceConstant constant, + ) collector) + : _collector = _ConstantCollector(collector); + + void collect(ConstantExpression expression) => _collector.collect(expression); +} + +/// A visitor traversing constants and storing instance constants with the +/// `@RecordUse` annotation using the [collector] callback. +class _ConstantCollector implements ConstantVisitor { + /// The collector callback which records the constant. + final void Function( + ConstantExpression context, + InstanceConstant constant, + ) collector; + + /// The expression in which the constant was found. + ConstantExpression? _expression; + + /// A cache to avoid having to re-check for annotations. + final Map _hasRecordUseAnnotation = {}; + + final Set _visited = {}; + + _ConstantCollector(this.collector); + + void collect(ConstantExpression node) { + _expression = node; + handleConstantReference(node.constant); + _expression = null; + } + + void handleConstantReference(Constant constant) { + if (_visited.add(constant)) { + constant.accept(this); + } + } + + @override + void visitListConstant(ListConstant constant) { + for (final entry in constant.entries) { + handleConstantReference(entry); + } + } + + @override + void visitMapConstant(MapConstant constant) { + for (final entry in constant.entries) { + handleConstantReference(entry.key); + handleConstantReference(entry.value); + } + } + + @override + void visitSetConstant(SetConstant constant) { + for (final entry in constant.entries) { + handleConstantReference(entry); + } + } + + @override + void visitRecordConstant(RecordConstant constant) { + for (final value in constant.positional) { + handleConstantReference(value); + } + for (final value in constant.named.values) { + handleConstantReference(value); + } + } + + @override + void visitInstanceConstant(InstanceConstant constant) { + assert(_expression != null); + final classNode = constant.classNode; + if (_hasRecordUseAnnotation[classNode] ??= + recordUse.findRecordUseAnnotation(classNode).isNotEmpty) { + collector(_expression!, constant); + } + for (final value in constant.fieldValues.values) { + handleConstantReference(value); + } + } + + @override + visitAuxiliaryConstant(AuxiliaryConstant node) { + throw UnsupportedError('Cannot record an `AuxiliaryConstant`.'); + } + + @override + visitBoolConstant(BoolConstant node) {} + + @override + visitConstructorTearOffConstant(ConstructorTearOffConstant node) {} + + @override + visitDoubleConstant(DoubleConstant node) {} + + @override + visitInstantiationConstant(InstantiationConstant node) {} + + @override + visitIntConstant(IntConstant node) {} + + @override + visitNullConstant(NullConstant node) {} + + @override + visitRedirectingFactoryTearOffConstant( + RedirectingFactoryTearOffConstant node) {} + + @override + visitStaticTearOffConstant(StaticTearOffConstant node) {} + + @override + visitStringConstant(StringConstant node) {} + + @override + visitSymbolConstant(SymbolConstant node) {} + + @override + visitTypeLiteralConstant(TypeLiteralConstant node) {} + + @override + visitTypedefTearOffConstant(TypedefTearOffConstant node) {} + + @override + visitUnevaluatedConstant(UnevaluatedConstant node) {} +} diff --git a/pkg/vm/lib/transformations/record_use/record_call.dart b/pkg/vm/lib/transformations/record_use/record_call.dart index 9211e6f7e75e..eeadb237b026 100644 --- a/pkg/vm/lib/transformations/record_use/record_call.dart +++ b/pkg/vm/lib/transformations/record_use/record_call.dart @@ -9,14 +9,29 @@ import 'package:record_use/record_use_internal.dart'; import 'package:vm/metadata/loading_units.dart'; import 'package:vm/transformations/record_use/record_use.dart'; -class StaticCallRecorder { - final Map> callsForMethod = {}; +/// Record calls and their constant arguments. Currently tracks +/// * static or top-level method calls through [recordStaticInvocation] +/// * tear-offs through [recordConstantExpression] +/// +/// The result of adding calls can be fetched from [foundCalls]. +class CallRecorder { + /// The collection of recorded calls found so far. + Iterable> get foundCalls => _callsForMethod.values; + + /// Keep track of the calls which are recorded, to easily add newly found + /// ones. + final Map> _callsForMethod = {}; + + /// The ordered list of loading units to retrieve the loading unit index from. final List _loadingUnits; - final Uri source; - StaticCallRecorder(this.source, this._loadingUnits); + /// The source uri to base relative URIs off of. + final Uri _source; - void recordStaticCall(ast.StaticInvocation node) { + CallRecorder(this._source, this._loadingUnits); + + /// Will record a static invocation if it is annotated with `@RecordUse`. + void recordStaticInvocation(ast.StaticInvocation node) { final annotations = recordUse.findRecordUseAnnotation(node.target); if (annotations.isNotEmpty) { final call = _getCall(node.target); @@ -26,18 +41,27 @@ class StaticCallRecorder { } } - void recordTearoff(ast.ConstantExpression node) { + /// Will record a tear-off if the target is annotated with `@RecordUse`. + void recordConstantExpression(ast.ConstantExpression node) { final constant = node.constant; if (constant is ast.StaticTearOffConstant) { - final annotations = recordUse.findRecordUseAnnotation(constant.target); - if (annotations.isNotEmpty) { - final call = _getCall(constant.target); - CallReference reference = _collectTearOff(constant, node); - call.references.add(reference); + final hasRecordUseAnnotation = + recordUse.findRecordUseAnnotation(constant.target).isNotEmpty; + if (hasRecordUseAnnotation) { + _recordTearOff(constant, node); } } } + void _recordTearOff( + ast.StaticTearOffConstant constant, + ast.ConstantExpression node, + ) { + final call = _getCall(constant.target); + final reference = _collectTearOff(constant, node); + call.references.add(reference); + } + /// Record a tear off as a call with all non-const arguments. CallReference _collectTearOff( ast.StaticTearOffConstant constant, @@ -53,7 +77,7 @@ class StaticCallRecorder { ), ); return CallReference( - location: node.location!.recordLocation(source), + location: node.location!.recordLocation(_source), arguments: Arguments(nonConstArguments: nonConstArguments), ); } @@ -62,10 +86,8 @@ class StaticCallRecorder { /// shared across multiple calls to the same method. Usage _getCall(ast.Procedure target) { final definition = _definitionFromMember(target); - return callsForMethod.putIfAbsent( - target, - () => Usage(definition: definition, references: []), - ); + return _callsForMethod[target] ??= + Usage(definition: definition, references: []); } CallReference _createCallReference(ast.StaticInvocation node) { @@ -89,7 +111,7 @@ class StaticCallRecorder { final namedGrouped = _groupByNull(namedArguments); return CallReference( - location: node.location!.recordLocation(source), + location: node.location!.recordLocation(_source), loadingUnit: loadingUnitForNode(node, _loadingUnits).toString(), arguments: Arguments( constArguments: ConstArguments( @@ -126,7 +148,7 @@ class StaticCallRecorder { Definition _definitionFromMember(ast.Member target) { final enclosingLibrary = target.enclosingLibrary; - String file = getImportUri(enclosingLibrary, source); + String file = getImportUri(enclosingLibrary, _source); return Definition( identifier: Identifier( @@ -134,7 +156,7 @@ class StaticCallRecorder { parent: target.enclosingClass?.name, name: target.name.text, ), - location: target.location!.recordLocation(source), + location: target.location!.recordLocation(_source), loadingUnit: loadingUnitForNode(enclosingLibrary, _loadingUnits).toString(), ); diff --git a/pkg/vm/lib/transformations/record_use/record_instance.dart b/pkg/vm/lib/transformations/record_use/record_instance.dart index be7f15a0e3fb..60cdf4c46ac9 100644 --- a/pkg/vm/lib/transformations/record_use/record_instance.dart +++ b/pkg/vm/lib/transformations/record_use/record_instance.dart @@ -2,30 +2,42 @@ // 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. -import 'package:front_end/src/kernel/record_use.dart' as recordUse; import 'package:kernel/ast.dart' as ast; import 'package:record_use/record_use_internal.dart'; import 'package:vm/metadata/loading_units.dart'; import 'package:vm/transformations/record_use/record_use.dart'; -class InstanceUseRecorder { - final Map> instancesForClass = {}; +import 'constant_collector.dart'; + +/// Record a const instance by calling [recordConstantExpression]. After all the +/// const instances have been recorded, retrieve them using [foundInstances]. +class InstanceRecorder { + /// The collection of recorded instances found so far. + Iterable> get foundInstances => + _instancesForClass.values; + + /// Keep track of the classes which are recorded, to easily add found + /// instances. + final Map> _instancesForClass = {}; + + /// The ordered list of loading units to retrieve the loading unit index from. final List _loadingUnits; - final Uri source; - InstanceUseRecorder(this.source, this._loadingUnits); + /// The source uri to base relative URIs off of. + final Uri _source; + + /// A visitor traversing and collecting constants. + late final ConstantCollector collector; - void recordAnnotationUse(ast.ConstantExpression node) { - final constant = node.constant; - if (constant is ast.InstanceConstant) { - if (recordUse.findRecordUseAnnotation(constant.classNode).isNotEmpty) { - _collectUseInformation(node, constant); - } - } + InstanceRecorder(this._source, this._loadingUnits) { + collector = ConstantCollector.collectWith(_collectInstance); } - void _collectUseInformation( - ast.ConstantExpression node, + void recordConstantExpression(ast.ConstantExpression node) => + collector.collect(node); + + void _collectInstance( + ast.ConstantExpression expression, ast.InstanceConstant constant, ) { // Collect the name and definition location of the invocation. This is @@ -33,47 +45,47 @@ class InstanceUseRecorder { final existingInstance = _getCall(constant.classNode); // Collect the (int, bool, double, or String) arguments passed in the call. - existingInstance.references.add(_createInstanceReference(node, constant)); + existingInstance.references + .add(_createInstanceReference(expression, constant)); } /// Collect the name and definition location of the invocation. This is /// shared across multiple calls to the same method. Usage _getCall(ast.Class cls) { final definition = _definitionFromClass(cls); - return instancesForClass.putIfAbsent( - cls, - () => Usage(definition: definition, references: []), - ); + return _instancesForClass[cls] ??= + Usage(definition: definition, references: []); } Definition _definitionFromClass(ast.Class cls) { final enclosingLibrary = cls.enclosingLibrary; - String file = getImportUri(enclosingLibrary, source); + final file = getImportUri(enclosingLibrary, _source); return Definition( identifier: Identifier(importUri: file, name: cls.name), - location: cls.location!.recordLocation(source), + location: cls.location!.recordLocation(_source), loadingUnit: loadingUnitForNode(cls.enclosingLibrary, _loadingUnits).toString(), ); } InstanceReference _createInstanceReference( - ast.ConstantExpression node, + ast.ConstantExpression expression, ast.InstanceConstant constant, ) => InstanceReference( - location: node.location!.recordLocation(source), + location: expression.location!.recordLocation(_source), instanceConstant: _fieldsFromConstant(constant), - loadingUnit: loadingUnitForNode(node, _loadingUnits).toString(), + loadingUnit: loadingUnitForNode(expression, _loadingUnits).toString(), ); InstanceConstant _fieldsFromConstant(ast.InstanceConstant constant) => InstanceConstant( - fields: constant.fieldValues.map( - (key, value) => MapEntry( - key.asField.name.text, - evaluateConstant(value), + fields: constant.fieldValues.map( + (key, value) => MapEntry( + key.asField.name.text, + evaluateConstant(value), + ), ), - )); + ); } diff --git a/pkg/vm/lib/transformations/record_use/record_use.dart b/pkg/vm/lib/transformations/record_use/record_use.dart index aa3c01282ad6..bb328305a2ae 100644 --- a/pkg/vm/lib/transformations/record_use/record_use.dart +++ b/pkg/vm/lib/transformations/record_use/record_use.dart @@ -36,16 +36,16 @@ ast.Component transformComponent( component.metadata[tag] as LoadingUnitsMetadataRepository; final loadingUnits = loadingMetadata.mapping[component]?.loadingUnits ?? []; - final staticCallRecorder = StaticCallRecorder(source, loadingUnits); - final instanceUseRecorder = InstanceUseRecorder(source, loadingUnits); + final staticCallRecorder = CallRecorder(source, loadingUnits); + final instanceUseRecorder = InstanceRecorder(source, loadingUnits); component.accept(_RecordUseVisitor( staticCallRecorder, instanceUseRecorder, )); final usages = _usages( - staticCallRecorder.callsForMethod.values, - instanceUseRecorder.instancesForClass.values, + staticCallRecorder.foundCalls, + instanceUseRecorder.foundInstances, ); var usagesStorageFormat = usages.toJson(); File.fromUri(recordedUsagesFile).writeAsStringSync( @@ -56,8 +56,8 @@ ast.Component transformComponent( } class _RecordUseVisitor extends ast.RecursiveVisitor { - final StaticCallRecorder staticCallRecorder; - final InstanceUseRecorder instanceUseRecorder; + final CallRecorder staticCallRecorder; + final InstanceRecorder instanceUseRecorder; _RecordUseVisitor( this.staticCallRecorder, @@ -66,18 +66,16 @@ class _RecordUseVisitor extends ast.RecursiveVisitor { @override void visitStaticInvocation(ast.StaticInvocation node) { - staticCallRecorder.recordStaticCall(node); - node.visitChildren(this); + staticCallRecorder.recordStaticInvocation(node); + + super.visitStaticInvocation(node); } @override void visitConstantExpression(ast.ConstantExpression node) { - staticCallRecorder.recordTearoff(node); + staticCallRecorder.recordConstantExpression(node); + instanceUseRecorder.recordConstantExpression(node); - final parent = node.parent; - if (parent is ast.Annotatable && parent.annotations.contains(node)) { - instanceUseRecorder.recordAnnotationUse(node); - } super.visitConstantExpression(node); } } diff --git a/pkg/vm/test/transformations/record_use_test.dart b/pkg/vm/test/transformations/record_use_test.dart index 4dae03557f1c..f34e0aba3f48 100644 --- a/pkg/vm/test/transformations/record_use_test.dart +++ b/pkg/vm/test/transformations/record_use_test.dart @@ -82,6 +82,7 @@ void main(List args) { .reversed) { if (file.path.endsWith('.dart') && !file.path.contains('helper') && + !file.path.contains('instance_complex') && (filter == null || file.path.contains(filter))) { test('${file.path} aot', () => runTestCaseAot(file.uri, file.path.contains('throws'))); diff --git a/pkg/vm/testcases/transformations/record_use/different.dart b/pkg/vm/testcases/transformations/record_use/different.dart index 81331e53be01..21120af34ea3 100644 --- a/pkg/vm/testcases/transformations/record_use/different.dart +++ b/pkg/vm/testcases/transformations/record_use/different.dart @@ -7,14 +7,12 @@ void main() { } class SomeClass { - @ResourceIdentifier('id') + @RecordUse() static someStaticMethod(int i) { return i + 1; } } -class ResourceIdentifier { - final Object? metadata; - - const ResourceIdentifier([this.metadata]); +class RecordUse { + const RecordUse(); } diff --git a/pkg/vm/testcases/transformations/record_use/instance_duplicates.dart b/pkg/vm/testcases/transformations/record_use/instance_duplicates.dart new file mode 100644 index 000000000000..64de2e642a68 --- /dev/null +++ b/pkg/vm/testcases/transformations/record_use/instance_duplicates.dart @@ -0,0 +1,18 @@ +// Copyright (c) 2024, 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. + +import 'package:meta/meta.dart' show RecordUse; + +void main() { + print(const MyClass(42)); + print(const MyClass(42)); + print(const MyClass(43)); +} + +@RecordUse() +class MyClass { + final int i; + + const MyClass(this.i); +} diff --git a/pkg/vm/testcases/transformations/record_use/instance_duplicates.dart.aot.expect b/pkg/vm/testcases/transformations/record_use/instance_duplicates.dart.aot.expect new file mode 100644 index 000000000000..a0e2b527369f --- /dev/null +++ b/pkg/vm/testcases/transformations/record_use/instance_duplicates.dart.aot.expect @@ -0,0 +1,29 @@ +library #lib; +import self as self; +import "package:meta/meta.dart" as meta; +import "dart:core" as core; + +import "package:meta/meta.dart" show RecordUse; + +@#C1 +class MyClass extends core::Object /*hasConstConstructor*/ { + + [@vm.inferred-type.metadata=dart.core::_Smi] + [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,getterSelectorId:1] + [@vm.unboxing-info.metadata=()->i] + final field core::int i; +} + +[@vm.inferred-return-type.metadata=dart.core::Null? (value: null)] +static method main() → void { + core::print(#C3); + core::print(#C3); + core::print(#C5); +} +constants { + #C1 = meta::RecordUse {} + #C2 = 42 + #C3 = self::MyClass {i:#C2} + #C4 = 43 + #C5 = self::MyClass {i:#C4} +} diff --git a/pkg/vm/testcases/transformations/record_use/instance_duplicates.dart.json.expect b/pkg/vm/testcases/transformations/record_use/instance_duplicates.dart.json.expect new file mode 100644 index 000000000000..9559444cf1a7 --- /dev/null +++ b/pkg/vm/testcases/transformations/record_use/instance_duplicates.dart.json.expect @@ -0,0 +1,70 @@ +{ + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.1.0" + }, + "uris": [ + "instance_duplicates.dart" + ], + "ids": [ + { + "uri": 0, + "name": "MyClass" + } + ], + "constants": [ + { + "type": "int", + "value": 42 + }, + { + "type": "Instance", + "value": { + "i": 0 + } + }, + { + "type": "int", + "value": 43 + }, + { + "type": "Instance", + "value": { + "i": 2 + } + } + ], + "instances": [ + { + "definition": { + "id": 0, + "@": { + "uri": 0, + "line": 14, + "column": 7 + }, + "loadingUnit": "1" + }, + "references": [ + { + "instanceConstant": 1, + "loadingUnit": "1", + "@": { + "uri": 0, + "line": 8, + "column": 15 + } + }, + { + "instanceConstant": 3, + "loadingUnit": "1", + "@": { + "uri": 0, + "line": 10, + "column": 15 + } + } + ] + } + ] +} \ No newline at end of file diff --git a/pkg/vm/testcases/transformations/record_use/instance_not_annotation.dart.json.expect b/pkg/vm/testcases/transformations/record_use/instance_not_annotation.dart.json.expect index d95dcddd5268..f2d8bb14044e 100644 --- a/pkg/vm/testcases/transformations/record_use/instance_not_annotation.dart.json.expect +++ b/pkg/vm/testcases/transformations/record_use/instance_not_annotation.dart.json.expect @@ -3,7 +3,42 @@ "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", "version": "0.1.0" }, - "uris": [], - "ids": [], - "constants": [] + "uris": [ + "instance_not_annotation.dart" + ], + "ids": [ + { + "uri": 0, + "name": "MyClass" + } + ], + "constants": [ + { + "type": "Instance" + } + ], + "instances": [ + { + "definition": { + "id": 0, + "@": { + "uri": 0, + "line": 13, + "column": 7 + }, + "loadingUnit": "1" + }, + "references": [ + { + "instanceConstant": 0, + "loadingUnit": "1", + "@": { + "uri": 0, + "line": 9, + "column": 9 + } + } + ] + } + ] } \ No newline at end of file diff --git a/pkg/vm/testcases/transformations/record_use/nested.dart b/pkg/vm/testcases/transformations/record_use/nested.dart new file mode 100644 index 000000000000..25690c07feb3 --- /dev/null +++ b/pkg/vm/testcases/transformations/record_use/nested.dart @@ -0,0 +1,28 @@ +// Copyright (c) 2024, 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. + +import 'package:meta/meta.dart' show RecordUse; + +void main() { + const list = [ + const MyClass(42), + { + const MyClass('test'), + const MyOtherClass(), + }, + ]; + print(list); +} + +@RecordUse() +class MyClass { + final Object i; + + const MyClass(this.i); +} + +@RecordUse() +class MyOtherClass { + const MyOtherClass(); +} diff --git a/pkg/vm/testcases/transformations/record_use/nested.dart.aot.expect b/pkg/vm/testcases/transformations/record_use/nested.dart.aot.expect new file mode 100644 index 000000000000..529dbd0ac578 --- /dev/null +++ b/pkg/vm/testcases/transformations/record_use/nested.dart.aot.expect @@ -0,0 +1,32 @@ +library #lib; +import self as self; +import "package:meta/meta.dart" as meta; +import "dart:core" as core; + +import "package:meta/meta.dart" show RecordUse; + +@#C1 +class MyClass extends core::Object /*hasConstConstructor*/ { + + [@vm.inferred-type.metadata=!] + [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasNonThisUses:false,hasTearOffUses:false,getterSelectorId:1] + final field core::Object i; +} +@#C1 +class MyOtherClass extends core::Object /*hasConstConstructor*/ { +} + +[@vm.inferred-return-type.metadata=dart.core::Null? (value: null)] +static method main() → void { + core::print(#C8); +} +constants { + #C1 = meta::RecordUse {} + #C2 = 42 + #C3 = self::MyClass {i:#C2} + #C4 = "test" + #C5 = self::MyClass {i:#C4} + #C6 = self::MyOtherClass {} + #C7 = {#C5, #C6} + #C8 = [#C3, #C7] +} diff --git a/pkg/vm/testcases/transformations/record_use/nested.dart.json.expect b/pkg/vm/testcases/transformations/record_use/nested.dart.json.expect new file mode 100644 index 000000000000..1bfdc0b89723 --- /dev/null +++ b/pkg/vm/testcases/transformations/record_use/nested.dart.json.expect @@ -0,0 +1,99 @@ +{ + "metadata": { + "comment": "Recorded usages of objects tagged with a `RecordUse` annotation", + "version": "0.1.0" + }, + "uris": [ + "nested.dart" + ], + "ids": [ + { + "uri": 0, + "name": "MyClass" + }, + { + "uri": 0, + "name": "MyOtherClass" + } + ], + "constants": [ + { + "type": "int", + "value": 42 + }, + { + "type": "Instance", + "value": { + "i": 0 + } + }, + { + "type": "String", + "value": "test" + }, + { + "type": "Instance", + "value": { + "i": 2 + } + }, + { + "type": "Instance" + } + ], + "instances": [ + { + "definition": { + "id": 0, + "@": { + "uri": 0, + "line": 19, + "column": 7 + }, + "loadingUnit": "1" + }, + "references": [ + { + "instanceConstant": 1, + "loadingUnit": "1", + "@": { + "uri": 0, + "line": 15, + "column": 9 + } + }, + { + "instanceConstant": 3, + "loadingUnit": "1", + "@": { + "uri": 0, + "line": 15, + "column": 9 + } + } + ] + }, + { + "definition": { + "id": 1, + "@": { + "uri": 0, + "line": 26, + "column": 7 + }, + "loadingUnit": "1" + }, + "references": [ + { + "instanceConstant": 4, + "loadingUnit": "1", + "@": { + "uri": 0, + "line": 15, + "column": 9 + } + } + ] + } + ] +} \ No newline at end of file