Run the verifier, because string needles cannot see what breaks

A call without a !dbg inside a function that has debug info is a hard LLVM
rejection, not a warning — it turns every debug build into a clang error. So
is a DISubprogram the compile unit does not reach. Neither is visible to an
assertion about the text of the module, and both are the kind of thing that
appears when this file grows a new call from somewhere other than a Tast node.

The program the cases run through is chosen for those calls specifically: a
bounds check, a condition signalled and handled, a restart transferred to, a
defer on the way out. Every one of them is a call the backend invents.

Emit.redefinition is the half that needed this. It had only ever run at the
default debug:false, and it differs from Emit.program in exactly the places
metadata goes wrong: hidden bodies, the by-name cell and global lookups, and
flan_reload_install and flan_reload_call, which are raw defines with no
subprogram that nonetheless contain calls. Both directions of `known' are
covered, because they emit almost entirely different code.
This commit is contained in:
Joseph Ferano 2026-09-12 04:00:24 +07:00
parent 5f5cc8bee9
commit e3f352321d

View File

@ -1157,6 +1157,77 @@ ERR@7 unexpected token: not the kind the caller was reading
print_endline "FAIL the transfer channel appeared as a local variable"
end;
(* LLVM's own verifier, over both entry points. String needles cannot see
a DISubprogram the compile unit does not reach, or a call without a
!dbg inside a function that has debug info and that second one is a
hard rejection, not a warning, so it would turn every debug build into
a clang error rather than into anything visible here.
[redefinition] is the half that needs this most. It is only ever run at
the default debug:false today, and it differs from [program] in exactly
the places metadata goes wrong: hidden bodies, the by-name cell and
global loads, and flan_reload_install and flan_reload_call, which are
raw defines with no subprogram that nonetheless contain calls. *)
if Sys.command "command -v opt > /dev/null 2>&1" = 0 then begin
let verifies name ir =
let f = Filename.concat scratch "flan-dwarf-verify.ll" in
Out_channel.with_open_bin f (fun ch -> Out_channel.output_string ch ir);
let log = Filename.concat scratch "flan-dwarf-verify.log" in
let code =
Sys.command
(Printf.sprintf "opt -passes=verify -disable-output %s > %s 2>&1"
(Filename.quote f) (Filename.quote log))
in
if code <> 0 then begin
incr failures;
Printf.printf "FAIL %s: LLVM's verifier rejected the module\n%s\n" name
(In_channel.with_open_bin log In_channel.input_all)
end;
(try Sys.remove f with Sys_error _ -> ());
(try Sys.remove log with Sys_error _ -> ())
in
(* A program with a bit of everything that emits a call the backend
invents rather than one a Tast node asked for: a bounds check, a
condition signalled and handled, a restart transferred to, a defer on
the way out. Each would be a verifier rejection without a location. *)
let src =
"(defstruct Missing [id i32])\n\
(defvar seen i64)\n\
(defvar arr [4 i32])\n\
(defn pick [xs [i32] i i32] i32 (at xs i))\n\
(defn fetch [n i32] i32\n\
\ (restart-case\n\
\ (do (error (Missing {:id n})) 0)\n\
\ (use-placeholder [] -1)))\n\
(defn run [] i32\n\
\ (defer (set seen (+ seen 1)))\n\
\ (handler-bind [(Missing [m] (invoke-restart 'use-placeholder))]\n\
\ (fetch 3)))\n\
(defn main [] i32\n\
\ (set (at arr 2) 9)\n\
\ (let [s (slice arr 0 4)]\n\
\ (print-i64 (i64 (pick s 2))) (newline)\n\
\ (print-i64 (i64 (run))) (newline)\n\
\ 0))\n"
in
let decls = Parse.program (Reader.read_all ~file:"<verify>" src) in
let p = Check.program decls in
verifies "the whole program, with debug info"
(Emit.program ~debug:true ~pnames:(pnames_of decls) p);
(* And a redefinition module against a host that has every name — the
shape C-c C-c produces. *)
verifies "a redefinition module, with debug info"
(Emit.redefinition ~dev:true ~debug:true ~known:(fun _ -> true) p
~fns:[ "fetch"; "run" ]);
(* And one against a host that has none of them, which is the other
path: every call goes through flan_dev_cell and every global through
flan_dev_global, so the module is almost entirely different code. *)
verifies "a redefinition of names the host does not have"
(Emit.redefinition ~dev:true ~debug:true ~known:(fun _ -> false) p
~fns:[ "fetch"; "run" ])
end
else print_endline "acceptance: the DWARF verifier cases skipped (no opt)";
(* A debug build and a release build must still be the same program. *)
let debug_compile ?(dev = false) path =
let exe =