~~@xs splices an unquote per element, a session can call a macro an expansion defined, and an enum member cannot be named else
This commit is contained in:
parent
d03b02b347
commit
de26d6e625
9
TODO.org
9
TODO.org
@ -86,16 +86,11 @@ CLOSED: [2026-09-25]
|
||||
=Expand.quote= counts depth the way SBCL's =*backquote-depth*= does: an unquote
|
||||
belongs to the innermost quasiquote and =~~x= reaches out two levels; deeper
|
||||
forms come back as data. A macro's answer is desugared again, and a top-level
|
||||
expansion that defines a macro re-runs the expander. =~~@x= is refused. There is
|
||||
expansion that defines a macro re-runs the expander, in a build and in a session.
|
||||
=~~@x= splices an unquote per element, SBCL's =unquote*=. There is
|
||||
no =,',x=, since =quote= takes a symbol, and a macro defined by an expansion is
|
||||
not exported from a package. docs/BUILT.md, "Quasiquote runs before the walk".
|
||||
|
||||
** TODO A session does not remember a macro that an expansion defined
|
||||
=Session.own_macros= reads =defmacro= heads off the forms as sent, so after
|
||||
=(defsquare sq)= is evaluated the session's next form cannot call =sq=. A build
|
||||
sees the whole file and can. The fix is collecting from the expanded forms,
|
||||
which =Load.program= does not hand back.
|
||||
|
||||
** DONE A form the prelude relies on is built in; a form only programs use is a macro
|
||||
CLOSED: [2026-09-25]
|
||||
=cond=, =when= and =dotimes= are special forms in parse.ml; =inc=, =++=, =into=,
|
||||
|
||||
@ -302,14 +302,35 @@ let rec quote ?(depth = 1) (f : Form.t) : Form.t =
|
||||
and seq ~depth loc items =
|
||||
List.fold_left
|
||||
(fun acc (item : Form.t) ->
|
||||
match splice_of item with
|
||||
| Some x when depth = 1 ->
|
||||
lst item.Form.loc [ sym item.Form.loc "form-append"; x; acc ]
|
||||
| _ ->
|
||||
match spliced ~depth item with
|
||||
| Some x -> lst item.Form.loc [ sym item.Form.loc "form-append"; x; acc ]
|
||||
| None ->
|
||||
lst item.Form.loc [ sym item.Form.loc "form-cons"; quote ~depth item; acc ])
|
||||
(lst loc [ sym loc "form-nil" ])
|
||||
(List.rev items)
|
||||
|
||||
(* The slice an item splices in, when it splices at all. At depth 1 that is
|
||||
~@x. Deeper, an unquote whose own argument splices at the level below —
|
||||
[~~@xs] — splices too: one (unquote x) per element, which is SBCL's
|
||||
[unquote*] in src/code/backq.lisp, so the inner template receives ~a ~b ~c.
|
||||
[~@~@xs] is the same with (unquote-splicing x). *)
|
||||
and spliced ~depth (item : Form.t) : Form.t option =
|
||||
let loc = item.Form.loc in
|
||||
match splice_of item with
|
||||
| Some x when depth = 1 -> Some x
|
||||
| _ when depth = 1 -> None
|
||||
| _ ->
|
||||
let wrap head x =
|
||||
match spliced ~depth:(depth - 1) x with
|
||||
| Some e ->
|
||||
Some (lst loc [ sym loc "form-wrap-each"; Form.make (Form.Str head) loc; e ])
|
||||
| None -> None
|
||||
in
|
||||
match unquote_of item, splice_of item with
|
||||
| Some x, _ -> wrap "unquote" x
|
||||
| _, Some x -> wrap "unquote-splicing" x
|
||||
| None, None -> None
|
||||
|
||||
(* Every quasiquote in a form, outermost first. Pure, total, and dependent on
|
||||
nothing but Form, which is what lets [Parse] run it on the way in rather
|
||||
than needing the whole expander wired up first. *)
|
||||
|
||||
@ -592,6 +592,12 @@ let rec program_n left (forms : Form.t list) : Form.t list =
|
||||
let before = macros_in forms in
|
||||
let out = with_module l (fun () -> List.map (expand_form l) forms) in
|
||||
let fresh = List.filter (fun n -> not (List.mem n before)) (macros_in out) in
|
||||
Parse.expansion_macros :=
|
||||
!Parse.expansion_macros
|
||||
@ List.filter
|
||||
(fun f -> match macro_name f with
|
||||
| Some n -> List.mem n fresh | None -> false)
|
||||
out;
|
||||
if fresh = [] then out
|
||||
else if left <= 0 then
|
||||
Loc.fail
|
||||
|
||||
27
lib/parse.ml
27
lib/parse.ml
@ -1619,10 +1619,24 @@ let rec decl (f : Form.t) : Ast.decl =
|
||||
(* Each member becomes its name, its value, whether that value was
|
||||
written, and where the name is. The last two exist only so the
|
||||
refusals here and below can be made; neither reaches the AST. *)
|
||||
(* :else is match's catch-all, so a member spelled else could be
|
||||
written everywhere but in a match arm, where it would mean every
|
||||
member. Refused here rather than quietly shadowed there. *)
|
||||
let member_ok (mf : Form.t) =
|
||||
no_sigil mf;
|
||||
match mf.v with
|
||||
| Form.Sym "else" ->
|
||||
Loc.failk "parse/enum-member-else" mf.loc
|
||||
"%s cannot have a member named else: :else is the catch-all arm \
|
||||
of a match, so a match could never name this member. Rename \
|
||||
it, for example to otherwise"
|
||||
ename
|
||||
| _ -> ()
|
||||
in
|
||||
let rec members next = function
|
||||
| [] -> []
|
||||
| ({ v = Form.Sym m; loc } as mf) :: { v = Form.Int k; _ } :: rest ->
|
||||
no_sigil mf;
|
||||
member_ok mf;
|
||||
(* The [let] is load-bearing rather than tidiness. OCaml leaves the
|
||||
evaluation order of [::]'s two operands unspecified and in
|
||||
practice takes the tail first, so an inlined [fits ... k] would
|
||||
@ -1639,14 +1653,14 @@ let rec decl (f : Form.t) : Ast.decl =
|
||||
number written; refused in the spelling it was written in. *)
|
||||
| ({ v = Form.Sym m; _ } as mf) :: { v = Form.UInt (_, text); loc = vloc }
|
||||
:: _ ->
|
||||
no_sigil mf;
|
||||
member_ok mf;
|
||||
Loc.failk "parse/enum-value-out-of-range" vloc
|
||||
"the member %s of %s is %s, which does not fit i32 — an enum's \
|
||||
members run from -2147483648 to 2147483647. Give %s a value in \
|
||||
that range, or use a defconst"
|
||||
m ename text m
|
||||
| ({ v = Form.Sym m; loc } as mf) :: rest ->
|
||||
no_sigil mf;
|
||||
member_ok mf;
|
||||
let next = fits m loc ~explicit:false next in
|
||||
(m, next, false, loc) :: members (Int64.add next 1L) rest
|
||||
| bad :: _ ->
|
||||
@ -1900,6 +1914,13 @@ let expander : (Form.t list -> Form.t list) ref = ref (fun fs -> fs)
|
||||
whole of an evaluation. Empty is the ordinary case and costs nothing. *)
|
||||
let imported_macros : Form.t list ref = ref []
|
||||
|
||||
(* Every [defmacro] an expansion produced at the top level, already desugared,
|
||||
appended by [Macro.program]. A build needs nothing from it — the expander
|
||||
re-runs over the whole file itself — but a session learns its macros from
|
||||
the forms it was sent, and [(defsq sq)] sends no [defmacro]. The session
|
||||
empties it before an evaluation and reads it after. *)
|
||||
let expansion_macros : Form.t list ref = ref []
|
||||
|
||||
(* What those macros are allowed to *call*, and it is the same list the
|
||||
importing program gets: the package's declarations, qualified under the
|
||||
alias, as [Load] already built them.
|
||||
|
||||
@ -2120,6 +2120,14 @@ let source = {flan|
|
||||
(set i (+ i 1)))
|
||||
(slice v)))
|
||||
|
||||
;; (head x) for every x, which is what ~~@xs splices into an inner template:
|
||||
;; one unquote per element, as SBCL's unquote* builds (src/code/backq.lisp).
|
||||
(defn form-wrap-each [head string xs [Form]] [Form]
|
||||
(let [v (vec-new Form)]
|
||||
(dotimes [i (length xs)]
|
||||
(push v (Form.List {.xs (form-pair (Form.Sym {.s head}) (at xs i))})))
|
||||
(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
|
||||
|
||||
@ -111,15 +111,33 @@ let own_macros (forms : Form.t list) : Form.t list =
|
||||
| _ -> None)
|
||||
forms
|
||||
|
||||
(* The macros [load] defined by expanding [forms], beside the ones written in
|
||||
them: [(defsq sq)] defines [sq] without a [defmacro] in sight, and the next
|
||||
evaluation has to be able to call it. Only those expanded from these forms'
|
||||
own file — [Load.program] also parses the packages they import, and a macro
|
||||
a package's expansion defined belongs to the package. Written first, so an
|
||||
expansion's newer body wins as a [defmacro]'s does. *)
|
||||
let with_expansion_macros (forms : Form.t list) (load : unit -> 'a) : 'a * Form.t list =
|
||||
Parse.expansion_macros := [];
|
||||
let r = load () in
|
||||
let files = List.map (fun (f : Form.t) -> f.Form.loc.Loc.file) forms in
|
||||
let defined =
|
||||
List.filter
|
||||
(fun (f : Form.t) -> List.mem (Loc.call_site f.Form.loc).Loc.file files)
|
||||
!Parse.expansion_macros
|
||||
in
|
||||
Parse.expansion_macros := [];
|
||||
(r, Load.macro_union (own_macros forms) defined)
|
||||
|
||||
let create ?(debug = false) ?(x86 = false) ~file () =
|
||||
let forms = Reader.read_file file in
|
||||
let l = Load.program ~file forms in
|
||||
let l, mine = with_expansion_macros forms (fun () -> Load.program ~file forms) in
|
||||
let p, env = Check.program_with_env l.Load.decls in
|
||||
({ file; decls = l.Load.decls; program = p; env; host = p; pkgs = l.Load.pkgs;
|
||||
(* The file's own first, so that if the file being edited is itself a
|
||||
package the program imports, the bare name wins for a form typed into
|
||||
that buffer. [macro_union] keeps the left. *)
|
||||
macros = Load.macro_union (own_macros forms) l.Load.macros;
|
||||
macros = Load.macro_union mine l.Load.macros;
|
||||
thunks = 0; debug; x86 }, l)
|
||||
|
||||
(* What a macro may call, for the same reason [macros] is held: an evaluation
|
||||
@ -594,7 +612,9 @@ let eval ?(origin = "<eval>") ?pause t src : change =
|
||||
the duplicate-name pass would reject it. *)
|
||||
let macros = ref t.macros in
|
||||
let incoming =
|
||||
let l = Load.program ~file:t.file forms in
|
||||
let l, mine =
|
||||
with_expansion_macros forms (fun () -> Load.program ~file:t.file forms)
|
||||
in
|
||||
(* An evaluated import *adds* to the session's set, so a macro brought in
|
||||
by C-c C-k is there for the C-c C-c after it. A union and not an
|
||||
assignment: [Load.program] answers the macros of the imports it was
|
||||
@ -632,8 +652,7 @@ let eval ?(origin = "<eval>") ?pause t src : change =
|
||||
name. [Macro.program] dedupes the same way on the same rule, because
|
||||
while this parse runs the old copy is still ambient. *)
|
||||
macros :=
|
||||
Load.macro_union (own_macros forms)
|
||||
(Load.macro_union l.Load.macros t.macros);
|
||||
Load.macro_union mine (Load.macro_union l.Load.macros t.macros);
|
||||
let ds = l.Load.decls in
|
||||
match package_of t origin with
|
||||
| None -> ds
|
||||
|
||||
@ -30,7 +30,20 @@
|
||||
`(defmacro ~inner [x]
|
||||
`(* ~x ~x))))
|
||||
|
||||
;; ~~@xs: each form the outer macro was handed becomes one ~x in the inner
|
||||
;; template, in a list and in a vector.
|
||||
(defmacro defmany [name & xs]
|
||||
`(defmacro ~name []
|
||||
`(+ 0 ~~@xs)))
|
||||
|
||||
(defmacro deflet [name & xs]
|
||||
`(defmacro ~name [body]
|
||||
`(let [~~@xs] ~body)))
|
||||
|
||||
(defsquare sq)
|
||||
(defmany six (Form.Int {.i 1}) (Form.Int {.i 2}) (Form.Int {.i 3}))
|
||||
(deflet with-ab (Form.Sym {.s "a"}) (Form.Int {.i 4})
|
||||
(Form.Sym {.s "b"}) (Form.Int {.i 5}))
|
||||
(defadder add5 (Form.Int {.i 5}))
|
||||
(defsum total)
|
||||
(defsquarer defsq2)
|
||||
@ -41,4 +54,6 @@
|
||||
(print (add5 10)) (println "")
|
||||
(print (total 1 2 3 4)) (println "")
|
||||
(print (sq2 9)) (println "")
|
||||
(print (six)) (println "")
|
||||
(print (with-ab (* a b))) (println "")
|
||||
0)
|
||||
|
||||
@ -4126,7 +4126,7 @@ level "1"
|
||||
(* A quasiquote inside a quasiquote: macros whose expansion is a defmacro,
|
||||
and the macros they define called from main. Two levels deep at the
|
||||
end, which is two re-passes of the expander. *)
|
||||
let macro_writing_out = "49\n15\n10\n81\n" in
|
||||
let macro_writing_out = "49\n15\n10\n81\n6\n20\n" in
|
||||
outputs "a macro that writes a macro" "programs/macro-writing.flan"
|
||||
macro_writing_out;
|
||||
outputs ~opt:"-O0" "a macro that writes a macro, -O0"
|
||||
|
||||
@ -684,9 +684,15 @@ let () =
|
||||
(* Three deep: ~~x under three quasiquotes is still data, one level short. *)
|
||||
desugars "~~x under three quasiquotes is data" "```~~x"
|
||||
(wrap "quasiquote" (wrap "quasiquote" (wrap "unquote" (wrap "unquote" (q "x")))));
|
||||
(* ~ holds one form, so a splice directly inside one at the evaluating level
|
||||
has nothing to splice into. *)
|
||||
parse_rejects "~~@x" "(defn f [] Form ``~~@xs)"
|
||||
(* ~~@xs inside a bracket splices one (unquote x) per element, SBCL's
|
||||
unquote*, in a list and in a vector alike. *)
|
||||
let each = "(form-append (form-wrap-each \"unquote\" xs) (form-nil))" in
|
||||
desugars "~~@xs in a list splices an unquote per element" "``(~~@xs)"
|
||||
(wrap "quasiquote" ("(Form.List {.xs " ^ each ^ "})"));
|
||||
desugars "~~@xs in a vector splices an unquote per element" "``[~~@xs]"
|
||||
(wrap "quasiquote" ("(Form.Vec {.xs " ^ each ^ "})"));
|
||||
(* Outside a bracket there is still nothing for it to splice into. *)
|
||||
parse_rejects "~~@x outside a bracket" "(defn f [] Form ``~~@xs)"
|
||||
~needle:"nothing here for it to splice into";
|
||||
(* Not a missing feature — an unquote outside a quasiquote is a mistake, and
|
||||
the reader cannot catch it because it does not track where it is. *)
|
||||
@ -779,6 +785,12 @@ let () =
|
||||
~needle:"an enum member is a name, optionally followed by an integer";
|
||||
parse_rejects "a defenum with no member vector" "(defenum E)"
|
||||
~needle:"defenum is (defenum Name [member value? ...])";
|
||||
(* :else is match's catch-all, so a member named else could never be
|
||||
matched. *)
|
||||
parse_rejects "an enum member named else" "(defenum E [foo else])"
|
||||
~needle:"E cannot have a member named else: :else is the catch-all arm";
|
||||
parse_rejects "an enum member named else, with a value" "(defenum E [else 3])"
|
||||
~needle:"cannot have a member named else";
|
||||
|
||||
(* ── Malformed syntax is caught with a location ────────────────── *)
|
||||
parse_rejects "odd let bindings" "(let [a])";
|
||||
@ -4089,6 +4101,11 @@ let () =
|
||||
(k ^ "(defn f [k K] i32 (match k :lo 1 _ 2))");
|
||||
accepts "match over an enum, :else for the rest"
|
||||
(k ^ "(defn f [k K] i32 (match k :lo 1 :else 2))");
|
||||
(* A data type's case is matched by a symbol, not a keyword, so a case
|
||||
named else does not collide with :else and is not refused. *)
|
||||
accepts "a data case named else is matched by name"
|
||||
"(defdata D [(else) (foo [x i32])])\n\
|
||||
(defn f [d D] i32 (match d else 1 (foo x) x))";
|
||||
rejects_check "match over an enum that misses a member"
|
||||
(k ^ "(defn f [k K] i32 (match k :lo 1 :hi 2))")
|
||||
~needle:"this match is not exhaustive — :mid has no arm";
|
||||
|
||||
@ -730,6 +730,26 @@ let () =
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "an edited defmacro: %s" m);
|
||||
|
||||
(* A macro that an expansion defined joins the session too. [(defsq sq6)]
|
||||
sends no [defmacro], so reading the forms as sent would miss it and the
|
||||
next evaluation would call [sq6] as a function taking a Form. *)
|
||||
(match Session.eval ~origin:"programs/pkg-macro.flan" tm
|
||||
"(defmacro defsq [name] `(defmacro ~name [x] `(* ~x ~x)))"
|
||||
with
|
||||
| _ -> ()
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "evaluating a macro-writing defmacro: %s" m);
|
||||
(match Session.eval ~origin:"programs/pkg-macro.flan" tm "(defsq sq6)" with
|
||||
| _ -> ()
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "evaluating a call to a macro-writing macro: %s" m);
|
||||
(match Session.eval_expr ~origin:"programs/pkg-macro.flan" tm "(sq6 6)" with
|
||||
| c ->
|
||||
if not (has c.Session.ir "6, 6") then
|
||||
fail "a macro defined by an expansion did not expand to its body"
|
||||
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||
fail "a macro defined by an expansion, called in the session: %s" m);
|
||||
|
||||
(* A mistake in a body the macro spliced, reported where it was written.
|
||||
Same machinery as a build — [Macro.expand_form] and [Expand.call] — and
|
||||
the point of asking it here is that the editor is where it is read: C-c
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user