You run a program under flan dev, it opens a raylib window, you close the window, main returns — and there is no way to get another window short of flan-dev-restart-program, which throws away the build, the session and every global with it. In Common Lisp or Clojure the image outlives main, so you call it again. The process here already outlived main: the exit hook flushed, closed stdout and sat in for (;;) pause(). Nothing could wake it. So main() is a loop. The hook records the status and longjmps back into a setjmp in main() — there is no return available, since flan_exit is reached from wherever the program happened to be — and the thread waits on a condition variable until the new rerun op signals it. The main thread is the one that runs main again: a window belongs to the thread that opened it, and on macOS to the first thread of the process. A longjmp pops no frame, so the park first empties the handler stack, the restart stack and the shadow frame chain, each of which was a chain of allocas in stack the next run is about to write over. Nothing else is reset; the second run reads whatever the first left in the globals, which is the semantics that was asked for. Closing stdout had to go with it. That was how the compiler learned the program was done, but a pipe delivers EOF once, so the signal and the program's output were the same resource and spending it left the second run with nowhere to print. The descriptor hazard the old code reopened /dev/null for goes away with the close that caused it. Liveness is asked for instead, through a weak symbol in the same style as the agent's, and is now three states rather than two: Live, Parked and Gone. Every guard branches on that before consulting the break state, because the agent's listener answers "running" while the program is parked and telling somebody whose program has finished that it is running is worse than saying nothing. Only eval accepts a parked program — it queues and waits for nothing, and the queued module installs at the first frame boundary of the next run, so a body can be fixed while parked and the re-run executes it. Everything else needs a frame boundary or a stopped stack, has neither, and says which, naming the command that gets the program back. A re-run while the program is running is refused rather than queued: the test and the signal happen under one mutex, so two mains writing the same globals at once never starts. :parked rides on every reply beside :stopped, for the reason :stopped does — finishing is as unannounced as stopping, more so when the way it happens is a mouse click on a title bar. Emacs shows flan:parked in the modeline and binds flan-rerun to C-c C-M-x.
235 lines
9.1 KiB
C
235 lines
9.1 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_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);
|
|
}
|
|
|
|
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);
|
|
}
|