flan/lib/expand.ml

517 lines
24 KiB
OCaml

(** Macro expansion: the pass between the reader and [Parse].
There is no interpreter and there is not going to be one (docs/BUILT.md, "Why
there is no interpreter"), so running a macro at compile time means
compiling it and loading it into this process. Every piece of that is
already built and measured — [Emit.macro_thunk], [Build.macro_module],
[Dynload] — and this file is the two halves nobody had written: the image
format the two sides share, and the walk that finds macro calls and
replaces them.
Expansion runs over [Form], before [Parse]. Not over [Ast]: [Parse] refuses
[defmacro] outright and there is no [Ast.Defmacro], so an Ast-level pass
would have nothing to work with. That refusal is the ordering. It is also
Clojure's ordering, and it is why a macro expanding to a special form is
ordinary here rather than a special case. *)
(* ── The image format ──────────────────────────────────────────────
A Form is { i32 tag, [2 x i64] payload }: 24 bytes, align 8, payload at
offset 8. Those three numbers are the whole agreement between this file and
the compiled macro, and they are not taken on trust — test_acceptance.ml's
"Form's image format" asks LLVM for each of them through the same ptrtoint
oracle the DWARF offsets go through. Change the prelude's defdata and that
test says which number moved.
The tag is the case's position in the prelude's (defdata Form ...), which
is why that list is a layout contract and says so. *)
let form_size = 24
let payload = 8
(* A string and a slice are both %slice = { ptr, i64 }: two words at the start
of the payload. Every case of Form holds one member, so there is no third
offset anywhere below. *)
let ptr_off = payload
let len_off = payload + 8
type tag =
| TSym | TKw | TInt | TFloat | TStr | TByte | TList | TVec | TMap
let tag_int = function
| TSym -> 0l | TKw -> 1l | TInt -> 2l | TFloat -> 3l | TStr -> 4l
| TByte -> 5l | TList -> 6l | TVec -> 7l | TMap -> 8l
let tag_of_int = function
| 0l -> TSym | 1l -> TKw | 2l -> TInt | 3l -> TFloat | 4l -> TStr
| 5l -> TByte | 6l -> TList | 7l -> TVec | 8l -> TMap
| n ->
failwith
(Printf.sprintf
"a macro returned a Form with tag %ld, and Form has nine cases. The \
prelude's (defdata Form ...) and lib/expand.ml's tag list are one \
contract and have come apart"
n)
(* ── Writing a Form into memory a macro can read ───────────────────
OCaml cannot address raw memory, so this goes through the poke family in
dynload_stubs.c, one field at a time. Everything allocated here is owned by
[Dynload] and released together after the call. *)
(* ── Which node a pointer belongs to ───────────────────────────────
A [Form] on the wire has no [loc] field and is not getting one. What it does
have, for every case but the three that fit inside the payload, is a
pointer: a string's bytes, or a bracket's children. This compiler allocated
that memory, so the address names the node it was written for — and when a
macro splices one of its arguments through untouched, the 24-byte struct is
copied but the pointer inside it is not. The address is the part that
survives the trip, so it is what a form coming back out is recognised by.
That is SBCL's [*source-paths*] (src/compiler/ir1tran.lisp), an EQ table
from the conses of a form to where they were read, which still answers after
a macro splices those same conses into its expansion. The address stands in
for [eq] because across a C ABI nothing else can: two sides that share no
heap share no notion of identity but the pointer.
One table per call, made in [call] and gone when it returns. A macro that
keeps a form from one call and answers with it in another gets a miss, and a
miss is the harmless direction. *)
type sites = (Dynload.addr, Loc.t) Hashtbl.t
(* ── A wide literal's round trip ───────────────────────────────────
A macro's [Form] has one integer case, so a literal at or above 2^63 crosses
as its bit pattern in [Int]'s payload. What marks it as wide is the second
payload word, which [Int] does not use and which a macro that passes the
form through copies along with the rest of its 24 bytes: [write] puts a
token there naming the literal's spelling in this table, and [unmarshal]
turns a node carrying one back into the [UInt] that went in. An [Int] the
macro built itself has no token, and is the [Int] it says it is. One table
per call, as [sites] is. *)
let wide_mark = 0x5749444500000000L
let wides : (int64, int64 * string) Hashtbl.t ref = ref (Hashtbl.create 1)
(* Into an existing 24 bytes, which is what an argument array needs: the macro
takes a [Form] slice, and a slice is contiguous elements and not an array of
pointers — so this writes *into* memory the caller took, and every caller
here takes it as part of an array. *)
let rec write (sites : sites) p (f : Form.t) =
let tag t = Dynload.poke_i32 p 0 (tag_int t) in
let note b = Hashtbl.replace sites b f.Form.loc in
let str t s =
tag t;
let n = String.length s in
(* A zero-length string still gets a pointer, because a slice with a null
base is not the same value as one with a live base and a zero length --
the difference shows the day something concatenates onto it. It also
keeps every node's key distinct, which the table above depends on. *)
let b = Dynload.take (max n 1) in
if n > 0 then Dynload.poke_bytes b 0 s;
note b;
Dynload.poke_ptr p ptr_off b;
Dynload.poke_i64 p len_off (Int64.of_int n)
in
let seq t xs =
tag t;
let n = List.length xs in
let b = Dynload.take (max (n * form_size) 1) in
List.iteri
(fun i x -> write sites (Nativeint.add b (Nativeint.of_int (i * form_size))) x)
xs;
note b;
Dynload.poke_ptr p ptr_off b;
Dynload.poke_i64 p len_off (Int64.of_int n)
in
match f.Form.v with
| Form.Sym s -> str TSym s
| Form.Kw s -> str TKw s
| Form.Str s -> str TStr s
| Form.Int i -> tag TInt; Dynload.poke_i64 p payload i
(* Crosses as an [Int] carrying a token; see [wides]. *)
| Form.UInt (i, text) ->
tag TInt;
Dynload.poke_i64 p payload i;
let token = Int64.logor wide_mark (Int64.of_int (Hashtbl.length !wides)) in
Hashtbl.replace !wides token (i, text);
Dynload.poke_i64 p len_off token
| Form.Float x -> tag TFloat; Dynload.poke_f64 p payload x
| Form.Byte b -> tag TByte; Dynload.poke_i32 p payload (Int32.of_int b)
| Form.List xs -> seq TList xs
| Form.Vec xs -> seq TVec xs
| Form.Map xs -> seq TMap xs
(* ── Reading one back ──────────────────────────────────────────────
Three ways a node gets a location, and which one applies is decided by the
table above.
A hit is a form the author wrote: it went in on some line, the macro passed
it through, and it comes back with the pointer it went in with. It keeps its
own line, tagged with the macro whose call it was written inside — so the
error is reported where the code is and the [note:] still says which call
put it there.
A miss is a node the macro built, and it has no line of its own anywhere. It
takes the nearest enclosing node that did have one, which is the smallest
piece of what the author wrote that contains it. Only a node with no located
ancestor at all falls back to the call site, and that means the whole
subtree was the macro's invention.
[Int], [Float] and [Byte] are always a miss, because their payload is the
value and there is no pointer to ask about — reading one as an address would
be a number pretending to be a node. So a bad [5] in [(foo (bar 5))] is
reported at [(bar 5)], the nearest thing that has a place. That is the same
line SBCL draws: its [source-form-has-path-p] excludes symbols, fixnums and
characters for the same reason. *)
let rec unmarshal ~(sites : sites) ~loc (p : Dynload.addr) : Form.t =
let ptr () = Dynload.peek_ptr p ptr_off in
let len () = Int64.to_int (Dynload.peek_i64 p len_off) in
let str () = let n = len () in if n = 0 then "" else Dynload.peek_bytes (ptr ()) 0 n in
(* The node's own location, or the one it inherits. [Loc.from_macro] is
outermost-wins and the call site is already tagged, so a form that came
through two expansions keeps the name of the macro the author wrote. *)
let here () =
match Hashtbl.find_opt sites (ptr ()) with
| None -> loc
| Some own ->
(match loc.Loc.macro with
| None -> own
| Some m -> Loc.from_macro ~at:(Loc.call_site loc) m own)
in
let seq loc =
let b = ptr () and n = len () in
List.init n (fun i ->
unmarshal ~sites ~loc (Nativeint.add b (Nativeint.of_int (i * form_size))))
in
match tag_of_int (Dynload.peek_i32 p 0) with
| TInt ->
let i = Dynload.peek_i64 p payload in
(match Hashtbl.find_opt !wides (Dynload.peek_i64 p len_off) with
| Some (w, text) when Int64.equal w i -> Form.make (Form.UInt (i, text)) loc
| _ -> Form.make (Form.Int i) loc)
| TFloat -> Form.make (Form.Float (Dynload.peek_f64 p payload)) loc
| TByte ->
(* A char is a code point; anything that is not a scalar value keeps the
byte it always was. *)
let b = Int32.to_int (Dynload.peek_i32 p payload) in
Form.make (Form.Byte (if Uchar.is_valid b then b else b land 0xff)) loc
| TSym -> Form.make (Form.Sym (str ())) (here ())
| TKw -> Form.make (Form.Kw (str ())) (here ())
| TStr -> Form.make (Form.Str (str ())) (here ())
| TList -> let l = here () in Form.make (Form.List (seq l)) l
| TVec -> let l = here () in Form.make (Form.Vec (seq l)) l
| TMap -> let l = here () in Form.make (Form.Map (seq l)) l
(* ── One call ──────────────────────────────────────────────────────
The arguments are one contiguous run of Forms, not an array of pointers,
because the macro's parameter is [[Form]] and a Flan slice is { ptr, len }
over elements. *)
let call ~loc (fn : Dynload.addr) (args : Form.t list) : Form.t =
let n = List.length args in
(* This call's, and no other's. Nothing [Dynload] hands out is freed before
the whole expansion is over — [Dynload.release] runs between the rounds in
[Macro] and on the way out of one — so no address recorded here can be
handed to a second allocation while the table still holds it, and the
table is dropped the moment this returns either way. *)
let sites : sites = Hashtbl.create 8 in
wides := Hashtbl.create 1;
let a = Dynload.take (max (n * form_size) 1) in
List.iteri
(fun i x -> write sites (Nativeint.add a (Nativeint.of_int (i * form_size))) x)
args;
let out = Dynload.take form_size in
Dynload.call fn a (Int64.of_int n) out;
unmarshal ~sites ~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, done in [quote] below. A macro that writes a
macro is the only thing that wants a quasiquote inside a quasiquote. *)
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
(* [depth] is how many quasiquotes enclose [f], counting the one being
desugared as 1 — SBCL's [*backquote-depth*] in src/code/backq.lisp. A
quasiquote inside raises it and an unquote lowers it, so an unquote belongs
to the innermost quasiquote around it and [~~x] reaches out two. Only the
unquotes at depth 1 are evaluated now; the rest are data, rebuilt as the
(unquote x) and (quasiquote x) forms they were read as, for the macro the
output defines to desugar in its turn. *)
let rec quote ?(depth = 1) (f : Form.t) : Form.t =
let loc = f.Form.loc in
(* (head x) as a Form, [x] already desugared. *)
let wrapped head x =
node loc "List" "xs"
(lst loc [ sym loc "form-cons"; node loc "Sym" "s" (Form.Str head);
lst loc [ sym loc "form-cons"; x; lst loc [ sym loc "form-nil" ] ] ]).Form.v
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 when depth = 1 -> x
| Some x -> wrapped "unquote" (quote ~depth:(depth - 1) x)
| None ->
match splice_of f with
| Some _ when depth = 1 ->
Loc.fail loc
"~@x splices into a list or a vector, and there is nothing here for it \
to splice into"
| Some x -> wrapped "unquote-splicing" (quote ~depth:(depth - 1) x)
| None ->
match f.Form.v with
| Form.List [ { Form.v = Form.Sym "quasiquote"; _ }; x ] ->
wrapped "quasiquote" (quote ~depth:(depth + 1) x)
| Form.Sym s -> node loc "Sym" "s" (Form.Str s)
| Form.Kw s -> node loc "Kw" "s" (Form.Str s)
| Form.Int i | Form.UInt (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 ~depth loc xs).Form.v
| Form.Vec xs -> node loc "Vec" "xs" (seq ~depth loc xs).Form.v
| Form.Map xs -> node loc "Map" "xs" (seq ~depth 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. A splice deeper than [depth] 1 is
data like any other item. *)
and seq ~depth loc items =
List.fold_left
(fun acc (item : Form.t) ->
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. *)
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
(* ── A macro's parameter list ──────────────────────────────────────
[(defmacro do-grid [[r rows c cols] & body] ...)] — positional parameters,
a destructuring vector wherever one is written, and [&] for the tail. The
grammar is [dvec]'s (lib/parse.ml), read over [Form] instead of over the
values a [let] binds, because a macro's arguments *are* Forms.
It lives here rather than in [Parse] because both sides of the feature need
it and they are on opposite sides of the parser: [Parse] turns the list into
the bindings a macro body opens with, and [Macro] checks a call against it
before the macro is ever run. This file is below both and depends on nothing
above [Form], which is what lets them share one reading of the list.
Map destructuring is not here. [dmap] is [{:keys [x y]}] over a *struct*, and
a macro's argument is a [Form] whose [Map] case is a flat list of alternating
forms with no field names in it at all — the pattern would have to mean
something new rather than the same thing over a different value. Refused by
name below, and written down in TODO.org, "Map destructuring in a macro's
parameter list". *)
type pat =
| Pname of string * Loc.t
(* A [ ] in the parameter list: the argument at this position must be a
[Form.Vec], and its elements are matched against these in turn. *)
(* The [Form] is the pattern as written: a refusal shows the shape the call
failed to match, and nothing else can render it back. *)
| Pvec of pat list * (string * Loc.t) option * Form.t
type msig = {
ps : pat list; (* the positional parameters, in order *)
rest : (string * Loc.t) option; (* [& name], if there is one *)
src : Form.t; (* the list as written, for the messages *)
}
(* [a b & rest], shared by the top level and by every destructuring vector
inside it. The three refusals are [dvec]'s, word for word where they say the
same thing: one grammar, so one set of sentences about getting it wrong. *)
let split_amp (items : Form.t list) : Form.t list * (string * Loc.t) option =
let rec go acc = function
| [] -> (List.rev acc, None)
| ({ Form.v = Form.Sym "&"; _ } as amp) :: rest ->
(match rest with
| [ { Form.v = Form.Sym r; loc } ] -> (List.rev acc, Some (r, loc))
| [] -> Loc.fail amp.Form.loc "& needs a name after it, as in [a b & rest]"
| [ bad ] ->
Loc.fail bad.Form.loc
"& binds one name for the rest of the arguments, and %s is not one \
— the rest is a slice of forms, so it cannot be destructured \
further"
(Form.to_string bad)
| _ :: extra :: _ ->
Loc.fail extra.Form.loc
"& takes one name and it is the last thing in the parameter list")
| x :: rest -> go (x :: acc) rest
in
go [] items
(* Never handed a [&]: every list of items reaching here has been through
[split_amp], which stops at the first one and refuses every way of getting
the tail wrong itself. So there is no arm for it and no sentence about it. *)
let rec pat_of (f : Form.t) : pat =
match f.Form.v with
| Form.Sym s -> Pname (s, f.Form.loc)
| Form.Vec items ->
let elems, rest = split_amp items in
(match elems, rest with
| [], None ->
Loc.fail f.Form.loc
"an empty pattern [] in a macro's parameter list binds nothing — \
write the names it should bind"
| [], Some (r, loc) ->
Loc.fail loc
"[& %s] binds the whole vector — write %s on its own instead of a \
pattern" r r
| _ -> ());
Pvec (List.map pat_of elems, rest, f)
| Form.Map _ ->
Loc.fail f.Form.loc
"map destructuring is not implemented in a macro's parameter list — a \
macro's argument is a Form, whose Map case is a flat run of alternating \
forms with no fields to name. Take the form and pick it apart in the \
body"
| _ ->
Loc.fail f.Form.loc
"a macro's parameter is a name or a [ ] pattern over one, and %s is \
neither"
(Form.to_string f)
(* Every name the list binds, so that two of them can be refused where they are
written rather than reaching the checker as a local declared twice. *)
let rec pat_names acc = function
| Pname (s, loc) -> (s, loc) :: acc
| Pvec (ps, rest, _) ->
let acc = List.fold_left pat_names acc ps in
(match rest with None -> acc | Some nl -> nl :: acc)
let params_of (v : Form.t) : msig =
let items = match v.Form.v with
| Form.Vec items -> items
| _ ->
Loc.fail v.Form.loc "a macro's parameter list is written in [ ], and %s is not"
(Form.to_string v)
in
let elems, rest = split_amp items in
let sg = { ps = List.map pat_of elems; rest; src = v } in
let names = List.fold_left pat_names [] sg.ps in
let names =
match sg.rest with None -> names | Some nl -> nl :: names in
let seen = Hashtbl.create 8 in
List.iter
(fun (n, loc) ->
if Hashtbl.mem seen n then
Loc.fail loc "%s is bound twice in this parameter list" n
else Hashtbl.add seen n ())
(List.rev names);
sg
(* ── Checking a call against it ────────────────────────────────────
Before expansion, so the refusal is about the call as it is written and
points at it. That is the whole reason this is a separate pass rather than
something the macro body could do: a macro has no error facility, and a call
that does not fit the parameter list is a mistake in the call rather than in
anything the expansion would go on to produce. *)
let written (sg : msig) = Form.to_string sg.src
let arity (sg : msig) ~name ~loc (args : Form.t list) =
let n = List.length sg.ps in
let got = List.length args in
let plural k = if k = 1 then "argument" else "arguments" in
match sg.rest with
| Some (r, _) when got < n ->
Loc.fail loc
"%s takes at least %d %s and this call gives %d — its parameter list is \
%s, where &%s is the rest"
name n (plural n) got (written sg) r
| Some _ -> ()
| None when got <> n ->
Loc.fail loc
"%s takes %d %s and this call gives %d — its parameter list is %s"
name n (plural n) got (written sg)
| None -> ()
let rec check_pat ~name (p : pat) (a : Form.t) =
match p with
| Pname _ -> ()
| Pvec (ps, rest, src) ->
let items =
match a.Form.v with
| Form.Vec items -> items
| _ ->
Loc.fail a.Form.loc
"%s destructures this argument with %s, so a [ ] belongs here and \
%s was written"
name (Form.to_string src) (Form.to_string a)
in
let n = List.length ps in
let got = List.length items in
if (rest = None && got <> n) || got < n then
Loc.fail a.Form.loc
"%s destructures this argument with %s, which takes %s%d, and %d %s \
written here"
name (Form.to_string src)
(if rest = None then "" else "at least ")
n got (if got = 1 then "is" else "are");
List.iteri (fun i q -> check_pat ~name q (List.nth items i)) ps
let check_call ~name ~loc (sg : msig) (args : Form.t list) =
arity sg ~name ~loc args;
List.iteri (fun i p -> check_pat ~name p (List.nth args i)) sg.ps