Skip to content

Commit

Permalink
Add CFE support to _macro_tool.
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmorgan committed Oct 9, 2024
1 parent a3a5ec0 commit 4812370
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 47 deletions.
10 changes: 9 additions & 1 deletion pkgs/_macro_tool/bin/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ import 'package:args/args.dart';
import 'package:path/path.dart' as p;

final argParser = ArgParser()
..addOption('host',
defaultsTo: 'analyzer', help: 'The macro host: "analyzer" or "cfe".')
..addOption('workspace', help: 'Path to workspace.')
..addOption('packageConfig', help: 'Path to package config.')
..addOption('script', help: 'Path to script.')
..addFlag('skip-cleanup');

Future<void> main(List<String> arguments) async {
final args = argParser.parse(arguments);

final host = HostOption.forString(args['host'] as String?);
final workspace = args['workspace'] as String?;
final packageConfig = args['packageConfig'] as String?;
final script = args['script'] as String?;

if (workspace == null || packageConfig == null || script == null) {
if (host == null ||
workspace == null ||
packageConfig == null ||
script == null) {
print('''
Runs a Dart script with `dart_model` macros.
Expand All @@ -29,6 +36,7 @@ ${argParser.usage}''');
}

final tool = MacroTool(
host: host,
workspacePath: p.canonicalize(workspace),
packageConfigPath: p.canonicalize(packageConfig),
scriptPath: p.canonicalize(script),
Expand Down
69 changes: 69 additions & 0 deletions pkgs/_macro_tool/lib/analyzer_macro_tool.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 'dart:io';

import 'package:_analyzer_macros/macro_implementation.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/src/summary2/macro_injected_impl.dart'
as injected_analyzer;
import 'package:macro_service/macro_service.dart';

import 'macro_tool.dart';

class AnalyzerMacroTool extends MacroTool {
AnalyzerMacroTool(
{required super.workspacePath,
required super.packageConfigPath,
required super.scriptPath,
required super.skipCleanup})
: super.internal();

/// Runs macros in [scriptFile] on the analyzer.
///
/// Writes any augmentation to [_augmentationFilePath].
///
/// Returns whether an augmentation file was written.
@override
Future<bool> augment() async {
final contextCollection =
AnalysisContextCollection(includedPaths: [workspacePath]);
final analysisContext = contextCollection.contexts.first;
injected_analyzer.macroImplementation =
await AnalyzerMacroImplementation.start(
protocol: Protocol(
encoding: ProtocolEncoding.binary,
version: ProtocolVersion.macros1),
packageConfig: Uri.file(packageConfigPath));

final resolvedLibrary = (await analysisContext.currentSession
.getResolvedLibrary(scriptPath)) as ResolvedLibraryResult;

final errors = (await analysisContext.currentSession.getErrors(scriptPath))
as ErrorsResult;
if (errors.errors.isNotEmpty) {
print('Errors: ${errors.errors}');
}

final augmentationUnits =
resolvedLibrary.units.where((u) => u.isMacroPart).toList();
if (augmentationUnits.isEmpty) {
return false;
}

print('Macro output (patched to use augment library): '
'$augmentationFilePath');
File(augmentationFilePath).writeAsStringSync(augmentationUnits
.single.content
// The analyzer produces augmentations in parts, but the CFE still
// wants them in augmentation libraries. Adjust the output accordingly.
.replaceAll('part of', 'augment library'));

return true;
}

@override
String toString() => 'analyzer';
}
84 changes: 84 additions & 0 deletions pkgs/_macro_tool/lib/cfe_macro_tool.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 'dart:io';
import 'dart:isolate';

import 'package:_cfe_macros/macro_implementation.dart';
import 'package:front_end/src/macros/macro_injected_impl.dart' as injected_cfe;
import 'package:frontend_server/compute_kernel.dart';
import 'package:macro_service/macro_service.dart';

import 'macro_tool.dart';

class CfeMacroTool extends MacroTool {
CfeMacroTool(
{required super.workspacePath,
required super.packageConfigPath,
required super.scriptPath,
required super.skipCleanup})
: super.internal();

/// Runs macros in [scriptFile] using the CFE.
///
/// Writes any augmentation to [augmentationFilePath].
///
/// Returns whether an augmentation file was written.
@override
Future<bool> augment() async {
// TODO(davidmorgan): this dill comes from the Dart SDK running the test,
// but `package:frontend_server` and `package:front_end` are used as a
// library, so we will see version skew breakage. Find a better way.
final productPlatformDill = File('${Platform.resolvedExecutable}/../../'
'lib/_internal/vm_platform_strong_product.dill');
if (!File.fromUri(productPlatformDill.uri).existsSync()) {
throw StateError('Failed to find platform dill: $productPlatformDill');
}
injected_cfe.macroImplementation = await CfeMacroImplementation.start(
protocol: Protocol(
encoding: ProtocolEncoding.json, version: ProtocolVersion.macros1),
packageConfig: Isolate.packageConfigSync!);

final packagesUri = Isolate.packageConfigSync;

// Don't directly use the compiler output: for consistency with the analyzer
// codepath, run from the resulting source.
// TODO(davidmorgan): maybe offer both as options? Not clear yet.
final outputFile = File('/dev/null');

final computeKernelResult = await computeKernel([
'--enable-experiment=macros',
'--no-summary',
'--no-summary-only',
'--target=vm',
'--dart-sdk-summary=${productPlatformDill.uri}',
'--output=${outputFile.path}',
'--source=$scriptPath',
'--packages-file=$packagesUri',
// TODO(davidmorgan): this is so we can pull the generated
// augmentation source out of incremental compiler state; find a less
// hacky way.
'--use-incremental-compiler',
// For augmentations.
'--enable-experiment=macros',
]);

final sources = computeKernelResult
.previousState!.incrementalCompiler!.context.uriToSource;
final applicationMacroOutput = sources.entries
.where((e) => e.key.scheme == 'dart-macro+file')
.singleOrNull;
if (applicationMacroOutput == null) return false;

print('Macro output: '
'$augmentationFilePath');
File(augmentationFilePath)
.writeAsStringSync(applicationMacroOutput.value.text);

return true;
}

@override
String toString() => 'CFE';
}
95 changes: 49 additions & 46 deletions pkgs/_macro_tool/lib/macro_tool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,62 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';
import 'dart:isolate';

import 'package:_analyzer_macros/macro_implementation.dart';
import 'package:_cfe_macros/macro_implementation.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/src/summary2/macro_injected_impl.dart' as injected;
import 'package:analyzer/src/summary2/macro_injected_impl.dart'
as injected_analyzer;
import 'package:front_end/src/macros/macro_injected_impl.dart' as injected_cfe;
import 'package:frontend_server/compute_kernel.dart';
import 'package:macro_service/macro_service.dart';
import 'package:path/path.dart' as p;

import 'analyzer_macro_tool.dart';
import 'cfe_macro_tool.dart';

/// Runs a Dart script with `dart_model` macros.
class MacroTool {
abstract class MacroTool {
String workspacePath;
String packageConfigPath;
String scriptPath;
bool skipCleanup;

MacroTool(
MacroTool.internal(
{required this.workspacePath,
required this.packageConfigPath,
required this.scriptPath,
required this.skipCleanup});

factory MacroTool(
{required HostOption host,
required String workspacePath,
required String packageConfigPath,
required String scriptPath,
required bool skipCleanup}) =>
host == HostOption.analyzer
? AnalyzerMacroTool(
workspacePath: workspacePath,
packageConfigPath: packageConfigPath,
scriptPath: scriptPath,
skipCleanup: skipCleanup)
: CfeMacroTool(
workspacePath: workspacePath,
packageConfigPath: packageConfigPath,
scriptPath: scriptPath,
skipCleanup: skipCleanup);

Future<void> run() async {
print('Running ${p.basename(scriptPath)} with macros.');
print('Running ${p.basename(scriptPath)} with macros on $this.');
print('~~~');
print('Package config: $packageConfigPath');
print('Workspace: $workspacePath');
print('Script: $scriptPath');

// TODO(davidmorgan): make it an option to run with the CFE instead.
if (!await _augmentUsingAnalyzer()) {
if (!await augment()) {
print('No augmentation was generated, nothing to do, exiting.');
exit(1);
}
Expand Down Expand Up @@ -71,53 +97,19 @@ class MacroTool {
}

/// The path where macro-generated augmentations will be written.
String get _augmentationFilePath => '$scriptPath.macro_tool_output';
String get augmentationFilePath => '$scriptPath.macro_tool_output';

/// Runs macros in [scriptFile].
/// Runs macros in [scriptFile] on the analyzer.
///
/// Writes any augmentation to [_augmentationFilePath].
/// Writes any augmentation to [augmentationFilePath].
///
/// Returns whether an augmentation file was written.
Future<bool> _augmentUsingAnalyzer() async {
final contextCollection =
AnalysisContextCollection(includedPaths: [workspacePath]);
final analysisContext = contextCollection.contexts.first;
injected.macroImplementation = await AnalyzerMacroImplementation.start(
protocol: Protocol(
encoding: ProtocolEncoding.binary,
version: ProtocolVersion.macros1),
packageConfig: Uri.file(packageConfigPath));

final resolvedLibrary = (await analysisContext.currentSession
.getResolvedLibrary(scriptPath)) as ResolvedLibraryResult;

final errors = (await analysisContext.currentSession.getErrors(scriptPath))
as ErrorsResult;
if (errors.errors.isNotEmpty) {
print('Errors: ${errors.errors}');
}

final augmentationUnits =
resolvedLibrary.units.where((u) => u.isMacroPart).toList();
if (augmentationUnits.isEmpty) {
return false;
}

print('Macro output (patched to use augment library): '
'$_augmentationFilePath');
File(_augmentationFilePath).writeAsStringSync(augmentationUnits
.single.content
// The analyzer produces augmentations in parts, but the CFE still
// wants them in augmentation libraries. Adjust the output accordingly.
.replaceAll('part of', 'augment library'));

return true;
}
Future<bool> augment();

/// Deletes the augmentation file created by this tool.
void _removeAugmentations() {
print('Deleting: $_augmentationFilePath');
File(_augmentationFilePath).deleteSync();
print('Deleting: $augmentationFilePath');
File(augmentationFilePath).deleteSync();
}

/// Adds `import augment` of the augmentation file.
Expand All @@ -128,7 +120,7 @@ class MacroTool {
print('Patching to import augmentations: $scriptPath');

// Add the `import augment` statement at the start of the file.
final partName = p.basename(_augmentationFilePath);
final partName = p.basename(augmentationFilePath);
final line = "import augment '$partName'; $_addedMarker\n";

final file = File(scriptPath);
Expand All @@ -153,3 +145,14 @@ class MacroTool {
}

final String _addedMarker = '// added by macro_tool';

enum HostOption {
analyzer,
cfe;

static HostOption? forString(String? option) => switch (option) {
'analyzer' => HostOption.analyzer,
'cfe' => HostOption.cfe,
_ => null,
};
}

0 comments on commit 4812370

Please sign in to comment.