plan.org milestone 5 says when, unless, until, cond and dotimes are special
forms only until macros land. This is the first one to stop being one, and
running test/programs/macro-unless.flan means the compiler built a shared
object, dlopened it into itself and called a Flan function to find out what
(unless c a b) means.
unless is the one that moved because it is the one nothing else needs: zero
uses in the prelude, so moving it cannot make the prelude depend on the
expander that compiles it. Its coverage is sand.flan, seven calls, compiled
through Session in test_session -- which is the in-process path and the reason
lib/dune now passes -linkall. Say plainly what that coverage is not: nothing
in test/programs used unless before today, so macro-unless.flan is a test
written after the feature. The corpus that was written before it is sand.flan
and web/examples/control.flan, and both compile unchanged.
lib/macro.ml is the half of expansion that has to compile something. Expand is
the image format and the quasiquote desugaring and depends on nothing above
Form; this needs Check, Build and Emit, so it sits above the parser it feeds
and arrives through Parse.expander.
What it does, in order:
- Collects every defmacro from the prelude and from the file. Not from an
imported package: Load learns a package's imports by parsing it, so
collecting from one means a second import resolver over Forms, and that is a
bigger thing than this.
- Builds them in rounds, because a macro's body may call a macro and a body
with an unexpanded call in it will not compile at all -- the call is a name
nothing defines. Round 0 takes every macro that names no macro still
waiting; round 1 expands the rest against round 0's module. A round that
takes nothing while macros remain is a ring and is named. macros.flan has
the round-1 case and macro-cycle.flan has the ring, and the distinction
between them is the one thing here that is easy to get wrong: a call inside
a quasiquote is *not* a compile-order dependency. It is part of what the
macro answers, and the answer is expanded again after it returns. The first
macro-cycle.flan written for this commit quasiquoted, and it was not a cycle
at all -- it hit the fuel instead, correctly.
- Walks bottom up, so a macro never sees a call to another macro in what it is
handed, and re-expands what comes back, so a macro that expands into a call
to itself keeps going. That loop is bounded at 200 and says which macro ran
out: macro-spin.flan.
- Skips all of it when the file names no macro, which is nearly every file.
Otherwise every build in the suite would pay a clang driver to answer a
question nobody asked. When it does build, the module is cached under the
object cache and keyed by the prelude's source plus the file's defmacros, so
a second process pays a dlopen.
lib/dune passes -linkall, which is the one line in another lane's file. The
module installs itself into Parse.expander at initialisation and nothing
references it, so without -linkall the linker drops it from every executable
that does not name the module -- bin/main.exe among them -- and a program
calling a macro fails with an unknown name. The alternative was an install
call at every entry point, including ones in files this lane must not touch.
The one thing a macro cannot do that parse.ml could is give a reason. A macro
runs inside the compiler and anything it signals aborts the compile with no
location, so a malformed (unless) answers a name nothing defines and the
report is "unknown name unless-takes-a-test-and-a-body" at the call site --
right place, wrong sentence. NEXT.md says so.
test_flan.ml's "unless -> if(not)" assertion is gone, because it asserted a
desugaring in a file that no longer does one. Nothing else in the suite
changed.