A macro's parameter list, and one grammar for it

(defmacro do-grid [[r rows c cols] & body] ...) — positional names, a [ ]
pattern wherever an argument is a vector, and & for the tail. The reading of
the list lives in Expand, below both sides that need it: Parse turns it into
the bindings a macro body opens with, and Macro checks a call against the same
reading before expanding it, so arity and shape are refused with the call's own
location rather than with the Loc.from_macro stamp every node of an expansion
carries.

The breaking half: [args] used to bind the whole argument list and now binds
the first argument. The whole list is [& args], and every defmacro in the tree
— prelude, vendor, tests, the elisp fixtures — was migrated to it. One grammar,
not a legacy mode.
This commit is contained in:
Joseph Ferano 2026-09-20 18:18:24 +07:00
parent 5afa707d76
commit 69646e534e
26 changed files with 570 additions and 90 deletions

View File

@ -1970,7 +1970,7 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; with the program still on screen.
(flan--request
(list :op "eval" :file file
:code "(defmacro spinner [args] `(spinner ~@args))"))
:code "(defmacro spinner [& args] `(spinner ~@args))"))
(goto-char (point-max))
(insert "\n(defn spun [] i32\n (spinner 1))\n")
(goto-char (point-max))
@ -2005,10 +2005,10 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; only shape where expanding in place has anything to do.
(flan--request
(list :op "eval" :file file
:code "(defmacro m-inner [args] `(+ ~(at args 0) 1))"))
:code "(defmacro m-inner [& args] `(+ ~(at args 0) 1))"))
(flan--request
(list :op "eval" :file file
:code "(defmacro m-outer [args] `(m-inner ~(at args 0)))"))
:code "(defmacro m-outer [& args] `(m-inner ~(at args 0)))"))
(goto-char (point-max))
(insert "\n(defn outered [] i32 (m-outer 5))\n")
(goto-char (point-max))

View File

@ -227,3 +227,174 @@ let rec quasiquote (f : Form.t) : Form.t =
| Form.Vec xs -> Form.make (Form.Vec (List.map quasiquote xs)) f.Form.loc
| Form.Map xs -> Form.make (Form.Map (List.map quasiquote xs)) f.Form.loc
| _ -> f
(* ── A macro's parameter list ──────────────────────────────────────
[(defmacro do-grid [[r rows c cols] & body] ...)] positional parameters,
a destructuring vector wherever one is written, and [&] for the tail. The
grammar is [dvec]'s (lib/parse.ml), read over [Form] instead of over the
values a [let] binds, because a macro's arguments *are* Forms.
It lives here rather than in [Parse] because both sides of the feature need
it and they are on opposite sides of the parser: [Parse] turns the list into
the bindings a macro body opens with, and [Macro] checks a call against it
before the macro is ever run. This file is below both and depends on nothing
above [Form], which is what lets them share one reading of the list.
Map destructuring is not here. [dmap] is [{:keys [x y]}] over a *struct*, and
a macro's argument is a [Form] whose [Map] case is a flat list of alternating
forms with no field names in it at all the pattern would have to mean
something new rather than the same thing over a different value. Refused by
name below, and written down in FIX.org. *)
type pat =
| Pname of string * Loc.t
(* A [ ] in the parameter list: the argument at this position must be a
[Form.Vec], and its elements are matched against these in turn. *)
(* The [Form] is the pattern as written: a refusal shows the shape the call
failed to match, and nothing else can render it back. *)
| Pvec of pat list * (string * Loc.t) option * Form.t
type msig = {
ps : pat list; (* the positional parameters, in order *)
rest : (string * Loc.t) option; (* [& name], if there is one *)
src : Form.t; (* the list as written, for the messages *)
}
(* [a b & rest], shared by the top level and by every destructuring vector
inside it. The three refusals are [dvec]'s, word for word where they say the
same thing: one grammar, so one set of sentences about getting it wrong. *)
let split_amp (items : Form.t list) : Form.t list * (string * Loc.t) option =
let rec go acc = function
| [] -> (List.rev acc, None)
| ({ Form.v = Form.Sym "&"; _ } as amp) :: rest ->
(match rest with
| [ { Form.v = Form.Sym r; loc } ] -> (List.rev acc, Some (r, loc))
| [] -> Loc.fail amp.Form.loc "& needs a name after it, as in [a b & rest]"
| [ bad ] ->
Loc.fail bad.Form.loc
"& binds one name for the rest of the arguments, and %s is not one \
the rest is a slice of forms, so it cannot be destructured \
further"
(Form.to_string bad)
| _ :: extra :: _ ->
Loc.fail extra.Form.loc
"& takes one name and it is the last thing in the parameter list")
| x :: rest -> go (x :: acc) rest
in
go [] items
let rec pat_of (f : Form.t) : pat =
match f.Form.v with
| Form.Sym "&" ->
(* Only reachable inside a [ ] that [split_amp] already walked, so a second
[&] is the one that has no name of its own to go with. *)
Loc.fail f.Form.loc "& appears twice in this parameter list"
| Form.Sym s -> Pname (s, f.Form.loc)
| Form.Vec items ->
let elems, rest = split_amp items in
(match elems, rest with
| [], None ->
Loc.fail f.Form.loc
"an empty pattern [] in a macro's parameter list binds nothing — \
write the names it should bind"
| [], Some (r, loc) ->
Loc.fail loc
"[& %s] binds the whole vector — write %s on its own instead of a \
pattern" r r
| _ -> ());
Pvec (List.map pat_of elems, rest, f)
| Form.Map _ ->
Loc.fail f.Form.loc
"map destructuring is not implemented in a macro's parameter list — a \
macro's argument is a Form, whose Map case is a flat run of alternating \
forms with no fields to name. Take the form and pick it apart in the \
body"
| _ ->
Loc.fail f.Form.loc
"a macro's parameter is a name or a [ ] pattern over one, and %s is \
neither"
(Form.to_string f)
(* Every name the list binds, so that two of them can be refused where they are
written rather than reaching the checker as a local declared twice. *)
let rec pat_names acc = function
| Pname (s, loc) -> (s, loc) :: acc
| Pvec (ps, rest, _) ->
let acc = List.fold_left pat_names acc ps in
(match rest with None -> acc | Some nl -> nl :: acc)
let params_of (v : Form.t) : msig =
let items = match v.Form.v with
| Form.Vec items -> items
| _ ->
Loc.fail v.Form.loc "a macro's parameter list is written in [ ], and %s is not"
(Form.to_string v)
in
let elems, rest = split_amp items in
let sg = { ps = List.map pat_of elems; rest; src = v } in
let names = List.fold_left pat_names [] sg.ps in
let names =
match sg.rest with None -> names | Some nl -> nl :: names in
let seen = Hashtbl.create 8 in
List.iter
(fun (n, loc) ->
if Hashtbl.mem seen n then
Loc.fail loc "%s is bound twice in this parameter list" n
else Hashtbl.add seen n ())
(List.rev names);
sg
(* ── Checking a call against it ────────────────────────────────────
Before expansion, so the location is the call's own and not the
[Loc.from_macro] stamp every node of an expansion carries. That is the whole
reason this is a separate pass rather than something the macro body could
do: a macro has no error facility, and by the time its body runs the only
location left is the one it was called from anyway stamped onto forms the
author never wrote. *)
let written (sg : msig) = Form.to_string sg.src
let arity (sg : msig) ~name ~loc (args : Form.t list) =
let n = List.length sg.ps in
let got = List.length args in
let plural k = if k = 1 then "argument" else "arguments" in
match sg.rest with
| Some (r, _) when got < n ->
Loc.fail loc
"%s takes at least %d %s and this call gives %d — its parameter list is \
%s, where &%s is the rest"
name n (plural n) got (written sg) r
| Some _ -> ()
| None when got <> n ->
Loc.fail loc
"%s takes %d %s and this call gives %d — its parameter list is %s"
name n (plural n) got (written sg)
| None -> ()
let rec check_pat ~name (p : pat) (a : Form.t) =
match p with
| Pname _ -> ()
| Pvec (ps, rest, src) ->
let items =
match a.Form.v with
| Form.Vec items -> items
| _ ->
Loc.fail a.Form.loc
"%s destructures this argument with %s, so a [ ] belongs here and \
%s was written"
name (Form.to_string src) (Form.to_string a)
in
let n = List.length ps in
let got = List.length items in
if (rest = None && got <> n) || got < n then
Loc.fail a.Form.loc
"%s destructures this argument with %s, which takes %s%d, and %d %s \
written here"
name (Form.to_string src)
(if rest = None then "" else "at least ")
n got (if got = 1 then "is" else "are");
List.iteri (fun i q -> check_pat ~name q (List.nth items i)) ps
let check_call ~name ~loc (sg : msig) (args : Form.t list) =
arity sg ~name ~loc args;
List.iteri (fun i p -> check_pat ~name p (List.nth args i)) sg.ps

View File

@ -53,8 +53,27 @@ let rec names_macro (known : string list) (f : Form.t) =
type loaded = {
handle : Dynload.handle;
fns : (string * Dynload.addr) list;
(* Every macro's parameter list as [Expand] read it, so that a call can be
checked against it *before* it is expanded. That ordering is the whole
point: the refusal then carries the call's own location, where an error
raised from inside a macro body would carry [Loc.from_macro]'s stamp on a
form the author never wrote. *)
sigs : (string * Expand.msig) list;
}
(* The parameter list of every [defmacro] in a run of forms. The prelude's are
read from the prelude itself, since its macros are compiled into every
module without ever appearing in [extra]. *)
let sigs_in (forms : Form.t list) : (string * Expand.msig) list =
List.filter_map
(fun (f : Form.t) ->
match f.Form.v with
| Form.List ({ Form.v = Form.Sym "defmacro"; _ }
:: { Form.v = Form.Sym n; _ } :: ps :: _ :: _) ->
Some (n, Expand.params_of ps)
| _ -> None)
forms
(* This compiler's own identity, and it belongs in the key for a reason the
other caches do not have. A [.o] under the object cache is decided entirely
by the C text and the C compiler that made it, so its key is total without
@ -294,7 +313,11 @@ let compile (names : string list) (extra : Form.t list) : loaded =
end;
let handle = Dynload.dl_open out in
{ handle;
fns = List.map (fun n -> (n, Dynload.dl_sym handle (Mangle.macro n))) names }
fns = List.map (fun n -> (n, Dynload.dl_sym handle (Mangle.macro n))) names;
(* [extra] is the file's own macros and an import's; the prelude's are only
ever in the prelude. A name in both is the file's, which is the same
shadowing [loaded_for] applies to the forms themselves. *)
sigs = sigs_in extra @ sigs_in (Prelude.forms ()) }
(* ── Where the call site is ────────────────────────────────────────
The one thing a macro cannot find out for itself and the one it needs to
@ -344,11 +367,27 @@ let dir_of (l : loaded) (loc : Loc.t) =
expanded again, because a macro that expands into a call to itself which
is what a recursive [cond] is has to keep going.
That re-expansion is what needs a bound. [(defmacro loop [args] `(loop))]
That re-expansion is what needs a bound. [(defmacro loop [& args] `(loop))]
settles at nothing, and the honest answer to a macro that will not settle is
to say which one it was, at the call site, rather than to run out of
memory. *)
(* Every call goes through here, and there are four ways in: the walk below,
[settle]'s re-expansion of what a macro answered, and the editor's
[expand_step] and [expand_all]. One place, so [C-c C-m] refuses exactly what
a build refuses.
[List.assoc_opt] rather than [List.assoc]: a macro compiled into the module
always has a signature, and the one thing that could put a name in [fns]
without one is the two lists coming apart in which case expanding
unchecked is the wrong half to lose. *)
let checked_call (l : loaded) n ~loc (args : Form.t list) : Form.t =
(match List.assoc_opt n l.sigs with
| Some sg -> Expand.check_call ~name:n ~loc sg args
| None -> ());
dir_of l loc;
Expand.call ~loc:(Loc.from_macro n loc) (List.assoc n l.fns) args
let fuel = 200
let rec expand_form (l : loaded) (f : Form.t) : Form.t =
@ -357,12 +396,11 @@ let rec expand_form (l : loaded) (f : Form.t) : Form.t =
| Form.List ({ Form.v = Form.Sym n; _ } :: args) when List.mem_assoc n l.fns ->
let args = List.map (expand_form l) args in
(* The call site, tagged with the macro it is a call to. [Expand.unmarshal]
stamps this onto every node the macro answers with, so from here down
stamps it onto every node the macro answers with, so from here down
every form it produced knows where it came from and an error on one of
them can say so. *)
let from = Loc.from_macro n loc in
dir_of l loc;
settle l n loc (Expand.call ~loc:from (List.assoc n l.fns) args) fuel
them can say so. [checked_call] is where that tagging happens, along
with the arity and destructuring check that has to come first. *)
settle l n loc (checked_call l n ~loc args) fuel
| Form.List xs -> Form.make (Form.List (List.map (expand_form l) xs)) loc
| Form.Vec xs -> Form.make (Form.Vec (List.map (expand_form l) xs)) loc
| Form.Map xs -> Form.make (Form.Map (List.map (expand_form l) xs)) loc
@ -379,10 +417,7 @@ and settle l first loc (f : Form.t) left =
first fuel
else begin
let args = List.map (expand_form l) args in
let from = Loc.from_macro m loc in
dir_of l loc;
settle l first loc (Expand.call ~loc:from (List.assoc m l.fns) args)
(left - 1)
settle l first loc (checked_call l m ~loc args) (left - 1)
end
(* Settled at the head. The rest of it may still hold macro calls — a cond
expands to an if whose else-branch is another cond so the ordinary walk
@ -570,10 +605,7 @@ let expand_step (f : Form.t) : Form.t * string option =
(* [C-c C-m] over a type provider reads the data file, which is the
whole of what makes the live loop live: edit the .edn, expand
again, see the struct that file now implies. *)
dir_of l f.Form.loc;
( Expand.call ~loc:(Loc.from_macro n f.Form.loc) (List.assoc n l.fns)
args,
Some n )
(checked_call l n ~loc:f.Form.loc args, Some n)
| _ -> (f, None))
(** To the fixpoint, through exactly the walk a build goes through — so the

View File

@ -30,6 +30,13 @@ let temps = ref 0
let fresh_temp what = incr temps; Printf.sprintf "%s~%d" what !temps
(* The one parameter every macro is compiled with, whatever its author wrote as
a parameter list: the slice of forms at the call site, which the bindings
[macro_body] generates read out of. A [~] in it for the same reason
[fresh_temp] puts one there the reader cannot produce the character in a
symbol, so nothing an author writes collides with it. *)
let macro_args = "macro~args"
(* Destructuring binds in [let] and nowhere else. Every other binding position —
a [defn] parameter, a [defstruct] field, an [fn] parameter, a [dotimes]
counter, a [match] arm's binds takes a plain name, and a pattern written
@ -1516,42 +1523,46 @@ let rec decl (f : Form.t) : Ast.decl =
| _ -> fail f "defconst is (defconst name Type? value)")
(* A macro is an ordinary function, and this is where it becomes one:
[(defmacro m [args] body)] is [(defn m [args [Form]] Form body)]. There is
no [Ast.Defmacro] and there is not going to be one -- a macro has the type
[[Form] -> Form], it is compiled by the same backend as everything else,
and the only thing that makes it a macro is that [Expand] calls it at
compile time instead of the program calling it at run time.
[(defmacro m [a b & body] ...)] is [(defn m [macro~args [Form]] Form (let
[a (at macro~args 0) b (at macro~args 1) body (form-rest macro~args 2)]
...))]. There is no [Ast.Defmacro] and there is not going to be one -- a
macro has the type [[Form] -> Form], it is compiled by the same backend as
everything else, and the only thing that makes it a macro is that [Expand]
calls it at compile time instead of the program calling it at run time.
One parameter, the slice of the argument forms, rather than one declared
parameter per argument. It needs no reader or parser change and it gives
variadics for free, which is what [unless] and [when] need in a language
with no &rest.
So the declared type is what it always was: one parameter, the slice of
the argument forms. What changed is that the parameter is the compiler's
now and the author writes a real list against it positional names, a
[ ] pattern wherever an argument is a vector, and [&] for the tail which
opens the body as bindings over that slice. [Expand]'s [msig] is the
reading of the list, and [Macro] checks a *call* against the same reading
before expanding it, which is where arity and shape are refused with the
call's own location.
The shape rules stay exactly as they were, because they were enforced
before the feature existed on purpose: getting the shape wrong and getting
the whole feature are different mistakes. *)
The one breaking change in this: [[args]] used to bind the whole argument
list and now binds the first argument, because one grammar that means one
thing everywhere is worth more than a legacy spelling. The whole list is
[[& args]], and every macro in the tree was migrated to it. *)
| List ({ v = Sym "defmacro"; _ } :: args) ->
(match args with
| n :: { v = Form.Vec [ p ]; _ } :: body when body <> [] ->
| n :: ({ v = Form.Vec _; _ } as ps) :: body when body <> [] ->
let sg = Expand.params_of ps in
let form_t = { Ast.t = Ast.Tname "Form"; tloc = f.loc } in
mk (Ast.Defn
{ Ast.name = sym n;
params = [ { Ast.fname = sym p;
fty = { Ast.t = Ast.Tslice form_t; tloc = p.loc };
floc = p.loc } ];
(* A name the reader cannot produce -- [~] opens an unquote, so
no symbol read out of a source file holds one -- which is
what keeps the compiler's own parameter out of the way of
every name the author might bind. Same trick as [gensym]. *)
params = [ { Ast.fname = macro_args;
fty = { Ast.t = Ast.Tslice form_t; tloc = ps.loc };
floc = ps.loc } ];
(* Written out, not deferred: a macro takes [[Form]] and
returns a [Form], and neither half of that is the user's to
leave off. *)
praw = None;
ret = Some form_t; fwhere = []; fbody = body_of body;
ret = Some form_t; fwhere = []; fbody = macro_body sg body;
nloc = n.loc })
| _ :: { v = Form.Vec ps; _ } :: body when body <> [] ->
List.iter (fun (p : Form.t) -> ignore (sym p)) ps;
fail f
"a macro takes one parameter, the forms at its call site, and this \
one names %d. There is no &rest and no arity: (defmacro m [args] \
...) and (len args) is how many were written"
(List.length ps)
| _ ->
fail f "defmacro is (defmacro name [param ...] body ...)")
@ -1567,6 +1578,57 @@ let rec decl (f : Form.t) : Ast.decl =
| List ({ v = Sym s; _ } :: _) -> fail f "unknown top-level form (%s ...)" s
| _ -> fail f "expected a top-level declaration, found %s" (Form.to_string f)
(* The bindings a macro body opens with, one per name its parameter list binds,
in the order they are written. Built as [Form]s and handed to [expr] rather
than assembled as [Ast] directly: the extraction is [(at ...)] and
[(form-rest ...)] over a slice and [(let ...)] around the body, which is
ordinary Flan and already has a parser. Nothing downstream learns that a
macro had a parameter list, exactly as nothing downstream learns that a
[let] had a pattern.
The extraction is unchecked on purpose. [Macro] has already run
[Expand.check_call] over this call by the time the body runs, so an [(at
macro~args 2)] here is an index that was counted, and a
[(form-vec-items ...)] is a form already known to be a [Form.Vec]. Checking
twice would mean a second set of sentences, said from inside an expansion
where the location is the call site's stamp rather than the call. *)
and macro_body (sg : Expand.msig) (body : Form.t list) : Ast.expr list =
let loc0 = sg.Expand.src.Form.loc in
let s loc n : Form.t = Form.make (Form.Sym n) loc in
let call loc xs : Form.t = Form.make (Form.List xs) loc in
let idx loc i : Form.t = Form.make (Form.Int (Int64.of_int i)) loc in
let nth loc src i = call loc [ s loc "at"; src; idx loc i ] in
let tail loc src i = call loc [ s loc "form-rest"; src; idx loc i ] in
let out = ref [] in
let add n v = out := (n, v) :: !out in
let rec go (p : Expand.pat) (src : Form.t) =
match p with
| Expand.Pname (n, loc) -> add (s loc n) src
| Expand.Pvec (ps, rest, pf) ->
let loc = pf.Form.loc in
(* The elements of the vector, bound once: every name under this pattern
reads that one slice rather than unwrapping the form again. *)
let t = fresh_temp "macro" in
add (s loc t) (call loc [ s loc "form-vec-items"; src ]);
List.iteri (fun i q -> go q (nth loc (s loc t) i)) ps;
(match rest with
| None -> ()
| Some (r, rl) -> add (s rl r) (tail rl (s loc t) (List.length ps)))
in
let av = s loc0 macro_args in
List.iteri (fun i p -> go p (nth loc0 av i)) sg.Expand.ps;
(match sg.Expand.rest with
| None -> ()
| Some (r, rl) -> add (s rl r) (tail rl av (List.length sg.Expand.ps)));
match List.rev !out with
(* [(defmacro m [] ...)] binds nothing, and [(let [] ...)] is refused a few
hundred lines up. The body is the body. *)
| [] -> body_of body
| bs ->
let items = List.concat_map (fun (n, v) -> [ n; v ]) bs in
body_of
[ call loc0 (s loc0 "let" :: Form.make (Form.Vec items) loc0 :: body) ]
and variant (f : Form.t) : Ast.variant =
match f.v with
| Sym n -> { Ast.vname = n; vfields = []; vloc = f.loc }

View File

@ -588,7 +588,7 @@ let source = {flan|
;; put a complaint: a macro has no error facility (see `unless` at the foot of
;; this file), so a diagnostic would have to be a run-time one, in the one
;; construct whose whole point is that it costs nothing at run time.
(defmacro clamp [args]
(defmacro clamp [& args]
(if (!= (len args) 3)
`(clamp-takes-a-value-a-low-and-a-high)
`(min ~(at args 2) (max ~(at args 1) ~(at args 0)))))
@ -1919,6 +1919,16 @@ let source = {flan|
(set i (+ i 1)))
(as-slice v)))
;; The elements of a vector form, which is what a [ ] pattern in a macro's
;; parameter list unwraps. The other arm is unreachable from a generated
;; binding -- lib/expand.ml's check_call refuses a non-vector argument at the
;; call site, before the macro runs -- and is here because a macro picking a
;; form apart by hand has the same question and no such guarantee.
(defn form-vec-items [f Form] [Form]
(match f
(Form.Vec xs) xs
_ (form-nil)))
;; A name no reader can produce. `~` is a delimiter now (it opens an unquote),
;; so no symbol coming out of read_all can contain one, and a gensym therefore
;; cannot collide with a name someone wrote. Non-hygienic expansion with an
@ -1960,7 +1970,7 @@ let source = {flan|
;; nothing defines, and the report is "unknown name unless-takes-a-test-and-a-
;; body" at the call site, which is the right place and the wrong sentence.
;; That is the next thing a macro needs and it is written down in NEXT.md.
(defmacro unless [args]
(defmacro unless [& args]
(if (< (len args) 2)
`(unless-takes-a-test-and-a-body)
`(if (not ~(at args 0)) (do ~@(form-rest args 1)))))
@ -2071,7 +2081,7 @@ let source = {flan|
;; what was written. len and at borrow, so used directly the source is only
;; read. A source that is a call and produces a Vec is still consumed, which is
;; right: nobody else is holding it.
(defmacro into [args]
(defmacro into [& args]
(if (< (len args) 2)
`(into-takes-a-source-a-destination-and-transforms)
(let [from (at args 0)

View File

@ -0,0 +1,9 @@
;;;; Too many arguments, which is the half a & would have allowed. There is no
;;;; & in this list, so the count is exact and a third argument has nowhere to
;;;; go.
(defmacro pair [a b]
`(+ ~a ~b))
(defn main [] i32
(print (pair 1 2 3))
0)

View File

@ -0,0 +1,12 @@
;;;; Too few arguments for the macro's parameter list, refused at the call.
;;;;
;;;; Nothing here is a run-time claim and nothing here expands: check_call
;;;; counts the call against the list before do-grid is ever run, so the
;;;; location is the line below rather than a node of an expansion stamped
;;;; with Loc.from_macro.
(defmacro do-grid [[r rows c cols] & body]
`(dotimes [~r ~rows] (dotimes [~c ~cols] ~@body)))
(defn main [] i32
(do-grid)
0)

View File

@ -10,10 +10,10 @@
;;;; to exist first -- see macro-spin.flan, which is bounded rather than
;;;; refused.
(defmacro ping [args]
(defmacro ping [& args]
(pong args))
(defmacro pong [args]
(defmacro pong [& args]
(ping args))
(defn main [] i32

View File

@ -0,0 +1,7 @@
;;;; A vector of the wrong length for the pattern. Four names, three forms.
(defmacro do-grid [[r rows c cols] & body]
`(dotimes [~r ~rows] (dotimes [~c ~cols] ~@body)))
(defn main [] i32
(do-grid [i 2 j] (println "never"))
0)

View File

@ -0,0 +1,9 @@
;;;; A [ ] pattern meeting an argument that is not a vector. The pattern says
;;;; what the call has to look like, so this is refused where the argument is
;;;; written rather than inside an expansion that read (at ... 0) of a Sym.
(defmacro do-grid [[r rows c cols] & body]
`(dotimes [~r ~rows] (dotimes [~c ~cols] ~@body)))
(defn main [] i32
(do-grid 7 (println "never"))
0)

View File

@ -0,0 +1,71 @@
;;;; A macro's parameter list: positional names, [ ] patterns, and &.
;;;;
;;;; macros.flan is the other half of this and is deliberately not merged with
;;;; it: everything there is written [& args] and picks its arguments apart by
;;;; hand, which is what every macro in the tree looked like before this. Here
;;;; the parameter list does the picking, and the two files together are the
;;;; claim that both spellings are the same grammar rather than two.
;;;;
;;;; Nothing below checks its own arity. It cannot be reached with the wrong
;;;; one: lib/expand.ml's check_call runs over the call *before* the macro is
;;;; expanded, so a miscount is refused at the call with the call's own
;;;; location — see macro-arity.flan and the three beside it.
;; The shape the feature was asked for (DISCUSS.org): a binding vector
;; destructured in the signature, and & for the body. Without a parameter list
;; this is (at args 0), a match on Form.Vec to unwrap it, four more (at ...)
;; inside that, and (form-rest args 1) for the body.
(defmacro do-grid [[r rows c cols] & body]
`(dotimes [~r ~rows]
(dotimes [~c ~cols]
~@body)))
;; One positional parameter, which is where the grammar changed: [x] used to
;; bind the whole argument list and now binds the first argument. The gensym is
;; the ordinary reason it is there — expansion is not hygienic — and not
;; anything to do with the parameter list.
(defmacro doubled [x]
(let [v (gensym)]
`(let [~v ~x] (+ ~v ~v))))
;; Patterns nest, because a pattern's elements are patterns. And & is not only
;; the top level's: the tail of a pattern is the tail of that vector.
(defmacro nested [[a [b c]] & body]
`(do (print ~a) (print ~b) (print ~c) ~@body))
;; & inside a pattern, which is the same & and means the same thing one level
;; down: the tail of the vector written at the call.
(defmacro first-of [[a & more]]
`(do (print ~a) ~@more))
;; & with nothing after it at the call: the rest is an empty slice, ~@ splices
;; nothing, and the expansion is the wrapper alone. The arity check says "at
;; least 1" and one is what this is given.
(defmacro shout [label & body]
`(do (print ~label) ~@body (println "!")))
;; The whole argument list, which is what [args] used to mean and is now spelled
;; [& args]. Every macro in the tree was migrated to this line, so it is the
;; one that has to keep working unchanged.
(defmacro all-of [& args]
(if (= (len args) 0)
`true
`(if ~(at args 0) (all-of ~@(form-rest args 1)) false)))
(defn main [] i32
(do-grid [i 2 j 3]
(print i) (print j))
(println "")
(print (doubled 21)) (println "")
(nested [1 [2 3]] (println " nested"))
(first-of [4 (print " and") (println " more")])
(shout "alone")
(shout "with" (print " body"))
(print (all-of)) (print " ")
(print (all-of true true true)) (print " ")
(print (all-of true false true)) (println "")
0)

View File

@ -3,7 +3,7 @@
;;;; that does not terminate, so it is bounded and the bound says which macro
;;;; ran out rather than the compiler running out of memory.
(defmacro spin [args]
(defmacro spin [& args]
`(spin ~@args))
(defn main [] i32

View File

@ -11,12 +11,12 @@
;; The simplest one there is: two forms, in order. It proves the call site's
;; arguments arrive as forms and come back as code.
(defmacro both [args]
(defmacro both [& args]
`(do ~(at args 0) ~(at args 1)))
;; Splicing, which is the only reason ~@ exists: the body is however many forms
;; were written, and they go where a list is expected.
(defmacro when2 [args]
(defmacro when2 [& args]
`(if ~(at args 0) (do ~@(form-rest args 1))))
;; Expansion is not hygienic -- Common Lisp's rule and Clojure's, settled in
@ -27,7 +27,7 @@
;;
;; Without this, `twice` would bind `tmp` and the caller's own `tmp` would be
;; shadowed inside it. The two calls below are the difference.
(defmacro twice [args]
(defmacro twice [& args]
(let [v (gensym)]
`(let [~v ~(at args 0)]
(+ ~v ~v))))
@ -36,7 +36,7 @@
;; nothing: `both` is inside the quasiquote, so it is part of what this macro
;; *returns* and is expanded again after it returns, and `announce` can be
;; compiled without `both` existing.
(defmacro announce [args]
(defmacro announce [& args]
`(both (print "-> ") ~(at args 0)))
;; This is the one that makes the pre-pass a fixpoint rather than a sweep. The
@ -45,16 +45,16 @@
;; and until it is, `id` is a name nothing defines and this body will not
;; compile at all. So round 0 takes `id`, round 1 expands this against it, and
;; the module that finally answers a call holds both.
(defmacro id [args]
(defmacro id [& args]
(at args 0))
(defmacro quiet [args]
(defmacro quiet [& args]
(id `(println "a macro that called a macro")))
;; And a macro that expands into a call to itself, which is what every
;; conditional macro in every Lisp is. It gets smaller each time and stops at
;; the empty case, so the expander's fuel never comes into it.
(defmacro all-of [args]
(defmacro all-of [& args]
(if (= (len args) 0)
`true
`(if ~(at args 0) (all-of ~@(form-rest args 1)) false)))

View File

@ -16,9 +16,17 @@
(import mac "pkgs/mac")
;; A macro of the program's own, coexisting with the package's.
(defmacro tenfold [args]
(defmacro tenfold [& args]
`(* ~(at args 0) 10))
;; The same macro again, written with a parameter list instead of by hand.
;; Nothing calls it: it is here for the equivalence case in test_session, which
;; expands (tenfold 7) and (tenfold-listed 7) and requires the same text out of
;; both. [& args] and [n] are one grammar, and this is where that is asserted
;; rather than assumed.
(defmacro tenfold-listed [n]
`(* ~n 10))
(defn show [n i32] () (print n) (println ""))
(defn main [] i32

View File

@ -11,7 +11,7 @@
(defn double [n i32] i32 (* n 2))
;; The plain case: one macro, nothing else needed to compile it.
(defmacro twice [args]
(defmacro twice [& args]
`(+ ~(at args 0) ~(at args 0)))
;; A macro that quasiquotes a call to another macro of this package. That is
@ -19,30 +19,30 @@
;; macro answers and is expanded again after it returns -- so it needs nothing
;; compiled first. What it does need is the name coming out qualified, because
;; the answer lands in the importer's file, where [twice] is not a name.
(defmacro quad [args]
(defmacro quad [& args]
`(twice (twice ~(at args 0))))
;; And one whose output names a *function* of this package, which has the same
;; problem and the same answer.
(defmacro doubled [args]
(defmacro doubled [& args]
`(double ~(at args 0)))
;; [wrap] takes a form-valued expression and answers one, so it is a macro
;; another macro's *body* can call for real.
(defmacro wrap [args]
(defmacro wrap [& args]
`(do ~(at args 0)))
;; A macro that really calls another, outside a quasiquote. This one *is* a
;; compile-order dependency: [wrap] has to be compiled and loaded before this
;; body will compile at all, which is what the rounds in [Macro] are for, and
;; it is the case a quasiquoted call deliberately is not.
(defmacro also-twice [args]
(defmacro also-twice [& args]
(wrap `(+ ~(at args 0) ~(at args 0))))
;; A shadowing local named like a top-level of this package. The rename must
;; leave it alone, or the expansion would name [mac/double] where the author
;; wrote a let binding.
(defmacro shadowed [args]
(defmacro shadowed [& args]
`(let [double ~(at args 0)]
(+ double 1)))

View File

@ -3,8 +3,8 @@
;;;; the same rounds as the file's own, so the refusal has to fire here too.
;;;; Nothing in this package calls them, so the ring is found by the importer.
(defmacro ping [args]
(defmacro ping [& args]
(pong args))
(defmacro pong [args]
(defmacro pong [& args]
(ping args))

View File

@ -4,5 +4,5 @@
;;;; site. The name in that message is the qualified one, because that is what
;;;; the importer wrote.
(defmacro spin [args]
(defmacro spin [& args]
`(spin ~@args))

View File

@ -30,7 +30,7 @@
;;; declares it. Nothing in this program names it, so no macro module is built
;;; for the build itself -- the first one is paid by the evaluation that calls
;;; it.
(defmacro tenfold [args]
(defmacro tenfold [& args]
`(* ~(at args 0) 10))
(defn main [] i32

View File

@ -3345,6 +3345,32 @@ level "1"
outputs ~opt:"-O0" "macros, -O0" "programs/macros.flan" macros_out;
outputs ~dev:true "macros, dev" "programs/macros.flan" macros_out;
(* The same feature written the other way round: a real parameter list on
the defmacro, so the arguments are picked apart by the signature rather
than by hand. macros.flan above is every macro in the tree as it was
written before this [& args] and (at args 0) and both files are here
because both spellings are one grammar: [& args] is the trivial case of
the list, not a legacy mode kept alive beside it.
Three opt levels for the reason macros.flan has them, and the dev row
because the dev path is this project's priority. *)
let macro_params_out =
"000102101112
42
123 nested
4 and more
alone!
with body!
true true false
"
in
outputs "a macro's parameter list" "programs/macro-params.flan"
macro_params_out;
outputs ~opt:"-O0" "a macro's parameter list, -O0"
"programs/macro-params.flan" macro_params_out;
outputs ~dev:true "a macro's parameter list, dev"
"programs/macro-params.flan" macro_params_out;
(* A macro declared in an imported *package*, which is the half the
refusal at [a package's macro is not visible unqualified] above leaves
out. The program calls six of them qualified and one of its own
@ -3469,6 +3495,23 @@ level "1"
is an ordinary loop and it is bounded. *)
refuses "a ring of macros" "programs/macro-cycle.flan"
"none can be compiled first";
(* A call that does not fit the macro's parameter list, in all four of the
ways it can fail to. Every one of them is refused *before* the macro is
expanded, which is why each message carries the call's own location
rather than the [Loc.from_macro] stamp every node of an expansion gets
that stamping is a documented limitation waiting on the structured-error
rewrite, and these four are the part of it that does not have to wait. *)
refuses "a macro call with too few arguments" "programs/macro-arity.flan"
"do-grid takes at least 1 argument and this call gives 0 — its parameter list is [[r rows c cols] & body], where &body is the rest";
refuses "a macro call with too many arguments"
"programs/macro-arity-extra.flan"
"pair takes 2 arguments and this call gives 3 — its parameter list is [a b]";
refuses "a destructuring parameter meeting a form that is not a vector"
"programs/macro-destructure.flan"
"do-grid destructures this argument with [r rows c cols], so a [ ] belongs here and 7 was written";
refuses "a destructuring parameter meeting a vector of the wrong length"
"programs/macro-destructure-arity.flan"
"do-grid destructures this argument with [r rows c cols], which takes 4, and 3 are written here";
refuses "a macro that does not settle" "programs/macro-spin.flan"
"did not settle after";

View File

@ -3928,7 +3928,7 @@ let () =
is also the case NEXT.md describes literally: a macro whose module
does not build. *)
let macro_defn =
"(defmacro plusone [args] `(+ ~(at args 0) 1)) \
"(defmacro plusone [& args] `(+ ~(at args 0) 1)) \
(defn probe-one [] i64 (plusone 41))"
in
let before = knows () in

View File

@ -479,24 +479,46 @@ let () =
be one: a macro is [Form] -> Form, compiled by the same backend as
everything else, and what makes it a macro is that the expander calls it
at compile time rather than the program calling it at run time. *)
(match (parse_decl "(defmacro m [args] (at args 0))").d with
(match (parse_decl "(defmacro m [& args] (at args 0))").d with
| Defn { name = "m"; params = [ p ]; ret = Some r; _ } ->
(match p.fty.t, r.t with
| Tslice { t = Tname "Form"; _ }, Tname "Form" -> ()
| _ -> check "defmacro is [Form] -> Form" false)
| _ -> check "defmacro parses as a defn" false);
(* One parameter, the forms at the call site. Two is not an arity mistake, it
is a misunderstanding of what a macro takes, and it gets its own reason. *)
parse_rejects "defmacro with two parameters" "(defmacro m [a b] a)"
~needle:"a macro takes one parameter";
(* The declared type is the same whatever the author wrote as a parameter
list: the list is bindings over the one slice, opened by [macro_body], and
nothing below the parser learns there was a list at all. *)
(match (parse_decl "(defmacro m [[a b] c & rest] (at rest 0))").d with
| Defn { name = "m"; params = [ p ]; ret = Some r; _ } ->
(match p.fty.t, r.t with
| Tslice { t = Tname "Form"; _ }, Tname "Form" -> ()
| _ -> check "a parameter list is still [Form] -> Form" false)
| _ -> check "a macro with a parameter list parses as a defn" false);
(* Several parameters is the feature now. What is still refused is a list
that cannot be read: [&] with nothing or too much after it, a pattern that
binds nothing, a name bound twice, and a map pattern which is deferred
rather than unimplemented by accident, see FIX.org. *)
parse_rejects "defmacro with a dangling &" "(defmacro m [a &] a)"
~needle:"& needs a name after it";
parse_rejects "defmacro with two names after &" "(defmacro m [& a b] a)"
~needle:"& takes one name and it is the last thing";
parse_rejects "defmacro with a pattern after &" "(defmacro m [& [a b]] a)"
~needle:"& binds one name for the rest of the arguments";
parse_rejects "defmacro with an empty pattern" "(defmacro m [a []] a)"
~needle:"binds nothing";
parse_rejects "defmacro binding a name twice" "(defmacro m [a [b a]] a)"
~needle:"a is bound twice in this parameter list";
parse_rejects "defmacro with a map pattern" "(defmacro m [{:keys [a]}] a)"
~needle:"map destructuring is not implemented in a macro's parameter list";
(* Shape and feature were separate mistakes and stay separate reasons. *)
parse_rejects "defmacro with no body" "(defmacro m [x])"
~needle:"defmacro is (defmacro name [param ...] body ...)";
parse_rejects "defmacro with no params" "(defmacro m x)"
~needle:"defmacro is (defmacro name [param ...] body ...)";
parse_rejects "defmacro with a non-name param" "(defmacro m [1] x)"
~needle:"expected a name";
~needle:"a macro's parameter is a name or a [ ] pattern";
parse_rejects "defmacro in expression position" "(defn f [] () (defmacro m [] 1))"
~needle:"top-level declaration";
@ -3582,7 +3604,7 @@ let () =
let synth src = Reader.read_all ~file:"<synth>" src in
let chain =
synth
"(defmacro m [args] `(do))\n\
"(defmacro m [& args] `(do))\n\
(defn a [] () (m))\n\
(defn b [] () (a))\n\
(defn c [] () (do))\n"
@ -3592,7 +3614,7 @@ let () =
(* And the one rule that stays: a prelude macro may not call a macro. It used
to fail as an unknown name inside a clang build; it names itself now. *)
let ring = synth "(defmacro m [args] `(do))\n(defmacro n [args] (m args))\n" in
let ring = synth "(defmacro m [& args] `(do))\n(defmacro n [& args] (m args))\n" in
check "a prelude macro calling a macro is refused by name"
(match Macro.reduce ring with
| _ -> false

View File

@ -155,7 +155,7 @@ let () =
the sentence naming it a declaration rather than an arity complaint
about an unknown function. C-c C-c is where a declaration goes, which
is the case below. *)
refuses "a defmacro at C-x C-e" "(defmacro m [args] args)"
refuses "a defmacro at C-x C-e" "(defmacro m [& args] args)"
"top-level declaration";
(* And the session is untouched by all of it: an evaluation is not a
@ -172,7 +172,7 @@ let () =
(let r =
request c
(Printf.sprintf "(:op \"eval\" :code %s :file \"/tmp/buf.flan\")"
(quote "(defmacro thrice [args] `(* ~(at args 0) 3))"))
(quote "(defmacro thrice [& args] `(* ~(at args 0) 3))"))
in
if status r <> "ok" then
fail "evaluating a defmacro over the socket: %s"
@ -278,7 +278,7 @@ let () =
(request c
(Printf.sprintf
"(:op \"macroexpand\" :code %s :file \"/tmp/buf.flan\")"
(quote "(defmacro looked-at [args] `(* ~(at args 0) 5))")));
(quote "(defmacro looked-at [& args] `(* ~(at args 0) 5))")));
(let r = evals "(looked-at 3)" in
if status r = "ok" then
fail "a defmacro joined the session by being macroexpanded");

View File

@ -431,7 +431,7 @@ let () =
Out_channel.with_open_bin path (fun oc -> Out_channel.output_string oc text)
in
let macro op =
Printf.sprintf "(defmacro grow [args]
Printf.sprintf "(defmacro grow [& args]
`(%s ~(at args 0) ~(at args 0)))
" op
in
@ -546,7 +546,7 @@ let () =
case here that the create-time seed cannot explain. Two evaluations,
because that is what the claim is about. *)
(match Session.eval ~origin:"programs/pkg-macro.flan" tm
"(defmacro thrice [args] `(* ~(at args 0) 3))"
"(defmacro thrice [& args] `(* ~(at args 0) 3))"
with
| _ -> ()
| exception Loc.Error { Loc.dmsg = m; _ } ->
@ -562,7 +562,7 @@ let () =
ordinary editing action and the one that would have reached
[Check.program] as a duplicate declaration without the second of those. *)
(match Session.eval ~origin:"programs/pkg-macro.flan" tm
"(defmacro thrice [args] `(* ~(at args 0) 4))"
"(defmacro thrice [& args] `(* ~(at args 0) 4))"
with
| _ -> ()
| exception Loc.Error { Loc.dmsg = m; _ } ->
@ -582,7 +582,7 @@ let () =
and fails at the checker which is the only interesting place to fail,
because a parse failure never reaches the union either. *)
(match Session.eval ~origin:"programs/pkg-macro.flan" tm
"(defmacro nope [args] (no-such-function args))"
"(defmacro nope [& args] (no-such-function args))"
with
| _ -> fail "a defmacro whose body does not check was accepted"
| exception Loc.Error _ -> ());
@ -693,6 +693,30 @@ let () =
of what one step is for. *)
expands "one step does not expand the arguments first"
"(mac/twice (mac/twice 3))" "(+ (mac/twice 3) (mac/twice 3))";
(* One macro written both ways, expanded to the same text. [tenfold] picks
its argument out of the slice by hand and [tenfold-listed] names it in the
parameter list; there is one grammar under both, so the two expansions
have to be the same string and not merely the same shape.
This is the migration's evidence. Every [defmacro] in the tree was
rewritten from [args] to [& args] when the list stopped meaning "the whole
call" and started meaning "the first argument", and what makes that a
spelling change rather than a behaviour change is exactly this. *)
expands "a macro that picks its argument out by hand" "(tenfold 7)" "(* 7 10)";
expands "the same macro with a parameter list" "(tenfold-listed 7)" "(* 7 10)";
(* And the call-site check on this path, which is the editor's rather than a
build's. [Macro.checked_call] is one function for all four ways in the
walk, [settle], C-c C-m's one step and its fixpoint so C-c C-m over a
miscounted call refuses with the sentence a build would give. *)
(match Session.macroexpand ~origin:"programs/pkg-macro.flan" ~all:false tm
"(tenfold-listed 1 2)"
with
| _ -> fail "C-c C-m expanded a macro call with the wrong arity"
| exception Loc.Error { Loc.dmsg = m; _ } ->
if not (has m "tenfold-listed takes 1 argument and this call gives 2")
then fail "C-c C-m over a miscounted call said: %s" m);
(* Not a macro call at all. The form comes back as it was, and the answer
that matters is [xmacro]: nothing ran. *)
(match Session.macroexpand ~origin:"programs/pkg-macro.flan" ~all:false tm
@ -724,7 +748,7 @@ let () =
*aftermath*, and it is checked the only way it can be by calling the
name and requiring it to still be unknown. *)
(match Session.macroexpand ~origin:"programs/pkg-macro.flan" ~all:false tm
"(defmacro looked-at [args] `(* ~(at args 0) 5))"
"(defmacro looked-at [& args] `(* ~(at args 0) 5))"
with
| _ -> ()
| exception Loc.Error { Loc.dmsg = m; _ } ->

View File

@ -528,7 +528,7 @@
;; points over the whole thing. `C-c C-m` over the call shows all of it, which
;; is the point of generating readable code rather than the smallest code — a
;; provider whose output nobody can look at is a plugin.
(defmacro defedn [args]
(defmacro defedn [& args]
(if (!= (len args) 2)
(refuse "defedn is (defedn Name \"path.edn\") — a name for the struct, and a path to the file its shape is read out of")
(match (at args 1)

View File

@ -409,7 +409,7 @@
;; It answers a `do`, which the top level splices: the nested structs innermost
;; first, then the struct named here, a reader per struct, and the two entry
;; points over the whole thing.
(defmacro defjson [args]
(defmacro defjson [& args]
(if (!= (len args) 2)
(refuse "defjson is (defjson Name \"path.json\") — a name for the struct, and a path to the file its shape is read out of")
(match (at args 1)

View File

@ -76,7 +76,7 @@
;; The frame. Everything drawn lands on the back buffer; end-drawing swaps it
;; and waits out the frame time set by set-target-fps.
(defmacro with-drawing [args]
(defmacro with-drawing [& args]
(if (< (len args) 1)
`(with-drawing-takes-a-body)
`(do (begin-drawing)
@ -86,7 +86,7 @@
;; The 2D camera. The argument is a Camera2D value, evaluated once where it
;; always was. Remember that a fresh (Camera2D {}) has zoom 0.0 and is not
;; usable as an identity — raylib.flan says so beside the struct.
(defmacro with-mode-2d [args]
(defmacro with-mode-2d [& args]
(if (< (len args) 2)
`(with-mode-2d-takes-a-camera-and-a-body)
`(do (begin-mode-2d ~(at args 0))
@ -96,7 +96,7 @@
;; The 3D camera. Same shape, same argument-once rule, and the pair matters
;; more here than anywhere: ending a 3D mode with end-mode-2d type-checks
;; fine and leaves the projection matrix wrong for everything after it.
(defmacro with-mode-3d [args]
(defmacro with-mode-3d [& args]
(if (< (len args) 2)
`(with-mode-3d-takes-a-camera-and-a-body)
`(do (begin-mode-3d ~(at args 0))
@ -107,7 +107,7 @@
;; of the GPU upside down, so drawing it back wants a negative source height —
;; that correction is the caller's and is deliberately not hidden here, since
;; it belongs with the draw and not with the mode.
(defmacro with-texture-mode [args]
(defmacro with-texture-mode [& args]
(if (< (len args) 2)
`(with-texture-mode-takes-a-target-and-a-body)
`(do (begin-texture-mode ~(at args 0))
@ -117,7 +117,7 @@
;; Clip to a rectangle, in screen pixels with y down from the top. Four
;; scalars rather than a Rectangle, because that is what BeginScissorMode
;; takes and this file is not the place to invent a second spelling.
(defmacro with-scissor-mode [args]
(defmacro with-scissor-mode [& args]
(if (< (len args) 5)
`(with-scissor-mode-takes-x-y-width-height-and-a-body)
`(do (begin-scissor-mode ~(at args 0) ~(at args 1) ~(at args 2) ~(at args 3))