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
|
fail loc
|
||||||
"embed is (embed \"path\") for a [u8], or (embed \"path\" string)")
|
"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" ->
|
| "embed-dir" ->
|
||||||
arity loc name 1 args;
|
arity loc name 1 args;
|
||||||
let arg = List.hd args in
|
let arg = List.hd args in
|
||||||
@ -5330,6 +5365,10 @@ let builtins : (string * string * string) list =
|
|||||||
("embed-dir", "embed-dir [\"path\"] [n EmbedFile]",
|
("embed-dir", "embed-dir [\"path\"] [n EmbedFile]",
|
||||||
"Every file in the directory, read at compile time, as a fixed array of \
|
"Every file in the directory, read at compile time, as a fixed array of \
|
||||||
EmbedFile. It does not descend.");
|
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 *)
|
(* files *)
|
||||||
("slurp", "slurp [string Allocator?] (Vec u8)",
|
("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_of (l : loaded) (loc : Loc.t) =
|
||||||
let dir = Filename.dirname loc.Loc.file in
|
let dir = Filename.dirname loc.Loc.file in
|
||||||
let dir = if String.equal dir "." then "" else dir in
|
let dir = if String.equal dir "." then "" else dir in
|
||||||
match Dynload.dl_sym l.handle "flan_macro_dir" with
|
(* Not guarded. The symbol is in the module this just built, so its absence
|
||||||
| exception _ -> ()
|
means [runtime/flan_rt.c] and this file have come apart — and the shape
|
||||||
| buf ->
|
that failure would take if it were swallowed is a relative path resolving
|
||||||
let n = Dynload.dl_sym l.handle "flan_macro_dir_n" in
|
against the compiler's working directory, which reads some *other* file
|
||||||
(* 4096 is FLAN_PATH_MAX, and a path at or over it is left unset rather
|
and says nothing. A missing symbol raises out of [dl_sym] instead. *)
|
||||||
than truncated: half a directory is a path that resolves to the wrong
|
let buf = Dynload.dl_sym l.handle "flan_macro_dir" in
|
||||||
file, where none at all resolves to none. *)
|
let n = Dynload.dl_sym l.handle "flan_macro_dir_n" in
|
||||||
if String.length dir > 0 && String.length dir < 4096 then begin
|
(* 4096 is FLAN_PATH_MAX, and a path at or over it is left unset rather than
|
||||||
Dynload.poke_bytes buf 0 dir;
|
truncated: half a directory is a path that resolves to the wrong file,
|
||||||
Dynload.poke_i64 n 0 (Int64.of_int (String.length dir))
|
where none at all resolves to none. *)
|
||||||
end
|
if String.length dir > 0 && String.length dir < 4096 then begin
|
||||||
else Dynload.poke_i64 n 0 0L
|
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 ──────────────────────────────────────────────────────
|
(* ── The walk ──────────────────────────────────────────────────────
|
||||||
Bottom up: a macro's arguments are expanded before it is called, so nothing
|
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 ...)")
|
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
|
| 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)
|
| _ -> 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
|
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
|
bad one costs nothing and cannot lose its place. Inside a declaration there
|
||||||
is no such landmark, so one bad [defn] is one error. *)
|
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 =
|
let parse_forms ~keep_going (forms : Form.t list) : Ast.decl list =
|
||||||
(* Quasiquote first and always, because it is pure and needs nothing loaded:
|
(* 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
|
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.
|
macros have to parse in a process that has not built a macro module yet.
|
||||||
Then expansion, which may need one. *)
|
Then expansion, which may need one. *)
|
||||||
let forms = !expander (List.map Expand.quasiquote forms) in
|
let forms = !expander (List.map Expand.quasiquote forms) in
|
||||||
|
let forms = List.concat_map splice forms in
|
||||||
temps := 0;
|
temps := 0;
|
||||||
let s = Loc.sink ~on:keep_going in
|
let s = Loc.sink ~on:keep_going in
|
||||||
let decls = List.filter_map (fun f -> Loc.caught s (fun () -> decl f)) forms 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