-
Notifications
You must be signed in to change notification settings - Fork 209
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
Macros: augmenting a private final field with a getter creates a chicken-and-egg problem #3592
Comments
I'm starting to wonder if the only way to be safe is to reanalyze the entire program (or as much as needed) after each macro phase. The types, and other derivable properties, that you see before a phase are tentative, because we cannot assign definite types to a partial Dart program, and anything which is changed by macro code additions was by definition only a partial program. Macros are part of the library. If a macro changes an instance variable to be augmented with a getter, then that variable is not promotable. It never was, because it's always been a getter, there is no such thing as the library without the macro applied. That's at most an intermediate step during compilation, but it's not the program. So for this concrete problem: The step 3 macro sees the type that the step 2 macro left it with. If that's not the same type as what step 3 leaves the program with, that's just what it is. If that leads to a compilation error, write better macros. Or don't mix incompatible macros. |
All we should have to do here is disallow augmenting final field getters, which it is anyways pretty reasonable to disallow. It is a very unexpected thing for a field which is declared final, to have a getter which does not return the value of its backing store, or worse an unstable value. If we get stable getters, we could discuss allowing augmenting a final field getter with a stable getter, which wouldn't break promotability at least, but would still be pretty weird. |
I this a problem for plain getters too? At least it's not possible to augment an abstract getter into a variable. |
IIRC from the stable getters work, what people usually want/mean when they write a final field is that it is stable, even if there is no such concept in the language. However it is perfectly reasonable to implement that as a getter, for example one that does caching. I would even say that a macro that does so is a particularly appropriate use of a macro: if you design your code so it's always a human writing the getter implementation, they might accidentally write an unstable getter; but if you use a macro you can easily get it right and have it generate only stable getters, no help needed from the language. So an alternative approach would be to leave it up to the augmentation/macro author and say: if the language decided this field is promotable, and you augmented it with an unstable getter, you get undefined behaviour--the implementation fallout is on you. It doesn't feel particularly different to other design/implementation issues macro authors face: it's always about creating a regular structure which gives useful guarantees on top of whatever manual code might get thrown at you. So it's anyway a matter of figuring out the awkward corner cases, figuring out what people want in those cases, and covering with tests. I realize though that this doesn't apply at all if people are manually writing augmentation libraries. That would be a pretty big foot gun. So maybe it's a non-starter ;) or maybe people should not manually write augmentation libraries... |
It is different imo because it introduces unsoundness in the field promotion logic. If we were OK with that, we probably would have just shipped field promotion generally, as an unsound but useful feature. Fields are very rarely if ever overridden with unstable getters. |
Hmm interesting example, but yes that sounds correct. |
Which means you get runtime errors. Our native compilers won't be able to abide a type soundness violation, so if we stick with a promotion that is then invalidated by a macro step, then the compilers will need to test the type (or value identity, semantics of stableness aren't specified) of the promoted variable when it's read at a promoted type. I'd rather just give a compile-time error saying that your code promotes, the macro invalidates the promotion, so try to not promote. (At which point the author can rightfully say "no, YOU try not to promote". After all, they didn't ask for it, we just did it to them.) Or rather, I'd rather re-analyze everything that we think needs it after a macro step. Whatever the program ends up with determines which errors you'll get. If that changes the type of something, making the macro generated code be type invalid, that's the error you get. That's a bug in the macro. It assumed something that wasn't stable under macro modification. We shouldn't try to be clever. That's ratholes all the way down, and can get in the way of future features too. Give macros what code generation has had so far: The ability to inspect the program so far, to some level of detail, and try to derive from that what will happen when adding code to that. Then generate code, and see if it compiles. |
This already does cause problems for codegen authors today, here is one example of that. I do think it is worth trying to ensure we do not give a macro a wrong answer to a question they ask about the program. An incomplete answer is OK (and unavoidable), throwing is also OK (if unavoidable), but not a straight up wrong answer. In general, it hasn't actually been particularly challenging up until this point to uphold those aspirations. The one special rule I can think of that augmentations (and thus macros) can't contribute to type inference. And that has been enough. Obviously, field promotion interacts with this such that even adding a body at all affects promotability, which affects type inference. This does feel like an extremely niche scenario in general. You have to have a concrete class with a private final field, which is extended by a class that overrides the implicit getter from that field with an originally abstract getter, whose body is filled in by a macro. And on top of that, you then have to use that field in such a way that its promotability affects the type inference of a field through its initializer. I don't mean to dismiss this scenario entirely, but I do think it is sufficiently niche that simply throwing an error would be perfectly acceptable (at compile time). |
Oh yeah, that would definitely be a problem too. Thanks for spotting that. I'll edit the original description to include this possibility. I guess in order to close this loophole we would have to prohibit augmenting an abstract getter with a concrete one, which seems like a little less of a nice use case than augmenting a final field with a concrete getter
Yeah, I agree that this is different because it's a soundness issue. Elsewhere in the language, we've been very diligent about avoiding unsoundness, and I think that's really important. It's one of the key differentiators between Dart and languages with more "gradual" type systems (e.g. TypeScript) that I proudly talk about when introducing Dart to developers who aren't familiar with it. Personally I would really prefer to weaken the macros and/or augmentations feature slightly to preserve soundness. I don't have strong opinions about how we preserve soundness, though. If people feel like it's too limiting to fully prohibit final fields and abstract getters from being augmented by a concrete getter, I would definitely be open to something along the lines of what @lrhn suggested above. Concretely, perhaps after macro phase 3, we could re-analyze the type of any |
In particular, this is actually how a lot of macros are expected to work - in phase two they emit a getter with no body, and in phase 3 they fill in the body, since that is the safest/best time to do deep introspection. |
Yes a solution such as this is in particular I think a good solution, at least from a usability perspective. We only error when this situation actually occurs, which should be very rare, and we can avoid limiting the feature. If we go this route, I would be inclined to also allow augmenting implicit final field getters with getters, just to avoid the extra complexity of specifying that you can't. Even if I don't believe its something a macro should necessarily do 🤣 . There are imaginable situations where it could be done in a sound manner. |
cc @johnniwinther @scheglov wdyt about the proposal to re-infer the type of any |
I don't like re-inferring, it does not fit well with the current analyzer implementation. |
I suggested elsewhere that the macro could perhaps be forced to pre-declare whether it intends to add a body or not. That works for declarations entirely declared by macros, but not if the user writes the original abstract declaration and intends for the macro to fill in the implementation. That's where we currently use So, suggestion:
Not necessarily enough. If someone adds, completely without querying anything, an implementation for It probably doesn't need "spreadsheet-level" dependency tracking to detect problems. Just knowing that a specific private instance variable is promoted as part of type inference of some member, should be enough to go back and recompute that if any non-promotable implementation of that name is added to the same library. (And if that makes any code downstream from the promotion invalid, or changes the type of any declaration, so bounded by the surrounding declaration, then it's an error.) Still, I do worry that this is a problem that can affect future improvements to type inference too. Or make it hard to add any future promotions because it can then break macros. Macros look below the interface, at the implementation. As long as that only applies to the same library where the macro is applied, it's a localized issue. Disabling private field promotion if macros are used can work, but has one issue: We only have on language semantics, which is unaware of macros. It just sees the result of applying macros to a source file, as a resulting library file with one or more augmentation files, then treats them as completely normal files with the one Dart semantics. (We could possibly choose to disable it based on library URI. If the URI scheme starts with |
I think it's reasonable to want to apply a macro to a final field to, for example, log all accesses to the field. Honestly, I think the original sin in all of this is field inference. If a macro turned a final field into something non-promotable, that doesn't cause any problems for macros except that that same field could be used in some other field's initializer and thus to infer the type of the field. But if we didn't have field promotion in the first place, then it wouldn't matter whether a macro changed the promotability of any field. The compiler wouldn't use the promotability until it was doing inference inside function bodies which doesn't happen until after Phase 3 is complete. If we didn't allow access to inferred field types during macro introspection at all—so you can access the annotated type if there is one, but that's in—would that fix this problem? It would still be the case that a macro could change the promotability of a field. But that promotability wouldn't have any user-visible effect until after all macros have completed and function bodies are being inferred. It's already the case that macros can easily affect the way types are inferred in function bodies. (For example, by adding an overriding declaration to a subclass that narrows a return type.) |
Moving this to the macros project - afaict this isn't an issue for normal augmentations, it is more about the macros feature and how it wants to introspect on incomplete programs. |
The current rules for field promotion say that a getter is promotable as long as the name of the getter is private to the library, and the library doesn't contain one of the following things:
noSuchMethod
).This means that if a macro were to augment a private final field with a getter, then that field (and other private final fields with the same name in the same library) might be considered promotable before the augmentation, but not promotable after it.
That's a problem because during phase 3, a macro implementation is allowed to query the inferred type of an
OmittedTypeAnnotation
through the builder APIs. But if the type of a field or top level variable needs to be inferred from its initializer, then that initializer must be analyzed, and its type can depend on whether a field is promotable. This creates a chicken-and-egg problem: the inferred type is needed by the macro, but it can't be correctly computed until all macros have completed.It's tempting to solve this chicken-and-egg problem by allowing the builder API to query an
OmittedTypeAnnotation
to block and invoke other macros, and to throw aMacroIntrospectionCycleException
if there's a dependency cycle (much as we currently do in phase 2), but it would be challenging, because (a) it would require knowing ahead of time the names of the fields that a phase 3 macro might augment (and I don't think that's currently possible), and (b) it would require rewriting the logic for field promotion so that it can operate one name at a time (rather than one library at a time, as it currently does).I think the better approach would be to prohibit final fields from being augmented by a getter.
@dart-lang/language-team FYI
Edit: as pointed out below, it would also be a problem if an abstract getter were augmented by a concrete getter. So we would conceivably have to prohibit that as well.
The text was updated successfully, but these errors were encountered: