A release build links the agent package again

vendor/agent/flan_agent.c calls flan_dev_result_get, which lives in flan_dev.c,
which build.ml compiled only for a dev build - so flan build sand.flan died at
the link with an undefined symbol. A regression from 7ce1d09, where C-x C-e
gave the agent a result to report.

A package's C sources are collected whatever main does, so the agent's C is in
every build that imports it. flan_dev.c is now compiled into all of them.
Nothing in a release build reaches it: the compiler emits a registry lookup
only for a name the host was not built with, and without cells there is no such
name. The table is BSS, so the cost is address space rather than binary size,
and -rdynamic and the cells are still what --dev means.

test_agent.ml now links the same program both ways. It runs only the dev one -
with no cells the agent refuses every module, so linking is the whole claim.
This commit is contained in:
Joseph Ferano 2026-09-11 08:43:09 +07:00
parent ec0d845822
commit 9e845fd980
3 changed files with 34 additions and 3 deletions

11
NEXT.md
View File

@ -389,6 +389,17 @@ none. `Emit.signature` is now the single place a function's LLVM signature is
spelled, because a `define` here and a `declare` there drift the moment one of
them grows a case for `Unit` or for a slice parameter.
**`flan_dev.c` is compiled into every build, not only a dev one.** Nothing in a
release build calls into it — the compiler only emits a registry lookup for a
name the host was not built with, which cannot arise without cells — but the
agent package's C refers to it, and a package's C sources are collected
whatever `main` does. Leaving it out of release builds made `flan build
sand.flan` fail at the link with an undefined `flan_dev_result_get`, which
reads as a compiler bug rather than as a missing flag. The table is BSS, so the
cost is address space and not binary size; `-rdynamic` and the cells are still
what `--dev` means. `test_agent.ml` links the agent program both ways for this
reason.
**`-rdynamic` is load-bearing.** A normal executable exports nothing: `nm -D
calc-me | grep 'flan\.'` is empty, so a loaded module's `declare`s would have
nothing to bind to. The test passes it through `lflags`, which keeps it a

View File

@ -120,11 +120,18 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = [])
let dir = workdir () in
let ll = Filename.concat dir (Filename.basename out ^ ".ll") in
write ll (Emit.program ~checks:opts.checks ~dev:opts.dev p);
(* [flan_dev.c] is compiled into every build, not only a dev one. Nothing in
a release build calls into it the compiler only emits a registry lookup
for a name the host was not built with, which cannot arise without cells
but the agent package's C refers to it, and a package's C sources are
collected whatever [main] does. Leaving it out made [flan build sand.flan]
fail at the link with an undefined symbol, which reads as a compiler bug
rather than as a missing flag. The table is BSS, so this costs address
space and not binary size, and [-rdynamic] and the cells are still what
[--dev] means. *)
let objs =
compile_c ~opts ~src:Runtime_src.source ~name:"flan_rt.c"
:: (if opts.dev then
[ compile_c ~opts ~src:Runtime_src.dev_source ~name:"flan_dev.c" ]
else [])
:: [ compile_c ~opts ~src:Runtime_src.dev_source ~name:"flan_dev.c" ]
@ List.map
(fun c ->
compile_c ~opts ~src:(read_file c) ~name:(Filename.basename c))

View File

@ -77,6 +77,19 @@ let () =
(Build.executable ~opts:dev ~csrcs:l.Load.csrcs ~lflags:l.Load.lflags
t.Session.host ~out:exe);
(* And the same program without [--dev], which has to *link*. A package's C
sources are collected whatever [main] does, so the agent's C is in every
build that imports it, and it refers to the dev runtime leaving that
out made this an undefined symbol at the link rather than a missing
flag. Nothing is run: with no cells the agent refuses every module, and
linking is the whole claim. *)
(match
Build.executable ~opts:Build.default ~csrcs:l.Load.csrcs
~lflags:l.Load.lflags t.Session.host ~out:(tmp "prog-release")
with
| _ -> ()
| exception Failure m -> fail "a release build of the agent: %s" m);
(* Two evaluations from the one session, which is the daemon's loop and
the thing no earlier test does. The first introduces a global the
process was never built with; the second only reads it, and can only