diff --git a/TODO.org b/TODO.org index f5269f09..40ebfa21 100644 --- a/TODO.org +++ b/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=, diff --git a/lib/expand.ml b/lib/expand.ml index 455aa1cd..e1319623 100644 --- a/lib/expand.ml +++ b/lib/expand.ml @@ -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. *) diff --git a/lib/macro.ml b/lib/macro.ml index cd354d59..ae2577e7 100644 --- a/lib/macro.ml +++ b/lib/macro.ml @@ -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 diff --git a/lib/parse.ml b/lib/parse.ml index 4c8b1025..519103b1 100644 --- a/lib/parse.ml +++ b/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. diff --git a/lib/prelude.ml b/lib/prelude.ml index ef6aed24..7187b1d5 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -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 diff --git a/lib/session.ml b/lib/session.ml index 00a4c241..634ff648 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -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 = "") ?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 = "") ?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 diff --git a/test/programs/macro-writing.flan b/test/programs/macro-writing.flan index e7102fc3..83888893 100644 --- a/test/programs/macro-writing.flan +++ b/test/programs/macro-writing.flan @@ -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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index f1d85516..4ea28b2d 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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" diff --git a/test/test_flan.ml b/test/test_flan.ml index e18b2406..1f20889f 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -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"; diff --git a/test/test_session.ml b/test/test_session.ml index 78a3936d..869f444e 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -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