diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 7a009a9..0b958c3 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -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)) diff --git a/lib/expand.ml b/lib/expand.ml index e771d4b..bdb65d1 100644 --- a/lib/expand.ml +++ b/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 diff --git a/lib/macro.ml b/lib/macro.ml index d636183..b67826c 100644 --- a/lib/macro.ml +++ b/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 diff --git a/lib/parse.ml b/lib/parse.ml index 6a5ac37..de337c2 100644 --- a/lib/parse.ml +++ b/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 } diff --git a/lib/prelude.ml b/lib/prelude.ml index b015fd9..63a3c1e 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -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) diff --git a/test/programs/macro-arity-extra.flan b/test/programs/macro-arity-extra.flan new file mode 100644 index 0000000..337bca4 --- /dev/null +++ b/test/programs/macro-arity-extra.flan @@ -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) diff --git a/test/programs/macro-arity.flan b/test/programs/macro-arity.flan new file mode 100644 index 0000000..5b5d994 --- /dev/null +++ b/test/programs/macro-arity.flan @@ -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) diff --git a/test/programs/macro-cycle.flan b/test/programs/macro-cycle.flan index 73b48b5..fa3cd69 100644 --- a/test/programs/macro-cycle.flan +++ b/test/programs/macro-cycle.flan @@ -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 diff --git a/test/programs/macro-destructure-arity.flan b/test/programs/macro-destructure-arity.flan new file mode 100644 index 0000000..e979e48 --- /dev/null +++ b/test/programs/macro-destructure-arity.flan @@ -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) diff --git a/test/programs/macro-destructure.flan b/test/programs/macro-destructure.flan new file mode 100644 index 0000000..3328755 --- /dev/null +++ b/test/programs/macro-destructure.flan @@ -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) diff --git a/test/programs/macro-params.flan b/test/programs/macro-params.flan new file mode 100644 index 0000000..3eb1fdc --- /dev/null +++ b/test/programs/macro-params.flan @@ -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) diff --git a/test/programs/macro-spin.flan b/test/programs/macro-spin.flan index cf8ed78..bccc2bd 100644 --- a/test/programs/macro-spin.flan +++ b/test/programs/macro-spin.flan @@ -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 diff --git a/test/programs/macros.flan b/test/programs/macros.flan index c84cbf2..796e6df 100644 --- a/test/programs/macros.flan +++ b/test/programs/macros.flan @@ -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))) diff --git a/test/programs/pkg-macro.flan b/test/programs/pkg-macro.flan index c749a62..956329c 100644 --- a/test/programs/pkg-macro.flan +++ b/test/programs/pkg-macro.flan @@ -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 diff --git a/test/programs/pkgs/mac/mac.flan b/test/programs/pkgs/mac/mac.flan index fb25972..0c5c5c4 100644 --- a/test/programs/pkgs/mac/mac.flan +++ b/test/programs/pkgs/mac/mac.flan @@ -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))) diff --git a/test/programs/pkgs/macring/macring.flan b/test/programs/pkgs/macring/macring.flan index dfdd6c8..a18f6d5 100644 --- a/test/programs/pkgs/macring/macring.flan +++ b/test/programs/pkgs/macring/macring.flan @@ -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)) diff --git a/test/programs/pkgs/macspin/macspin.flan b/test/programs/pkgs/macspin/macspin.flan index c84c254..82454b4 100644 --- a/test/programs/pkgs/macspin/macspin.flan +++ b/test/programs/pkgs/macspin/macspin.flan @@ -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)) diff --git a/test/programs/printers.flan b/test/programs/printers.flan index 88aff65..1dcda6e 100644 --- a/test/programs/printers.flan +++ b/test/programs/printers.flan @@ -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 diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index bcc7366..838014e 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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"; diff --git a/test/test_dev.ml b/test/test_dev.ml index e8cfbb2..c6bef30 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -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 diff --git a/test/test_flan.ml b/test/test_flan.ml index 753bc77..e3e1617 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -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:"" 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 diff --git a/test/test_repl.ml b/test/test_repl.ml index 2687cc2..de6bba7 100644 --- a/test/test_repl.ml +++ b/test/test_repl.ml @@ -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"); diff --git a/test/test_session.ml b/test/test_session.ml index 6dff651..360b2e8 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -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; _ } -> diff --git a/vendor/edn/provide.flan b/vendor/edn/provide.flan index c4b9589..4d2e95f 100644 --- a/vendor/edn/provide.flan +++ b/vendor/edn/provide.flan @@ -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) diff --git a/vendor/json/provide.flan b/vendor/json/provide.flan index 3da211f..af0c56c 100644 --- a/vendor/json/provide.flan +++ b/vendor/json/provide.flan @@ -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) diff --git a/vendor/raylib/modes.flan b/vendor/raylib/modes.flan index 7910e7f..79819d6 100644 --- a/vendor/raylib/modes.flan +++ b/vendor/raylib/modes.flan @@ -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))