Running a macro means compiling it and loading it into the compiler, and the step that reads as small in NEXT.md is not: OCaml has no dlopen for ELF, and lib/dune had no foreign_stubs. So the boundary is built first and the expander not at all. lib/dynload_stubs.c is the whole of it — dlopen, dlsym, a four-argument call into a macro thunk, and a peek/poke family, because OCaml cannot address the raw memory a Form image has to be laid out in. Nothing aggregate crosses to C. The unions lane verified a union's memory layout against clang, which is a different claim from LLVM's convention for an aggregate passed or returned by value in hand-written IR, so Emit.macro_thunk wraps every macro in void(ptr,i64,ptr,ptr): the slice is built and the result stored on the LLVM side, and the compiler's side is four pointers. Build.macro_module links the runtime in rather than declaring it external, so the module has no undefined symbols and the compiler's own link needs no -rdynamic. That is the difference from Build.shared, whose host is a running Flan program. defunion Form and the list-building surface quasiquote will desugar into are in the prelude. Form mirrors Form.value and not Form.t: no loc field, so the compiler stamps the call site's location onto everything a macro returns. The compiler builds. dune test was not run, and Form's layout is asserted nowhere — NEXT.md's new handoff section says what the three numbers are, what the next two commits should be, and the four decisions this made that the design did not settle.
49 lines
2.0 KiB
OCaml
49 lines
2.0 KiB
OCaml
(** The compiler's own dlopen, and raw memory to lay a [Form] out in.
|
|
|
|
Every function here is a stub in [dynload_stubs.c]; the comment at the top
|
|
of that file is the design. Addresses are [nativeint] because that is the
|
|
only OCaml type that is exactly a machine word and carries no tag bit. *)
|
|
|
|
type handle = nativeint
|
|
type addr = nativeint
|
|
|
|
external dl_open : string -> handle = "flan_dl_open"
|
|
external dl_sym : handle -> string -> addr = "flan_dl_sym"
|
|
external dl_close : handle -> unit = "flan_dl_close"
|
|
|
|
(** [call fn args n out] runs one macro: [args] is an array of [n] [Form]s,
|
|
[out] is room for the one it answers. *)
|
|
external call : addr -> addr -> int64 -> addr -> unit = "flan_macro_call"
|
|
|
|
external alloc : int -> addr = "flan_mem_alloc"
|
|
external free : addr -> unit = "flan_mem_free"
|
|
|
|
external poke_i32 : addr -> int -> int32 -> unit = "flan_poke_i32"
|
|
external poke_i64 : addr -> int -> int64 -> unit = "flan_poke_i64"
|
|
external poke_f64 : addr -> int -> float -> unit = "flan_poke_f64"
|
|
external poke_ptr : addr -> int -> addr -> unit = "flan_poke_ptr"
|
|
external poke_bytes : addr -> int -> string -> unit = "flan_poke_bytes"
|
|
|
|
external peek_i32 : addr -> int -> int32 = "flan_peek_i32"
|
|
external peek_i64 : addr -> int -> int64 = "flan_peek_i64"
|
|
external peek_f64 : addr -> int -> float = "flan_peek_f64"
|
|
external peek_ptr : addr -> int -> addr = "flan_peek_ptr"
|
|
external peek_bytes : addr -> int -> int -> string = "flan_peek_bytes"
|
|
|
|
(* Every allocation a macro call makes on this side, kept so the whole lot can
|
|
be released at once. A macro's *own* allocations are the macro process's --
|
|
which is this process -- and are leaked on purpose: a returned Form points
|
|
into them, and the compiler reads it after the call returns. An expansion is
|
|
bounded by the size of the program being compiled, so leaking it costs what
|
|
holding the program costs. *)
|
|
let owned : addr list ref = ref []
|
|
|
|
let take n =
|
|
let p = alloc n in
|
|
owned := p :: !owned;
|
|
p
|
|
|
|
let release () =
|
|
List.iter free !owned;
|
|
owned := []
|