6.1
16.2.6 General Phase Levels
A phase can be thought of as a way to separate computations in
a pipeline of processes where one produces code that is used by the
next. (E.g., a pipeline that consists of a preprocessor process, a
compiler, and an assembler.)
Imagine starting two Racket processes for this purpose. If you ignore
inter-process communication channels like sockets and files, the
processes will have no way to share anything other than the text that is
piped from the standard output of one process into the standard input of
the other. Similarly, Racket effectively allows multiple invocations of
a module to exist in the same process but separated by phase. Racket
enforces separation of such phases, where different phases cannot
communicate in any way other than via the protocol of macro expansion,
where the output of one phases is the code used in the next.
16.2.6.1 Phases and Bindings
Every binding of an identifier exists in a particular phase. The link
between a binding and its phase is represented by an integer
phase level. Phase level 0 is the phase used for “plain”
(or “runtime”) definitions, so
(define age 5)
adds a binding for age into phase level 0. The identifier
age can be defined at a higher phase level using
begin-for-syntax:
With a single begin-for-syntax wrapper, age is
defined at phase level 1. We can easily mix these two definitions in
the same module or in a top-level namespace, and there is no clash
between the two ages that are defined at different phase
levels:
The age binding at phase level 0 has a value of 3, and the
age binding at phase level 1 has a value of 9.
Syntax objects capture binding information as a first-class value.
Thus,
#'age
is a syntax object that represents the age binding—but
since there are two ages (one at phase level 0 and one at
phase level 1), which one does it capture? In fact, Racket imbues
#'age with lexical information for all phase levels, so the
answer is that #'age captures both.
The relevant binding of age captured by #'age is
determined when #'age is eventually used. As an example, we
bind #'age to a pattern variable so we can use it in a
template, and then we evaluate the template: We
use eval here to demonstrate phases, but see
Reflection and Dynamic Evaluation for caveats about eval.
The result is 3 because age is used at phase 0 level.
We can try again with the use of age inside
begin-for-syntax:
In this case, the answer is 9, because we are using
age at phase level 1 instead of 0 (i.e.,
begin-for-syntax evaluates its expressions at phase level 1).
So, you can see that we started with the same syntax object,
#'age, and we were able to use it in two different ways: at
phase level 0 and at phase level 1.
A syntax object has a lexical context from the moment it first exists.
A syntax object that is provided from a module retains its lexical
context, and so it references bindings in the context of its source
module, not the context of its use. The following example defines
button at phase level 0 and binds it to 0, while
see-button binds the syntax object for button in
module a:
The result of the m macro is the value of see-button,
which is #'button with the lexical context of the a
module. Even though there is another button in b, the
second button will not confuse Racket, because the lexical
context of #'button (the value bound to see-button) is
a.
Note that see-button is bound at phase level 1 by virtue of
defining it with define-for-syntax. Phase level 1 is needed
because m is a macro, so its body executes at one phase higher
than the context of its definition. Since m is defined at
phase level 0, its body is at phase level 1, so any bindings referenced
by the body must be at phase level 1.
16.2.6.2 Phases and Modules
A phase level is a module-relative concept. When importing from
another module via require, Racket lets us shift imported
bindings to a phase level that is different from the original one:
That is, using for-syntax in require means that all of
the bindings from that module will have their phase levels increased by
one. A binding that is defined at phase level 0 and imported
with for-syntax becomes a phase-level 1 binding:
Let’s see what happens if we try to create a binding for the
#'button syntax object at phase level 0:
Now both button and see-button are defined at phase
0. The lexical context of #'button will know that there is a
binding for button at phase 0. In fact, it seems like things
are working just fine if we try to eval see-button:
Now, let’s use see-button in a macro:
|
|
> (m) |
see-button: undefined; |
cannot reference undefined identifier |
Clearly, see-button is not defined at phase level 1, so we
cannot refer to it inside the macro body. Let’s try to use
see-button in another module by putting the button definitions
in a module and importing it at phase level 1. Then, we will get
see-button at phase level 1:
|
|
|
eval:1:0: button: unbound identifier; |
also, no #%top syntax transformer is bound |
in: button |
Racket says that button is unbound now! When a is
imported at phase level 1, we have the following bindings:
button at phase level 1 |
see-button at phase level 1 |
So the macro m can see a binding for see-button at
phase level 1 and will return the #'button syntax object, which
refers to button binding at phase level 1. But the use of
m is at phase level 0, and there is no button at phase
level 0 in b. That is why see-button needs to be
bound at phase level 1, as in the original a. In the original
b, then, we have the following bindings:
button at phase level 0 |
see-button at phase level 1 |
In this scenario, we can use see-button in the macro, since
see-button is bound at phase level 1. When the macro expands,
it will refer to a button binding at phase level 0.
Defining see-button with (define see-button #'button) isn’t inherently wrong; it depends on how we intend to use
see-button. For example, we can arrange for m to
sensibly use see-button because it puts it in a phase level 1
context using begin-for-syntax:
In this case, module b has both button and
see-button bound at phase level 1. The expansion of the macro
is
which works, because button is bound at phase level 1.
Now, you might try to cheat the phase system by importing a at
both phase level 0 and phase level 1. Then you would have the following
bindings
button at phase level 0 |
see-button at phase level 0 |
button at phase level 1 |
see-button at phase level 1 |
You might expect now that see-button in a macro would work, but
it doesn’t:
|
|
|
eval:1:0: button: unbound identifier; |
also, no #%top syntax transformer is bound |
in: button |
The see-button inside the m macro comes from the
(for-syntax 'a) import. For the macro to work, there must be a
button at phase 0 bound, and there is such a binding implied by
(require 'a). However, (require 'a) and
(require (for-syntax 'a)) are different instantiations
of the same module. The see-button at phase 1 only refers to
the button at phase level 1, not the button bound at
phase 0 from a different instantiation—even from the same source
module.
Mismatches like the one above can show up when a macro tries to match
literal bindings—using syntax-case or syntax-parse.
|
|
|
eval:2.0: process: expected the identifier `button' |
at: button |
in: (process (0 button)) |
In this example, make is being used in y at phase
level 2, and it returns the #'button syntax object—which
refers to button bound at phase level 0 inside x and
at phase level 2 in y from (for-meta 2 'x). The
process macro is imported at phase level 1 from
(for-meta 1 'x), and it knows that button should be
bound at phase level 1. When the syntax-parse is executed
inside process, it is looking for button bound at
phase level 1 but it sees only a phase level 2 binding and doesn’t
match.
To fix the example, we can provide make at phase level 1
relative to x, and then we import it at phase level 1 in
y: