diff --git a/lib/expand.ml b/lib/expand.ml index b99fde8..009f61b 100644 --- a/lib/expand.ml +++ b/lib/expand.ml @@ -144,3 +144,90 @@ let call ~loc (fn : Dynload.addr) (args : Form.t list) : Form.t = let out = Dynload.take form_size in Dynload.call fn a (Int64.of_int n) out; unmarshal ~loc out + +(* ── Quasiquote ──────────────────────────────────────────────────── + A desugaring over [Form], and nothing more: a quasiquoted (if ~t ~b) becomes + calls to the prelude's form-building surface, which the checker then sees as + ordinary code. There is no quasiquote left in the language after this runs, + which is why the expander's own walk needs no idea that quoting exists: by + the time it looks for macro calls, a [cond] written inside a quasiquote is a + (Form.Sym {.s "cond"}) and there is no head there to mistake for a call the + compiler should make now. + + The reader stays dumb and produces (quasiquote x), (unquote x) and + (unquote-splicing x) with no idea whether one is inside another. Counting + levels is this file's job, and it does not: a quasiquote inside a quasiquote + is refused by name. A macro that writes a macro is the only thing that wants + one, nothing in the corpus does, and CL's level arithmetic has a real cost + that no use case has asked for. *) + +let sym loc s = Form.make (Form.Sym s) loc +let lst loc xs = Form.make (Form.List xs) loc + +(* (Form.Case {.field value}) — a node of the image, written as the Flan + constructor the prelude declares. *) +let node loc case field v = + lst loc [ sym loc ("Form." ^ case); + Form.make (Form.Map [ sym loc ("." ^ field); Form.make v loc ]) loc ] + +let unquote_of (f : Form.t) = + match f.Form.v with + | Form.List [ { Form.v = Form.Sym "unquote"; _ }; x ] -> Some x + | _ -> None + +let splice_of (f : Form.t) = + match f.Form.v with + | Form.List [ { Form.v = Form.Sym "unquote-splicing"; _ }; x ] -> Some x + | _ -> None + +let rec quote (f : Form.t) : Form.t = + let loc = f.Form.loc in + match unquote_of f with + (* The escape: whatever the program wrote, evaluated. It is already a Form, + because a Form is what a macro body deals in. *) + | Some x -> x + | None -> + match splice_of f with + | Some _ -> + Loc.fail loc + "~@x splices into a list or a vector, and there is nothing here for it \ + to splice into" + | None -> + match f.Form.v with + | Form.List ({ Form.v = Form.Sym "quasiquote"; _ } :: _) -> + Loc.fail loc + "a quasiquote inside a quasiquote is not implemented: the reader does \ + not count nesting levels and neither does this, so the inner one has \ + no meaning to give. Build the inner form with form-cons" + | Form.Sym s -> node loc "Sym" "s" (Form.Str s) + | Form.Kw s -> node loc "Kw" "s" (Form.Str s) + | Form.Int i -> node loc "Int" "i" (Form.Int i) + | Form.Float x -> node loc "Float" "x" (Form.Float x) + | Form.Str s -> node loc "Str" "s" (Form.Str s) + | Form.Byte b -> node loc "Byte" "b" (Form.Int (Int64.of_int b)) + | Form.List xs -> node loc "List" "xs" (seq loc xs).Form.v + | Form.Vec xs -> node loc "Vec" "xs" (seq loc xs).Form.v + | Form.Map xs -> node loc "Map" "xs" (seq loc xs).Form.v + +(* The [Form] slice one bracket's worth of items comes to. Built right to left, + so each item is consed onto what follows it and a splice is an append — the + three prelude functions and no fourth. *) +and seq loc items = + List.fold_left + (fun acc (item : Form.t) -> + match splice_of 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 item; acc ]) + (lst loc [ sym loc "form-nil" ]) + (List.rev items) + +(* 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. *) +let rec quasiquote (f : Form.t) : Form.t = + match f.Form.v with + | Form.List [ { Form.v = Form.Sym "quasiquote"; _ }; x ] -> quote x + | Form.List xs -> Form.make (Form.List (List.map quasiquote xs)) f.Form.loc + | 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 diff --git a/lib/parse.ml b/lib/parse.ml index 1fc6dfd..6ceaccb 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -296,9 +296,14 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr = (* The reader now produces these three, so they arrive here as ordinary heads and would fall through to Call — coming back from the checker as "unknown name quasiquote", which says nothing about what is actually missing. *) + (* [Expand.quasiquote] runs over every form on the way into [program] and + [decl], so a quasiquote is gone before this file looks at it and this arm + cannot be reached by anything that came through either. It is kept as the + backstop for the path that did not: a form built by hand and handed + straight to [expr]. *) | Sym "quasiquote" -> - fail f "`x is read, but not expanded: macro expansion is not wired up yet \ - (NEXT.md says what it needs)" + fail f "a quasiquote reached the parser undesugared, which means this form \ + did not come through Parse.program or Parse.decl" (* Not a milestone, a mistake: these two mean nothing anywhere else, and the reader cannot tell, because it does not track where it is. *) @@ -311,13 +316,6 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr = | Sym "defmacro" -> fail f "defmacro is a top-level declaration, not an expression" - (* Neither a reader token nor a special form: an ordinary function that a - macro body calls while the macro runs. There is nowhere for it to run - yet, so it says that rather than arriving as an unknown name. *) - | Sym "gensym" -> - fail f "gensym is only meaningful inside a macro body, and macro expansion \ - is not wired up yet (NEXT.md says what it needs)" - (* Recognised, deliberately unimplemented. Rejected rather than left to fall through to Call, where they would parse and mean nothing. *) | Sym ("handler-case" @@ -760,25 +758,38 @@ let rec decl types (f : Form.t) : Ast.decl = | [ n; t; v ] -> mk (Ast.Defconst (sym n, Some (texpr t), expr v)) | _ -> fail f "defconst is (defconst name Type? value)") - (* Checked for shape and then refused, which is deliberate. Getting the shape - wrong and getting the whole feature are two different mistakes, and a - "defmacro is (defmacro ...)" that only ever fired after expansion landed - would be a rule nothing enforced in the meantime. + (* 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. - The refusal is not about parsing. Expanding a macro means running it, and - there is no interpreter — the compiled path is the only backend. So it - means compiling the macro and dlopening it into the compiler, which is - what Emit.redefinition and Build.shared already do for the dev loop. - NEXT.md writes down how that goes together. *) + 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. + + 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. *) | List ({ v = Sym "defmacro"; _ } :: args) -> (match args with - | n :: { v = Form.Vec ps; _ } :: body when body <> [] -> - let name = sym n in + | n :: { v = Form.Vec [ p ]; _ } :: body when body <> [] -> + 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 } ]; + ret = Some form_t; fbody = body_of body; nloc = n.loc }) + | _ :: { v = Form.Vec ps; _ } :: body when body <> [] -> List.iter (fun (p : Form.t) -> ignore (sym p)) ps; fail f - "defmacro %s parses, but is not expanded: running a macro means \ - compiling it and loading it into the compiler, which is not wired \ - up yet (NEXT.md says what it needs)" name + "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 ...)") @@ -902,11 +913,37 @@ let prelude_types = let declared_types (forms : Form.t list) : Names.t = types_in (Lazy.force prelude_types) forms +(* Macro expansion, which runs over [Form] and therefore before anything in + this file. It cannot be called directly: expanding a macro means compiling + it and dlopening it, so the expander sits above [Check] and [Build] and this + module sits below them. [Macro] fills this in, and lib/dune passes -linkall + so that it always has -- an executable that links the library gets the + installation whether or not it names the module. + + The default is the identity because [Macro] is what knows which names are + macros; with nothing installed, a call to one arrives at the checker as an + unknown name, which is wrong but not silent. *) +let expander : (Form.t list -> Form.t list) ref = ref (fun fs -> fs) + let program (forms : Form.t list) : Ast.decl list = + (* Quasiquote first and always, because it is pure and needs nothing loaded: + it is what turns a macro body into ordinary code, and the prelude's own + macros have to parse in a process that has not built a macro module yet. + Then expansion, which may need one. *) + let forms = !expander (List.map Expand.quasiquote forms) in let types = declared_types forms in temps := 0; List.map (decl types) forms (* Single-declaration entry point, for tests and the REPL. Sees the builtin and prelude types plus whatever this one form declares. *) -let decl (f : Form.t) : Ast.decl = temps := 0; decl (declared_types [ f ]) f +let decl (f : Form.t) : Ast.decl = + temps := 0; + match !expander [ Expand.quasiquote f ] with + | [ f ] -> decl (declared_types [ f ]) f + | fs -> + (* One declaration in, one out. A macro at the top level would break that, + and there is no top-level macro call: [decl] dispatches on the head and + a macro name is not one of the heads it knows. *) + Loc.fail f.loc "expanding this declaration produced %d of them" + (List.length fs) diff --git a/test/test_flan.ml b/test/test_flan.ml index 991a142..9fc5449 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -389,12 +389,23 @@ let () = parse_rejects "restart-case" "(restart-case body (r [] 1))"; parse_rejects "loop/recur" "(loop [x 1] (recur x))"; - (* ── Macros: the front half is here, the expander is not ───────── *) - (* Was "unknown top-level form (defmacro ...)" — refused, but not by name and - with no reason, which is the hole the house rule had at the top level. *) - parse_rejects "defmacro declaration" "(defmacro m [x] x)" - ~needle:"not expanded"; - (* Shape and feature are separate mistakes and get separate reasons. *) + (* ── Macros ─────────────────────────────────────────────────────── *) + (* A defmacro is a defn. There is no Ast.Defmacro and there is not going to + 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 + | 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"; + (* 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)" @@ -404,18 +415,45 @@ let () = parse_rejects "defmacro in expression position" "(defn f [] (defmacro m [] 1))" ~needle:"top-level declaration"; - (* The reader now hands these three to the parser, so each says what is - actually wrong rather than arriving at the checker as an unknown name. *) - parse_rejects "quasiquote in a function" "(defn f [] `(a b))" - ~needle:"not expanded"; + (* Quasiquote is a desugaring over Form, and it has already run by the time + the parser sees anything, so what is written here is what a macro body + actually compiles to: the prelude's three form-building functions and + nothing else. Spelled out rather than described, because the desugaring + *is* the contract with the prelude. *) + let desugars name src want = + match read src with + | [ f ] -> + let got = Form.to_string (Expand.quasiquote f) in + if got <> want then begin + incr failures; + Printf.printf "FAIL %s\n got: %s\n wanted: %s\n" + name got want + end + | _ -> check (name ^ ": one form") false + in + desugars "a quasiquoted list is form-cons over Form nodes" "`(a ~b)" + "(Form.List {.xs (form-cons (Form.Sym {.s \"a\"}) (form-cons b (form-nil)))})"; + desugars "a splice is form-append" "`(a ~@bs)" + "(Form.List {.xs (form-cons (Form.Sym {.s \"a\"}) (form-append bs (form-nil)))})"; + (* A vector keeps its bracket through the desugaring: a binding vector is the + commonest thing a macro builds and Form.Vec is not Form.List. *) + desugars "a quasiquoted vector stays a vector" "`[~x 1]" + "(Form.Vec {.xs (form-cons x (form-cons (Form.Int {.i 1}) (form-nil)))})"; + (* Levels are not counted -- not by the reader, deliberately, and not here, + which is why the inner one is refused by name rather than given a meaning + nobody chose. *) + parse_rejects "a quasiquote inside a quasiquote" "(defn f [] Form `(a `(b)))" + ~needle:"quasiquote inside a quasiquote"; (* 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. *) parse_rejects "unquote outside a quasiquote" "(defn f [] ~x)" ~needle:"means nothing outside a quasiquote"; parse_rejects "splice where a splice makes no sense" "(defn f [] (+ 1 ~@xs))" ~needle:"splices only into a list or a vector"; - parse_rejects "gensym outside a macro" "(defn f [] (gensym))" - ~needle:"only meaningful inside a macro body"; + (* A splice with no bracket around it. The quasiquote is real here, so this + one is the desugaring's refusal and not the parser's. *) + parse_rejects "splice not inside a bracket" "(defn f [] Form `~@xs)" + ~needle:"nothing here for it to splice into"; (* ── Malformed syntax is caught with a location ────────────────── *) parse_rejects "odd let bindings" "(let [a])";