-
Notifications
You must be signed in to change notification settings - Fork 214
Structured data in macro arguments #3522
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
Comments
On that topic, is there a reason why we don't have a ConstantReader equivalent for non-primitive objects? There are already cases like this in the wild. class DateTimeConverter extends JsonConverter {
const DateTimeConverter();
...
}
@JsonSerializable(
fieldRename: FieldRename.kebab, // Enums
converters: [DateTimeConverter()], // Custom objects implementing a specific interface
) Or Riverpod: @riverpod // Will generate "const fooProvider =..."
int foo() {}
@Riverpod(
dependencies: [fooProvider], // A list of object this function depends on
)
int bar(ref) {
ref.watch(fooProvider);
} |
I definitely agree it would be desirable.
Unfortunately, not as simple as it would seem on the surface, but might be doable with some constraints. There are a couple issues:
I think we could possibly come up with workable answers to all of these things, but today we don't allow it mostly just to simplify the feature. Expanding it later on to allow this kind of data is non-breaking etc.
There is some WIP constant evaluation stuff, mostly this just hasn't been figured out yet exactly how we want it to work. It is challenging to specify (for a lot of the same reasons as above). There is also a hope that in many cases, macros can just emit back into their own code the Code argument they receive, instead of needing to evaluate it at compile time. It is understood that this won't solve all use cases though. |
I don't think any of the examples I listed above would work with that. Afaik json_serializable's JsonConverter logic isn't a plain copy-paste either. From the top of my head, it supports generics converters: class GenericConverter<T> implements JsonConverter<T, Map<String, dynamic>> {
const GenericConverter();
@override
T fromJson(Map<String, dynamic> json) => null as T;
@override
Map<String, dynamic> toJson(T object) => {};
} And invoke it with various generics based on the situation. The enum's case is probably the simplest to handle. A String instead of an enum would work. But at the very least, we'd lose out in terms of autocompletion and discovery. |
For some of these examples, you can take an |
It depends on how introspectable "Code" is. In Riverpod's case for instance, given: const fooProvider = Provider(
dependencies: [barProvider],
);
...
@Riverpod(depencencies: [fooProvider])
int example() {..} This generates: const exampleProvider = Provider(
dependencies: [fooProvider, barProvider] // dependencies of "fooProvider" are also listed here
); You can view it as a spread: const exampleProvider = Provider(
dependencies: [fooProvider, ...fooProvider.dependencies],
); I don't want to do this at runtime, as the chain can be quite long. This would be inneficient. And it matters to be that those objects are consts too. I could be wrong, but afaik a |
Assuming general purpose constant evaluation is available, and that Basically you would const evaluate your Code arguments, check that they are |
In JsonSerializable's case, given The generator may generate |
As long as constant evaluation is indeed possible, I have no worries then :) |
Thanks for the detailed reply Jake :) makes sense.
Ah, I see what you mean. If there is a macro Requiring I think without additional requirements it's already sufficiently under control of the macro author: if they are explicitly asking for an argument of type Similarly if the macro has a
Yes, in some senses it's incremental; I guess most macros would want to use it and change their APIs, though, so it could feel more like 2.0 than a 1.1. BTW, I'm not saying it's a good idea, but "macro that serializes macro args" is a pretty well defined use case for a first macro to publish / showcase. |
I do think it would be a pretty well defined macro, but it also wouldn't be a very useful one, until users can actually write their own macros :) |
Hmmm I think it would be useful for other use cases too. I guess the biggest difference to a general serialization macro is that the wire format is not published or stable; so no good for RPCs with non-Dart servers. But it would presumably be pretty good for passing data between isolates or for temporary caches on disk, for example. |
Another issue is I think we probably would want to use the Serializer/Deserializer interfaces which are internal to the macro implementation, and we really don't want to expose because then they would be hard to change. |
We might want that :) my guess is that we will have to expose enough of the serialization internals that it's possible to write a macro to assist with a package you don't own, and to accept data types from it as arguments. Anyway, all speculation at this point :) thanks. |
Oh, another hiccup I had forgot about - on the compiler/analyzer side of things there is no actual instance of the class at all - just some abstract representation of it. So we can't rely on user code for the serialization. Probably, what we would want is to use the existing |
Oh ... right :) Something like the That should work fine. The only problem I see is that it breaks the illusion that the macro is like a class; values get transformed on the way "in", so not:
but somehow
--which we don't have a way to write. And when we do figure out a way to write it, there is a risk it will be horrible to unit test, because there is no general Bar->Argument transformation available for unit testing! Quite a puzzle. |
We could consider how LINQ in C# works. It allows you to write expressions into LINQ queries, like Which one it is depends on the static type of the implicit receiver. So if the expression after So, could we make the behavior depend on the static type of the parameter to the macro annotation constructor: class MyMacro implements Macro {
final int depth;
MyMacro(this.depth);
}
class MyFancyMacro implements Macro {
final Argument<int> depth;
MyFancyMacro(this.depth);
} so that a macro annotation of If |
Closing in favour of #3847 |
Re: the spec and this comment.
Macro args have the limitation that they must be serializable, and as currently specified this forces them to be trivially mappable to JSON, i.e. primitive collections and values only. Or, they can be expressions, which are serialized as strings.
It seems like it would be an improvement if there can be a way to support structured data; by which I mean records, enums, custom classes, etc.
As a simplest possible example, people will want to define enums for type safe options:
MyMacro(foo: FooSetting.veryEnabled, other: OtherSetting.mostlyDisabled})
.Without support for structured data my guess is that macro authors will (ab)use
Code
arguments by providing top level "builder" methods that make the macro applications look nice, then writing parsing and static checking for the "code" to extract the argument values. We know from existing codegen that authors will use any implementation trick whatsoever to provide a better / safer API for their users.From a technical point of view I think supporting structured data is straightforward? The macro API would need to gain serialize/deserialize methods, so instead of relying on the macro being trivially serializable we simply ask it to serialize itself.
If macro authors are going to (ab)use
Code
to pass arguments then writingserialize
anddeserialize
would be less work for them. Still a headache, but at least there is only one valid solution so it will lead to the same experience across all macros, if they manage to avoid bugs.But the next step seems obvious, we could provide a macro that writes
serialize
anddeserialize
for you? :) ... everyone wins!The text was updated successfully, but these errors were encountered: