From 65d14f42f058617116e4c62fb903054465eda08c Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Mon, 14 Sep 2026 22:26:52 +0700 Subject: [PATCH] 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. --- lib/build.ml | 14 +++++++++++++- lib/dev.ml | 22 +++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/build.ml b/lib/build.ml index d5383c4..6d8eb50 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -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 diff --git a/lib/dev.ml b/lib/dev.ml index 2f41940..69139c4 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -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