diff --git a/lib/parse.ml b/lib/parse.ml index c5db730..dc166f4 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -266,6 +266,32 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr = and they need argument marshalling and a runtime arity check that \ this version does not do") + (* ── macros ────────────────────────────────────────────────────── *) + (* 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. *) + | Sym "quasiquote" -> + fail f "`x is read, but not expanded: macro expansion is not wired up yet \ + (NEXT.md says what it needs)" + + (* Not a milestone, a mistake: these two mean nothing anywhere else, and the + reader cannot tell, because it does not track where it is. *) + | Sym "unquote" -> + fail f "~x means nothing outside a quasiquote" + | Sym "unquote-splicing" -> + fail f "~@x means nothing outside a quasiquote, and splices only into a \ + list or a vector" + + | 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" @@ -275,7 +301,7 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr = the restart stack without committing to one. *) | "find-restart" | "compute-restarts" | "errdefer" | "with-allocator" | "loop" | "recur" - | "defmacro" | "await" as name) -> + | "await" as name) -> fail f "%s is not implemented yet (see the build sequence in plan.org)" name (* ── field access: (.pos c) ────────────────────────────────────── *) @@ -480,6 +506,28 @@ 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. + + 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. *) + | List ({ v = Sym "defmacro"; _ } :: args) -> + (match args with + | n :: { v = Form.Vec ps; _ } :: body when body <> [] -> + let name = sym n in + 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 + | _ -> + fail f "defmacro is (defmacro name [param ...] body ...)") + | 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) diff --git a/test/test_flan.ml b/test/test_flan.ml index 12c2e60..54a7edc 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -179,10 +179,19 @@ let parse_decl src = | [ f ] -> Parse.decl f | _ -> failwith "test source must be exactly one form" -let parse_rejects name src = +(* [needle] again: the house rule is that an unimplemented form is refused by + name with the reason, so a test that only proves *something* failed does not + observe the rule it is there for. *) +let parse_rejects ?needle name src = match Reader.read_all ~file:"" src |> Parse.program with | _ -> incr failures; Printf.printf "FAIL %s: expected a parse error\n" name - | exception Loc.Error _ -> () + | exception Loc.Error (_, msg) -> + (match needle with + | Some n when not (contains msg n) -> + incr failures; + Printf.printf "FAIL %s: wrong reason\n wanted: %s\n got: %s\n" + name n msg + | _ -> ()) let () = let open Ast in @@ -291,7 +300,34 @@ let () = parse_rejects "handler-bind" "(handler-bind [E h] body)"; parse_rejects "restart-case" "(restart-case body (r [] 1))"; parse_rejects "loop/recur" "(loop [x 1] (recur x))"; - parse_rejects "defmacro" "(defmacro m [] 1)"; + + (* ── 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. *) + 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"; + 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"; + (* 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"; (* ── Malformed syntax is caught with a location ────────────────── *) parse_rejects "odd let bindings" "(let [a])";