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.
45 lines
1.5 KiB
Plaintext
45 lines
1.5 KiB
Plaintext
;;;; unless, which used to be a special form in parse.ml and is a defmacro in
|
|
;;;; the prelude now. plan.org milestone 5 says the five conditional sugars are
|
|
;;;; special forms only until macros land; this is the first one to stop being
|
|
;;;; one, and running this file means the expander compiled a macro into a
|
|
;;;; shared object, dlopened it into the compiler, and called it -- before the
|
|
;;;; first line below was parsed.
|
|
;;;;
|
|
;;;; Nothing here is new syntax. Every line of it compiled the same way before
|
|
;;;; the move, which is the point: the test for the feature is the corpus that
|
|
;;;; was written against the special form.
|
|
|
|
(defn classify [n i32] string
|
|
(let [out "even"]
|
|
(unless (= 0 (% n 2))
|
|
(set out "odd"))
|
|
out))
|
|
|
|
(defn main [] i32
|
|
;; One body form, the common case.
|
|
(unless false (println "the test was false"))
|
|
(unless true (println "NOT PRINTED"))
|
|
|
|
;; Several, which is what the do in the expansion is for.
|
|
(unless false
|
|
(print "a")
|
|
(print "b")
|
|
(println "c"))
|
|
|
|
;; A computed test, so the argument is a form the macro had to put back
|
|
;; rather than a literal it could have ignored.
|
|
(let [n 7]
|
|
(unless (< n 3) (println "7 is not less than 3")))
|
|
|
|
;; Inside a function that returns a value, and inside a loop: the expansion
|
|
;; is an if with no else, so it is Unit and it does not decide the body's
|
|
;; value.
|
|
(println (classify 4))
|
|
(println (classify 5))
|
|
|
|
(let [seen 0]
|
|
(dotimes [i 5]
|
|
(unless (= i 2) (set seen (+ seen 1))))
|
|
(print seen) (println ""))
|
|
0)
|