-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Code generation (metaprogramming) proposal v2 #1565
Comments
This is really cool, I appreciate the lack of magic, and the highly readable code. It looks like it would be quite easy to support some special cases in auto-dispose as well, like: // Usage
@shouldDispose(useCancel: true)
Timer timer = Timer();
...
// Macro
String getDisposeMethodForType(Type type){
if(type is Timer || type is StreamSubscription || useCancel) return "cancel";
return "dispose";
}
...
if (field.annotation is ShouldDispose){
"${field.name}.${getMethodForType(field.type)}();",
} This example could also be made more realworld if it supports null aware operators. It should write out Another thing to consider is how refactoring will work. If I re-factor MyButton via IDE, would the "MyButton" string be updated somehow? It might be nicer for maintainability if a prefix-system were used here, like: @WidgetCreator()
Widget createMyButton(){ // Some error could occur if create is not the prefix here
ElevatedButton(
child: Text(""),
onPressed: () {},
);
} Where |
I like the syntax. Although:
To be honnest this looks like a buider that is built in dart and runs before each compilation. I stand by my position that most use cases there could or even should be resolved another way, but if that means we get all those features all at once, so be it. |
@esDotDev you can make it even simpler! class ShouldDispose {
final String methodName;
const ShouldDispose({this.methodName = "dispose"});
}
// ...
@shouldDispose(methodName: "cancel")
Timer timer = Timer();
// ...
if (field.annotation is ShouldDispose){
"${field.name}.${field.annotation.methodName}();",
}
Sorry I don't use an IDE with a "refactor" button, what exactly would it do? I'm trying to limit the magic here, so the name would be in the annotation and the body of the function is simply copied into the new widget. So if you refactor your widget code, the code gen will copy that new code into
Again, to reduce magic, I don't want to introduce any new mechanism if we don't have to. An annotation would be enough, and you only declare (and maintain) the name of the widget in one place: in the annotation. Keep in mind, not all macros create widgets, so you can't introduce a new mechanism just for that. |
Refer to #252, not my idea
Well, it was just an example -- plus, I still don't have concrete details on reflection yet (I need to take a deeper look at the
Yes, I think Dart should automatically inject the
No, these files are actually created in the filesystem, totally accessible to the user. I mean, they'll probably go in the
Yeah, that's pretty much exactly what this is. Although, hopefully we can optimize it to only run when we need it. I agree that most problems can be solved effectively with OOP and more functions, but there are some cases where the boilerplate just gets too much. But I'll bring up another point that @esDotDev once made: Making a Same idea with |
IDE lets you right-click on any Class or Method, and rename it, it scans the entire codebase and does a "safe" find and replace for those names. It's basically a glorified find and replace, with some sort of analyzer ability. http://screens.gskinner.com/shawn/d3ATmjLVlP.mp4 The importance as it relates here, is that it would make it very easy for anyone, anywhere in the codebase, to rename
I think having the class handle super common cases like Timer and StreamSubscription manually is better than forcing developers to remember to type a magic "cancel" string, but we're into implementation details there, and all cases look easy enough regardless. |
Remember, |
Totally, no need to get into the weeds on implementation details, other than to reveal use cases. In this case would be interesting to see how type checks would work (in general), and how null aware code might be written (in general). But I guess this is mostly a feature of the yet-to-be-written analyzer? |
Type safety is something I've pretty much given up on with code-gen. As you probably noticed from my first proposal, I tried very hard to shoehorn it in. It was a
All these, (particularly number 2!), showed me that there was no good way to handle this. That's why my new proposal focuses more on how to effectively integrate with the analyzer/regular way of writing Dart code. If the analyzer can instantly react when you apply a macro to a class, then that's essentially free type-safety. In other words, yes there will be a lot of type errors, but they'll be "errors" similar to the
I was referring to the analyzer that currently comes with Dart. I plan to integrate this proposal specifically with the |
Sorry I just meant null aware code from the perspective of generation, like:
Similarly with type, the macro might want to check for whatever reason, I'm assuming something like this would work? I really like the idea of instant code changes, and then just leaning on the compiler to flag errors. I'm still not sure who would be checking for errors on strings literals though... if MyButton class gets renamed to MyButton2 manually by a programmer (which will change all usages across the codebase to MyButton2), seems like the macro would just immediately pump out a new MyButton. Now your code is referencing MyButton2, which is de-coupled from the src method. |
In my example,
Okay let's go a little more into detail for your example: // function.dart
part "function.g.dart";
@WidgetCreator(widgetName: "MyButton1")
Widget buildButton() => ElevatedButton(/* ... */); // main.dart
import "function.dart";
void main() => runApp(MyButton1()); Now, let's say we change
All this can be summed up by saying that the code behaves to Dart as though it was written by a human. You as the developer still have to make sure you know what you're doing. If your IDE can help you, great. Otherwise, you just have to be a little careful. Or you can make As for the actual code gen, there are two "stages" of debugging generated code. The first is the macro writer, who writes the macro. The string literal itself, like you said, won't report any errors. But as soon as you put |
This is where refactoring with IDE comes into play. Rather than "rename in main.dart", you can think of it as "rename everywhere that class is referenced across the entire project". This means, when a dev hits F2, and Types At a high level, the IDE needs to tell the code gen system: This is what I just changed, and then the code-gen system could respond, but I don't think that is in scope of what you're proposing... or maybe it is? If you were to receive an event from IDE that "ClassA" is now "ClassB", could the declaration |
It would be great if we could somehow eliminate use of |
Presumably, the compiler would be able to detect if the macro or the affected code changes, and only re-generate the code if it has. For example, if you import a macro that someone else made, it's very unlikely to change. That way, code-gen becomes a set-and-forget process, and won't impact the build process in the long-term. Where the generated code ends up going won't affect this process. An often-requested feature of code-gen (which I agree with) is that generated code should not only behave like hand-written code, but should be just as accessible to devs, one reason being it makes debugging simpler. Which means not only inspecting generated code, but also seeing analyzer output, using an IDE's "Go to definition" feature, etc. In that sense it would be better to have a regular file. Dart is remarkable in it's lack of "magic", and hiding code from devs would really disrupt that. |
Ah, here's where we were talking past each other -- you should never (even with current code-gen tools) edit a |
Ah I see. Looking at how functional_widget works, you can rename the This seems basically inline with what you'd expect, any changes to generated code are overwritten next time generator is run. A quality of life feature could potentially be built into the IDE to not allow refactor on classes that originate in a .g file. |
Wouldn't it be more extensible to give back some kind of representation of the code which can be generated via a string or another way (codegen API)? You still have all the benefits of the strings but still a way to use a "real" API later / as another way. (don't know if AST is the right word) AST generate(CodeGenerator generator) {
return generator.fromString('String greet() => "This is a $sourceName class";');
} then you could also later or as another API add more of a method based approach: return generator.createMethod(
name: 'toString', // name of created method
returnType: String,
parameters: [ // matches parameter in body reference function
context.mirror.className,
context.mirror.fields,
Parameter(type: int, value: someParam), // just for API demonstration
],
body: toJsonBody, // function for reference
);
|
@Levi-Lesches Is there any way you could fix the small typo in the title? 😁 ❤️
|
Sigh, no matter how much you review something mistakes always slip through! Fixed it, thanks.
Well, two points on this: First, the way this proposal is set up, the Dart team doesn't have to build in any new code to teach the compiler what the AST is, or how to generate code methodically. All it has to do is write strings to a file, which is easily done with Secondly, after discussing it both in #1482 and #1507, I think many people came to the conclusion that having an API can end up making code-gen messier, not cleaner. Let's try fleshing out a full example @override
Code generate(CodeGenerator generator) => generator.createMethod(
name: 'toJson',
// Dart doesn't support nullables or generics as `Type` values. So you can't have Map<String, dynamic>
returnType: Map,
parameters: [ // are these going to be used in the generated function?
context.mirror.className,
context.mirror.fields,
],
// what goes here?
// Is it the actual function or a function that generates code based on `className` and `fields`?
// If it's the actual function, how do you use variables like the name of the class and return type?
// If it's a function to generate the code, you might as well use that instead of an API
body: toJsonBody,
); VS @override
String generate() => ""; // start writing code immediately. Some other issues with an API, as mentioned in the above issues:
In other words, I stand by my original comment:
If you have a specific use-case in mind that feels awkward with strings, please feel free to share. |
I completely understand your arguments "string vs AST". I guess for me it just feels weird to have something as "low level" as a string directly as a return value from the My argument is just that it would be more extensible to create the code via string-templating with something like my example of a Also what about some parameters to tune the code generation? return generator.fromString(`...`, someTemplatingOption: true);
// Or
generator.setSomeOption(false);
generator.appendString(`...`);
generator.appendString(`...`);
return generator.code; My point is that it AST was just some example of another Api that could thus added later in time without make breaking changes. Is there a way to also set options by overriding getter in May there be use cases where named arguments or methods on the In the end my biggest concern was really just my gut feeling having something like a String given back. I know it makes sense if you only use string templating to write code. Still there may be some benefits to an approach as described. |
I am surprised that all the discussions around Dart code generation remains at the level of string concatenation that is later parsed by the compiler. This seems unnecessarily low-level and detached from the actual language 👎🏻 Lisp and Scheme have a really nice concept around self-evaluating forms and quoting. I realize this is harder to adopt for a more complicated language like Dart; but I think it is worth to investigate and has successfully been done for other languages such as R, Rust, Scala, Smalltalk, Haskell, OCaml, C#, Julia, ... |
@Jonas-Sander, I'd agree with you if there was some @renggli, based on that Wikipedia section you linked, it seems like "quoting" is just... string concatenation?
I haven't used Lisp, so tell me if I'm wrong, but when you define I can understand to many that generating strings can feel wrong, but really everything is a string to begin with. When you write code in a Take the linked C# article:
That's equivalent to: String add(int a, int b) => "a + b";
String times2(String expression) => "($expression) * 2";
String generate() => double(add(3, 4)); // (3 + 4) * 2 The only difference is that there needs to be a way to convert strings back to code -- that's why this proposal uses There are also problems with using actual reflection (like tree-shaking) and type safety. While I'm not part of the Dart team, they've spoken up numerous times saying something like this would be incompatible with Dart today, or at least, very very hard. This proposal focuses on easing that strain to make code-gen more compatible with Dart and easier to implement, by treating code as strings, like humans would. Maybe this way, the Dart team may pick this up sooner rather than later. I'll quote @eernstg from #592:
Now all that being said, I can certainly understand that this isn't going to satisfy a lot of people, and I encourage those who want to to try to find a way to fit code-gen more "natively" into Dart, but I would ask that it be in a new issue, because I consider it out-of-scope for this proposal. From Dart's perspective, they really are two entirely different problems -- generating strings and writing files is easily doable today, whereas full langauge support for metaprogramming would be a much greater effort. |
@tatumizer, based on your experience, do you feel this proposal would play nicely with the code you've written so far? Would it have made any parts of it easier? Harder? |
This approach is really hard to argue with because:
So in the end, the argument for something more complex doesn't seem to justify it's existence. It would not open up any new use cases we can't already do, it would take much longer to get developed, it would likely have bugs for many mths or yrs of it's existence, etc etc all the fun stuff that comes with an order of magnitude more complexity. And for what? Compile safety on snippets that most developers never see, and are virtually never modified? It's not worth it. Especially when you consider the test/debug flow of the proposed solution, it has instant string output, so as you are typing and saving, you could see the new code being generated, it would be extremely easy to debug, and the compiler would flag errors in realtime as you're hitting save. |
Also, fundamentally, quick assists are tools built-in to the IDE, which means they're a tool devs use for development. Code-gen, on the other hand, would be a part of the finished product and would stay in the code base. Meaning, They're two different tools with their own uses. Assists serve to, well, assist the developer in writing code, like a smarter auto-complete, whereas code-gen will bridge the dev's intent to the Dart compiler, similar to how OOP already does. I think we can all agree that even though |
Correct.
To be clear, the results of assists are also part of the finished product. A concrete example might help. If you have code like the following:
there is a quick assist to "Convert to async function body" that will update your code to be
There are also assists to wrap Flutter widgets in other widgets to aid in the construction of a widget hierarchy.
Correct. |
Correct, what I meant to say is, the heaps of code replaced by a single annotation will remain that way. IDEs add code to your files, code-gen removes it. |
The primary authors of the proposal (@jakemac53 and @munificent) would be able to give more authoritative answers than I can, but ...
I believe that this is still under discussion, but the expectation / requirement is that the source would be available in a form that allows the code to be displayed for the purposes of debugging, code navigation, etc. I'm guessing it will probably be generated to a file, because it seems like the most straight-forward approach, but I believe that the answer to that is still being discussed. I'm also guessing that the file will be in the same directory used by the
I believe that there are a couple of proposals being discussed. I would expect that at least one of the proposals would be for the file to be a part file similar to the one generated by the
I'm only aware of three common solutions to the kind of problem we're trying to solve:
Each has its own set of advantages and disadvantages. It isn't an area that I've followed closely, so there might well be other solutions that I'm not aware of. |
That would be me :)
@themisir and I suggested a format for this:
As a concrete example, let's say you have a dataclass under
// dataclass.dart
part "generated:src/data/dataclass.dart"; // <-- can be auto-inserted by Dart
@MyMacro()
partial class MyDataClass { } // dataclass.g.dart
part of "package:src/data/dataclass.dart";
partial class MyDataClass { } Keep in mind that since |
@tatumizer I think the point of code-gen is to ultimately have less, not more. No point in copy-pasting existing code into a new file with the generated pieces if Broadly, I'm glad we went from "what do we want out of code-gen" to discussing details. I believe that's progress 😊 |
I'm pulling from existing concepts, like OOP. When you extend a class, Dart doesn't copy the implementation into a new class A {
String greet() => "Hello, I am A";
void dance() => print("I am dancing");
}
class B extends A {
@override
String greet() => "Hello, I am B";
} Dart doesn't literally copy over |
Let's imagine my class If the debugger would step into foo within a .g.dart file it would be annoying because fixes in foo I'm making while using the debugger wouldn't directly go into my existing code. If bar invokes foo and I step through bar and press Crtl+B on foo I want to go to the initial file that's editable and not a .g.dart file or a .staging.dart file. When doing a code search, it's also unhelpful to have an additional nonwriteable Only |
@tatumizer Depending on the syntax chosen for "partial classes", maybe partial class Foo {
void methodB() { ... }
} Then, when compiling, all the partial class declarations are combined into one (impartial?) class declaration containing all the members (and you'll get errors if there is more than one non-abstract version of any member, or more than one superclass declared). For something like observable, I'd probably do |
Yep, pretty much. That, and it makes
Yep, I (very roughly) oultined in the original post how I would think it can be implemented, and it's pretty similar. It shouldn't (to my konwledge) require too much change:
That's the beauty of annotations, or at least an annotation-like syntax. You can simply choose an ordering, like bottom-up, and stick with that. @DebugAllMethods() // last
@ToJson() // second
@DataclassMethods(vopyWith: true) // first
class Foo { } |
Dart shouldn't do this automatically, but we could use a simple lint: If the anlayzer sees a line with Long answer: Why is removing an obsolete In fact, this makes |
Even though part files are discouraged in Effective Dart, they're still usable outside of code-gen. Flutter used to be built around part files instead of libraries. Including a import "generated:file.g.dart";
export "generated:file.g.dart"; That's all. No magic, no code-gen-specific implementation. You can totally use regular
Also, as much as I hope this proposal can consolidate code-gen, there will certainly be other
This proposal has two restrictions: generate all code into a human-readable All that being said, my stance is that there should be a convenience factor, just not baked so deep in to the compiler. A lint like |
I also don't see the added value of part. Say you have a
This seems to me already like enough information to infer that there's a Then
I'm not sure whether extension methods can currently invoked from inside the original class, so that might require a slight change, but nothing major. |
Correct, extensions have several limitations, that's why we can use partial classes instead.
I agree at a high-level with this (and with @tatumizer) -- this should be loosely handled automatically. But in the interest of transparency, I'm not sure it's best to remove the Again, with that being said, the IDE has shortcuts to auto-insert a Flutter import. Same thing here: we can have a lint (which maps to an IDE quick-fix) help us insert/remove the
Yes, there is a relation between
Agreed. |
I think analyzer already shows error (not just warning) message if the main file is removed then part file becomes invalid because part files have to define 'part of' which points to the original file - which will be missing if removed. |
@tatumizer: This propsal doesn't have any such idea as "comptime". That's the point -- no new features. Instead, the compiler simply generates code and reprocesses in an iterative process. Say you have three files,
Minimal work, minimal changes to the compiler, no side-effects, etc. |
Does it? The compilation of // a.dart
part "generated:a.g.dart";
// Error: a.g.dart has not been generated
// Dart doesn't know whether SomeUnkown is actually undefined or to-be-generated.
// That's okay, it doesn't need to output anything meaningful just yet.
class MySubclass extends SomeUnknown { }
@Dataclass() // compiler will still see this
// as proof, the compiler will flag this as a syntax error
partial class A { blah }
Yes, compiled and executed. When compiling Flutter code, for instance, Dart doesn't actually run the main function until the app is opened, whereas macros must run immediately. When I said Think of it as "compile - stop - generate - restart" rather than "parse - generate - compile". Assuming the compiler works somewhat like class Compiler extends RecursiveElementVisitor {
@override
visitMacroElement(MacroElement element) {
if (element.needsToGenerate()) {
macro.generate();
throw GeneratingCode();
}
}
}
void compile() {
while (true) {
try {
return compiler.compileCode();
} on GeneratingCode {
continue;
}
}
} @lrhn, @eernstg or anyone on the Dart team, is what I'm proposing realistic? |
Another solution would be completely separating code generation from compilation, if what @Levi-Lesches said is impossible on current compilation platform. void compileCode() {
analyze(skipMissingFileErrors: true); // Initial analyzation to parse expression tree
generateCode(analyzedData); // Generate code if needed
analyze(); // Re-analyze again to check for possible errors & warnings
compile(); // Compile to final result
} But unfortunately this will result in increased compile time. |
No that's our point. By separating the logic for generating and compiling, you don't have to make any assumptions about the compiler. We know the analyzer can read and find macros, because it can currently find annotations. Based on that alone we can generate the code necessary without running the compiler on our code, just the macros. And that will work because the macros don't depend on anything else. It's abstraction. @themisir's script doesn't have to be embedded into the Dart compiler, it can be written as a whole separate Dart or bash script. |
It's cool that adding compile-time macro definitions allows for more runtime actionability 😎
|
It would be interesting to have a @JsonCodable verion of this proposal to compare it with what's currently being implemented |
So this proposal was trying to include the least amount of new features, only But back to your point, there are a few examples at the top of this post, but the |
This is my second draft of a proposal for code generation ("code gen" from now on) through specialized classes called "macros". The issue where this is being discussed is #1482 and my first proposal is #1507.
The key idea for me in code generation is that generated code should function exactly as though it were written by hand. This means that code gen should be strictly an implementation detail -- invisible to others who import your code. This further strengthens the need for code gen to be simple and expressive, so that any code that could be written by hand can be replaced by code gen, and vice-versa.
Another critical point is that code generation should not modify user-written code in any way. For this reason,
part
andpart of
are used to separate the human-written code from the generated. Partial classes (#252) help facilitate this, and this proposal heavily relies on them.Definition of a macro
A macro is a class that's responsible for generating code. You can make a macro by extending the built-in
Macro
superclass. There are two types of macros: those that augment classes, and those that generate top-level code from functions. TheMacro
class consists of thegenerate
function and some helpful reflection getters. The base definition of a macro is as follows:Here are class-based and function-based macros, with
Variable
representing basic reflection.Variable
ClassMacro
FunctionMacro
That being said, I'm not an expert on reflection so this is not really the focus of this proposal. I do know enough to know that
dart:mirrors
cannot be used, since it is exclusively for runtime. We want something more like theanalyzer
package, which can statically analyze code without running it. Again, the point is that code gen should work exactly like a human would, and the analyzer can be thought of as a second pair of eyes (as opposed todart:mirrors
which is a different idea entirely). Depending on how reflection is implemented, we may need to restrict subclasses ofMacro
, since that's where all the initialization happens.Using macros
To create a macro, simply extend
ClassMacro
orFunctionMacro
and override thegenerate
function. All macros will output code into a.g.dart
file. The code generated by aClassMacro
will go in a partial class, whereas the code generated by aFunctionMacro
will be top-level. Because the function outputs a String,List<String>.join()
can be a clean way to generate many lines of code while keeping each line separate.dart format
will be run on the generated code, so macros don't need to worry about indentation/general cleanliness. To apply a macro, use it like an annotation on a given class/function. Here is a minimal example:They key points are that writing the
generate()
method felt a lot like writing the actual code (no code-building API) and the generated code can be used as though it were written by hand. Also, the generated code is kept completely separate from the user-writtenPerson
class.Commonly requested examples:
The best way to analyze any new feature proposal is to see how it can impact existing code. Here are a few commonly-mentioned use-cases for code generation that many have a strong opinion on.
JSON serialization
Dataclass methods
Auto-dispose
Functional Widgets
Implementation
Since I'm not an expert on the Dart compiler, this proposal is targeted at the user-facing side of code generation. Anyway, I'm thinking that the compiler can parse user-code like normal. When it finds an annotation that extends
Macro
, it runs the macro'sgenerate
function and saves the output. Since more than one macro can be applied to any class/function (by stacking annotations), the compiler holds onto the output until it has generated all code for a given file. Then, it saves the generated code into a.g.dart
file (creating partial classes when applicable), injects thepart
directive if needed, and compiles again. This process is repeated until all code is generated. Dart does not need to support incremental compilation for this to work: the compiler can simply quit and restart every time new code is generated, and eventually, the full code will be compiled. It may be slow, but only needs to happen when compiling a macro for the first time. Perhaps the compiler can hash or otherwise remember each macro so it can regenerate code when necessary.More detailed discussions in #1578 and #1483 discuss how incremental/modular compilation can be incorporated into Dart.
This behavior should be shared by the analyzer so it can analyze the generated code. Thus, any generated code with errors (especially syntax errors) can be linted by the analyzer as soon as the macro is applied. Especially since generating strings as code is inherently unsound, this step is really important to catch type errors.
Syntax highlighting can be implemented as well, but is not a must if the analyzer is quick to scan the generated code. A special comment flag (like
// highlight
) may be needed.How IDE's will implement "Go to definition" will entirely depend on how that is resolved for partial classes in general, but since these will be standard
.g.dart
files, I don't foresee any big issues.cc from previous conversations: @mnordine, @jakemac53, @lrhn, @eernstg, @leafpetersen
FAQ
Why do you use partial classes instead of extensions?
Extensions are nice. They properly convey the idea of augmenting a class you didn't write. However, they have fundamental limitations:
Foo.fromJson()
is impossibleDisposer
andDataclass
wouldn't be possible.I experimented with mixins that solve some of those problems, but you can't declare a mixin
on
a class that mixes in said mixin, because it creates a recursive inheritance. Also, I want generated code to be an implementation detail -- if we use mixins, other libraries can import it and use it. Partial classes perfectly solve this by compiling all declarations ofpartial class Foo
as if they were a singleclass Foo
declaration.Why not use a keyword for macros?
I toyed around with the idea of
macro MyMacro
(likemixin MyMixin
) instead ofclass MyMacro extends ClassMacro
. There are two big problems with this. The first is that it is not obvious what is expected of a macro. By extending a class, you can easily look up the class definition and check out the documentation for macros. The other problem is that if we distinguish between functions and classes, there's no easy way to say that with amacro
keyword. By using regular classes, you can extendFunctionMacro
andClassMacro
separately, and possibly more. This also means that regular users can write extensions on these macros if they want to build their own reflection helpers.Also, the idea of special behavior applying to an object and not a special syntax isn't new. The
async/await
keywords only apply toFuture
s,await for
applies toStream
,int
cannot be extended, etc.Can I restrict a macro to only apply to certain types?
This is something I thought about briefly. I don't see any big problems with this, and it could let the macro use fields knowing that they will exist. There are two reasons I didn't really put much work into this. Because I use macros as regular class, you can't simply use
on
. Would we use generics onClassMacro
? I'm impartial to it, but we'd have to have a lint to check for it since there is no connection between annotations and generics. Obviously this wouldn't apply toFunctionMacro
or anything else. The second reason was that I want to encourage using reflection in code generation instead of simply relying on certain fields existing. For example,Disposer
would be safer to write with this feature, but instead I opted to use reflection, and as such created an annotation that the user can apply to each field they want to dispose. And by using@override
in the generated code, the analyzer will detect when the macro is applied to a class that doesn't inherit avoid dispose()
.Why just classes and functions? What about parameters, fields, top-level constants and anywhere an annotation is allowed?
I couldn't think of a good example. Reply if you have one and we can think about it. There were two reasons why it's unusual: One, reflection is a big part of macros. If there's nothing to reflect on, maybe code generation is not the right way to approach it. Two, macros should be logically tied to a location in the human-written code. Classes and functions were the most obvious. It's not so obvious (to me anyway) what sort of code would be generated from a parameter in a function. I suppose you may be able to extend
Macro
directly (depending on how reflection is implemented) and apply that to any code entity you like.Aren't Strings error-prone?
Yes, but APIs are clunky and can quickly become out-of-date when new features/syntax are introduced. Strings reduce the learning curve, ease maintenance needed by the Dart team, as well as being maximally expressive. Win/win/win!
I'd love to hear feedback on this, but let's try to keep the conversation relevant :)
The text was updated successfully, but these errors were encountered: