A quasiquote inside a quasiquote nests, and a macro that writes a macro defines one the program can call
This commit is contained in:
parent
bf827dc55b
commit
0167fcef43
12
TODO.org
12
TODO.org
@ -81,10 +81,14 @@ back after. It counts across every module a compiler process loads — each roun
|
||||
the program's module, and every expansion in a session. Rules out a counter per
|
||||
module, seeded or not.
|
||||
|
||||
** NEXT A quasiquote inside a quasiquote is refused
|
||||
Decided 2026-09-25: nest the way SBCL and Clojure both do. The desugaring counts depth, an unquote belongs to the innermost quasiquote, and =~~x= reaches out two levels — SBCL's =*backquote-depth*= in =src/code/backq.lisp=. The reader stays as it is.
|
||||
Nothing counts nesting levels — not the reader, deliberately, and not the
|
||||
desugaring. Only a macro that writes a macro wants one.
|
||||
** DONE A quasiquote inside a quasiquote nests
|
||||
CLOSED: [2026-09-25]
|
||||
=Expand.quote= counts depth the way SBCL's =*backquote-depth*= does: an unquote
|
||||
belongs to the innermost quasiquote and =~~x= reaches out two levels; deeper
|
||||
forms come back as data. A macro's answer is desugared again, and a top-level
|
||||
expansion that defines a macro re-runs the expander. =~~@x= is refused. There is
|
||||
no =,',x=, since =quote= takes a symbol, and a macro defined by an expansion is
|
||||
not exported from a package. docs/BUILT.md, "Quasiquote runs before the walk".
|
||||
|
||||
** TODO until, cond, when and dotimes are still special forms in parse.ml
|
||||
=until= and =cond= are free to move to the prelude whenever somebody wants them.
|
||||
|
||||
@ -3482,8 +3482,22 @@ quasiquoted call to itself; with the quasiquote still standing, the walk would s
|
||||
there, against the wrong arguments. Desugared first, that subform is a `(Form.Sym {.s "cond"})` and there is no head
|
||||
left to mistake — so the walk needs no idea that quoting exists.
|
||||
|
||||
Nesting levels are counted nowhere: not by the reader, which was written that way deliberately, and not by the
|
||||
desugaring. A quasiquote inside a quasiquote is refused by name. Only a macro that writes a macro wants one.
|
||||
Nesting levels are counted by the desugaring and not by the reader, which stays as it was written. A quasiquote
|
||||
inside a quasiquote raises the depth and an unquote lowers it, SBCL's `*backquote-depth*` in `src/code/backq.lisp`:
|
||||
an unquote belongs to the innermost quasiquote around it, `~~x` reaches out two levels, and only the unquotes at
|
||||
depth 1 are evaluated. Everything deeper comes back as the `(quasiquote x)`, `(unquote x)` and
|
||||
`(unquote-splicing x)` forms it was read as, and is desugared in its turn when the macro that holds it runs.
|
||||
|
||||
That second desugaring is why `Macro.settle` runs `Expand.quasiquote` over whatever a macro answers, before it looks
|
||||
at a single head: a macro that writes a macro answers a `defmacro` whose body still holds the inner quasiquote, and
|
||||
the walk must not read a call inside it as a call. And because the macro module is fixed before the walk starts, a
|
||||
macro that the expansion *defined* is not in it: `Macro.program` expands the whole run again when an expansion leaves
|
||||
a new `defmacro` at the top level, bounded by the same fuel as `settle`.
|
||||
|
||||
`~~n` pastes the *form* `n` holds into the inner template as an expression the inner macro evaluates. There is no
|
||||
`,',n`, because `quote` takes a symbol and not a form, so a literal reaches the inner macro as a `Form` constructor —
|
||||
`(Form.Int {.i 5})` — and not as `5`. A `defmacro` produced by an expansion inside a package is not one of the
|
||||
package's exported macros, because `Load` collects them from the forms as written.
|
||||
|
||||
### A call inside a quasiquote is output, not a dependency
|
||||
|
||||
|
||||
@ -210,10 +210,8 @@ let call ~loc (fn : Dynload.addr) (args : Form.t list) : Form.t =
|
||||
|
||||
The reader stays dumb and produces (quasiquote x), (unquote x) and
|
||||
(unquote-splicing x) with no idea whether one is inside another. Counting
|
||||
levels is this file's job, and it does not: a quasiquote inside a quasiquote
|
||||
is refused by name. A macro that writes a macro is the only thing that wants
|
||||
one, nothing in the corpus does, and CL's level arithmetic has a real cost
|
||||
that no use case has asked for. *)
|
||||
levels is this file's job, done in [quote] below. A macro that writes a
|
||||
macro is the only thing that wants a quasiquote inside a quasiquote. *)
|
||||
|
||||
let sym loc s = Form.make (Form.Sym s) loc
|
||||
let lst loc xs = Form.make (Form.List xs) loc
|
||||
@ -234,43 +232,59 @@ let splice_of (f : Form.t) =
|
||||
| Form.List [ { Form.v = Form.Sym "unquote-splicing"; _ }; x ] -> Some x
|
||||
| _ -> None
|
||||
|
||||
let rec quote (f : Form.t) : Form.t =
|
||||
(* [depth] is how many quasiquotes enclose [f], counting the one being
|
||||
desugared as 1 — SBCL's [*backquote-depth*] in src/code/backq.lisp. A
|
||||
quasiquote inside raises it and an unquote lowers it, so an unquote belongs
|
||||
to the innermost quasiquote around it and [~~x] reaches out two. Only the
|
||||
unquotes at depth 1 are evaluated now; the rest are data, rebuilt as the
|
||||
(unquote x) and (quasiquote x) forms they were read as, for the macro the
|
||||
output defines to desugar in its turn. *)
|
||||
let rec quote ?(depth = 1) (f : Form.t) : Form.t =
|
||||
let loc = f.Form.loc in
|
||||
(* (head x) as a Form, [x] already desugared. *)
|
||||
let wrapped head x =
|
||||
node loc "List" "xs"
|
||||
(lst loc [ sym loc "form-cons"; node loc "Sym" "s" (Form.Str head);
|
||||
lst loc [ sym loc "form-cons"; x; lst loc [ sym loc "form-nil" ] ] ]).Form.v
|
||||
in
|
||||
match unquote_of f with
|
||||
(* The escape: whatever the program wrote, evaluated. It is already a Form,
|
||||
because a Form is what a macro body deals in. *)
|
||||
| Some x -> x
|
||||
| Some x when depth = 1 -> x
|
||||
| Some x -> wrapped "unquote" (quote ~depth:(depth - 1) x)
|
||||
| None ->
|
||||
match splice_of f with
|
||||
| Some _ ->
|
||||
| Some _ when depth = 1 ->
|
||||
Loc.fail loc
|
||||
"~@x splices into a list or a vector, and there is nothing here for it \
|
||||
to splice into"
|
||||
| Some x -> wrapped "unquote-splicing" (quote ~depth:(depth - 1) x)
|
||||
| None ->
|
||||
match f.Form.v with
|
||||
| Form.List ({ Form.v = Form.Sym "quasiquote"; _ } :: _) ->
|
||||
Loc.fail loc
|
||||
"a quasiquote inside a quasiquote is not implemented — build the \
|
||||
inner form with form-cons"
|
||||
| Form.List [ { Form.v = Form.Sym "quasiquote"; _ }; x ] ->
|
||||
wrapped "quasiquote" (quote ~depth:(depth + 1) x)
|
||||
| Form.Sym s -> node loc "Sym" "s" (Form.Str s)
|
||||
| Form.Kw s -> node loc "Kw" "s" (Form.Str s)
|
||||
| Form.Int i | Form.UInt (i, _) -> node loc "Int" "i" (Form.Int i)
|
||||
| Form.Float x -> node loc "Float" "x" (Form.Float x)
|
||||
| Form.Str s -> node loc "Str" "s" (Form.Str s)
|
||||
| Form.Byte b -> node loc "Byte" "b" (Form.Int (Int64.of_int b))
|
||||
| Form.List xs -> node loc "List" "xs" (seq loc xs).Form.v
|
||||
| Form.Vec xs -> node loc "Vec" "xs" (seq loc xs).Form.v
|
||||
| Form.Map xs -> node loc "Map" "xs" (seq loc xs).Form.v
|
||||
| Form.List xs -> node loc "List" "xs" (seq ~depth loc xs).Form.v
|
||||
| Form.Vec xs -> node loc "Vec" "xs" (seq ~depth loc xs).Form.v
|
||||
| Form.Map xs -> node loc "Map" "xs" (seq ~depth loc xs).Form.v
|
||||
|
||||
(* The [Form] slice one bracket's worth of items comes to. Built right to left,
|
||||
so each item is consed onto what follows it and a splice is an append — the
|
||||
three prelude functions and no fourth. *)
|
||||
and seq loc items =
|
||||
three prelude functions and no fourth. A splice deeper than [depth] 1 is
|
||||
data like any other item. *)
|
||||
and seq ~depth loc items =
|
||||
List.fold_left
|
||||
(fun acc (item : Form.t) ->
|
||||
match splice_of item with
|
||||
| Some x -> lst item.Form.loc [ sym item.Form.loc "form-append"; x; acc ]
|
||||
| None -> lst item.Form.loc [ sym item.Form.loc "form-cons"; quote item; acc ])
|
||||
| Some x when depth = 1 ->
|
||||
lst item.Form.loc [ sym item.Form.loc "form-append"; x; acc ]
|
||||
| _ ->
|
||||
lst item.Form.loc [ sym item.Form.loc "form-cons"; quote ~depth item; acc ])
|
||||
(lst loc [ sym loc "form-nil" ])
|
||||
(List.rev items)
|
||||
|
||||
|
||||
28
lib/macro.ml
28
lib/macro.ml
@ -423,6 +423,12 @@ let rec expand_form (l : loaded) (f : Form.t) : Form.t =
|
||||
| _ -> f
|
||||
|
||||
and settle l first loc (f : Form.t) left =
|
||||
(* A macro that writes a macro answers a form with a quasiquote still in it —
|
||||
the inner one, which the outer desugaring kept as data. It is desugared
|
||||
here, before the walk below looks at heads, for the reason the program's
|
||||
own forms are desugared before the first walk: a call written inside a
|
||||
quasiquote is output, not a call. *)
|
||||
let f = Expand.quasiquote f in
|
||||
match f.Form.v with
|
||||
| Form.List ({ Form.v = Form.Sym m; _ } :: args) when List.mem_assoc m l.fns ->
|
||||
if left <= 0 then
|
||||
@ -572,10 +578,28 @@ let with_module (l : loaded) (f : unit -> 'a) : 'a =
|
||||
Dynload.release ())
|
||||
f
|
||||
|
||||
let program (forms : Form.t list) : Form.t list =
|
||||
(* A top-level macro call may answer a [defmacro], and the macro it defines was
|
||||
not in the module the call ran against. So when an expansion leaves the top
|
||||
level declaring a macro it did not declare before, the whole run is expanded
|
||||
again against a module that has it. The forms already expanded have no call
|
||||
left in them, so a second pass only reaches the calls to the new names. A
|
||||
macro that defines a macro whose expansion defines another costs one pass
|
||||
per level, and the fuel is the same bound [settle] uses. *)
|
||||
let rec program_n left (forms : Form.t list) : Form.t list =
|
||||
match loaded_for forms with
|
||||
| None -> forms
|
||||
| Some l -> with_module l (fun () -> List.map (expand_form l) forms)
|
||||
| Some l ->
|
||||
let before = macros_in forms in
|
||||
let out = with_module l (fun () -> List.map (expand_form l) forms) in
|
||||
let fresh = List.filter (fun n -> not (List.mem n before)) (macros_in out) in
|
||||
if fresh = [] then out
|
||||
else if left <= 0 then
|
||||
Loc.fail
|
||||
(List.find (fun f -> macro_name f = Some (List.hd fresh)) out).Form.loc
|
||||
"macros defining macros did not settle after %d levels" fuel
|
||||
else program_n (left - 1) out
|
||||
|
||||
let program (forms : Form.t list) : Form.t list = program_n fuel forms
|
||||
|
||||
let () = Parse.expander := program
|
||||
|
||||
|
||||
44
test/programs/macro-writing.flan
Normal file
44
test/programs/macro-writing.flan
Normal file
@ -0,0 +1,44 @@
|
||||
;;;; Macros that write macros: a quasiquote inside a quasiquote.
|
||||
;;;;
|
||||
;;;; The outer quasiquote is desugared when the file is read, and the inner one
|
||||
;;;; is data until the outer macro runs. What that macro answers is a defmacro
|
||||
;;;; holding the inner quasiquote, which is desugared then, and the macro it
|
||||
;;;; defines is compiled and called like one written by hand.
|
||||
|
||||
;; The inner ~x belongs to the inner quasiquote, so it is the generated
|
||||
;; macro's own parameter and nothing the outer macro evaluates.
|
||||
(defmacro defsquare [name]
|
||||
`(defmacro ~name [x]
|
||||
`(* ~x ~x)))
|
||||
|
||||
;; ~~n reaches out two levels: n is evaluated when defadder runs, and what it
|
||||
;; holds is pasted into the inner template as an expression the generated
|
||||
;; macro evaluates. A Form constructor is such an expression, so the literal
|
||||
;; arrives as one.
|
||||
(defmacro defadder [name n]
|
||||
`(defmacro ~name [x]
|
||||
`(+ ~x ~~n)))
|
||||
|
||||
;; ~@ inside the inner template is the inner macro's splice.
|
||||
(defmacro defsum [name]
|
||||
`(defmacro ~name [& xs]
|
||||
`(+ 0 ~@xs)))
|
||||
|
||||
;; Two levels down: a macro that writes a macro that writes a macro.
|
||||
(defmacro defsquarer [name]
|
||||
`(defmacro ~name [inner]
|
||||
`(defmacro ~inner [x]
|
||||
`(* ~x ~x))))
|
||||
|
||||
(defsquare sq)
|
||||
(defadder add5 (Form.Int {.i 5}))
|
||||
(defsum total)
|
||||
(defsquarer defsq2)
|
||||
(defsq2 sq2)
|
||||
|
||||
(defn main [] i32
|
||||
(print (sq 7)) (println "")
|
||||
(print (add5 10)) (println "")
|
||||
(print (total 1 2 3 4)) (println "")
|
||||
(print (sq2 9)) (println "")
|
||||
0)
|
||||
@ -3984,6 +3984,17 @@ level "1"
|
||||
outputs ~dev:true "a macro's parameter list, dev"
|
||||
"programs/macro-params.flan" macro_params_out;
|
||||
|
||||
(* A quasiquote inside a quasiquote: macros whose expansion is a defmacro,
|
||||
and the macros they define called from main. Two levels deep at the
|
||||
end, which is two re-passes of the expander. *)
|
||||
let macro_writing_out = "49\n15\n10\n81\n" in
|
||||
outputs "a macro that writes a macro" "programs/macro-writing.flan"
|
||||
macro_writing_out;
|
||||
outputs ~opt:"-O0" "a macro that writes a macro, -O0"
|
||||
"programs/macro-writing.flan" macro_writing_out;
|
||||
outputs ~dev:true "a macro that writes a macro, dev"
|
||||
"programs/macro-writing.flan" macro_writing_out;
|
||||
|
||||
(* A gensym drawn in one round's module and one drawn in the module the
|
||||
program is expanded with are different names. 200 is the two colliding. *)
|
||||
outputs "gensym counts across macro modules"
|
||||
|
||||
@ -664,11 +664,30 @@ let () =
|
||||
commonest thing a macro builds and Form.Vec is not Form.List. *)
|
||||
desugars "a quasiquoted vector stays a vector" "`[~x 1]"
|
||||
"(Form.Vec {.xs (form-cons x (form-cons (Form.Int {.i 1}) (form-nil)))})";
|
||||
(* Levels are not counted -- not by the reader, deliberately, and not here,
|
||||
which is why the inner one is refused by name rather than given a meaning
|
||||
nobody chose. *)
|
||||
parse_rejects "a quasiquote inside a quasiquote" "(defn f [] Form `(a `(b)))"
|
||||
~needle:"quasiquote inside a quasiquote";
|
||||
(* Levels are counted here, SBCL's way. The inner quasiquote is data, an
|
||||
unquote belongs to the innermost quasiquote around it, and only the
|
||||
unquotes at depth 1 are evaluated -- everything deeper comes back as the
|
||||
(unquote x) it was read as, for the macro the output defines to desugar. *)
|
||||
let q s = "(Form.Sym {.s \"" ^ s ^ "\"})" in
|
||||
let wrap h x = "(Form.List {.xs (form-cons " ^ q h ^ " (form-cons " ^ x ^ " (form-nil)))})" in
|
||||
let list1 x = "(Form.List {.xs (form-cons " ^ x ^ " (form-nil))})" in
|
||||
desugars "a quasiquote inside a quasiquote is data" "``(b)"
|
||||
(wrap "quasiquote" (list1 (q "b")));
|
||||
desugars "an unquote at depth 2 is data" "``~x"
|
||||
(wrap "quasiquote" (wrap "unquote" (q "x")));
|
||||
desugars "~~x reaches the outer quasiquote" "``~~x"
|
||||
(wrap "quasiquote" (wrap "unquote" "x"));
|
||||
desugars "a splice at depth 2 is one item of data" "``(~@xs)"
|
||||
(wrap "quasiquote" (list1 (wrap "unquote-splicing" (q "xs"))));
|
||||
desugars "~@~xs splices at the inner level what the outer evaluates" "``(~@~xs)"
|
||||
(wrap "quasiquote" (list1 (wrap "unquote-splicing" "xs")));
|
||||
(* Three deep: ~~x under three quasiquotes is still data, one level short. *)
|
||||
desugars "~~x under three quasiquotes is data" "```~~x"
|
||||
(wrap "quasiquote" (wrap "quasiquote" (wrap "unquote" (wrap "unquote" (q "x")))));
|
||||
(* ~ holds one form, so a splice directly inside one at the evaluating level
|
||||
has nothing to splice into. *)
|
||||
parse_rejects "~~@x" "(defn f [] Form ``~~@xs)"
|
||||
~needle:"nothing here for it to splice into";
|
||||
(* Not a missing feature — an unquote outside a quasiquote is a mistake, and
|
||||
the reader cannot catch it because it does not track where it is. *)
|
||||
parse_rejects "unquote outside a quasiquote" "(defn f [] () ~x)"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user