flan/lib/dynload.ml
Joseph Ferano 9e0ae3a4ce The compiler can dlopen now, and Form is declared; the expander is not written
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.
2026-09-12 17:18:26 +07:00

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 := []