Skip to content
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

"Merge to source" for macros that only need Phase 3 #3925

Open
davidmorgan opened this issue Jun 20, 2024 · 26 comments
Open

"Merge to source" for macros that only need Phase 3 #3925

davidmorgan opened this issue Jun 20, 2024 · 26 comments
Labels
static-metaprogramming Issues related to static metaprogramming

Comments

@davidmorgan
Copy link
Contributor

Here's an alternative to Phase 1+2. @jakemac53 @johnniwinther @scheglov

It's not new: it's the recommended way of doing codegen in Java at Google, and I tried to do it for built_value with a plugin.

It's interesting both for the UX and for what it means for various pieces of the implementation.

Merge to source

This is a name I just made up, open to suggestions for a better one :)

What I mean is: public declarations are pushed to checked in source; "merged to source". So if you write:

@BuiltValue
class Foo {}

and @BuiltValue wants the declarations:

@BuiltValue
class Foo {
  FooBuilder toBuilder();
}

class FooBuilder {
  Foo build();
}

it doesn't emit them as augmentations: it emits an error telling you to add the declarations.

The error is pretty closely equivalent to the error you get if a concrete class "implements" an interface without actually providing the methods, whereupon the compiler tells you to add them. Of course, just as with that case, there can be a quick fix to make the work easier.

Private declarations are still allowed, as per Phase 3 today.

Consequences for UX

The reason I arrived at this very old idea ... again ... is that I was experimenting with codegen for JSON extension types in dart-lang/macros, and it seemed like a really good fit.

There problem there is that I want the extension types to match a schema, with generated boilerplate for constructors and getters. But I also want to write my own methods in them, and add docs. They are the most important types in dart_model, the user facing ones, so I really want the code to "look" good.

We're not going to use macros for it, because it's part of the macros implementation :) ... but if we did, I think the user experience if the macro emitted declarations would be not great. You would have to visit the macro-generated augmentation to see the most important details of each type. You can't do that at all if you're looking at the code e.g. on GitHub, so you'd be totally lost. You can do it in the IDE, and the UX is as good as it can be with the separate augmentations file, but it's still not great: in this particular case the macro-added declarations are just as important as manually-added declarations, so you would hop between augmented and checked in a lot.

"Merge to source" would I think give the best possible UX for this case. The macro would run in Phase 3, merge declaration to source for any mismatches between code and schema, and provide just the definitions as augmentations.

Consequences for Implementations

The world where macros only use "merge to source" looks quite different for implementations.

There is no longer a need for Phase 1 or Phase 2; phase 3 might fail with a new type of error, which needs UX support to easily add the missing declarations, but otherwise it just adds definitions as before.

All macros can run in parallel, and it's always possible to analyze fully, without a possibility of changes: "merge to source" is external to analysis/compile.

Macro-generated augmentations become much less interesting to users, they are just definitions / private declarations, so perhaps they don't need combining to a single file; there is some discussion about the performance implications of that.

Downsides

Obviously, there is more churn to checked in code. Exactly when this is better for UX or not depends a bit on the specific macro, a bit on preference, and a bit on how the code will be seen/reviewed/used :)

Changing a macro so that it requires a different declaration is always breaking with "merge to source".

Arguably, it already was a breaking change: it's just that it was only breaking if some code used the declaration that changed. With "merge to source" it becomes breaking always, because the declaration is checked in.

In cases where a macro declarations depend on local information, there are more steps but they seem harmless: you update foo.dart, the macro in it requires new declarations, you update foo.dart per what the macro requires.

If the macro declaration depends on distributed information, that can be a problem. You update bar.dart, the macro in foo.dart notices and requires new declarations. The change to bar.dart is now breaking for foo.dart, where before it would have been for free. Or would it? The declaration changes to foo.dart might be breaking if something is using them. It would be interesting to look for concrete use cases here that can only be solved with Phase 1+2.

Possible Path

We could think of launching a v1 macros feature that has only Phase 3 + "merge to source", then working on Phase 1+2 as macros v2.

In particular, that would split augmentations into two features, v1 for definitions and v2 for declarations.

The fact that a lot of the hard questions about performance and correctness crop up in Phase 1+2 makes this particularly interesting :)

@davidmorgan davidmorgan added the static-metaprogramming Issues related to static metaprogramming label Jun 20, 2024
@jakemac53
Copy link
Contributor

@mit-mit should probably comment. I don't believe this fits within the feature goals though, as it removes some of the primary reasons for wanting macros/augmentations in the first place (removal of boilerplate).

Especially in the cases where a macro is generating a large number of declarations which are just implementation details (for example freezed).

All macros can run in parallel, and it's always possible to analyze fully, without a possibility of changes

if we block constants from being augmented, otherwise if macros can evaluate constants and augment them, there are still ordering concerns.

If the macro declaration depends on distributed information, that can be a problem. You update bar.dart, the macro in foo.dart notices and requires new declarations.

Yeah this case definitely immediately came to mind. Build_runner allows a "build to source" (checked in) mode and also a "build to cache" (not checked in, re-built for all users) mode, for this reason.

Mockito comes to mind as a specific example that potentially might need to generate different code depending on non-local information (imagine a public member is added to a super type).

@tatumizer
Copy link

tatumizer commented Jun 20, 2024

What is the difference between two scenarios:

  1. The macro returns an "error", which forces the user to invoke IDE feature to generate the "missing declarations".

  2. The macro creates a temporary copy of the source file containing all missing declarations and asks the user to confirm the changes (selectively and/or "Yes to all"). After that, the original source gets replaced by the generated "source".

It seems that the option 1 is just a more awkward version of the option 2. (Plus, IDE has to be able to decode the error message to figure out what the changes are and where they have to be applied)

In both scenarios, there's a problem with editing. (Maybe the generated fragments of code should be protected from editing? Maybe some checksums have to be computed to make sure the user hasn't edited the protected parts outside of IDE? Etc.)

@davidmorgan
Copy link
Contributor Author

Thanks Jake! Sorry, I should have been clearer: I think it's only a good fit for some macros, so it would only be a v1 launch, with phase 1-2 to follow in v2.

Also we can think of designing so it's easy to switch between "merge to source" and writing to declaration files. built_value does this :) ... if you want to write the Builder type, you can, and that triggers a different codepath in the generator to the one where the generator hides the Builder. It's annoying to maintain, honestly, but it should be possible to support the general principle: the macro says the declarations it wants, and the host either merges to source or adds them as declarations.

The question then is whether it would be a useful way to split: a meaningful launch and a way to reduce risk. There's also a feature request for supporting "merge to source" in a nice way, so the macro can say what it wants and the host checks and issues an error. But that's a small feature we could build any time.

Yes, augmenting constants would be something to exclude from v1 if we're trying to make our lives easy :)

Re: difficult cases, I thought about Mockito; but actually with Mockito I think the declarations are already there, because you "implement" the target class? Would have to check. Anyway I'm sure there will be cases that don't work well.

@tatumizer Yes, exactly :) ... the way the built_value plugin worked was that it would report errors with fixes, exactly edits to the code that make the desired changes. The code wasn't maintained so it's deleted, but you can still see it in the repo history here.

@lrhn
Copy link
Member

lrhn commented Jun 21, 2024

Still not sure I understand the desired behavior completely.

The macro will require that some member declaration to already exists. Is that all it does, or does it also do more?
If it doesn't find that declaration, then an error is reported, making the macro application fail, and the user is given the option of creating the member.

That just sounds like the macro reporting an error, which I assume it would be able to anyway, plus the user being given an easy quick-fix to fix the error.
I fully assume there will be macros which report errors if things are not how they assume. They shouldn't just add an augmentation to a method that doesn't exist, so reporting an error in that case is expected macro behavior, nothing new should be needed.

If we give package:macros the ability to report an error with a fix, or have specific kinds of errors for "missing declaration", which the analysis server can recognize and react to by suggesting creating missing declaration with provided signature,
then wouldn't that solve the issue without having a different kind of macro behavior?
Or package:macros could directly have a suggestNewDeclaration operation, which it can use instead of an error, but it'll likely end up failing macro application anyway, so combining it with errors seems reasonable.

Adding a member is only really possible while developing, which means while using the analyzer. The compilers won't be able to use the "merge-to-source" behavior for anything, all they can do is report the error on stderr. That again suggests that this is about adding a useful response to the macro error, not about the macro itself doing anything special.

@davidmorgan
Copy link
Contributor Author

Yes, reporting errors for missing declarations is something most generators have to do today, and there are cases where macros have to do it too, or want to do it. The only new implementation here might be some convenience code like suggestNewDeclaration or better still requireDeclaration where the host checks, issues an error if there's a mismatch, and possibly offers a fix.

I've written tooling with generators that exactly goes and updates the files to make the generator happy, so that you get something better than the stderr output :) ... in those cases the error message would tell you to go and run the tool, much like updating a "golden" test.

The overall suggestion is not really about doing more, rather about doing less :) ... it's trivial to "support", the question is really twofold:

  • does a phase 3 only macro feature make sense as a feature? How much (if at all) does it help us to split out and launch that?
  • does a definitions only augmentation feature make sense as a feature? How much (if at all) does it help us to split out and launch that?

@rrousselGit
Copy link

Are you saying that a macro wouldn't be able to add new methods on a class but rather only implement them ; for the sake of simplifying the feature?

If so, that sounds incompatible with many highly popular code-generators.
For instance, generating a copyWith(...) would be extremely burdensome. And a @data wouldn't be able to remove the constructor+field duplication

Of course, I could be misunderstanding.

@jakemac53
Copy link
Contributor

If the suggestion is to simplify the initial feature launch by only enabling definition macros (phase 3) initially, sure that could be reasonable. However, it wouldn't help very much with some of the highest priority macros we want to enable - such as a data class macro. The generated constructor would anyways have no body, so the macro couldn't provide any value there. It could help a bit with generating ==/hashCode/toString/copyWith still, although it would be a dangerous way to ship copyWith (its signature relies on dependencies potentially).

We would definitely still want/need to ship the other phases. But, it could help us by getting something concrete shipped, so that we can then work towards the phase 1/2 macros.

@davidmorgan
Copy link
Contributor Author

@rrousselGit

That's right, definitions but not declarations; not for the sake of simplifying the feature, but for the sake of a v1 we can launch sooner :) ... with v2 to follow supporting declarations.

I agree it's disappointing if you wanted the macro to be able to output declarations; so I do think that adding declarations is probably something we should probably support.

This whole thing is an interesting tension in the design of generators, that I've given a lot of thought to over the years :)

See for example @AutoValue for Java. I mentioned generator best practices requiring "merge to source" for Java at Google, and @AutoValue is an example of following it to the letter: you have to write the whole interface of what will be generated, the generator only provides implementations. I was involved a bit in adding builder support and was pretty disappointed that you have to write the whole builder API--it's a lot. But it's very heavily used in spite of that.

To some extent it plays into "code is read a lot more times than it's written". In cases where it's more readable to just have the declarations immediately in the code, it's arguably worth the extra effort to put them there and not hidden in an augmentation.

In built_value I had that same choice, and decided to pass the choice to the user: you can choose whether or not to explicitly write the Builder declaration. People mostly prefer to omit it :)

When built_value had a plugin that could auto fix your code for you to add the declarations, I thought that was really neat. I do think it's a great experience for some macros. I don't claim it's a fit for all of them :) and for some macros a mix of "merge to source" and augmented declarations might be the best UX, or leaving it a user choice as I did for built_value.

@jakemac53

Yes, that's the suggestion.

It's curious you should mention data types, because the generator that preceded built_value started life as pure "merge to source": it did no compile-time generation at all, it just checked, and on error the user ran a tool merge all the suggested boilerplate changes (including implementations) into the manually maintained source. It was pretty well liked. It turns out the thing people hate most about boilerplate is maintaining it and having bugs in it ... if it's automatically checked/maintained then it's not terrible to just have it right there. Moving the definitions out and leaving the declarations would be even better.

The "check and require an update if needed" functionality applying to your data type constructor is maybe more valuable than you're thinking. Boilerplate that comes with guarantees is a big step up from boilerplate that you maintain by hand :)

But that curiosity aside, yes, I agree, there are certainly macros that want to emit declarations :) so I only suggest doing definition macros as a feature if it helps us get to declaration macros.

Thanks :)

@rrousselGit
Copy link

I think the community would be quite disappointed if macro launched, yet they couldn't use it (as their build_runner generators need phase 1/2) and we told them to wait longer.

@scheglov
Copy link
Contributor

The types/declarations phases are already mostly implemented, I don't see why we should split them into v2.

I don't agree that separation between the user written library and the generated augmentation file is a big UX obstacle. This is just 1 + 1, not 1 + 100. Navigation between them in IntelliJ would be as easy as Cmd+E, or Cmd+Alt+ArrowLift/Right. OTOH, the limitation would significantly limit the usefulness.

@jakemac53
Copy link
Contributor

Fwiw, macros which want to opt in to this model can always choose to do so - they can even pick and choose individual parts of the API that they think users should hand write, or not.

I hadn't previously considered macro authors choosing to force users to write certain things out, but I do think it is an interesting choice that some of them may opt in to. It would certainly be better if we also supported quick fixes for macro diagnostics, but we want that regardless.

@tatumizer
Copy link

Inserting some summary of the code to be generated by a macro into the source will be helpful IMO - even in the form of comments. Without it, anyone reading the source outside of IDE will be baffled. (This functionality might be controlled by a parameter of a macro, or something)

@tatumizer
Copy link

tatumizer commented Jun 22, 2024

A much bigger problem is that the idea of "composability" of macros is not formalized, and can lead to unexpected behavior.

Consider an example. One macro (let's call it S) generates toJson/fromJson, another macro (H) generates hash code and equals.
During the declaration phase, macro S detects an extra field hashCode and has to decide whether to incorporate it in toJson/fromJson. Let's assume S has no knowledge of the semantics of hashCode - so there's no reason for S to ignore this field.

But the code for fromJson has to rely on the knowledge of the details of the object initialization. Should fromJson call a constructor? Or a Builder? Or use the cascade? If it chooses the call the constructor, then which constructor? Maybe S has to generate the constructor, too, which includes hashCode parameter? Or maybe upon seeing the constructor without the hashCode, it can decide that hashCode should be ignored?

I admit that I have no mental model of collective macro behavior. There's a sense that macros cannot be composed unless they are aware of each other. How much "merge-to-source" feature can help is an open question. Even if it can, the user writing the declarations should do it with full awareness of the logic of the macros involved. (Incidentally, the macro may not even know whether the declaration is entered by the user explicitly, or was generated by another macro, which makes the reasoning problematic anyway).

@davidmorgan
Copy link
Contributor Author

@rrousselGit @scheglov Although we have end to end code working for phase 1+2, there are some large unanswered questions; @tatumizer mentions one of them with macro ordering; macro metadata is another, because const evaluation is not defined for phase 1+2; performance is another. There are also some open questions for augmentations.

So, I'm looking for anything we can split out and either finish first or finish and launch first to make progress. The more we can finish, the easier it will be to make progress on the remaining unanswered questions :)

I am not worried about disappointing users: a feature that we don't know how to launch, is not good for users either ;) we will keep working on this until either users are happy or we have proved it's impossible to make them happy ;)

Re: whether build runner generator owners would be happy with a "definitions only" launch, actually I think they will be plenty happy: they could start using augmentations for definitions, which would make the generators quite a lot nicer, and bring them much closer to being equivalent to the end goal macro feature.

I am not at all worried about the idea of taking smaller steps to launch macros, quite the reverse, I think it makes the whole feature likely to launch sooner.

@scheglov re: UX; there are different opinions here, as I mentioned the Java@Google guidance is that you have to merge the whole API, generators are only allowed to generate implementation. They believe that is "more usable" overall, because you don't need a tool to interact with the declarations. I don't agree fully :) which is why built_value has both modes. But, I do see their point. I also see your point, I think generating declarations and leveraging the IDE works really welll for some cases. So we don't want to limit to just definitions in the end :)

Thanks everyone.

@rrousselGit
Copy link

If we want to ship something early, I'd ship augmentation libraries. Not phase 3 macros.

A large chunk of code-generators wouldn't be able to use macros in a "phase 3 only" world.
The whole point of macros is to stop depending on build_runner, for developer experience. Yet with phase 3 only, many generators would have to keep using build_runner. In that case, the whole point of macros is moot.

That's different for augmentation libraries. They are a standalone feature, and there's value in using them without macros.

@davidmorgan
Copy link
Contributor Author

@rrousselGit augmentation libraries are not ready to ship, either :)

The challenge with augmentation libraries is that it's not really an independent feature, it's 95% motivated by macros. But with macros not being complete yet, it's hard to know whether we have designed augmentation libraries correctly. There is a very real worry that if we ship augmentation libraries ahead of macros we'll find we made some decision that is not what macros want/need.

So if there is a way to do a part launch of augmentation libraries and a part launch of macros, that would give us a lot more confidence in completing both.

@tatumizer
Copy link

As soon as we internalize the fact that the macros generally won't compose (other than by chance), the design becomes 100x simpler :-)
The augmentations are still useful, they will allow to write clean code:

@freezed
class Person {
  const Person({
    required String firstName,
    required String lastName,
    required int age,
  });
}

Now, the class will have one main annotation (in this example - "freezed") which will be processed by a corresponding macro.
All other annotations (on a class level or field level) should be interpreted by the "freezed" macro as configuration parameters. The macro can call other generators ("freezed" does it today when calling the json_serializable generator), but the macro is in full control. In particular, it knows what each (nested) annotation means. Because now it's a 1-step process, there's no need to impose complicated restrictions on what augmentation is or isn't allowed to do. It may even fully replace the source code of the class (the limits are dictated only by the considerations of readability) (maybe this is the best option, resulting in a complete code of the generated class).

@scheglov
Copy link
Contributor

@tatumizer @davidmorgan I don't think that composability is a serious issue. The order of macro applications is well specified, the user has control which macros are applied after which, and knows what each macro generates.

The JSON vs. HashCode macros example per se does not make sense to me, hashCode is usually a getter, not an actual field. But lets pretends that there is a generated field. Then we need a way to configure the JSON macro to exclude this field, we probably want it anyway to exclude even some user written fields.

@JsonSerializable(exclude: {'_hashCodeCache'})
@HashCode()
class A {
  final int foo;
  final int bar;
}

or

@JsonSerializable(excludeGeneratedBy: {HashCode})
@HashCode()
class A {
  final int foo;
  final int bar;
}

The other questions about JSON macro are separate from macro capabilities, and either require decisions what your macro wants to do, or a way to configure it. Whatever, you can write anything - as simple as your use case requires, or as complex as your customers demand, and you can handle. Not the macro specification problem.

@davidmorgan I have a feeling that we overcomplicate the constants evaluation problem. Allow any constants from dependency library cycles, literals, object instantiations with these, and this will cover everything that is practically necessary.

@davidmorgan About UX. I don't think that Java@Google guidelines have much weight for Dart. Java is know to require a lot of manual boilerplate, and if people wanted write in Java, they would continue to do so. For Dart we should aim for more convenience. And yes, IDE is absolutely necessary for modern development.

@tatumizer
Copy link

@scheglov : True, hashCode example was a contrived one, but you can imagine a field called gorgonzola in place of hashCode and have the same problem. If the combination of macros A and B has never been tested together, they are unlikely to work together.
What may happen is that in the end, one macro (like freezed) will gradually evolve to accumulate most of the (directly or indirectly) related functionality (configurable by more parameters) anyway, and there will be 2-3 more super-macros like this, but they will never be combined. This will make composability a non-goal: in every case, a single macro will essentially be responsible for the code generation of several interdependent features. This will make 3-phase design of augmentations unnecessary. That's my logic, but I can be wrong. (Just a gut feeling).

@scheglov
Copy link
Contributor

@tatumizer Indeed, it will require the developer who adds macro application to understand what these macros do. This is the point where macros are "tested" together.

Sometimes you want to see the following macro to see for example fields of generated by the previous macro, sometimes you don't.

@JsonSerializable() // The author of "class A" wants these fields
@GenerateFields(namePrefix: "foo_", count: 10)
class A {}
@JsonSerializable(excludeGeneratedBy: {Memoize})
class A {
  @Memoize()
  int compute() => 42;
}

The author of the macro cannot know which fields should be serialized, it is up to the end user to decide. The macro author, if he is nice enough, will provide ways to configure the macro. Or says "no" to the users, who will either agree, or look for something more flexible.

On the macro tooling side we should support such ability to filtering capabilities. For example, the provenance of fields - ask only user written fields, or filter out generated by a specific macro.

It is OK if there are macros that are "thing-in-itself", it is the macro author decision to make, and the users to accept. But I don't think that this will preclude macros that can be usefully combined, and so I don't think that we should remove the ability to do this from the macro tooling.

@davidmorgan
Copy link
Contributor Author

I filed #3884 with some ideas re: macros seeing the output of other macros--maybe a better default would be that they don't interact.

@scheglov I hope we can get to a simple first set of features for the macro metadata problem :) that looks like it could be a good way to go.

Re: UX, I think IDEs are great, but that functionality still doesn't fill 100% of the case, so there is still some consideration for when there isn't an IDE. Which is where I started with with this issue :) I don't want the most important types in a package to be split 50/50 between checked in and generated code. This is not necessarily a super important case--in most cases you're not quite so worried about people reading the code--but it is a use case.

@tatumizer
Copy link

tatumizer commented Jun 24, 2024

@davidmorgan : I'm afraid "push to source", while solving some problems, creates others. Suppose you need to create an immutable type with 42 fields, now what? Are you supposed to declare 42 fields, a constructor with 42 fields, copyWith with 42 fields, etc. - and it all goes to the source?
There must be a better way.
One idea is to think of the generated code as a mixin (although formally it is not - it's just a mental model).
By definition, mixin is saying to the user: if you provide the implementations of methods a, b and c in your class, then I will add the implementations of methods d and e (which rely on a, b, c).
The macro can say: if you provide the definitions of a, b, c in your class, I will generate d and e. The difference is that while a normal mixin requires concrete a, b and c, macros are more flexible. A macro can say: let a1, a2, ... aN - set of fields in your class. Then I will generate the definition of a default constructor. Another macro can say: if you have a default constructor, I can provide the fromJson method. Etc. If this idea can be formalized, then the order of macro applications will be derived from here. (Just a raw idea for consideration).

@davidmorgan
Copy link
Contributor Author

@tatumizer that's exactly how @AutoValue works, and it's very widely used. So it's a workable model--even if people will always complain about the boilerplate, the fact that the boilerplate is checked means it's pretty good from a maintenance point of view :) you can build huge codebases on top of it, and people have.

To be clear though I'm not suggesting we only implement this model. Just that it is a workable model, and so if implementing it first is helpful, we might think about doing that.

Macro ordering is a big discussion, let's not try to solve it here ;) but indeed the idea of tying together macros by what they input and output is an interesting one; in the context of dart_model and queries we've talked a bit about macros declaring what they will output as a query, and then computing the intersection of macro inputs and outputs to build a graph of macro execution :)

@tatumizer
Copy link

tatumizer commented Jun 26, 2024

@davidmorgan: the idea of @AutoValue doesn't easily translate to dart IMO.

  1. in java, they define an an abstract class, but the macro doesn't add methods to it - instead, it generates another class from scratch.
  2. the example given in javadoc is too trivial: it has some redundancy, but not so much of it to be noticeable.

To illustrate, consider a more realistic example. For simplicity, let's suppose primary constructors proposal gets implemented:

class Point({final int x, final int y}); 

We may want to add some functionality by applying independent macros (those can be mixed and matched freely and can be written in any order b/c orthogonality).

  • generator for hashCode/equals
  • generator for copyWith
  • generator for toJson method
  • generator for fromJson method
  • generator for toString

What is the best way to trigger invocations of these macros?
We can write it like this (using a made-up set of macros)

@JsonCodable(toJson: true, fromJson: false)
@Hashable()
@Stringable()
@CopyWithable() 
class Point(final int x, final int y]); 

But if we conform to push-to-source idea, we still have to write the declarations of the methods.

@JsonSerializable(toJson: true, fromJson: true)
@Hashable()
@Stringable()
@CopyWithable() 
class Point({final int x, final int y}]) {
   Map<String, dynamic> toJson();
   static Point fromJson(Map<String, dynamic> fromJson);
   Point copyWith({int? x, int? y});
   String toString();
   // etc.
} 

It's not clear whether we need to provide complete signatures for the methods (In case of copyWith, the signature may contain 42 parameters)
There's a lot of repetition anyway (each method is essentially mentioned twice).
It's not clear how macros may interpret the declared methods: e.g. if we omit toJson declaration, will the macro complain? Or it will consider the omission of the method as a signal that the method should not be generated?
Maybe alongside each declaration, we should put an annotation saying who generates this method? (instead of doing it on a class level)
How to distinguish the methods that are supposed to be generated from the ones cast in stone by the user?

(There's more to be said about the orthogonality of the above macros :-).

@davidmorgan
Copy link
Contributor Author

@tatumizer you have good questions ... I have good answers, since I built exactly this, for Dart and Java, and it works well :)

When I say "exactly this" ... it was serialization, hashing, comparison, toString ... and also builder. (This was before built_value).

The way that worked is that the generator would check the whole source file, if there were any issues--any missing declarations--it would fail the build with a message including a command to run. You would run the command and it would update everything in place.

The generator knows what type things it owns, and in the "fix" tool will delete anything that's no longer needed.

When I say "works well", it's been used by multiple teams for over a decade now :)

The equivalent for a macro would be that you get an IDE quick fix and ideally also a CLI tool that you can run that applies all fixes. (I guess dart fix). It should be able to work out what to remove, also: anything that is missing an implementation and no macro actually generated one.

It's for sure that this approach works because ... it does work.

Whether it's the best approach is a different question that we don't particularly have to answer, it may be best for some users / use cases and not for others. The important thing for now is that it's a lot simpler, so we can implement it first.

@tatumizer
Copy link

tatumizer commented Jun 27, 2024

I think there's some benefit in splitting the definition into "prototype" and "generated implementation".
That's basically what java does, but dart can double down on that.
The user writes a prototype class containing the declarations that give enough information to the macros (and to the readers).
But the macros generate a real class from scratch. The macro can even generate another prototype, to be processed by other macros down the pipeline.
The difference is that there's much more freedom for the macros compared with the augmentations-based approach (the rules of which are unnecessarily restrictive and convoluted).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
static-metaprogramming Issues related to static metaprogramming
Projects
Development

No branches or pull requests

6 participants