Skip to content

Commit c76d490

Browse files
authored
Add a section on Aspect Macros (#2237)
1 parent 4477f6d commit c76d490

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed

working/macros/aspect-macros.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
**Disclaimer**: This is just an ideas document, provided purely for discussion
2+
at this time.
3+
4+
# Goals
5+
6+
The goal of this proposal is to solve the problem of duplicate helper code for
7+
macros. Today, two macro applications, even running in the same library, cannot
8+
realistically share helper code with each other. They may even attempt to
9+
produce duplicate helpers and cause naming conflicts, without explicitly doing
10+
anything "wrong".
11+
12+
## Aspect Macros
13+
14+
**NOTE**: This feature is a work in progress, and has many TODOs.
15+
16+
An AspectMacro is used by another macro, to generate shared helper code which
17+
will not be duplicated across every library where that helper was needed.
18+
19+
Concretely, let's consider the following example:
20+
21+
```dart
22+
@fromJsonExtension
23+
class A {}
24+
25+
@fromJsonExtension
26+
class B {
27+
final A a;
28+
29+
B(this.a);
30+
}
31+
32+
// Generated extension from the macro on A
33+
extension AFromJson on Map<String, Object?> {
34+
A toA();
35+
}
36+
37+
// Generated extension from the macro on B
38+
extension BFromJson on Map<String, Object?> {
39+
A toA(); // This also needs to generate a helper for converting `A`!
40+
41+
B toB();
42+
}
43+
```
44+
45+
Both of these macro applications need to generate the same shared code for
46+
converting a Map into an instance of `A` (since `B` has a field of that type),
47+
but they can't actually _see_ what each other has generated.
48+
49+
In the worst case, this would result in them both outputting conflicting
50+
members, and in the best case they would end up generating duplicate code,
51+
causing unnecessary code bloat.
52+
53+
Aspect Macros exist to solve this problem, by allowing macros to share these
54+
implementations. This is done by associating a single, canonical declaration
55+
with an Aspect Macro Instance + Declaration pair, and giving back an opaque
56+
identifier for the resulting declaration.
57+
58+
### Applying an Aspect Macro
59+
60+
Aspect Macros can only be applied by other macros (including aspect macros),
61+
while the macro is running. They do this through the following api:
62+
63+
```dart
64+
Future<Identifier> applyAspect(AspectMacro macro, Declaration declaration);
65+
```
66+
67+
This API does not promise to immediately invoke `macro` on `declaration`, it
68+
only returns an opaque `Identifier`, which refers to the declaration that will
69+
be produced by running `macro` on `declaration`. It returns a `Future` because
70+
all `Identifier` objects must come from the host compiler, so there is some
71+
async communication that has to happen.
72+
73+
When a macro implementation receives a call to `applyAspect`, it must register
74+
that `macro` should be _eventually_ applied to `declaration`. Any two identical
75+
calls to `applyAspect` must always return an "equal" `Identifier` object,
76+
regardless of if they are compiled in separate modules.
77+
78+
- Identical is defined by the concrete type of the Aspect Macro and the
79+
specific declaration it is attached to. Aspect Macros are not allowed to
80+
have fields so we do not need to do more than look at the type.
81+
82+
**TODO**: You may need to be able to navigate to the declaration of one of these
83+
`Identifier`s, for instance in order to get a reference to one of its members or
84+
inspect the shape of the generated declaration. How do we want to expose this?
85+
Is it an ok restriction to just say you can't? See the section below on ordering
86+
and invocation for possible constraints.
87+
88+
### Declaring an Aspect Macro
89+
90+
Aspect Macros are declared similarly to regular macros. They are classes with
91+
the `macro` keyword, but implement one of the subtypes of `AspectMacro` instead
92+
of `Macro`.
93+
94+
An example AspectMacro could be something like the following:
95+
96+
**TODO**: Fully flesh out all these APISs.
97+
98+
```dart
99+
macro class FromJson extends ClassDeclarationsAspectMacro {
100+
FutureOr<Declaration> buildDeclarationForClass(
101+
ClassDeclaration clazz, MacroAspectBuilder builder) {
102+
// Implementation that returns a Declaration.
103+
}
104+
}
105+
```
106+
107+
#### Restrictions on Aspect Macros
108+
109+
- Aspect Macros must be serializable.
110+
- The serializable aspect is enforced by not permitting Aspect Macros to have
111+
any fields, and they only have a single const constructor with no arguments.
112+
- **TODO**: Should we make this constructor explicit?
113+
- **TODO**: Should the api only take the Type of the aspect instead of an
114+
instance?
115+
116+
### Ordering of Aspect Macros
117+
118+
Aspect Macros are conceptually applied in a 4th macro phase. All non-aspect
119+
macros must be invoked on a library prior to aspect macros being invoked on any
120+
declaration in that library.
121+
122+
Aspect Macros are allowed to invoke other aspect macros, so this 4th phase is
123+
iterative.
124+
125+
- This is only safe because we don't allow introspection of Aspect Macro
126+
generated identifiers today. If we did then we would need to figure out how
127+
that should work and how cycles should be handled.
128+
129+
### Modular compilation
130+
131+
Aspect Macros may be ran modularly, which may require merging of generated
132+
declarations at link time. Esssentially, the result of each module should have
133+
a table of all the outputs of the Aspect Macros it had to run, and if multiple
134+
dependency modules ran on the same Aspect Module + Declaration pair, the results
135+
should be deduplicated when reading them in.
136+
137+
### Location of Aspect Macro generated code
138+
139+
TODO: Figure this out. A special library "next" to each library which has aspect
140+
macros applied to its declarations? A single "special" library with all aspects
141+
merged in? A library per aspect macro application?
142+
143+
### Possible extra feature: Data Collection
144+
145+
**TODO**: Fully explore this use case.
146+
147+
Another possible use case for something like an Aspect Macro, would be to
148+
generate a declaration at the top level of the program, which is based on only
149+
information collected from the rest of the program.
150+
151+
In particular for dependency injection this would be useful, but also
152+
potentially for other use cases such as generating an automatic route handler
153+
for a server side framework, or possibly ORM applications as well.
154+
155+
Instead of generating declarations, these Aspect Macros would generate Data,
156+
which would be merged/deduplicated in a similar fashion, and could be retreived
157+
_at compile time_ by another macro.
158+
159+
## Other features under consideration
160+
161+
### Allow adding private declarations in phase 3, with an optional key
162+
163+
Instead of having another type of macro, we could possibly just allow any macro
164+
to emit new, private declarations in Phase 3. You would not be able to control
165+
the names of these declarations, and could not guess them in earlier phases.
166+
167+
This new API would have an optional `String? key` argument, or possibly a
168+
`OpaqueKey? key` argument, which would allow you to create deduplicated helpers.
169+
170+
This gives the user control over the key used for deduplication, which has both
171+
advantages and disadvantages.
172+
173+
### Make Aspect Macros run in phase 3
174+
175+
We could allow phase 3 macros to call an API similar to the one described above,
176+
to generate deduplicated helpers. They would not pass a user computed key, but
177+
instead rely on deduplication in the same manner as above.
178+
179+
We could likely still allow cyclic aspect macros, as we would not synchronously
180+
be invoking them.
181+
182+
We could also possibly not allow aspect macros to call other aspect macros.

0 commit comments

Comments
 (0)