|
| 1 | +# Reflected Imports Feature Specification |
| 2 | + |
| 3 | +Authors: Bob Nystrom, Jake Macdonald |
| 4 | + |
| 5 | +Status: In progress |
| 6 | + |
| 7 | +Version 1.0 (see [CHANGELOG](#CHANGELOG) at end) |
| 8 | + |
| 9 | +## Motivation |
| 10 | + |
| 11 | +When a macro is applied to a declaration, it can introspect over various |
| 12 | +properties of the declaration. It can use that to generate code as well as |
| 13 | +building up generated Dart code "from scratch" as literal source. In many cases, |
| 14 | +a macro only needs to introspect over the applied declaration to know what code |
| 15 | +to produce. But often, a macro wants to refer to some known existing code. |
| 16 | + |
| 17 | +### Generating references to known code |
| 18 | + |
| 19 | +For example, a macro that generates a serialization method for a class might |
| 20 | +want to implement the body of the method by generating some calls to a known |
| 21 | +utility function: |
| 22 | + |
| 23 | +```dart |
| 24 | +// package:serialize/helpers.dart: |
| 25 | +Map<String, Object?> serializeIterable(Iterable<Object?> elements) { |
| 26 | + // Helper functionality... |
| 27 | +} |
| 28 | +
|
| 29 | +// package:serialize/serialize.dart: |
| 30 | +macro class Serialize { |
| 31 | + // Generate code that calls to `serializeIterable()` |
| 32 | + // from "serialize_helpers.dart"... |
| 33 | +} |
| 34 | +
|
| 35 | +// my_app.dart |
| 36 | +import 'package:serialize/serialize.dart'; |
| 37 | +
|
| 38 | +@Serialize |
| 39 | +class Foo { |
| 40 | + // ... |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +In theory, the macro could just inline the code for any utility functionality it |
| 45 | +needs right into the generated code, but that leads to code duplication and |
| 46 | +larger executables. We want it to be easy for macros to reuse code so that |
| 47 | +macros don't lead to bloated applications. |
| 48 | + |
| 49 | +This is challenging because the macro generated code may refer to a library |
| 50 | +that hasn't been imported where the macro is applied. In the example here, |
| 51 | +"my_app.dart" doesn't know anything about "serialize/helpers.dart". |
| 52 | + |
| 53 | +### Accessing known declarations during introspection |
| 54 | + |
| 55 | +In the above example, the macro doesn't *use* "serialize/helpers.dart" or even |
| 56 | +ask any questions about it while the macro is running. It just inserts a |
| 57 | +reference to it in the generated code. But sometimes a macro may want to |
| 58 | +introspect over a specific known declaration independent of the one that macro |
| 59 | +was applied to. |
| 60 | + |
| 61 | +For example, consider a serialization macro. It walks over the fields of the |
| 62 | +class the macro is applied to. If it encounters a field whose type implements |
| 63 | +`CustomSerializer`, then it wants to generate code that defers to the field's |
| 64 | +own serialization behavior. Otherwise, it generates some default serialization |
| 65 | +code. |
| 66 | + |
| 67 | +To do that, while the macro is running and introspecting over the class, it |
| 68 | +needs to be able to ask "Is this field's type a subtype of `CustomSerializer`?" |
| 69 | +Note that the macro can't just use Dart's own `is` operator for this: the |
| 70 | +field's type exposed to the macro is a *metaobject* representating the |
| 71 | +*introspection* of the field's type. While the macro is running, there is no |
| 72 | +actual field and no value for it. |
| 73 | + |
| 74 | +What the macro needs is a way to get a corresponding metaobject for |
| 75 | +`CustomSerializer` that it can use in [the macro introspection API][api] for |
| 76 | +things like subtype tests, as in: |
| 77 | + |
| 78 | +[api]: https://github.com/dart-lang/sdk/blob/main/pkg/_fe_analyzer_shared/lib/src/macros/api/introspection.dart |
| 79 | + |
| 80 | +```dart |
| 81 | +for (var field in await builder.fieldsOf(clazz)) { |
| 82 | + if (await field.type.isSubtypeOf(customSerializerType)) { |
| 83 | + // Generate code to call custom serializer... |
| 84 | + } else { |
| 85 | + // Generate default serialization code... |
| 86 | + } |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +Here, `customSerializerType` needs to be an instance of `StaticType` (the macro |
| 91 | +introspection API's notion of a type) that refers to the Dart class |
| 92 | +`CustomSerializer`. In other words, the macro author needs a way to "lift" the |
| 93 | +`CustomSerializer` from a normal Dart type to the meta-level that macros operate |
| 94 | +at. |
| 95 | + |
| 96 | +### Static import graphs |
| 97 | + |
| 98 | +A straightforward solution would be for the macro API to provide an imperative |
| 99 | +function that takes a library and declaration name and gives back an |
| 100 | +introspection object referring to it, like: |
| 101 | + |
| 102 | +```dart |
| 103 | +var customSerializerType = Identifier.fromLibrary( |
| 104 | + 'package:serialize/serialize.dart', 'CustomSerializer'); |
| 105 | +``` |
| 106 | + |
| 107 | +However, this doesn't work with the compilation process. Macros require a Dart |
| 108 | +program to be compiled in a series of stages. A Dart library defining a macro |
| 109 | +must be completely compiled before the macro is applied by some other library. |
| 110 | +That ensures the macro is compiled and able to be run before any uses of it are |
| 111 | +encountered. |
| 112 | + |
| 113 | +To enable that, a Dart compiler must be able to traverse the import graph of the |
| 114 | +entire program and stratify it into a well-defined compilation order where macro |
| 115 | +definitions are built before their uses. That process breaks if a new connection |
| 116 | +between libraries can spontaneously appear while a macro is running. With a |
| 117 | +procedural API, there is no way to know what *other* libraries the macro will |
| 118 | +refer to until the macro itself is running and calling that API. |
| 119 | + |
| 120 | +To ensure that the entire dependency graph is known statically before any code |
| 121 | +is compiled, the compiler needs to be able to statically tell which known |
| 122 | +libraries the macro generate references to or introspects over. |
| 123 | + |
| 124 | +### References to unavailable libraries |
| 125 | + |
| 126 | +An imperative API doesn't work, so maybe the macro should just directly import |
| 127 | +any known library that it wants to generate references to or use in |
| 128 | +introspection. Since the library applying the macro imports the library where |
| 129 | +the macro is defined, that ensures there is a transitive import from the library |
| 130 | +where the macro is used to the library the generated code refers to. |
| 131 | + |
| 132 | +This solves the static import graph problem, but causes another one: It means |
| 133 | +that the entire known library must be compiled and available for use by the |
| 134 | +macro. It's unlikely that the macro needs to *use* the known library. It |
| 135 | +probably doesn't need to construct instances of its types or call functions |
| 136 | +when the macro itself is running. It just needs to generate code that does that. |
| 137 | + |
| 138 | +And, in many cases, it may not be *possible* to use the library while the macro |
| 139 | +is running. Macros run in their own limited execution environment where core |
| 140 | +libraries like "dart:html" and "dart:io" aren't available. Any library that |
| 141 | +imports those directly or indirectly can't be imported by a macro, but we do |
| 142 | +want to support macros that can generate code that *refers to* those libraries. |
| 143 | + |
| 144 | +## Goals |
| 145 | + |
| 146 | +Summarizing the above, the requirements are: |
| 147 | + |
| 148 | +* Give macros a way to insert references to known declarations in generated |
| 149 | + code. |
| 150 | +* Let macros introspect over declarations in known libraries for things like |
| 151 | + subtype tests. |
| 152 | +* Enable the Dart compiler to statically understand the library dependency |
| 153 | + graph of the program. |
| 154 | +* Allow macros to refer to declarations even in libraries that can't be run |
| 155 | + in the macro execution environment. |
| 156 | + |
| 157 | +## Reflected imports |
| 158 | + |
| 159 | +To do all of the above, we add a new kind of import, a *reflected import*. The |
| 160 | +grammar is: |
| 161 | + |
| 162 | +``` |
| 163 | +importSpecification ::= |
| 164 | + 'import' configurableUri ('deferred'? 'as' identifier)? combinator* ';' |
| 165 | + | 'import' uri 'reflected' 'as' identifier combinator* ';' |
| 166 | +``` |
| 167 | + |
| 168 | +It looks like |
| 169 | + |
| 170 | +```dart |
| 171 | +import 'package:serializable/serializable.dart' reflected as serializable; |
| 172 | +``` |
| 173 | + |
| 174 | +The design is akin to deferred imports. An import marked with the `reflected` |
| 175 | +modifier provides access to the imported library for use in metaprogramming. |
| 176 | +This is *not* the same as a regular import. A reflected import doesn't provide |
| 177 | +direct access to the declarations or code in the library. |
| 178 | + |
| 179 | +Instead, the import prefix (which must be provided) defines an object-like |
| 180 | +namespace that can be used to access reflective metaobjects *describing* the |
| 181 | +imported library. The prefix exposes a getter for every public declaration |
| 182 | +defined by the imported library. Each getter returns an `Identifier` object that |
| 183 | +is resolved to the corresponding declaration in the library. |
| 184 | + |
| 185 | +```dart |
| 186 | +import 'package:listenable/listenable.dart' reflected as listenable; |
| 187 | +
|
| 188 | +main() { |
| 189 | + print(listenable.Listenable.runtimeType); // "Identifier". |
| 190 | +} |
| 191 | +``` |
| 192 | + |
| 193 | +Getters are only defined for the declarations the library actually contains, so |
| 194 | +if a macro author mistakenly tries to refer to an unknown or misspelled |
| 195 | +declaration from a reflected import, the macro will fail to compile. |
| 196 | + |
| 197 | +```dart |
| 198 | +import 'package:listenable/listenable.dart' reflected as listenable; |
| 199 | +
|
| 200 | +main() { |
| 201 | + print(listenable.Listenible.runtimeType); |
| 202 | + // ^^^^^^^^^^ Error: Unknown getter "Listenible". |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +### Using reflected imports in generated code |
| 207 | + |
| 208 | +These `Identifier` objects can be inserted into code generated by a macro. When |
| 209 | +they are, the `Identifier` retains its original resolution to the imported |
| 210 | +library and generates an unambiguous reference to the corresponding declaration |
| 211 | +in the library. |
| 212 | + |
| 213 | +**TODO: Show an example of using a reflected import to refer to a class and a |
| 214 | +function from a library in macro generated code.** |
| 215 | + |
| 216 | +In concrete terms, when the macro execution environment compiles the macro's |
| 217 | +generated code to an augmentation library, it finds every `Identifier` that is |
| 218 | +resolved to a library. For each one, it generates a unique prefix. Then in the |
| 219 | +produced augmentation library, it synthesizes an import for the resolved library |
| 220 | +with that prefix and then compiles all `Identifiers` that resolve to that |
| 221 | +library to prefixed identifiers using that prefix. |
| 222 | + |
| 223 | +In practical terms, the macro author doesn't have to worry about |
| 224 | +the name being shadowed by other names in scope where the generated code is |
| 225 | +output. They can call a getter on the reflected import's prefix, insert the |
| 226 | +result directly in generated code, and end up with a valid reference to the |
| 227 | +imported declaration. |
| 228 | + |
| 229 | +### Using reflected imports in introspection |
| 230 | + |
| 231 | +In order to introspect over a declaration from a reflected import, the |
| 232 | +`Identifier` must first be resolved to a declaration. The [macro introspection |
| 233 | +API exposes][api] methods to do that. |
| 234 | + |
| 235 | +**TODO: Are these only available in certain phases? Can reflected imports be |
| 236 | +resolved in any phase? Once resolved, can they be deeply reflected over (i.e. |
| 237 | +walking the members of a class, etc.) or only used for things like subtype |
| 238 | +tests?** |
| 239 | + |
| 240 | +**TODO: Would be good to show a concrete example of a macro using a reflected |
| 241 | +import and introspecting over it in a subtype test.** |
| 242 | + |
| 243 | +Requiring the macro to go through those APIs instead of eagerly exposing the |
| 244 | +declaration introspection objects directly on the reflected import prefix spares |
| 245 | +the compiler and macro execution environment from having to build full |
| 246 | +introspection objects for every public declaration in the reflected on library. |
| 247 | + |
| 248 | +## Restrictions |
| 249 | + |
| 250 | +Reflected imports pierce the boundary between compile-time and runtime. In order |
| 251 | +to avoid adding significantly complexity to our compilers and runtimes, and to |
| 252 | +avoid bloating end user programs, there are some limitations in how they can be |
| 253 | +used. |
| 254 | + |
| 255 | +### Using reflected imports |
| 256 | + |
| 257 | +In order to define getters returning `Identifier` objects for every declaration |
| 258 | +in a reflected imported library, the compiler needs to know the static API |
| 259 | +signature of the library—the set of all public top level declarations. |
| 260 | +This way, it knows which getters are available. |
| 261 | + |
| 262 | +Then, at runtime, the Dart program needs to be able to instantiate and return |
| 263 | +instances of the `Identifier` class whenever those getters are called. This |
| 264 | +class is defined in the macro API and is only available in the macro execution |
| 265 | +environment. |
| 266 | + |
| 267 | +This implies that **reflected imports can only appear in libraries that are run |
| 268 | +in the macro execution environment.** You can think of the macro environment as |
| 269 | +a distinct Dart "platform" and only it supports reflected imports. When |
| 270 | +targeting any other execution environment, a Dart compiler reports a |
| 271 | +compile-time error if it encounters a reflected import. |
| 272 | + |
| 273 | +**TODO: What about unit tests for macros? They presumably run in the normal |
| 274 | +command line VM execution environment. Can they use reflected imports?** |
| 275 | + |
| 276 | +### Config-specific imports |
| 277 | + |
| 278 | +When a reflected import is encountered, the compiler needs to know which |
| 279 | +declarations it contains. The compiler needs this when it is compiling *the |
| 280 | +macro* (which is targeting the macro execution environment) and not when it is |
| 281 | +compiling *the end user program* (which may target the web, Flutter, etc.). |
| 282 | + |
| 283 | +That means that when the reflected import is compiled, the compiler doesn't know |
| 284 | +the final targeted platform. That in turn means that the compiler doesn't know |
| 285 | +which branch to choose in any config-specific imports since it doesn't know |
| 286 | +which platform flags will have which values. (Macros themselves are |
| 287 | +configuration-independent and don't have access to the program's target |
| 288 | +configuration either.) |
| 289 | + |
| 290 | +Thus, when determining what declarations are available for a reflected import, |
| 291 | +**the compiler always uses the default URI for any config-specific imports or |
| 292 | +exports that it encounters.** |
| 293 | + |
| 294 | +**TODO: Consider making it a compile-time error for a reflected import to |
| 295 | +having any config-specific imports, directly or transitively?** |
| 296 | + |
| 297 | +## Changelog |
0 commit comments