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:
parent
5afa707d76
commit
69646e534e
@ -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))
|
||||
|
||||
171
lib/expand.ml
171
lib/expand.ml
@ -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
|
||||
|
||||
62
lib/macro.ml
62
lib/macro.ml
@ -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
|
||||
|
||||
110
lib/parse.ml
110
lib/parse.ml
@ -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 }
|
||||
|
||||
@ -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)
|
||||
|
||||
9
test/programs/macro-arity-extra.flan
Normal file
9
test/programs/macro-arity-extra.flan
Normal 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)
|
||||
12
test/programs/macro-arity.flan
Normal file
12
test/programs/macro-arity.flan
Normal 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)
|
||||
@ -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
|
||||
|
||||
7
test/programs/macro-destructure-arity.flan
Normal file
7
test/programs/macro-destructure-arity.flan
Normal 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)
|
||||
9
test/programs/macro-destructure.flan
Normal file
9
test/programs/macro-destructure.flan
Normal 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)
|
||||
71
test/programs/macro-params.flan
Normal file
71
test/programs/macro-params.flan
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)))
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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; _ } ->
|
||||
|
||||
2
vendor/edn/provide.flan
vendored
2
vendor/edn/provide.flan
vendored
@ -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)
|
||||
|
||||
2
vendor/json/provide.flan
vendored
2
vendor/json/provide.flan
vendored
@ -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)
|
||||
|
||||
10
vendor/raylib/modes.flan
vendored
10
vendor/raylib/modes.flan
vendored
@ -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))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user