Skip to content

add section on macro instantiation and parameters #1861

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

Merged
merged 4 commits into from
Sep 28, 2021
Merged

Conversation

jakemac53
Copy link
Contributor

@jakemac53 jakemac53 commented Sep 17, 2021

Followup to our discussion on making macro parameters more meta-y

@TimWhiting
Copy link

TimWhiting commented Sep 17, 2021

This resolves to some extent jakemac53/macro_prototype#26 via the Code interface if I am right? The only limitation would be that the AST will be unresolved and in the form of Code objects.

Is there a reason that Symbol is omitted? With Code parameters I guess you could just take an unresolved identifier which will be a Code expression object which is a lot nicer than a String / Symbol based api for telling a macro what you want it to name some declaration in my opinion, but I just wondered why Symbol wasn't included since it seems like another 'primitive' object in dart.

@jakemac53
Copy link
Contributor Author

This resolves to some extent jakemac53/macro_prototype#26 via the Code interface if I am right? The only limitation would be that the AST will be unresolved and in the form of Code objects.

Correct it does in effect provide the interface for doing that - since you get Code objects you can use them directly in your output as well.

Is there a reason that Symbol is omitted?

Nope, I just took a look through the dart:core types and listed the ones that seemed reasonable to support. I am actually not entirely sure about Symbol... passing a Symbol from one app to the other might not actually make sense (especially in release mode, the symbols lose all references to the textual value of the symbol I think?). cc @lrhn any thoughts on that?

Macros should be able to be able to be instantiated using only the libraries
that are available to the macro definition library. This allows us to compile an
app (or spawn an isolate) that can run macro applications for that macro using
only its own dependencies.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who performs this instantiation? Where do they get the arguments from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the other wording below starts to address this - I don't want to get overly specific as this could be implementation specific? For now its just "the compiler".

- bool
- null (really this just allows nullable types)
- Uri
- DateTime
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just allow any value which can be safely deserialized by the isolate which is going to run the macro?
(And make it a compile-time error to specify any argument which can't)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be an interesting approach. It gets complicated though because arguments to macros might themselves require macro expansion in the current library to happen before they are valid. We could make it an error to reference anything from the current library cycle though.

Forcing everything to be literals or provided as a Code object is primarily designed to avoid that problem.

### Macro Constructor Arguments

When any type other than [Code][] is used, arguments are only allowed to be
object literals. Or in the case of `Uri` and `DateTime` they must be direct
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the source code for this macro occurs as an annotation in the library using the macro. This source code is then executed in a separate isolate. Therefore the source code must be valid in that library. Is that correctly understood?
(And the annotation is not available at run-time in the original library).

In that case, I'd definitely allow any literal (including symbol literals, but not including "type literals").

I'm vary about Uri and DateTime because they do not have const constructors, and therefore shouldn't be in an annotation to begin with. I guess we'll need special rules for macro annotations to allow them to do anything, becaus they'll be instantiated in a non-const way eventually. Or you can just put in the string and parse it at the other end.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the source code for this macro occurs as an annotation in the library using the macro. This source code is then executed in a separate isolate. Therefore the source code must be valid in that library. Is that correctly understood?

Right

In that case, I'd definitely allow any literal (including symbol literals, but not including "type literals").

Ya that might be an easier and more general way to define this.

I'm vary about Uri and DateTime because they do not have const constructors, and therefore shouldn't be in an annotation to begin with. I guess we'll need special rules for macro annotations to allow them to do anything, becaus they'll be instantiated in a non-const way eventually. Or you can just put in the string and parse it at the other end.

They really aren't necessary anyways, ill remove them.

Copy link
Member

@munificent munificent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggestions on how to word it, but overall I'm excited to see you filling in this part of the proposal. :)

@@ -246,6 +246,41 @@ introspection APIs.
These macros can fully introspect on any type reachable from the declarations
they annotate, including introspecting on members of classes, etc.

## Macro Instantiation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of drops the reader in the deep end. How about something like:


To apply a macro, a Dart compiler constructs an instance of the applied macro's class, and then invokes methods on it that implement the macro API. The arguments on the metadata annotation for the macro application get passed to the macro as constructor parameters. For example, on a macro application like:

@myMacro(1, 'a string')
class SomeClass {}

The compiler constructs an instance of the myMacro class and passes 1 and 'a string' to it. We want a Dart compiler to be able to construct and call methods on these macros while they are potentially running in a separate isolate or process. That in turn means that the constructor arguments need to be values that are easily serialized and sent to an isolate.

Macro constructor arguments can only be one of these types:

  • The primitive types bool, double, Null, num, and String
  • [Code][] and its subtypes
  • Uri
  • DateTime

Also, what about lists, sets, and maps?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took that suggestion but s/arguments/parameters (the arguments can be anything, but the macro will get them as Code if they aren't these types).

Also, what about lists, sets, and maps?

I added those as well - but require that they are passed as literals (and all arguments are literals).

- DateTime

### Macro Constructor Arguments

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little unclear to me about whether arguments are values or meta-values. How about something kind of like:


Arguments to macros are passed as unevaluated code. In other words, if you apply a macro like:

@myMacro(1 + 2)
class SomeClass {}

The argument passed is not the number 3, but a Code object representing the expression 1 + 2. This is useful if a macro wants to take an arbitrary piece of syntax as an argument that it then injects into the generated code.

In many cases, though, what the macro author wants is an argument value. For example:

@coordinates(dimensions: 3)
class Vec3 {}

Here, the @coordinates macro generates, x, y, etc. fields based on the given dimension passed to the macro. To support this, a macro constructor can accept parameters of primitive types and it will receive the arguments as values, not Code objects representing the value. However, the argument expression must be a literal of that type, and not any arbitrary expression that happens to evaluate to that type.

For example:

@coordinates(dimensions: 1 + 2) // Error.
class Vec3 {}

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the arguments are recorded as expressions, then can you use variables references?
Say:

const string = "something or other";
...
@MyMacro(string, 42)
...

How about interpolations:

@MyMacro("Header:$string:Trailer")
...

If you can't access const variables, then you also can't use enums, which is likely going to affect the APIs that you'd want to build around the macro annotation.

(How about just serializing the macro annotation constant, deserializing it in the isolate running the macro, then failing compilation if you can't do both).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the arguments are recorded as expressions, then can you use variables references?

Yes

How about interpolations:

Yes

If you can't access const variables, then you also can't use enums, which is likely going to affect the APIs that you'd want to build around the macro annotation.

You could but you wouldn't be able to read their value, only a Code reference to them (might be an expression even). You can emit that reference in your own Code objects (maybe a switch statement etc) but you wouldn't be able to base the code you output on the value of those objects.

Loosening this would have some valid use cases for sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took another shot at rewording this also. Borrowed some stuff from @munificent, but tried not to mislead people by oversimplifying things early on still.... I think its important to make the pass by value vs Code object distinction early on.

@jakemac53 jakemac53 merged commit a5ac135 into master Sep 28, 2021
@jakemac53 jakemac53 deleted the macro-parameters branch September 28, 2021 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants