-
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
Static Metaprogramming #1482
Comments
Would static function composition be in the scope of this feature? At the moment, higher-order functions in Dart are fairly limited since they require knowing the full prototype of the decorated function. An example would be a void Function() debouce(Duration duration, void Function() decorated) {
Timer? timer;
return () {
timer?.cancel();
timer = Timer(duration, () => decorated());
};
} which allows us to, instead of: class Example {
void doSomething() {
}
} write: class Example {
final doSomething = debounce(Duration(seconds: 1), () {
});
} but that comes with a few drawbacks:
With static meta-programming, our class Example {
@Debounce(Duration(seconds: 1))
void doSomething() {
print('doSomething');
}
@Debounce(Duration(seconds: 1))
void doSomethingElse(int value, {String named}) {
print('doSomethingElse $value named: $named');
}
} |
There is a delicate balance re: static function composition, but there are certainly many useful things that could be done with it. I think ultimately it is something we would like to support as long as we can make it obvious enough that this wrapping is happening. The specific balance would be around user confusion - we have a guiding principle that we don't want to allow overwriting of code in order to ensure that programs keep their original meaning. There are a lot of useful things you could do by simply wrapping a function in some other function (some additional ones might include uniform exception handling, analytics reporting, argument validation, etc). Most of these things would not change the meaning really of the original function, but the code is being "changed" in some sense by being wrapped. Ultimately my sense is this is something we should try to support though. I think the usefulness probably outweighs the potential for doing weird things. |
I like Lisp approach (in my opinion, the utmost language when it comes to meta-programming). Instead of defining a |
For something like debounce, a more aspect-like approach seems preferable. Say, if you could declaratively wrap a function body with some template code: class Example {
void doSomething() with debounce(Duration(seconds: 1)) {
print('doSomething');
}
void doSomethingElse(int value, {String named}) with debounce(Duration(seconds: 1)) {
print('doSomethingElse $value named: $named');
}
}
template debounce<R>(Duration duration) on R Function {
template final Stopwatch? sw;
template late R result;
if (sw != null && sw.elapsed < duration) {
return result;
} else {
(sw ??= Stopwatch()..start()).reset();
return result = super;
}
} This defines a "function template" (really, a kind of function mixin) which can be applied to other functions. (Maybe we just need AspectD for Dart.) |
But an important part of function composition is also the ability to inject parameters and ask for more parameters. For example, a good candidate is functional stateless-widgets, to add a @statelessWidget
Widget example(BuildContext context, {required String name}) {
return Text(name);
} and the resulting prototype after composition would be: Widget Function({Key? key, required String name}) where the final code would be: class _Example extends StatelessWidget {
Example({Key? key, required String name}): super(key: key);
final String name;
@override
Widget build(BuildContext) => originalExampleFunction(context, name: name);
}
Widget example({Key? key, required String name}) {
return _Example(key: key, name: name);
} |
I definitely agree we don't want to allow for changing the signature of the function from what was written. I don't think that is prohibitive though as long as you are allowed to generate a new function/method next to the existing one with the signature you want. The original function might be private in that case. |
That's what functional_widget does, but the consequence is that the developer experience is pretty bad. A major issue is that it breaks the "go to definition" functionality because instead of being redirected to their function, users are redirected to the generated code It also causes a lot of confusion around naming. Because it's common to want to have control on whether the generated class/function is public or private, but the original function to always be private. By modifying the prototype instead, this gives more control to users over the name of the generated functions. |
Allowing the signature to be modified has a lot of disadvantages as well. I think its probably worse to see a function which is written to have a totally different signature than it actually has, than to be navigated to a generated function (which you can then follow through to the real one). You can potentially blackbox those functions in the debugger as well so it skips right to the real one if you are stepping through. |
I suppose this will allow generating |
Yes. |
@tatumizer This issue is just for the general problem of static metaprogramming. What you describe would be one possible solution to it, although we are trying to avoid exposing a full AST api because that can make it hard to evolve the language in the future. See https://github.com/dart-lang/language/blob/master/working/static%20metaprogramming/intro.md for an intro into the general design direction we are thinking of here which I think is not necessarily so far off from what you describe (although the mechanics are different). |
Great intro & docs. Hopefully we'll stay (far far) away from annotations to develop/work with static meta programming?! |
The main reason we use this as an example is its well understood by many people, and it is also actually particularly demanding in terms of features to actually implement due to the public api itself needing to be generated :).
Can you elaborate? Default values for parameters are getting some important upgrades in null safe dart (at least the major loophole of being able to override them accidentally by passing |
I believe the issue is that we cannot easily differentiate between freezed supports this, but only because it relies on factory constructors and interface to hide the internals of |
Right, this is what I was describing which null safety actually does fix at least partially. You can make the parameter non-nullable (with a default), and then null can no longer be passed at all. Wrapping functions are required to copy the default value, basically it forces you to explicitly handle this does cause some extra boilerplate but is safe. For nullable parameters you still can't differentiate (at least in the function wrapping case, if they don't provide a default as well) |
Metaprogramming is a broad topic. How to rationalize? We should start with what gives the best bang for buck (based on use cases). Draft topics for meta programming 'output' code:
Also on output code:
Would be great if this could work without saving the file, a IDE-like syntax (hidden) code running continuously if syntax is valid. I refuse to use build_runner's |
Metaprograming opens doors to many nice features Other language that does a great job at implementing macros is Haxe you can use Haxe language to define macros I guess there are many challenges to implement this. |
can we extend classes with analyzer plugin? |
I'm not sure if I like the idea having this added to Dart because the beauty of Dart is its simplicity. The fact that it isn't as concise as other languages it in reality an advantage because it makes Dart code really easy to read and to reason about. |
I agree with this. |
@escamoteur Writing less code does not make it more complicated necessarily. It can, I agree, if someone does not fully understand the new syntax. But the trade-off is obvious: time & the number of lines saved vs the need for someone to learn a few capabilities. Generated code is normal simple code. I just suggested real-time code generation instead of running the builder every time or watching it to save. That way you get real time goto. But if you are using notepad then of course you need to run a process. |
Just to be 100% clear, we are intensely focused on these exact questions. We will not ship something which does not integrate well with all of our tools and workflows. You should be able to read code and understand it, go to definition, step through the code in the debugger, get good error messages, get clear and comprehensible stack traces, etc. |
In my honest opinion: things must be obvious, not magical.
^ this |
But there is nothing beautiful about writing data classes or running complicated and and slow code-generation tools. I'm hoping this can lead to more simplicity not less. Vast mounds of code will be removed from our visible classes. StatefulWidget can maybe just go away? (compiler can run the split macro before it builds?). Things can be auto-disposed. Seems like this could hit a lot of pain points, not just data classes and serialization.. |
Since dart currently offers code generation for similar jobs-to-be-done, I'd suggest evaluating potential concerns with that consideration:
On the other hand, besides being an upgrade from codegen for developers, metaprogramming could provide healthier means for language evolution beyond getting data classes done. Quoting Bryan Cantrill:
PS @jakemac53 the |
this would be fantastic if it allowed, the longed-for serialization for JSON natively without the need for manual code generation or reflection in time of execution Today practically all applications depend on serialization for JSON, a modern language like dart should already have a form of native serialization in the language, being obliged to use manual codegen or typing serialization manually is something very unpleasant |
My approach on a macro mechanism. Basically tagging a certain scope with a macro annotation, that refers to one or multiple classes to 1:1 replace the code virtually... like a projection. It's very easy to understand and QOL can be extended by providing utility classes. #ToStringMaker() // '#' indicates macro and will rewrite all code in next scope
class Person {
String name;
int age;
}
// [REWRITTEN CODE] => displayed readonly in IDE
// class Person {
// String name;
// int age;
//
// toString() => 'Person(vorname:$name, age:$age)'
// }
class ToStringMaker extends Macro {
// fields and constructor can optionally obtain parameters
@override
String generate(String code, MacroContext context) { // MacroContext provides access to other Dart files in project and other introspection features
var writer = DartClassWriter(code); // DartClassWriter knows the structure of Dart code
writer.add('String toString() => \'${writer.className}(${writer.fields.map(field => '${field.name}:\${field.name}').join(', ')})\'');
return writer.code; // substitute code for referenced scope
}
} |
Yes, augmentations likely do work already to some extent. The implementations are still at a very early stage though so it isn't really ready for people to start trying it out, you will quickly hit unimplemented things and things that behave incorrectly. |
Considering how unwieldy and tedious the current build runner is, I doubt the criteria about dev experience and usefulness would be a problem. |
I still wonder if we couldn't improve build_runner. Macros will be better no-matter what. But maybe it's worth degrading experience of build_runner package authors, so that resulting packages are more efficient. |
In regards to IDE tooling, will generated augmentation libraries be generated as on-disk files alongside the user's files, or elsewhere in the file system? Also, assuming that generated code is easily visible to the developer, if there's a compile time error with the generated code due to developer misuse, will the augmentation code still be generated with an error or will the code not be generated with an error appearing at the macro annotation site? Will macro authors have control over this? E.g., given the following code: // Dto class that is missing `@JsonSerializable()`
class UserDto {
const UserDto(this.name);
final String name;
} // Serializer class that consumes dtos that are expected
// to be serializable
@Mappable(UserDto)
class UserMapper { } Would the below semantically-invalid code be generated or not? augment class UserMapper {
Map<String, dynamic> serialize(UserDto data) =>
data.toJson(); // error from a missing `toJson()`method on `UserDto`
} |
Macros don't create actual files. Their output is somewhere in memory. There will be special IDE considerations to show those files. But generally you shouldn't have to read them IMO, unless you're a macro author.
It depends on the macro. Macros have the ability to emit custom compilation errors. Depending on how But if the macro forgot to handle all error cases, you should see an error in the virtual file, yes. |
The good ol' transformer system that had no output on the disk was not easy
to debug. Macros can be powerful but if writing macros is the privilege of
the select few, in other words the learning curve is steep and maintenance
is hard, the adoption rate will be much lower than what it could be. If I
could pick between fewer but well supported features vs a lot of obsecure
ones, I'd pick the few and extend that over time.
This is just something I'm weary about given the history with analyzer,
builders. I'm very hopeful though that I'll be proven wrong and most
everyone can be part of the "few".
Remi Rousselet ***@***.***> ezt írta (időpont: 2024. jan. 2.,
K 21:12):
… In regards to IDE tooling, will generated augmentation libraries be
generated as on-disk files alongside the user's files, or elsewhere in the
file system?
Macros don't create actual files. Their output is somewhere in memory.
There will be special IDE considerations to show those files. But generally
you shouldn't have to read them IMO, unless you're a macro author.
Would the below semantically-invalid code be generated or not?
It depends on the macro.
Macros have the ability to emit custom compilation errors. Depending on
how @mappable is implemented, it could show an error on the annotation
instead of generating anything.
But if the macro forgot to handle all error cases, you should see an error
in the virtual file, yes.
Although I'd report that case as a bug in the macro. Then, the package
should be updated to better handle errors for this case.
—
Reply to this email directly, view it on GitHub
<#1482 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAULOIH3JCE7BGOMAU623KTYMRSZVAVCNFSM4YM2CUWKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOBXGQ2DSNRRHA3A>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
In-memory representation does make sense for macros, though I completely disagree with users not needing to read the generated code. In the case of a generated
I can't find any info on error handling, off the top of your head do you know the source for the macro error APIs?
My concern is not regarding handling or not handling errors at all, its about creating a better UX for showing the user what the cause of a macro error is. Let's say that class Email {...} // NOTE: no @JsonSerializable() annotation
@JsonSerializable()
class UserDto {
const UserDto(this.name, this.email);
final String name;
final Email email; // invalid type due to missing `JsonSerializable` annotation on `Email`
}
// macro-generated augmentation
augment class UserDto {
Map<String, dynamic> toJson() => {
'name': name,
'email': email.toJson(), // ERROR: `Email` type is not serializable
};
}
In this particular case, IMO the best UX is that Phase 1 and 2 of the JsonSerializable macro should complete, despite the user's misuse of the macro. Another way to look at it is that the method declaration of This subtle difference in implementation makes the most difference for nested macro consumers. Since Phase 2 of @Mappable(UserDto)
class UserMapper { }
// macro-generated augmentation
augment class UserMapper {
Map<String, dynamic> serialize(UserDto data) => data.toJson(); // NO ERROR
} Otherwise, if the entire The better UX IMO is to show the user the single source of error (at However, other macro applications may require different UX or feel differently. What I'm suggesting is that |
Ability to inspect generated code and insert breakpoints is important for macro users too. (PS: I'm not arguing for generating files on disk. Special IDE support is fine.) |
There will be some way of seeing generated code, I definitely think it will be important. If there is one thing package:build got right, I think it was this. It will be tricky for some situations, in particular non-IDE use cases, like seeing a stack trace from production which points at generated code. We will have to work out all the details here but it is definitely possible to come up with solutions here.
The work in progress APIs are here https://github.com/dart-lang/sdk/tree/main/pkg/_fe_analyzer_shared/lib/src/macros/api, see specifically https://github.com/dart-lang/sdk/blob/main/pkg/_fe_analyzer_shared/lib/src/macros/api/builders.dart#L16. Any macro can also throw, and that will get converted into an error diagnostic attached to the macro annotation. But they can create diagnostics explicitly attached to specific nodes using this API which should be quite helpful.
This is exactly how it will work. Macro authors can choose to only emit a diagnostic and no code, or intentionally emit broken code and rely on the compile time errors from that, or both. If a macro emits code, it will be visible, even if it also emits some error diagnostics. I definitely agree that there can be a lot of pain if a macro fails in a way that causes a lot of other errors in the program. Mostly, I think this is something the analyzer team is going to need to discuss. The specification does not specifically say how this should be handled. For instance, the analyzer could choose to not show any errors related to macro classes which had a macro application that failed, and instead only show the macro failures. That wouldn't catch all cases, but likely would help. It is a fine line to draw though, and we will just need to explore what feels right when we are a bit further along imo. Whether subsequent macro applications on the same declaration should even run after a single failed one is another open question. |
@jakemac53 What happened to the idea of "const reflection" as you mentioned here ? Macros also sounds like we should have const functions somehow, since it's code executing at compilation phase. Allowing Also is it possible to put a debugger during the compilation phase a when macro would be running ? |
@jakemac53 @cedvdb something like zig comptime would be very flexible and powerful and would avoid using runtime reflection https://ziglang.org/documentation/master/#comptime |
doesn't Dart compiler already does that kind of thing when it encounters at least that is what happens with generated JS code from dart2js. |
@jodinathan it seems like it allows const functions |
Macros do run at compile time, but being more restricted than general const functions has some compile time advantages. We did explore enhanced const evaluation via the const evaluator (basically, this is just an interpreter). But it ends up being much more complicated (and slow) to compile programs and manage invalidation, in particular for large modular builds such as we have internally. It also would be a lot of work for us to maintain an interpreter which is fully spec compliant. Macros are actually compiled and executed like regular dart programs, and don't need an interpreter. The boundaries of what code runs at compile time or not are also a lot more well defined, which also helps for compilation/invalidation. |
Considering the upcoming macros feature I've created a Dart Element/Node Tree visualizer for VSCode: https://marketplace.visualstudio.com/items?itemName=jodirez.dart-element-tree-viewer. It think It can be useful to devs to get to know the Element properties and maybe give us ideas for source generation stuff. The extension is completely written in Dart. |
Not on topic but I'm interested in how you build a vs extension in dart.
…On Fri, 23 Feb 2024, 11:50 am Jonathan Rezende, ***@***.***> wrote:
Considering the upcoming macros feature I've created a Dart Element/Node
Tree visualizer for VSCode:
https://marketplace.visualstudio.com/items?itemName=jodirez.dart-element-tree-viewer
.
It think It can be useful to devs to get to know the Element properties
and maybe give us ideas for source generation stuff.
The extension is completely written in Dart.
—
Reply to this email directly, view it on GitHub
<#1482 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAG32ODINNXLGGZPB4634ZTYU7RV5AVCNFSM4YM2CUWKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOJWGA2TQNRQGE4Q>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Since this moved to "Being implemented" does it mean that static metaprogramming is sure to become a feature in dart, or is it still possible that the implementation will have problems and revert back? |
The proposal has not yet moved to "accepted" in the language repo - that is the signal that it is for sure a go. We could still run into implementation or user experience issues in theory, that prevent us from shipping it. But, I am generally getting more confident and not less, the further along we get. I would expect more tweaks to the proposal for a while, to work around smaller issues, or just because we think its a better design. |
I'm curious if/how the following read/write scenarios (which are more-or-less supported by
As it stands, the macros APIs are focused solely on introspecting types and declarations, not implementation details (this obviously makes sense). On the other end of the spectrum, the So would the above use cases still be technically possible? To what extent would they be non-advisable, if at all? |
Definitely not in V1. Not sure about later, but almost certainly we would only expose that information for the immediately annotated declaration, or possibly the surrounding library, if anything.
The Resource API is the intended way to support reading non-Dart files. It still has to be driven by an annotation in Dart code somewhere. I could see a pattern where users create empty libraries with a macro only, which points at a resource that is some data file, and it generates the entire contents of the library from that.
Very unlikely. |
It would be important for the macro API to support the possibility of reading project files, for example in AngularDart to read html files of a component's template and generate the ComponenteNgFactory |
Metaprogramming refers to code that operates on other code as if it were data. It can take code in as parameters, reflect over it, inspect it, create it, modify it, and return it. Static metaprogramming means doing that work at compile-time, and typically modifying or adding to the program based on that work.
Today it is possible to do static metaprogramming completely outside of the language - using packages such as build_runner to generate code and using the analyzer apis for introspection. These separate tools however are not well integrated into the compilers or tools, and it adds a lot of complexity where this is done. It also tends to be slower than an integrated solution because it can't share any work with the compiler.
Sample Use Case - Data Classes
The most requested open language issue is to add data classes. A data class is essentially a regular Dart class that comes with an automatically provided constructor and implementations of
==
,hashCode
, andcopyWith()
(calledcopy()
in Kotlin) methods based on the fields the user declares in the class.The reason this is a language feature request is because there’s no way for a Dart library or framework to add data classes as a reusable mechanism. Again, this is because there isn’t any easily available abstraction that lets a Dart user express “given this set of fields, add these methods to the class”. The
copyWith()
method is particularly challenging because it’s not just the bodyof that method that depends on the surrounding class’s fields. The parameter list itself does too.
We could add data classes to the language, but that only satisfies users who want a nice syntax for that specific set of policies. What happens when users instead want a nice notation for classes that are deeply immutable, dependency-injected, observable, or differentiable? Sufficiently powerful static metaprogramming could let users define these policies in reusable abstractions and keep the slower-moving Dart language out of the fast-moving methodology business.
Design
See this intro doc for the general design direction we are exploring right now.
The text was updated successfully, but these errors were encountered: