Skip to content

Commit 91da80e

Browse files
committed
In-progress proposal for reflected imports.
1 parent ee1135e commit 91da80e

File tree

1 file changed

+297
-0
lines changed

1 file changed

+297
-0
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
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&mdash;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

Comments
 (0)