flan/lib/agent.ml
Joseph Ferano 65a62bbb97 A socket that looped back into its own address space, deleted
flan dev is one process, so the unix socket between the compiler and the
program was a connect, a write and a read that never left the machine's own
memory. A delivery is now a call into the agent's verb table.

flan_agent.c's handlers write into a sink that is either an fd or a growing
buffer, so serve() and the new flan_agent_request() share one verb table rather
than two that would drift -- every answer in there is assembled from several
pieces, so the seam had to be the writing. Dev.deliver, Dev.result and Dev.ask
were three copies of the same socket dance and are now three lines over one
Dev.request.

Which path is taken is the linker's answer, not a flag: flan_agent_request is
weak in dynload_stubs.c, null in every binary that links no flan_agent.o, and
Agent.request is None there. So --two-process, flan reload and every test go on
using the socket with nothing to configure.

Three things the direct path must not quietly change. The ring's room check is
separate from its store, which was safe only while the accept loop was the sole
producer, so handle_line runs under a mutex. A delivery is still only published
-- the install is one store per function on the game thread at a frame
boundary, and doing it on the spot would be a frame running half in the old
code and half in the new. And the OCaml runtime system is released across the
call, because a delivery is a dlopen.

Measured, and the number is the point: the transport was ~50us of a 23ms
redefinition, so end to end did not move. Code generation is 19 of the 22
milliseconds. The merge's prize was never latency.

The test is a deletion, because a reply cannot say which way it came:
test_dev.ml unlinks the agent's socket file once the merged program has bound
it, and every evaluation after that still installs.
2026-09-12 22:22:46 +07:00

24 lines
1.3 KiB
OCaml

(** The agent, as the compiler reaches it when both are in one process.
[flan dev] builds one binary that is the compiled program and holds this
compiler; the agent's listener thread and this one are threads in it. So a
request that used to be a connect, a write and a read on a unix socket
looping back into this address space is a call into
[vendor/agent/flan_agent.c] instead.
[None] means there is no agent in this process — the [flan] binary that
builds the merged program, [flan reload], the two-process daemon, every
test. The answer comes from the linker rather than from a flag: the symbol
is weak in [dynload_stubs.c] and is null in a binary that links no
[flan_agent.o]. [Dev] falls back to the socket on [None], which is what
keeps [--two-process] working unchanged.
What does *not* change is where a delivery lands. The agent still only
publishes to its ring; the game thread picks the job up when it next
reaches a frame boundary. The rule that keeps the collector away from the
frame thread — the game thread never calls into OCaml, requests are left
somewhere and picked up — is the same rule with the same mechanism. This is
the compiler thread calling C, never the other way. *)
external request : string -> string option = "flan_agent_direct"