One macro call, several declarations, and a refusal that carries a sentence
A type provider produces a struct and a reader over it, and a struct per nesting level in the data. Expansion is form-for-form, so one call could only ever become one declaration — which was enough while every macro expanded to an expression. A top-level (do ...) is now its items, spliced in place, after expansion and before the declaration walk. Nobody writes one in a file, and the single-declaration entry point says so by name for anyone who tries. And (compile-error "...") is what a macro expands to when it has to refuse. The prelude's `unless` records the gap: a macro has no error facility, so a malformed call answers a name nothing defines and the report is the right place with the wrong sentence. A name carries a name. A type provider's refusals are all sentence — the third element of this vector is a string where the first two were integers, at line 3 column 9 of a file the compiler is not reading — and no symbol an expansion could invent holds that. Loc.from_macro already stamps the call site onto the expansion, so the location is the form the author wrote. A builtin because it has to fail while checking: a declared function would compile, link and run, and the compile it was meant to stop would have succeeded.
This commit is contained in:
parent
32ff350001
commit
a2004ae7a6
39
lib/check.ml
39
lib/check.ml
@ -4289,6 +4289,41 @@ and named_call ctx ~want loc name args =
|
||||
| _ ->
|
||||
fail loc
|
||||
"embed is (embed \"path\") for a [u8], or (embed \"path\" string)")
|
||||
(* ── What a macro says when it has to refuse ───────────────────
|
||||
The one thing a macro could not do, written down in the prelude where
|
||||
[unless] settles for it: "a macro has no error facility: it runs inside
|
||||
the compiler and anything it signals aborts the compile with no location.
|
||||
So a malformed (unless) answers a name 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."
|
||||
|
||||
A name nothing defines carries a name. It cannot carry a sentence, and a
|
||||
type provider's refusals are all sentence: the third element of this
|
||||
vector is a string where the first two were integers; there is no file at
|
||||
assets/x.edn; :size is a map with keys of two kinds. Those name a position
|
||||
in a *data* file, which no symbol the expansion could invent will hold.
|
||||
|
||||
So a macro that has to refuse expands to a call to this, and the string is
|
||||
the report. [Loc.from_macro] has already stamped the call site onto every
|
||||
node of the expansion, so the location is the [defedn] the author wrote
|
||||
and the sentence is the macro's — which is the two halves the prelude's
|
||||
note says are never both right at once.
|
||||
|
||||
A builtin and not a declaration, because it has to fail *here*: a declared
|
||||
function would compile, link and run, and the compile it was meant to stop
|
||||
would have succeeded. The whole of it is one arm, and the argument is a
|
||||
literal for the same reason [embed]'s path is one — there is nothing at
|
||||
this point in a compile to compute a string from. *)
|
||||
| "compile-error" ->
|
||||
arity loc name 1 args;
|
||||
(match (List.hd args).Ast.e with
|
||||
| Ast.Str s -> fail loc "%s" s
|
||||
| _ ->
|
||||
fail (List.hd args).Ast.loc
|
||||
"compile-error takes a literal string — it is reported while the \
|
||||
program is being checked, so there is nothing here to build one \
|
||||
from. A macro that has to refuse builds the sentence as it expands \
|
||||
and puts it in the form")
|
||||
| "embed-dir" ->
|
||||
arity loc name 1 args;
|
||||
let arg = List.hd args in
|
||||
@ -5330,6 +5365,10 @@ let builtins : (string * string * string) list =
|
||||
("embed-dir", "embed-dir [\"path\"] [n EmbedFile]",
|
||||
"Every file in the directory, read at compile time, as a fixed array of \
|
||||
EmbedFile. It does not descend.");
|
||||
("compile-error", "compile-error [\"message\"] ()",
|
||||
"Refuses the compile with that message, at the form it is written in. \
|
||||
What a macro expands to when it has to say why: a name nothing defines \
|
||||
carries a name, and this carries a sentence.");
|
||||
|
||||
(* files *)
|
||||
("slurp", "slurp [string Allocator?] (Vec u8)",
|
||||
|
||||
27
lib/macro.ml
27
lib/macro.ml
@ -311,18 +311,21 @@ let compile (names : string list) (extra : Form.t list) : loaded =
|
||||
let dir_of (l : loaded) (loc : Loc.t) =
|
||||
let dir = Filename.dirname loc.Loc.file in
|
||||
let dir = if String.equal dir "." then "" else dir in
|
||||
match Dynload.dl_sym l.handle "flan_macro_dir" with
|
||||
| exception _ -> ()
|
||||
| buf ->
|
||||
let n = Dynload.dl_sym l.handle "flan_macro_dir_n" in
|
||||
(* 4096 is FLAN_PATH_MAX, and a path at or over it is left unset rather
|
||||
than truncated: half a directory is a path that resolves to the wrong
|
||||
file, where none at all resolves to none. *)
|
||||
if String.length dir > 0 && String.length dir < 4096 then begin
|
||||
Dynload.poke_bytes buf 0 dir;
|
||||
Dynload.poke_i64 n 0 (Int64.of_int (String.length dir))
|
||||
end
|
||||
else Dynload.poke_i64 n 0 0L
|
||||
(* Not guarded. The symbol is in the module this just built, so its absence
|
||||
means [runtime/flan_rt.c] and this file have come apart — and the shape
|
||||
that failure would take if it were swallowed is a relative path resolving
|
||||
against the compiler's working directory, which reads some *other* file
|
||||
and says nothing. A missing symbol raises out of [dl_sym] instead. *)
|
||||
let buf = Dynload.dl_sym l.handle "flan_macro_dir" in
|
||||
let n = Dynload.dl_sym l.handle "flan_macro_dir_n" in
|
||||
(* 4096 is FLAN_PATH_MAX, and a path at or over it is left unset rather than
|
||||
truncated: half a directory is a path that resolves to the wrong file,
|
||||
where none at all resolves to none. *)
|
||||
if String.length dir > 0 && String.length dir < 4096 then begin
|
||||
Dynload.poke_bytes buf 0 dir;
|
||||
Dynload.poke_i64 n 0 (Int64.of_int (String.length dir))
|
||||
end
|
||||
else Dynload.poke_i64 n 0 0L
|
||||
|
||||
(* ── The walk ──────────────────────────────────────────────────────
|
||||
Bottom up: a macro's arguments are expanded before it is called, so nothing
|
||||
|
||||
38
lib/parse.ml
38
lib/parse.ml
@ -1152,6 +1152,15 @@ let rec decl (f : Form.t) : Ast.decl =
|
||||
| _ ->
|
||||
fail f "defmacro is (defmacro name [param ...] body ...)")
|
||||
|
||||
(* Only reachable from the single-declaration entry point below: a file's
|
||||
forms go through [splice] first, and a [do] there is its items. Said by
|
||||
name because the two paths differ and the difference is not the author's
|
||||
fault to guess at. *)
|
||||
| List ({ v = Sym "do"; _ } :: _) ->
|
||||
fail f
|
||||
"a top-level (do ...) is several declarations spliced in place, and this \
|
||||
is a position that takes exactly one — a macro answering several is a \
|
||||
file's form, not an expression's"
|
||||
| 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)
|
||||
|
||||
@ -1236,12 +1245,41 @@ let with_imported ?(decls = []) (ms : Form.t list) (f : unit -> 'a) : 'a =
|
||||
here: the reader already found where each declaration ends, so skipping a
|
||||
bad one costs nothing and cannot lose its place. Inside a declaration there
|
||||
is no such landmark, so one bad [defn] is one error. *)
|
||||
(* ── One call, several declarations ────────────────────────────────
|
||||
Expansion is form-for-form: [Macro.expand_form] answers one [Form.t] per
|
||||
input and the loop below turns each into one [Ast.decl]. Every macro written
|
||||
until now expands to an *expression* — [unless], [into], raylib's [with-*] —
|
||||
so one-for-one was the whole of what was needed.
|
||||
|
||||
A type provider is the first thing that is not. [(defedn Tileset "t.edn")]
|
||||
has to produce the struct *and* the reader over it, and a nested map in the
|
||||
data means a struct per nesting level: three declarations and more from one
|
||||
form. There is no arrangement of one-for-one that reaches that.
|
||||
|
||||
So a [do] at the top level is its items, in place. It is the sequencing
|
||||
spelling the language already has, it is Clojure's answer to exactly this,
|
||||
and it is only ever reachable by a macro: nobody writes [(do (defn ...))] in
|
||||
a file, and the message below still says so for anyone who tries and wrote
|
||||
it wrong. Recursive, because a macro that splices what another macro
|
||||
answered has a [do] inside a [do] and the nesting is not the author's to
|
||||
flatten by hand.
|
||||
|
||||
It is spliced *after* expansion and before the declaration walk, so what is
|
||||
spliced is already fully expanded — a [do] holding a call to another macro
|
||||
settled before it got here. *)
|
||||
let rec splice (f : Form.t) : Form.t list =
|
||||
match f.Form.v with
|
||||
| Form.List ({ Form.v = Form.Sym "do"; _ } :: items) ->
|
||||
List.concat_map splice items
|
||||
| _ -> [ f ]
|
||||
|
||||
let parse_forms ~keep_going (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 forms = List.concat_map splice forms in
|
||||
temps := 0;
|
||||
let s = Loc.sink ~on:keep_going in
|
||||
let decls = List.filter_map (fun f -> Loc.caught s (fun () -> decl f)) forms in
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user