diff --git a/working/macros/api/expansion_protocol.dart b/working/macros/api/expansion_protocol.dart index b95fea1fbe..316528da33 100644 --- a/working/macros/api/expansion_protocol.dart +++ b/working/macros/api/expansion_protocol.dart @@ -57,6 +57,10 @@ abstract class MacroExecutor { /// augmentation file, and returns a [String] representing that file. Future buildAugmentationLibrary( Iterable macroResults); + + /// Tell the executor to shut down and clean up any resources it may have + /// allocated. + void close(); } /// The arguments passed to a macro constructor. diff --git a/working/macros/api/src/protocol/isolate_mirror_executor.dart b/working/macros/api/src/protocol/isolate_mirror_executor.dart new file mode 100644 index 0000000000..bfd1fac34a --- /dev/null +++ b/working/macros/api/src/protocol/isolate_mirror_executor.dart @@ -0,0 +1,131 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:mirrors'; + +import 'isolate_mirror_impl.dart'; +import 'protocol.dart'; +import '../../builders.dart'; +import '../../expansion_protocol.dart'; +import '../../introspection.dart'; + +/// A [MacroExecutor] implementation which relies on [IsolateMirror.loadUri] +/// in order to load macros libraries. +/// +/// All actual work happens in a separate [Isolate], and this class serves as +/// a bridge between that isolate and the language frontends. +class IsolateMirrorMacroExecutor implements MacroExecutor { + /// The actual isolate doing macro loading and execution. + final Isolate _macroIsolate; + + /// The channel used to send requests to the [_macroIsolate]. + final SendPort _sendPort; + + /// The stream of responses from the [_macroIsolate]. + final Stream _responseStream; + + /// A map of response completers by request id. + final _responseCompleters = >{}; + + /// A function that should be invoked when shutting down this executor + /// to perform any necessary cleanup. + final void Function() _onClose; + + IsolateMirrorMacroExecutor._( + this._macroIsolate, this._sendPort, this._responseStream, this._onClose) { + _responseStream.listen((event) { + var completer = _responseCompleters.remove(event.requestId); + if (completer == null) { + throw StateError( + 'Got a response for an unrecognized request id ${event.requestId}'); + } + completer.complete(event); + }); + } + + /// Initialize an [IsolateMirrorMacroExecutor] and return it once ready. + /// + /// Spawns the macro isolate and sets up a communication channel. + static Future start() async { + var receivePort = ReceivePort(); + var sendPortCompleter = Completer(); + var responseStreamController = + StreamController(sync: true); + receivePort.listen((message) { + if (!sendPortCompleter.isCompleted) { + sendPortCompleter.complete(message as SendPort); + } else { + responseStreamController.add(message as GenericResponse); + } + }).onDone(responseStreamController.close); + var macroIsolate = await Isolate.spawn(spawn, receivePort.sendPort); + + return IsolateMirrorMacroExecutor._( + macroIsolate, + await sendPortCompleter.future, + responseStreamController.stream, + receivePort.close); + } + + @override + Future buildAugmentationLibrary( + Iterable macroResults) { + // TODO: implement buildAugmentationLibrary + throw UnimplementedError(); + } + + @override + void close() { + _onClose(); + _macroIsolate.kill(); + } + + @override + Future executeDeclarationsPhase( + MacroInstanceIdentifier macro, + Declaration declaration, + TypeResolver typeResolver, + ClassIntrospector classIntrospector) { + // TODO: implement executeDeclarationsPhase + throw UnimplementedError(); + } + + @override + Future executeDefinitionsPhase( + MacroInstanceIdentifier macro, + Declaration declaration, + TypeResolver typeResolver, + ClassIntrospector classIntrospector, + TypeDeclarationResolver typeDeclarationResolver) => + _sendRequest(ExecuteDefinitionsPhaseRequest(macro, declaration, + typeResolver, classIntrospector, typeDeclarationResolver)); + + @override + Future executeTypesPhase( + MacroInstanceIdentifier macro, Declaration declaration) { + // TODO: implement executeTypesPhase + throw UnimplementedError(); + } + + @override + Future instantiateMacro( + MacroClassIdentifier macroClass, + String constructor, + Arguments arguments) => + _sendRequest(InstantiateMacroRequest(macroClass, constructor, arguments)); + + @override + Future loadMacro(Uri library, String name) => + _sendRequest(LoadMacroRequest(library, name)); + + /// Sends a request and returns the response, casting it to the expected + /// type. + Future _sendRequest(Request request) async { + _sendPort.send(request); + var completer = Completer>(); + _responseCompleters[request.id] = completer; + var response = await completer.future; + var result = response.response; + if (result != null) return result; + throw response.error!; + } +} diff --git a/working/macros/api/src/protocol/isolate_mirror_impl.dart b/working/macros/api/src/protocol/isolate_mirror_impl.dart new file mode 100644 index 0000000000..48b20e57bf --- /dev/null +++ b/working/macros/api/src/protocol/isolate_mirror_impl.dart @@ -0,0 +1,247 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:mirrors'; + +import '../../code.dart'; +import 'protocol.dart'; +import '../../builders.dart'; +import '../../expansion_protocol.dart'; +import '../../introspection.dart'; +import '../../macros.dart'; + +/// Spawns a new isolate for loading and executing macros. +void spawn(SendPort sendPort) { + var receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + receivePort.listen((message) async { + if (message is LoadMacroRequest) { + var response = await _loadMacro(message); + sendPort.send(response); + } else if (message is InstantiateMacroRequest) { + var response = await _instantiateMacro(message); + sendPort.send(response); + } else if (message is ExecuteDefinitionsPhaseRequest) { + var response = await _executeDefinitionsPhase(message); + sendPort.send(response); + } else { + throw StateError('Unrecognized event type $message'); + } + }); +} + +/// Maps macro identifiers to class mirrors. +final _macroClasses = <_MacroClassIdentifier, ClassMirror>{}; + +/// Handles [LoadMacroRequest]s. +Future> _loadMacro( + LoadMacroRequest request) async { + try { + var identifier = _MacroClassIdentifier(request.library, request.name); + if (_macroClasses.containsKey(identifier)) { + throw UnsupportedError( + 'Reloading macros is not supported by this implementation'); + } + var libMirror = + await currentMirrorSystem().isolate.loadUri(request.library); + var macroClass = + libMirror.declarations[Symbol(request.name)] as ClassMirror; + _macroClasses[identifier] = macroClass; + return GenericResponse(response: identifier, requestId: request.id); + } catch (e) { + return GenericResponse(error: e, requestId: request.id); + } +} + +/// Maps macro instance identifiers to instances. +final _macroInstances = <_MacroInstanceIdentifier, Macro>{}; + +/// Handles [InstantiateMacroRequest]s. +Future> _instantiateMacro( + InstantiateMacroRequest request) async { + try { + var clazz = _macroClasses[request.macroClass]; + if (clazz == null) { + throw ArgumentError('Unrecognized macro class ${request.macroClass}'); + } + var instance = clazz.newInstance( + Symbol(request.constructorName), request.arguments.positional, { + for (var entry in request.arguments.named.entries) + Symbol(entry.key): entry.value, + }).reflectee as Macro; + var identifier = _MacroInstanceIdentifier(); + _macroInstances[identifier] = instance; + return GenericResponse( + response: identifier, requestId: request.id); + } catch (e) { + return GenericResponse(error: e, requestId: request.id); + } +} + +Future> _executeDefinitionsPhase( + ExecuteDefinitionsPhaseRequest request) async { + try { + var instance = _macroInstances[request.macro]; + if (instance == null) { + throw StateError('Unrecognized macro instance ${request.macro}\n' + 'Known instances: $_macroInstances)'); + } + var declaration = request.declaration; + if (instance is FunctionDefinitionMacro && + declaration is FunctionDeclaration) { + var builder = _FunctionDefinitionBuilder( + declaration, + request.typeResolver, + request.typeDeclarationResolver, + request.classIntrospector); + await instance.buildDefinitionForFunction(declaration, builder); + return GenericResponse(response: builder.result, requestId: request.id); + } else { + throw UnsupportedError( + ('Only FunctionDefinitionMacros are supported currently')); + } + } catch (e) { + return GenericResponse(error: e, requestId: request.id); + } +} + +/// Our implementation of [MacroClassIdentifier]. +class _MacroClassIdentifier implements MacroClassIdentifier { + final String id; + + _MacroClassIdentifier(Uri library, String name) : id = '$library#$name'; + + operator ==(other) => other is _MacroClassIdentifier && id == other.id; + + int get hashCode => id.hashCode; +} + +/// Our implementation of [MacroInstanceIdentifier]. +class _MacroInstanceIdentifier implements MacroInstanceIdentifier { + static int _next = 0; + + final int id; + + _MacroInstanceIdentifier() : id = _next++; + + operator ==(other) => other is _MacroInstanceIdentifier && id == other.id; + + int get hashCode => id; +} + +/// Our implementation of [MacroExecutionResult]. +class _MacroExecutionResult implements MacroExecutionResult { + @override + final List augmentations = []; + + @override + final List imports = []; +} + +/// Custom implementation of [FunctionDefinitionBuilder]. +class _FunctionDefinitionBuilder implements FunctionDefinitionBuilder { + final TypeResolver typeResolver; + final TypeDeclarationResolver typeDeclarationResolver; + final ClassIntrospector classIntrospector; + + /// The declaration this is a builder for. + final FunctionDeclaration declaration; + + /// The final result, will be built up over `augment` calls. + final result = _MacroExecutionResult(); + + _FunctionDefinitionBuilder(this.declaration, this.typeResolver, + this.typeDeclarationResolver, this.classIntrospector); + + @override + void augment(FunctionBodyCode body) { + result.augmentations.add(DeclarationCode.fromParts([ + 'augment ', + declaration.returnType.code, + ' ', + declaration.name, + if (declaration.typeParameters.isNotEmpty) ...[ + '<', + for (var typeParam in declaration.typeParameters) ...[ + typeParam.name, + if (typeParam.bounds != null) ...['extends ', typeParam.bounds!.code], + if (typeParam != declaration.typeParameters.last) ', ', + ], + '>', + ], + '(', + for (var positionalRequired + in declaration.positionalParameters.where((p) => p.isRequired)) ...[ + ParameterCode.fromParts([ + positionalRequired.type.code, + ' ', + positionalRequired.name, + ]), + ', ' + ], + if (declaration.positionalParameters.any((p) => !p.isRequired)) ...[ + '[', + for (var positionalOptional in declaration.positionalParameters + .where((p) => !p.isRequired)) ...[ + ParameterCode.fromParts([ + positionalOptional.type.code, + ' ', + positionalOptional.name, + ]), + ', ', + ], + ']', + ], + if (declaration.namedParameters.isNotEmpty) ...[ + '{', + for (var named in declaration.namedParameters) ...[ + ParameterCode.fromParts([ + if (named.isRequired) 'required ', + named.type.code, + ' ', + named.name, + if (named.defaultValue != null) ...[ + ' = ', + named.defaultValue!, + ], + ]), + ', ', + ], + '}', + ], + ') ', + body, + ])); + } + + @override + Future> constructorsOf(ClassDeclaration clazz) => + classIntrospector.constructorsOf(clazz); + + @override + Future> fieldsOf(ClassDeclaration clazz) => + classIntrospector.fieldsOf(clazz); + + @override + Future> interfacesOf(ClassDeclaration clazz) => + classIntrospector.interfacesOf(clazz); + + @override + Future> methodsOf(ClassDeclaration clazz) => + classIntrospector.methodsOf(clazz); + + @override + Future> mixinsOf(ClassDeclaration clazz) => + classIntrospector.mixinsOf(clazz); + + @override + Future declarationOf(NamedStaticType annotation) => + typeDeclarationResolver.declarationOf(annotation); + + @override + Future superclassOf(ClassDeclaration clazz) => + classIntrospector.superclassOf(clazz); + + @override + Future resolve(TypeAnnotation typeAnnotation) => + typeResolver.resolve(typeAnnotation); +} diff --git a/working/macros/api/src/protocol/protocol.dart b/working/macros/api/src/protocol/protocol.dart new file mode 100644 index 0000000000..bbc9e1b10e --- /dev/null +++ b/working/macros/api/src/protocol/protocol.dart @@ -0,0 +1,58 @@ +/// Defines the objects used for communication between the macro executor and +/// the isolate doing the work of macro loading and execution. +library protocol; + +import '../../expansion_protocol.dart'; +import '../../introspection.dart'; +import '../../builders.dart'; + +/// Base class all requests extend, provides a unique id for each request. +class Request { + final int id; + + Request() : id = _next++; + + static int _next = 0; +} + +/// A generic response object that is either an instance of [T] or an error. +class GenericResponse { + final T? response; + final Object? error; + final int requestId; + + GenericResponse({this.response, this.error, required this.requestId}) + : assert(response != null || error != null), + assert(response == null || error == null); +} + +/// A request to load a macro in this isolate. +class LoadMacroRequest extends Request { + final Uri library; + final String name; + + LoadMacroRequest(this.library, this.name); +} + +/// A request to instantiate a macro instance. +class InstantiateMacroRequest extends Request { + final MacroClassIdentifier macroClass; + final String constructorName; + final Arguments arguments; + + InstantiateMacroRequest( + this.macroClass, this.constructorName, this.arguments); +} + +/// A request to execute a macro on a particular declaration in the definition +/// phase. +class ExecuteDefinitionsPhaseRequest extends Request { + final MacroInstanceIdentifier macro; + final Declaration declaration; + final TypeResolver typeResolver; + final ClassIntrospector classIntrospector; + final TypeDeclarationResolver typeDeclarationResolver; + + ExecuteDefinitionsPhaseRequest(this.macro, this.declaration, + this.typeResolver, this.classIntrospector, this.typeDeclarationResolver); +} diff --git a/working/macros/pubspec.yaml b/working/macros/pubspec.yaml index ffb160bdce..05c334cd37 100644 --- a/working/macros/pubspec.yaml +++ b/working/macros/pubspec.yaml @@ -1,4 +1,6 @@ name: macro_proposal publish_to: none +dev_dependencies: + test: ^1.19.0 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/working/macros/test/isolate_mirror_executor/isolate_mirror_executor_test.dart b/working/macros/test/isolate_mirror_executor/isolate_mirror_executor_test.dart new file mode 100644 index 0000000000..5ba5fedb63 --- /dev/null +++ b/working/macros/test/isolate_mirror_executor/isolate_mirror_executor_test.dart @@ -0,0 +1,138 @@ +import 'dart:io'; + +import '../../api/code.dart'; +import '../../api/src/protocol/isolate_mirror_executor.dart'; +import '../../api/expansion_protocol.dart'; +import '../../api/builders.dart'; +import '../../api/introspection.dart'; + +import 'package:test/fake.dart'; +import 'package:test/test.dart'; + +void main() { + late MacroExecutor executor; + + setUp(() async { + executor = await IsolateMirrorMacroExecutor.start(); + }); + + test('can load macros and create instances', () async { + var clazzId = await executor.loadMacro( + File('test/isolate_mirror_executor/simple_macro.dart').absolute.uri, + 'SimpleMacro'); + expect(clazzId, isNotNull, reason: 'Can load a macro.'); + + var instanceId = + await executor.instantiateMacro(clazzId, '', Arguments([], {})); + expect(instanceId, isNotNull, + reason: 'Can create an instance with no arguments.'); + + instanceId = + await executor.instantiateMacro(clazzId, '', Arguments([1, 2], {})); + expect(instanceId, isNotNull, + reason: 'Can create an instance with positional arguments.'); + + instanceId = await executor.instantiateMacro( + clazzId, 'named', Arguments([], {'x': 1, 'y': 2})); + expect(instanceId, isNotNull, + reason: 'Can create an instance with named arguments.'); + + var definitionResult = await executor.executeDefinitionsPhase( + instanceId, + _FunctionDeclaration( + isAbstract: false, + isExternal: false, + isGetter: false, + isSetter: false, + name: 'foo', + namedParameters: [], + positionalParameters: [], + returnType: + _TypeAnnotation(Code.fromString('String'), isNullable: false), + typeParameters: [], + ), + _FakeTypeResolver(), + _FakeClassIntrospector(), + _FakeTypeDeclarationResolver()); + expect(definitionResult.augmentations, hasLength(1)); + expect(definitionResult.augmentations.first.debugString().toString(), + equalsIgnoringWhitespace(''' + augment String foo() { + print('x: 1, y: 2'); + return augment super(); + }''')); + }); +} + +class _FakeClassIntrospector with Fake implements ClassIntrospector {} + +class _FakeTypeResolver with Fake implements TypeResolver {} + +class _FakeTypeDeclarationResolver + with Fake + implements TypeDeclarationResolver {} + +class _FunctionDeclaration implements FunctionDeclaration { + @override + final bool isAbstract; + + @override + final bool isExternal; + + @override + final bool isGetter; + + @override + final bool isSetter; + + @override + final String name; + + @override + final Iterable namedParameters; + + @override + final Iterable positionalParameters; + + @override + final TypeAnnotation returnType; + + @override + final Iterable typeParameters; + + _FunctionDeclaration({ + required this.isAbstract, + required this.isExternal, + required this.isGetter, + required this.isSetter, + required this.name, + required this.namedParameters, + required this.positionalParameters, + required this.returnType, + required this.typeParameters, + }); +} + +class _TypeAnnotation implements TypeAnnotation { + @override + final Code code; + + @override + final bool isNullable; + + _TypeAnnotation(this.code, {required this.isNullable}); +} + +extension _ on Code { + StringBuffer debugString([StringBuffer? buffer]) { + buffer ??= StringBuffer(); + for (var part in parts) { + if (part is Code) { + part.debugString(buffer); + } else { + buffer.write(part.toString()); + } + } + return buffer; + } +} diff --git a/working/macros/test/isolate_mirror_executor/simple_macro.dart b/working/macros/test/isolate_mirror_executor/simple_macro.dart new file mode 100644 index 0000000000..abe2b577f4 --- /dev/null +++ b/working/macros/test/isolate_mirror_executor/simple_macro.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import '../../api/introspection.dart'; +import '../../api/builders.dart'; +import '../../api/code.dart'; +import '../../api/macros.dart'; + +/// A very simple macro that annotates functions (or getters) with no arguments +/// and adds a print statement to the top of them. +class SimpleMacro implements FunctionDefinitionMacro { + final int? x; + final int? y; + + SimpleMacro([this.x, this.y]); + + SimpleMacro.named({this.x, this.y}); + + @override + FutureOr buildDefinitionForFunction( + FunctionDeclaration method, FunctionDefinitionBuilder builder) { + if (method.namedParameters + .followedBy(method.positionalParameters) + .isNotEmpty) { + throw ArgumentError( + 'This macro can only be run on functions with no arguments!'); + } + builder.augment(FunctionBodyCode.fromString('''{ + print('x: $x, y: $y'); + return augment super(); + }''')); + } +}