-
Notifications
You must be signed in to change notification settings - Fork 7
Description
Thought I'd separate this out from #6 (comment) since it is a different request.
The basic idea is to be able to manipulate and consume the AST of expressions as macro arguments, similarly to the proposed statement macros in #24 (comment), but in the declaration context.
(Copied from #6 (comment))
The advantage of AST inspection means that you can have fewer magic strings and utilize more of what the programmer has already written to ensure valid programs with raw unresolved identifiers. If you could inspect the AST of the macro arguments as well it could solve the issues with raw identifiers instead of strings in #23 (comment) as well . The nim programming language has a concept of untyped / typed / static arguments for their macros. https://nim-lang.org/docs/tut3.html. TLDR; untyped arguments just have to be parseable and are not semantically checked or resolved or expanded, and are passed to the macro as a syntax tree. Typed are similar but are resolved. Static are passed as their value.
In Dart that might look like:
class MyUntypedMacro implements ClassDeclarationMacro { MyUntypedMacro(UntypedExpression ast, TypedExpression typedAst, int value); void visitClassDeclaration( ClassDeclaration declaration, ClassDeclarationBuilder builder){ // `ast` is the actual unresolved AST for (1 + 2) // `typedAst` is the actual resolved AST for SomeClass.someMethod // `value` is 10 } } @MyUntypedMacro((1 + 2), SomeClass.someMethod, 10); class MyClass {}Where the macro system encounters an UntypedExpression or TypedExpression in the macro constructor it would hand the unresolved or resolved ast when actually constructing the macro instance, whereas the exact value 10 gets passed into the constructor since it is neither an Untyped or Typed expression. This would be limited to expressions, possibly blocks? (though in Nim that is not the case). From the user's point of view, a TypedExpression or UntypedExpression argument of a macro is essentially typed as dynamic.
As a more concrete example, I implemented basic support for such macros in my fork:
Here is a functional widget macro using this approach:
https://github.com/TimWhiting/macro_prototype/blob/main/flutter_example/lib/macros/functional_widget2.dart
Macro Implementation
extension on analyzer.ParameterElement {
TypeReference get typeRef =>
AnalyzerTypeReference(type.element! as analyzer.TypeDefiningElement,
originalReference: type);
}
class FunctionalWidget2 implements ClassDeclarationMacro {
final ResolvedAST buildMethod;
const FunctionalWidget2(this.buildMethod);
@override
void visitClassDeclaration(
ClassDeclaration declaration, ClassDeclarationBuilder builder) {
final build = buildMethod.ast;
if (build is! ast.FunctionExpression) {
throw ArgumentError(
'Build Method is not a Function Expression ${build.runtimeType}');
}
final positionalParams =
build.parameters!.parameterElements.where((p) => p!.isPositional);
if (positionalParams.isEmpty ||
positionalParams.first!.type.getDisplayString(withNullability: true) !=
'BuildContext') {
throw ArgumentError(
'FunctionalWidget functions must have a BuildContext argument as the '
'first positional argument');
}
final namedParams = {
for (var p = 0; p < build.parameters!.parameters.length; p++)
if (!build.parameters!.parameterElements[p]!.isPositional)
build.parameters!.parameters[p]:
build.parameters!.parameterElements[p]
};
var positionalFieldParams = positionalParams.skip(1);
var fields = <Code>[
for (var param in positionalFieldParams)
Declaration('final ${param!.typeRef.toCode()} ${param.name};'),
for (var param in namedParams.values)
Declaration('final ${param!.typeRef.toCode()} ${param.name};'),
];
var constructorArgs = <Code>[
for (var param in positionalFieldParams)
Fragment('this.${param!.name}, '),
Fragment('{'),
for (var param in namedParams.keys)
Fragment(
'${param.isRequired ? 'required ' : ''}this.${param.identifier!.name}, '),
Fragment('Key? key, }'),
];
var widgetName = declaration.name;
var constructor = Declaration.fromParts(
['const $widgetName(', ...constructorArgs, ') : super(key: key);']);
var method = Declaration.fromParts([
'''
@override
Widget build(BuildContext context) ${build.body.toSource()}
'''
]);
builder.addToClass(Declaration.fromParts([
...fields,
constructor,
method,
]));
}
}and the application:
@FunctionalWidget2((BuildContext context,
{String? appTitle, String? homePageTitle}) {
return MaterialApp(
title: appTitle ?? 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(title: homePageTitle ?? 'Flutter Demo Home Page'));
})
class MyApp2 extends StatelessWidget {}Ideally I'd like to get rid of the class declaration and generate that as well using an additional UntypedAst parameter, but this is currently impossible since annotations have to be attached to a declaration
@FunctionalWidget2(MyApp2, (BuildContext context,
{String? appTitle, String? homePageTitle}) {
return MaterialApp(
title: appTitle ?? 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(title: homePageTitle ?? 'Flutter Demo Home Page'));
})
// Problem no element to attach the annotation toIn order to make this work the annotation would have to be on the library or be like the proposed statement macro (omitting the @ - which would be actually really nice).
FunctionalWidget2(MyApp2, (BuildContext context,
{String? appTitle, String? homePageTitle}) {
return MaterialApp(
title: appTitle ?? 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(title: homePageTitle ?? 'Flutter Demo Home Page'));
})In general with this you can use bare identifiers / references instead of Strings in the macro instantiation, which makes it look a ton better. It looks more like a complete program, rather than a partial hacky program that magically turns Strings into real identifiers and code.