|
| 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