Skip to content

Commit 0cc1e25

Browse files
authored
First pass at a guide for compiling macros to augmentations. (#1973)
* First pass at a guide for compiling macros to augmentations. This is a fairly high level imperative process for how a Dart implementation could process macros and generate an augmentation. I didn't want to go into too much unnecessary detail, but hopefully it's concrete enough. * Apply review feedback.
1 parent 39bfd1f commit 0cc1e25

File tree

1 file changed

+123
-21
lines changed

1 file changed

+123
-21
lines changed

working/macros/feature-specification.md

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ ways.
662662

663663
**TODO**: Explain library cycles and compiling to library augmentations.
664664

665-
### Macro compilation order
665+
### Library cycles
666666

667667
Applying a macro involves executing the Dart code inside the body of the macro.
668668
Obviously, that code must be type-checked and compiled before it can be run. To
@@ -687,35 +687,137 @@ have the following restrictions:
687687

688688
[modules]: https://github.com/dart-lang/language/tree/master/working/modules
689689

690-
### Complete macro application order
690+
A Dart implementation can enforce this restriction by organizing a program into
691+
**library cycles**. The build package [already does this][build lib cycle]. A
692+
library cycle is a set of libraries containing import cycles. If two libraries
693+
are not in the same cycle, it is guaranteed that there is no cyclic import
694+
between them.
691695

692-
When all of these are put together, an idealized compilation and macro
693-
application of a Dart program looks like this:
696+
### Ideal compilation process
694697

695-
**TODO**: Update to use library cycles.
698+
Here's a (non-normative) illustration of how a Dart implementation could compile
699+
a Dart program containing macro applications:
696700

697-
1. For each library, ordered topologically by imports:
701+
#### 1. Break the program into library cycles
698702

699-
1. For each declaration, with nested declarations ordered first:
703+
Starting at the entrypoint library, traverse all imports, exports, and
704+
augmentation imports to collect the full graph of libraries to be compiled.
705+
Calculate the [strongly connected components][] of this graph. Each component is
706+
a library cycle, and the edges between them determine how the cycles depend on
707+
each other. Sort the library cycles in topological order based on the connected
708+
component graph.
700709

701-
1. Apply each phase 1 macro to the declaration, from right to left.
710+
Report an error if macro application and its definition occur in the same
711+
library cycle.
702712

703-
1. At this point, all top level identifiers can be resolved.
713+
Each library cycle can now be fully compiled separately. When compiling a
714+
library cycle, it is guaranteed that all macros used by the cycle have already
715+
been compiled. Also, any types or other declarations used by that cycle have
716+
either already been compiled, or are defined in that cycle.
704717

705-
1. For each declaration, with nested declarations ordered first:
718+
[strongly connected components]: https://en.wikipedia.org/wiki/Strongly_connected_component
706719

707-
1. Apply each phase 2 macro to the declaration, from right to left.
720+
#### 2. Compile each cycle
721+
722+
Go through the library cycles in topological order. For each cycle, compile all
723+
of its libraries. First, merge in any hand-authored library augmentations into
724+
their libraries. At this point, you have a set of mutually interdependent
725+
libraries. They may contain references to declarations that don't exist because
726+
macros have yet to produce them.
727+
728+
Collect all the metadata annotations whose names can be resolved and that
729+
resolve to macro classes. Report an error if any application refers to a macro
730+
declared in this cycle.
731+
732+
**TODO**: The above resolution rules may change based on
733+
https://github.com/dart-lang/language/issues/1890.
734+
735+
#### 3. Apply macros
736+
737+
In a sandbox environment or isolate, create an instance of the corresponding
738+
macro class for each macro application. Pass in any macro application arguments
739+
to the macro's constructor. If a parameter's type is `Code` or a subclass,
740+
convert the argument expression to a `Code` object. Any bare identifiers in the
741+
argument expression are converted to `Identifier` instances whose scope is the
742+
library of the macro application.
743+
744+
Run all of the macros in phase order:
745+
746+
1. Invoke the corresponding visit method for all macros that implement phase 1
747+
APIs.
748+
749+
1. Invoke the corresponding visit method for all macros that implement phase 2
750+
APIs.
751+
752+
1. Invoke the corresponding visit method for all macros that implement phase 3
753+
APIs.
754+
755+
While these are running, the macro will likely call back into the host
756+
environment to introspect over code in the current library cycle or previously
757+
compiled cycles. The introspection API is mostly syntactic and structural: a
758+
macro can walk the members on a class declaration or look at the *name* of a
759+
type annotation without the compiler having to do any resolution or type
760+
checking.
761+
762+
When a macro wants to resolve an identifier in a type annotation, there is an
763+
explicit API for that. When that happens, the implementation attempts to resolve
764+
the identifier and return a reference to the resolved declaration. Macros do not
765+
have introspection access to the imperative code of a library, so that code
766+
doesn't need to be resolved or type-checked at this point.
767+
768+
Meanwhile, the macro is also producing new declarations and definitions. These
769+
are collected and held by the macro processor. When introspecting over code, the
770+
implementation needs to show not just the state of the code on disk, but any of
771+
these new declarations produced previously by macros.
772+
773+
#### 4. Generate an augmentation library
774+
775+
Once all macro applications have finished running, the implementation creates a
776+
new empty augmentation library for each library containing macro applications.
777+
All of the declarations created by macros and held by the processor are now
778+
added to the augmentation.
779+
780+
Entirely new declarations are simply added to the augmentation library as
781+
declarations. Declarations that wrap the original declaration's code are added
782+
as augmenting declarations. If a macro adds members to a type, then the type is
783+
added to the augmentation library as an augmenting type, and the members are
784+
added into that.
785+
786+
**TODO**: How are name collisions from private declarations handled?
787+
788+
The `Code` objects representing the signature and body of the declaration is
789+
serialized to Dart source. `Code` objects created from strings are inserted
790+
verbatim into the augmentation library. It's up to the macro author to take
791+
care when using unqualified identifiers in string-based `Code` objects.
792+
793+
**TODO**: Do we want to find identifiers in string-based `Code` objects and
794+
implicitly scope them somehow?
708795

709-
1. At this point, all declarations and their signatures exist. The library
710-
can be type checked.
711-
712-
1. For each declaration, with nested declarations ordered first:
713-
714-
1. Apply each phase 3 macro to the declaration, from right to left.
715-
716-
1. Now all macros have been applied, all imperative code exists, and the
717-
library can be completely compiled. Any macros defined in this library
718-
are ready to be used by later libraries.
796+
Instances of the `Identifier` class have special serialization. An import for
797+
library that the identifier resolves to is added to the augmentation with a new
798+
unique prefix identifier. The `Identifier` name is then serialized as a prefixed
799+
identifier for that prefix. (A more sophisticated implementation could reuse
800+
imports when multiple identifiers resolve to the same library, and may choose to
801+
omit the prefix entirely if the resulting identifier will still resolve
802+
correctly.)
803+
804+
This augmentation library may be written on disk in some implementation-defined
805+
location. It should be accessible to users so that it's possible to step into
806+
and debug macro-generated code. It should probably *not* be stored directly next
807+
to their source code. We don't expect users to commit these generated files to
808+
source control.
809+
810+
#### 5. Apply augmentation and compile
811+
812+
Finally, the implementation implicitly applies these macro-generated
813+
augmentation libraries onto their corresponding main libraries. After that, all
814+
of the libraries are fully complete. They should contain no unresolvable
815+
identifiers, even in imperative code, and every declared member should have a
816+
definition. Report an error if that's not true.
817+
818+
Otherwise, all of the libraries in the cycle can be fully compiled and the
819+
implementation can move on to the next cycle. Any macros declared in this cycle
820+
are ready to be loaded and executed when applied in libraries in later cycles.
719821

720822
## Executing macros
721823

0 commit comments

Comments
 (0)