The park waited on one flag and could do one thing, so C-x C-e on (+ 1 1) was refused for want of a frame boundary — an expression that needs nothing from the program, in a process holding every global the run left. It waits on two now. A re-run leaves the park; a wake drains the agent's ring and waits again, with the state still PARKED, which is what makes running the thunk there exactly as safe as running it at a frame boundary: while parked there is no concurrency to be unsafe against. The break loop is the precedent and CL's spawned worker is deliberately not copied. A thunk that stops now stops on a parked thread, so the restart ops refuse on whether a break is engaged rather than on the state, and a paused expression against the park is resumable.
246 lines
9.6 KiB
C
246 lines
9.6 KiB
C
/* Loading a compiled macro into the compiler's own process.
|
|
*
|
|
* NEXT.md's expander design: there is no interpreter, so running a macro means
|
|
* compiling it and dlopening it. The reload primitive does exactly this
|
|
* already, but its host is a running Flan program written in C; here the host
|
|
* is the OCaml compiler, which has no dlopen of its own -- Dynlink loads
|
|
* OCaml, not ELF. So the boundary needs stubs, and this is all of them.
|
|
*
|
|
* Two rules shape what is here:
|
|
*
|
|
* - Nothing but pointers and scalars crosses. A Flan `string`/slice is
|
|
* {ptr,len} and a `Form` is {i32, [2 x i64]}, and LLVM's calling
|
|
* convention for an aggregate passed or returned *by value* in hand-written
|
|
* IR is not promised to be clang's C ABI for the equivalent struct. The
|
|
* unions lane verified memory layout, so memory is the agreement we have:
|
|
* every macro is reached through a thunk taking (ptr,i64,ptr,ptr) and
|
|
* writing its result through the out pointer.
|
|
*
|
|
* - The macro module is self-contained: it links the runtime in and has no
|
|
* undefined Flan symbols, so the OCaml executable needs no -rdynamic and
|
|
* nothing in it has to be exported.
|
|
*
|
|
* The peek/poke family is how the marshaller writes a Form image into memory
|
|
* the macro can read. OCaml cannot address raw memory, so the bytes are laid
|
|
* out from here one field at a time.
|
|
*/
|
|
|
|
#include <caml/mlvalues.h>
|
|
#include <caml/alloc.h>
|
|
#include <caml/memory.h>
|
|
#include <caml/fail.h>
|
|
#include <caml/threads.h>
|
|
|
|
#include <dlfcn.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
|
|
CAMLprim value flan_dl_open(value path) {
|
|
CAMLparam1(path);
|
|
void *h = dlopen(String_val(path), RTLD_NOW | RTLD_LOCAL);
|
|
if (!h) caml_failwith(dlerror());
|
|
CAMLreturn(caml_copy_nativeint((intnat)h));
|
|
}
|
|
|
|
CAMLprim value flan_dl_sym(value handle, value name) {
|
|
CAMLparam2(handle, name);
|
|
void *p = dlsym((void *)Nativeint_val(handle), String_val(name));
|
|
if (!p) caml_failwith(dlerror());
|
|
CAMLreturn(caml_copy_nativeint((intnat)p));
|
|
}
|
|
|
|
CAMLprim value flan_dl_close(value handle) {
|
|
dlclose((void *)Nativeint_val(handle));
|
|
return Val_unit;
|
|
}
|
|
|
|
/* The one call shape a macro is reached through. See the thunk Emit writes. */
|
|
typedef void (*flan_macro_fn)(void *args, int64_t n, void *out, void *xfer);
|
|
|
|
CAMLprim value flan_macro_call(value fn, value args, value n, value out) {
|
|
CAMLparam4(fn, args, n, out);
|
|
/* The transfer channel every Flan signature carries (spec-conditions.md,
|
|
section 6). A macro that signals a condition with nothing above it to
|
|
handle it aborts inside the compiler, which is loud rather than silent;
|
|
the channel still has to be a real, zeroed slot. */
|
|
int64_t xfer[4] = { 0, 0, 0, 0 };
|
|
((flan_macro_fn)Nativeint_val(fn))((void *)Nativeint_val(args),
|
|
Int64_val(n),
|
|
(void *)Nativeint_val(out), xfer);
|
|
CAMLreturn(Val_unit);
|
|
}
|
|
|
|
CAMLprim value flan_mem_alloc(value n) {
|
|
CAMLparam1(n);
|
|
/* Zeroed, because ZII is the language's rule and an unwritten Form field
|
|
must read as the zero of its type rather than as whatever malloc had. */
|
|
void *p = calloc((size_t)Long_val(n), 1);
|
|
if (!p) caml_failwith("out of memory laying out a macro's arguments");
|
|
CAMLreturn(caml_copy_nativeint((intnat)p));
|
|
}
|
|
|
|
CAMLprim value flan_mem_free(value p) {
|
|
free((void *)Nativeint_val(p));
|
|
return Val_unit;
|
|
}
|
|
|
|
CAMLprim value flan_poke_i32(value p, value off, value x) {
|
|
int32_t v = (int32_t)Int32_val(x);
|
|
memcpy((char *)Nativeint_val(p) + Long_val(off), &v, 4);
|
|
return Val_unit;
|
|
}
|
|
|
|
CAMLprim value flan_poke_i64(value p, value off, value x) {
|
|
int64_t v = Int64_val(x);
|
|
memcpy((char *)Nativeint_val(p) + Long_val(off), &v, 8);
|
|
return Val_unit;
|
|
}
|
|
|
|
CAMLprim value flan_poke_f64(value p, value off, value x) {
|
|
double v = Double_val(x);
|
|
memcpy((char *)Nativeint_val(p) + Long_val(off), &v, 8);
|
|
return Val_unit;
|
|
}
|
|
|
|
CAMLprim value flan_poke_ptr(value p, value off, value q) {
|
|
void *v = (void *)Nativeint_val(q);
|
|
memcpy((char *)Nativeint_val(p) + Long_val(off), &v, sizeof v);
|
|
return Val_unit;
|
|
}
|
|
|
|
CAMLprim value flan_poke_bytes(value p, value off, value s) {
|
|
memcpy((char *)Nativeint_val(p) + Long_val(off), String_val(s),
|
|
caml_string_length(s));
|
|
return Val_unit;
|
|
}
|
|
|
|
CAMLprim value flan_peek_i32(value p, value off) {
|
|
int32_t v;
|
|
memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), 4);
|
|
return caml_copy_int32(v);
|
|
}
|
|
|
|
CAMLprim value flan_peek_i64(value p, value off) {
|
|
int64_t v;
|
|
memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), 8);
|
|
return caml_copy_int64(v);
|
|
}
|
|
|
|
CAMLprim value flan_peek_f64(value p, value off) {
|
|
double v;
|
|
memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), 8);
|
|
return caml_copy_double(v);
|
|
}
|
|
|
|
CAMLprim value flan_peek_ptr(value p, value off) {
|
|
void *v;
|
|
memcpy(&v, (char *)Nativeint_val(p) + Long_val(off), sizeof v);
|
|
return caml_copy_nativeint((intnat)v);
|
|
}
|
|
|
|
CAMLprim value flan_peek_bytes(value p, value off, value n) {
|
|
CAMLparam3(p, off, n);
|
|
CAMLlocal1(s);
|
|
s = caml_alloc_string((mlsize_t)Long_val(n));
|
|
memcpy((char *)Bytes_val(s), (char *)Nativeint_val(p) + Long_val(off),
|
|
(size_t)Long_val(n));
|
|
CAMLreturn(s);
|
|
}
|
|
|
|
/* ── The agent, when it is in this same process ─────────────────────── */
|
|
|
|
/* [flan dev] builds one binary that is the compiled program and holds this
|
|
* compiler, so a request to the agent need not leave the address space. The
|
|
* two ends still meet at one line of text and one answer — that is
|
|
* vendor/agent/flan_agent.c's [handle_line], and this is a call to it.
|
|
*
|
|
* Weak, because the same [flan] binary that builds a merged program does not
|
|
* itself contain an agent: the launcher, [flan reload], the two-process daemon
|
|
* and every test link no flan_agent.o, and there the symbol is null. So "is
|
|
* there an agent in this process" is answered by the linker rather than by a
|
|
* flag that could disagree with reality, and [None] here is what makes
|
|
* lib/dev.ml fall back to the socket.
|
|
*
|
|
* The runtime system is released across the call. A delivery does a [dlopen],
|
|
* which is milliseconds of relocation and the loader lock, and holding OCaml's
|
|
* lock through it stalls every other OCaml thread for no reason — docs/DISCUSS.md
|
|
* §14's third cost, in the one place this lane creates it. Nothing the agent
|
|
* does touches an OCaml value, so there is nothing to keep alive across it. */
|
|
extern char *flan_agent_request(const char *line, uint64_t *len)
|
|
__attribute__((weak));
|
|
extern void flan_agent_request_free(char *p) __attribute__((weak));
|
|
|
|
/* ── The program's own thread, when it is in this same process ──────── */
|
|
|
|
/* A merged [flan dev] binary runs the Flan program on its main thread and this
|
|
* compiler on a thread beside it, and a program that finishes no longer ends
|
|
* the process: the main thread parks and can be sent round again. These are
|
|
* the two questions the compiler has about that thread — what state it is in,
|
|
* and please run the program again — and both are defined in the C that
|
|
* lib/dev.ml generates for the merged entry point.
|
|
*
|
|
* Weak for [flan_agent_request]'s reason, which is the same reason: the [flan]
|
|
* binary that *builds* a merged program has no program of its own, and neither
|
|
* does the two-process daemon or any test. There the symbols are null, and
|
|
* "does this process have a program thread" is answered by the linker rather
|
|
* than by a flag that could disagree with it.
|
|
*
|
|
* The runtime system is *not* released across either call, unlike the agent
|
|
* request below. Each is a mutex, two stores and an unlock on a lock nothing
|
|
* holds for longer than that — releasing and re-acquiring OCaml's lock would
|
|
* cost more than the call. */
|
|
extern int flan_merged_rerun(void) __attribute__((weak));
|
|
extern int flan_merged_wake(void) __attribute__((weak));
|
|
extern int flan_merged_program_state(void) __attribute__((weak));
|
|
|
|
/* 0 running, 1 parked, 2 no program thread in this process. */
|
|
CAMLprim value flan_program_state(value unit) {
|
|
(void)unit;
|
|
if (flan_merged_program_state == NULL) return Val_int(2);
|
|
return Val_int(flan_merged_program_state() == 0 ? 0 : 1);
|
|
}
|
|
|
|
/* 0 taken, 1 refused because the program is running, 2 no program thread. */
|
|
CAMLprim value flan_program_rerun(value unit) {
|
|
(void)unit;
|
|
if (flan_merged_rerun == NULL) return Val_int(2);
|
|
return Val_int(flan_merged_rerun() == 0 ? 0 : 1);
|
|
}
|
|
|
|
/* 0 woken, 1 nothing to wake because the program is running, 2 no program
|
|
* thread. The third question the compiler has about that thread, and the one
|
|
* that makes a parked program answer a C-x C-e: the module is in the agent's
|
|
* ring already, and this is what gets the sleeper to look at it. */
|
|
CAMLprim value flan_program_wake(value unit) {
|
|
(void)unit;
|
|
if (flan_merged_wake == NULL) return Val_int(2);
|
|
return Val_int(flan_merged_wake() == 0 ? 0 : 1);
|
|
}
|
|
|
|
CAMLprim value flan_agent_direct(value line) {
|
|
CAMLparam1(line);
|
|
CAMLlocal2(s, r);
|
|
char *out;
|
|
uint64_t n = 0;
|
|
if (flan_agent_request == NULL) CAMLreturn(Val_int(0)); /* None */
|
|
{
|
|
/* Copied out first: [String_val] points into the OCaml heap, which may
|
|
move once the runtime system is released. */
|
|
size_t k = caml_string_length(line);
|
|
char *copy = malloc(k + 1);
|
|
if (copy == NULL) caml_failwith("out of memory asking the agent");
|
|
memcpy(copy, String_val(line), k);
|
|
copy[k] = '\0';
|
|
caml_release_runtime_system();
|
|
out = flan_agent_request(copy, &n);
|
|
caml_acquire_runtime_system();
|
|
free(copy);
|
|
}
|
|
s = caml_alloc_initialized_string((mlsize_t)n, out == NULL ? "" : out);
|
|
if (out != NULL && flan_agent_request_free != NULL)
|
|
flan_agent_request_free(out);
|
|
r = caml_alloc_some(s);
|
|
CAMLreturn(r);
|
|
}
|