A dlopened macro binds to its own copy, not the merged host's

flan dev's one-process build links the program and the compiler into one
-rdynamic executable, so it exports flan.rl/with-drawing -- the package's
defmacro compiled as an ordinary function, whose body was qualified at the
Ast level, after the quasiquote had already become a string literal. ELF
gives the executable precedence over a dlopened object, so the macro module
called the host's copy and (rl/with-drawing ...) expanded to an unqualified
begin-drawing the checker then refused. The module is self-contained, so
-Wl,-Bsymbolic is the whole fix.

merged_setup reports a Loc.Error as a diagnostic now, which is how the
failure had a location at all instead of an exception constructor after an
apparently successful build.
This commit is contained in:
Joseph Ferano 2026-09-14 22:26:52 +07:00
parent a4dce138c0
commit 65d14f42f0
2 changed files with 32 additions and 4 deletions

View File

@ -1053,7 +1053,19 @@ let macro_module ?(opts = default) ?(csrcs = []) ?(lflags = []) ~macros
let cmd =
String.concat " "
([ Filename.quote (compiler opts); opts.opt; "-Wno-override-module";
"-shared"; "-fPIC" ]
"-shared"; "-fPIC";
(* Bind this module's own references to its own definitions. ELF
gives the executable's symbols precedence over a shared object's,
and [flan dev]'s merged build is an executable that carries the
whole program -- including the package's [defmacro]s, compiled
from a body whose names were qualified after the quasiquote became
string literals and so are not the ones this module holds. Without
this, dlopening the macro module into that host makes
[flan.rl/with-drawing] resolve to the host's copy, and
[(rl/with-drawing ...)] expands to an unqualified [begin-drawing]
that the checker then refuses. The module is self-contained by
design, so there is nothing it wants from the host. *)
"-Wl,-Bsymbolic" ]
@ tflags
@ [ Filename.quote ll ]
@ List.map Filename.quote objs

View File

@ -3038,9 +3038,25 @@ let merged_setup () =
merged_state := Some (t, ls, sock);
Printf.eprintf "flan dev: %s ready on %s (%.0fms, one process)\n%!" file
sock ((Unix.gettimeofday () -. t0) *. 1000.)
with e ->
Printf.eprintf "flan dev: %s\n%!"
(match e with Failure m -> m | e -> Printexc.to_string e);
with
(* The executable has just replaced the launcher, and recreates the session
above in order to own it for the rest of the dev run. That is still a
frontend boundary: rendering [Loc.Error] as an exception constructor here
loses the source location, the reason, its span, and any notes. It also
made a source error look like a compiler crash after an apparently
successful build. Keep this identical to the command driver's reporting
for both the one-error and whole-file-error channels. *)
| Loc.Error d ->
Printf.eprintf "%s\n%!" (Loc.report d);
exit 1
| Loc.Errors ds ->
Printf.eprintf "%s\n%!" (Loc.report_all ds);
exit 1
| Failure m ->
Printf.eprintf "flan dev: %s\n%!" m;
exit 1
| e ->
Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e);
exit 1
(* Called from the compiler thread after the program has started. Never