-
Notifications
You must be signed in to change notification settings - Fork 217
Create Static Enough Metaprogramming proposal #4374
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. (Very interesting indeed!)
I could suggest the addition of 'a' or 'the' in many locations, but decided that this wouldn't be the top priority at this time.
Dart source code generation or which are supported by macro systems in other | ||
programming languages. For example the following capabilities are out of scope: | ||
|
||
- injecting new declarations into the program; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definitely does drastically simplify the proposal - but it also makes it a lot less useful. Ultimately trying to accomplish this was the downfall of the original macros proposal though.
But, I think it is important to look at the original motivations for macros, and which of those use cases are covered by this.
Ultimately, I think you can end up with some fairly decent solutions for automatic encoding/decoding, equality, toString, etc. But not copyWith or constructors due to the signatures being dependent on the shape of the class (you could generate the bodies of these but that's only half the boilerplate).
Edit: I see you have an interesting copyWith idea using records, that is fairly reasonable actually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added a section trying to cover some of the use cases mentioned peaked from an old spreadsheet I managed to find.
I could not exactly figure out what some of those entail, could you help me a bit (look for TODO: unclear what this means.
and if you have context please add it in the comment). I am interested about auto listenable and render accessors. I think union types (ala freezed) means more concise way of declaring sealed class hierarchies - which I think should just be its own feature (paired with primary constructors).
I think a number of use cases require property wrappers and/or ability to redirect methods - this should probably be its own feature. If property wrappers are available then @konst
can be used to handle the boilerplate.
Finally some cases (e.g. proxies, functional widgets and reduced widget classes boilerplate) require ability to create class declarations. I think it is actually an okay feature to give via @konst
reflection. As long as classes are anonymous and are otherwise not visible.
- Average code size overhead per class: 270 bytes | ||
- Average JIT kernel generation overhead per-class: 0.2ms (cost of producing | ||
specialized functions using Kernel-to-Kernel AST transformation) | ||
- Average AOT compilation overhead per-class: 2.6ms |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the baseline?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have numbers right now but I can try to revive the prototype and update this later.
code: | ||
|
||
```dart | ||
mixin DataClass<@konst T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be 🔥 (and help a lot with the reduction of boilerplate)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it would be useful in many places. Also it would solve a performance problem we occasionally encounter where inheritance or generics cause polymorphism and severely reduce performance.
I have made some updates based on comments. PTAL. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still have the same reservations as before, but landing this in the repo so we can discuss more sounds great. It's a very solid well-thought out proposal and the background information it provides is really helpful for any metaprogramming-related features we might do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused about how @konst
applies to statements and functions. Given this:
When
@konst
is applied to loop iteration variables it instructs the compiler to expand the loop at compile time by first computing the sequence of values for that iteration variable, then cloning the body for each value in order and substituting iteration variable with the corresponding constant.
(in particular, "expand the loop at compile time" suggests code generation based on constant values)
I would expect this function
void bar<@konst T>(@konst T v) {
}
to be monomorphised based on T
s passed. (and generate one more copy for non-const T
)
Otherwise when I do things like this code in the JSON examples:
Map<String, Object?> toJson<@konst T>(T value) => {
for (@konst final field in TypeInfo.of<T>().fields)
if (!field.isStatic) field.name: field.getFrom(value),
};
The loop cannot be unfold at compile time, because the unfolded loop needs to be different for each T
.
But the text doesn't mention monomorphisation at all. Am I confused?
If this allows monomorphisation (and it has to, if I get it right), then that's a pretty big capability for the langauge that's also worth mentioning.
@konst external Function defaultConstructor; | ||
|
||
/// Return the list of fields in `T`. | ||
@konst external List<FieldInfo<T, Object?>> get fields; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the Object?
here be TypeInfo<Object?>
(or TypeInfo
, TypeInfo<dynamic>
)? Can the field types be anything else other than a TypeInfo
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean FieldInfo<T, TypeInfo<Object?>>
? That would not work for the intended purpose, we would like use these type-arguments to ensure proper typing of access operations.
final class FieldInfo<HostType, FieldType> {
@konst external FieldType getFrom(HostType o);
@konst external void setOn(HostType o, FieldType fieldValue);
}
If you have field String x
in class C
its FieldInfo
will have reified type FieldInfo<C, String>
, which guarantees type safety of fi.getFrom(...)
and fi.setOn(...)
.
I also find it confusing to see what function calls (or other syntax) is part of the generated code vs. compile-time evaluation. For example, if I change the
( Just by looking at this code (without seeing I wonder if we could make stages explicit. There are many languages that do this that we could look for inspiration. For example Common Lisp's backquote and comma, MetaOCaml's ( |
Something else that I just realized is that if you evaluate some code in runtime in some cases and compile time in others, you can get runtime error in some cases and compile-time error in others. For example, consider this silly code: (modified from the mixin DataClass<@konst T> {
@override
blah(Object? other) {
final typeInfo = TypeInfo.of<T>();
for (@konst final field in typeInfo.fields) {
final value2 = field.getFrom(other as T);
...
}
return true;
}
} Here the line Does the proposal address this? (sorry if I can't find it..) |
There is specifically this paragraph which talks about it though it does not explicitly call it monomorphisation.
That's fair, though its somewhat intentional:
I am open for suggestions on how to make this better if you have any ideas. I think it would need some syntactic designator, but I don't immediately see anything that would be very readable.
I think MetaOCaml's or other similar systems approach (Template Haskell, Scala macros, etc) are somewhat different because they operate on pieces of code into which you splice other pieces of code (quotes and splices are two building blocks), so syntax is naturally very clear around what is code and what is a hole in that code into which you can splice more code. This proposal describes (optional) compile time specializaton - so I think the fact that syntax looks the same is kinda by design.
Yep, I think this is by design as well. Though maybe not written as explicitly. |
This moves content from #4271 into a markdown file in the repository to make discussion and revisions easier.
I have incorporated some of the feedback from discussions on the issue - but I continue to maintain focus on this as a toolchain feature. I have added some remarks that analyzer can't constant fold everything anyway because it does not have access to the compilation environment.
I would like to collect a few rounds of feedback and then rejuvenate the prototype implementation to get something experimental working across all platforms in the SDK so that we can have an idea of how well this could work in a real world.