516 lines
26 KiB
OCaml
516 lines
26 KiB
OCaml
(* The reload primitive, measured (NEXT.md, dev loop step 1).
|
|
|
|
One function is recompiled into its own object and loaded into a process
|
|
that is already running. Everything after this — indirection cells, the
|
|
agent in the game, the daemon — assumes this works and is fast; nothing in
|
|
the codebase had ever done it, and plan.org's 16ms was measured with clang
|
|
in isolation somewhere else.
|
|
|
|
The parts, all of them new here:
|
|
|
|
Emit.program ~dev a cell per function; every call goes through one
|
|
Emit.redefinition a form list defined, everything else [external],
|
|
plus [flan_reload_install] to publish it into its cell
|
|
flan_dev.c the by-name registry a run-time-new name needs
|
|
Build.shared that IR text through llc + ld -shared, timed
|
|
reload_host.c dlopen, install, call — twice, in one process
|
|
|
|
The host is C rather than OCaml because that is where it has to end up: the
|
|
agent of step 3 lives in the game process, next to flan_rt.c, and there is
|
|
no OCaml runtime there. *)
|
|
|
|
open Flan
|
|
|
|
(* The watchdog first: a hang is the one failure mode that reports
|
|
nothing at all. See watchdog.ml. *)
|
|
let () = Watchdog.arm ~seconds:600 "test_reload"
|
|
|
|
let failures = ref 0
|
|
let fail fmt = Printf.ksprintf (fun s -> incr failures; print_endline ("FAIL " ^ s)) fmt
|
|
|
|
let scratch = Filename.get_temp_dir_name ()
|
|
let tmp name = Filename.concat scratch ("flan-reload-" ^ name)
|
|
|
|
let checked path =
|
|
Check.program
|
|
(Load.program ~file:path (Reader.read_file path)).Load.decls
|
|
|
|
let ms f =
|
|
let t0 = Unix.gettimeofday () in
|
|
let x = f () in
|
|
(x, (Unix.gettimeofday () -. t0) *. 1000.)
|
|
|
|
let () =
|
|
match Sys.command "command -v clang > /dev/null 2>&1 && command -v llc > /dev/null 2>&1" with
|
|
| 0 ->
|
|
let p1 = checked "programs/reload.flan" in
|
|
let p2 = checked "programs/reload-v2.flan" in
|
|
let p3 = checked "programs/reload-v3.flan" in
|
|
let p4 = checked "programs/reload-v4.flan" in
|
|
|
|
(* What the running process was built with. Everything else — v3's [extra]
|
|
and [added] — has no symbol to bind to and goes through the registry.
|
|
A session would keep this set and grow it; the test states it. *)
|
|
let host_names =
|
|
List.map (fun (f : Tast.fn) -> f.Tast.name) p1.Tast.fns
|
|
@ List.map (fun (g : Tast.global) -> g.Tast.gname) p1.Tast.globals
|
|
in
|
|
let known n = List.exists (String.equal n) host_names in
|
|
|
|
(* [dev] is the two halves of a reloadable build together: cells, so a
|
|
call site can be made to follow a redefinition, and [-rdynamic], so the
|
|
cells and globals are visible to a dlopen'd object at all. [-ldl] is the
|
|
host's own, for its dlopen. *)
|
|
let dev = { Build.default with Build.dev = true } in
|
|
let host = tmp "host" in
|
|
ignore
|
|
(Build.executable ~opts:dev ~csrcs:[ "reload_host.c" ]
|
|
~lflags:[ "-ldl" ] p1 ~out:host);
|
|
|
|
(* Two paths, not one rewritten in place: dlopen caches by path and would
|
|
hand back the first handle, so the swap would silently not happen. *)
|
|
let module_of p fns name =
|
|
let out = tmp name in
|
|
let ir, emit_ms = ms (fun () -> Emit.redefinition ~dev:true ~known p ~fns) in
|
|
let t = Build.shared ~opts:dev ~ir ~out () in
|
|
(out, ir, emit_ms, t)
|
|
in
|
|
let so1, _ir1, emit_ms, t1 = module_of p1 [ "bump" ] "v1.so" in
|
|
let so2, ir2, emit2_ms, t2 = module_of p2 [ "bump" ] "v2.so" in
|
|
(* One module, two forms: the var and the function that uses it have to
|
|
arrive together or the intermediate state refers to storage that does
|
|
not exist. This is the C-c C-k unit. *)
|
|
let so3, ir3, _, _ = module_of p3 [ "bump"; "added" ] "v3.so" in
|
|
let so4, ir4, _, _ = module_of p4 [ "added" ] "v4.so" in
|
|
(* v5 retypes [extra], which v3 introduced at run time. It is built here
|
|
and loaded in a process of its own below: what it does is abort. *)
|
|
let p5 = checked "programs/reload-v5.flan" in
|
|
let so5, _, _, _ = module_of p5 [ "added" ] "v5.so" in
|
|
|
|
(* A redefinition module must not define what the host already owns:
|
|
defining [counter] would give the loaded object a private copy and the
|
|
state would reset on every reload, and defining [helper] would freeze a
|
|
stale copy of it into the module. *)
|
|
let has hay needle =
|
|
let n = String.length needle and h = String.length hay in
|
|
let rec go i = i + n <= h && (String.sub hay i n = needle || go (i + 1)) in
|
|
go 0
|
|
in
|
|
(* No Str, for the same reason the reader is hand-written. [`First] and
|
|
[`Last] are which occurrence; the pair shape keeps the match below
|
|
readable. *)
|
|
let find hay needle which =
|
|
let n = String.length needle and h = String.length hay in
|
|
let rec go i acc =
|
|
if i + n > h then acc
|
|
else if String.sub hay i n = needle then
|
|
match which with `First -> Some i | `Last -> go (i + 1) (Some i)
|
|
else go (i + 1) acc
|
|
in
|
|
go 0 None
|
|
in
|
|
if not (has ir2 "@\"flan.counter\" = external global i64") then
|
|
fail "redefinition defines the global instead of declaring it";
|
|
(* In a dev module a sibling is reached only through its cell, so there is
|
|
nothing to declare and a [define] would be a private copy. *)
|
|
if has ir2 "declare i64 @\"flan.helper\"" then
|
|
fail "dev redefinition declares a sibling it should reach by cell";
|
|
if has ir2 "define i64 @\"flan.helper\"" then
|
|
fail "redefinition emitted a second body for a function it does not own";
|
|
if has ir2 "define i32 @main" then fail "redefinition emitted an entry point";
|
|
(* [hidden], or the module's own [@"flan.bump"] is interposed by the host's
|
|
and the installer publishes the very function it is replacing. *)
|
|
if not (has ir2 "define hidden i64 @\"flan.bump\"") then
|
|
fail "redefinition's own body is interposable";
|
|
if not (has ir2 "@\"flan.cell.helper\" = external global ptr") then
|
|
fail "redefinition defines a cell instead of using the host's";
|
|
(* A name the host has is a symbol; a name it lacks is a registry lookup
|
|
cached in a module-local slot. Getting this backwards either fails to
|
|
link or silently gives each module its own copy. *)
|
|
if not (has ir3 "@\"flan.cellp.added\" = internal global ptr null") then
|
|
fail "a run-time-new function did not get a slot";
|
|
if not (has ir3 "@\"flan.gp.extra\" = internal global ptr null") then
|
|
fail "a run-time-new global did not get a slot";
|
|
if has ir3 "@\"flan.extra\" = " then
|
|
fail "a run-time-new global was given storage in the module";
|
|
(* Every lookup is resolved before any body is published: publishing first
|
|
exposes a function whose module-local slots are still null to anything
|
|
that calls it. Not race-testable, so it is asserted on the text. *)
|
|
(* v4 redefines a name that exists only in the registry, so it publishes
|
|
through the cell it looked up rather than into a symbol — there is no
|
|
[@"flan.cell.added"] anywhere to store into. *)
|
|
if has ir4 "@\"flan.cell.added\"" then
|
|
fail "a run-time-new function was published into a symbol";
|
|
if not (has ir4 "call ptr @flan_dev_cell") then
|
|
fail "v4 did not look its target up by name";
|
|
(match find ir3 "store ptr @\"flan." `First, find ir3 "@flan_dev_(" `Last with
|
|
| Some publish, Some resolve when resolve > publish ->
|
|
fail "flan_reload_install publishes a body before resolving a lookup"
|
|
| None, _ -> fail "flan_reload_install publishes nothing"
|
|
| _ -> ());
|
|
(* A dev host's calls are indirect; a release host's are not. That is the
|
|
only difference between the two, and the whole of C-c C-c rests on it. *)
|
|
let host_ir = Emit.program ~dev:true p1 in
|
|
if not (has host_ir "@\"flan.cell.bump\" = global ptr @\"flan.bump\"") then
|
|
fail "dev build emitted no cell";
|
|
if has (Emit.program p1) "flan.cell." then
|
|
fail "release build emitted a cell";
|
|
|
|
let out = tmp "out" in
|
|
let cmd =
|
|
(* stderr kept apart from stdout: the host times its own dlopen there,
|
|
and stdout is what the expected transcript is compared against. *)
|
|
Printf.sprintf "%s %s %s %s %s > %s 2> %s" (Filename.quote host)
|
|
(Filename.quote so1) (Filename.quote so2) (Filename.quote so3)
|
|
(Filename.quote so4) (Filename.quote out)
|
|
(Filename.quote (tmp "err"))
|
|
in
|
|
let (code, dlopen_ms) = ms (fun () -> Sys.command cmd) in
|
|
let text = In_channel.with_open_bin out In_channel.input_all in
|
|
let timings = In_channel.with_open_bin (tmp "err") In_channel.input_all in
|
|
|
|
(* Every call is [outer], compiled once into the host and never rebuilt, so
|
|
a changed answer can only mean its call site followed the redefinition.
|
|
The arithmetic, in order:
|
|
|
|
host counter 0 -> 1, helper 1 = 2
|
|
v1 counter 1 -> 2, helper 2 = 4 (a rebuild of the same)
|
|
v2 +10 and +1000, recursing through its own cell until the
|
|
counter passes 100: 2 -> 12 -> ... -> 102, ten "v2" lines,
|
|
helper 102 = 204, so 1204. An interposed self-call would reach
|
|
the host's v1 body, print "v1", and land nowhere near it.
|
|
v3 extra 0 -> 7, counter 102 -> 109, helper 109 = 218. [extra]
|
|
and [added] are new names, so both came from the registry.
|
|
v4 redefines [added] only. v3's [bump] is still the installed one
|
|
and is not rebuilt here, so it reaches v4 only through a cell
|
|
the two modules found by the same name: extra 7 -> 107,
|
|
counter 109 -> 216, helper 216 = 432. Had v3 cached the
|
|
function's address rather than its cell's, this would be 246.
|
|
|
|
The "v1"/"v2"/"v3" lines come from inside each [bump] and are what
|
|
exercise a redefinition module's own string constants. 1204 rather than
|
|
1236 is [helper]: v2's text for it multiplies by three, and the module
|
|
declares it rather than defining it, so the host's copy is the one that
|
|
ran. *)
|
|
let v2s = String.concat "" (List.init 10 (fun _ -> "v2\n")) in
|
|
let want =
|
|
"v1\nhost 2\nv1\nafter1 4\n" ^ v2s ^ "after2 1204\n\
|
|
v3\nafter3 218\nv3\nafter4 432\ncounter 216\n"
|
|
in
|
|
if code <> 0 || text <> want then
|
|
fail "reload\n got: %S (exit %d)\n wanted: %S" text code want;
|
|
|
|
(* The same thing again, compiled by the dev backend end to end (x86.ml's
|
|
header, docs/handoffs/HANDOFF-x86-rt.md item 1). Both halves, host and module, because
|
|
the two backends' conventions agree on every scalar and disagree on
|
|
every aggregate: an LLVM-built module dlopened into an --x86 host would
|
|
be correct until the first redefined function took or returned a struct.
|
|
So an --x86 host gets --x86 modules and the two never meet.
|
|
|
|
Only v1 and v2, which is the whole of what X86.redefinition compiles:
|
|
every name they touch is one the host was built with. v3 and v4
|
|
introduce a function and a global at run time, and the flan_dev_cell /
|
|
flan_dev_global lookups that needs are refused there by name.
|
|
|
|
Read by running, not by reading. A disassembly reads correctly beside a
|
|
wrong answer often enough (docs/DISCUSS.md item 15) that only the printed
|
|
transcript settles it: [outer] is compiled once into the host and never
|
|
rebuilt, so "after2 1204" can only mean its call site followed a body
|
|
that this backend emitted, published through a cell it reached via the
|
|
GOT. *)
|
|
let x86 = { dev with Build.x86 = true } in
|
|
let xhost = tmp "xhost" in
|
|
ignore
|
|
(Build.executable ~opts:x86 ~csrcs:[ "reload_host.c" ]
|
|
~lflags:[ "-ldl" ] p1 ~out:xhost);
|
|
let xmodule q fns name =
|
|
let o = tmp name in
|
|
let asm = X86.redefinition ~checks:true ~dev:true ~known q ~fns in
|
|
ignore (Build.shared_x86 ~opts:x86 ~asm ~out:o ());
|
|
o
|
|
in
|
|
let xso1 = xmodule p1 [ "bump" ] "xv1.so" in
|
|
let xso2 = xmodule p2 [ "bump" ] "xv2.so" in
|
|
let xout = tmp "xout" in
|
|
let xcode =
|
|
Sys.command
|
|
(Printf.sprintf "%s %s %s > %s 2> %s" (Filename.quote xhost)
|
|
(Filename.quote xso1) (Filename.quote xso2) (Filename.quote xout)
|
|
(Filename.quote (tmp "xerr")))
|
|
in
|
|
let xtext = In_channel.with_open_bin xout In_channel.input_all in
|
|
let xwant =
|
|
"v1\nhost 2\nv1\nafter1 4\n" ^ v2s ^ "after2 1204\ncounter 102\n"
|
|
in
|
|
if xcode <> 0 || xtext <> xwant then
|
|
fail "x86 reload\n got: %S (exit %d)\n wanted: %S" xtext xcode
|
|
xwant;
|
|
(* A name the host was never built with has no symbol to bind to, and the
|
|
registry path is not built here. It has to refuse rather than emit
|
|
something that links and then stores through a null. *)
|
|
(match X86.redefinition ~checks:true ~dev:true ~known p3 ~fns:[ "added" ] with
|
|
| _ -> fail "x86 redefinition accepted a name the host does not have"
|
|
| exception X86.Unsupported _ -> ());
|
|
|
|
(* The aggregate case, which is the whole reason X86.redefinition exists
|
|
rather than an --x86 host dlopening what Emit.redefinition made.
|
|
|
|
Everything above this point is scalar, and scalars are the half of the
|
|
calling convention the two backends cannot disagree about. They disagree
|
|
on every aggregate: x86.ml passes each one by pointer and returns it
|
|
through a hidden sret, LLVM classifies per eightbyte. So a redefined
|
|
function taking or returning a struct is the case that would expose a
|
|
mismatch, and until now the claim that an --x86 host plus --x86 modules
|
|
is same-convention-by-construction was an argument rather than a
|
|
measurement.
|
|
|
|
programs/reload-agg.flan crosses the boundary in four shapes at once —
|
|
two integer eightbytes, thirty-two bytes of MEMORY, two SSE eightbytes,
|
|
and one of each — because SysV treats those four differently and this
|
|
backend treats them identically, so a single shape would measure a
|
|
quarter of the disagreement and read like all of it. Each `step' takes
|
|
an aggregate and returns one, so a single call crosses in both
|
|
directions, and each calls a `weigh' the module does not define, which
|
|
hands an aggregate the other way.
|
|
|
|
Both backends run the same fixture and are compared against the same
|
|
transcript. The LLVM row is not decoration: a wrong expected number
|
|
would otherwise be indistinguishable from a backend that is right, and
|
|
two independently-built agreements on one string are what rule that
|
|
out. *)
|
|
let a1 = checked "programs/reload-agg.flan" in
|
|
let a2 = checked "programs/reload-agg-v2.flan" in
|
|
let agg_known =
|
|
let names =
|
|
List.map (fun (f : Tast.fn) -> f.Tast.name) a1.Tast.fns
|
|
@ List.map (fun (g : Tast.global) -> g.Tast.gname) a1.Tast.globals
|
|
in
|
|
fun n -> List.exists (String.equal n) names
|
|
in
|
|
let agg_fns = [ "step-pair"; "step-quad"; "step-duo"; "step-mix" ] in
|
|
(* The arithmetic, derived rather than observed, because a number read off
|
|
a run is a record of what happened and not a statement of what should:
|
|
|
|
v1 Pair {1,2} -> weigh 1+3*2 = 7, so {2, 2+7} and 2 + 100*9 = 902
|
|
Quad {1,2,3,4} -> weigh (1+6)+(15+28) = 50, so {2,4,6,54} and
|
|
2 + 400 + 60000 + 54000000 = 54060402
|
|
Duo {1,2} -> weigh 7, so {2.0, 9.0} and 902
|
|
Mix {1,2} -> weigh 7, so {2, 9.0} and 902
|
|
total 54063108
|
|
v2 Pair -> weigh is still the *host's* 7, so {11, 2+14} and 1611
|
|
Quad -> weigh still 50, so {11,22,33,104} and
|
|
11 + 2200 + 330000 + 104000000 = 104332211
|
|
Duo -> {11.0, 16.0} and 1611
|
|
Mix -> {11, 16.0} and 1611
|
|
total 104337044
|
|
|
|
1611 rather than 12411 in the first term is the tripwire: v2's text for
|
|
`weigh-pair' multiplies by thirty, and a module that grew its own copy
|
|
of a sibling rather than reaching the host's through a cell would say
|
|
so here. `counter' is stepped from inside the redefined body, by one in
|
|
v1 and by ten in v2, so 1 + 1 + 10 = 12 is the host's global being
|
|
written by three different bodies in turn. *)
|
|
let agg_want =
|
|
"a1\nhost 54063108\na1\nafter1 54063108\na2\nafter2 104337044\n\
|
|
counter 12\n"
|
|
in
|
|
let agg_run label opts mkmod =
|
|
let h = tmp ("agg-host-" ^ label) in
|
|
ignore
|
|
(Build.executable ~opts ~csrcs:[ "reload_host.c" ] ~lflags:[ "-ldl" ]
|
|
a1 ~out:h);
|
|
let m1 = mkmod a1 ("agg-" ^ label ^ "-1.so") in
|
|
let m2 = mkmod a2 ("agg-" ^ label ^ "-2.so") in
|
|
let o = tmp ("agg-out-" ^ label) and e = tmp ("agg-err-" ^ label) in
|
|
let code =
|
|
Sys.command
|
|
(Printf.sprintf "%s %s %s > %s 2> %s" (Filename.quote h)
|
|
(Filename.quote m1) (Filename.quote m2) (Filename.quote o)
|
|
(Filename.quote e))
|
|
in
|
|
let text = In_channel.with_open_bin o In_channel.input_all in
|
|
if code <> 0 || text <> agg_want then
|
|
fail "%s aggregate reload\n got: %S (exit %d)\n wanted: %S"
|
|
label text code agg_want;
|
|
List.iter (fun p -> try Sys.remove p with Sys_error _ -> ())
|
|
[ h; m1; m2; o; e ]
|
|
in
|
|
let agg_llvm_mod q name =
|
|
let o = tmp name in
|
|
let ir = Emit.redefinition ~dev:true ~known:agg_known q ~fns:agg_fns in
|
|
ignore (Build.shared ~opts:dev ~ir ~out:o ());
|
|
o
|
|
in
|
|
let agg_x86_mod q name =
|
|
let o = tmp name in
|
|
let asm =
|
|
X86.redefinition ~checks:true ~dev:true ~known:agg_known q ~fns:agg_fns
|
|
in
|
|
ignore (Build.shared_x86 ~opts:x86 ~asm ~out:o ());
|
|
o
|
|
in
|
|
agg_run "llvm" dev agg_llvm_mod;
|
|
agg_run "x86" x86 agg_x86_mod;
|
|
|
|
(* The same measurement run crossed, which is what the marker symbol is
|
|
for. Before it, an --x86 host given LLVM-built modules loaded them and
|
|
then died with SIGSEGV on the first call into a redefined aggregate
|
|
body:
|
|
|
|
got: "a1\nhost 54063108\na1\n" (exit 139)
|
|
|
|
That could not be asserted. It was undefined behaviour and what it
|
|
printed was a property of whichever LLVM happened to be installed; a
|
|
test pinning it would have been pinning the shape of a crash.
|
|
|
|
It is deterministic now, which is why it is here. A dev build defines a
|
|
marker naming the backend that built it — [flan.abi.x86] or
|
|
[flan.abi.llvm] — and a redefinition module holds a pointer to the one
|
|
it was itself built for. That pointer is a relocation the loader has to
|
|
resolve while it maps the object, so a crossed pair fails the [dlopen]
|
|
outright, before a single instruction of the new body runs. Both
|
|
directions, because a marker only one of the two backends emitted would
|
|
refuse in one direction and say nothing in the other.
|
|
|
|
Asserted on the message as well as the exit status, the way the
|
|
retyped-global and registry-overflow cases below are: a nonzero exit is
|
|
not by itself this refusal, and the point of the exercise is that what
|
|
reaches a user names the reason rather than repeating the loader's
|
|
"undefined symbol". *)
|
|
let agg_cross label opts mkmod wants =
|
|
let h = tmp ("agg-xhost-" ^ label) in
|
|
ignore
|
|
(Build.executable ~opts ~csrcs:[ "reload_host.c" ] ~lflags:[ "-ldl" ]
|
|
a1 ~out:h);
|
|
let m1 = mkmod a1 ("agg-cross-" ^ label ^ "-1.so") in
|
|
let o = tmp ("agg-xout-" ^ label) and e = tmp ("agg-xerr-" ^ label) in
|
|
let code =
|
|
Sys.command
|
|
(Printf.sprintf "%s %s > %s 2> %s" (Filename.quote h)
|
|
(Filename.quote m1) (Filename.quote o) (Filename.quote e))
|
|
in
|
|
let said = In_channel.with_open_bin e In_channel.input_all in
|
|
if code = 0 then
|
|
fail "%s: a crossed pair loaded and ran (exit 0)" label;
|
|
if not (has said "built by different backends") then
|
|
fail "%s: a crossed pair was refused without naming the reason: %S"
|
|
label said;
|
|
(* Which marker is missing is which backend built the module, so this is
|
|
also what says the refusal fired for the right direction rather than
|
|
for the other one. *)
|
|
if not (has said wants) then
|
|
fail "%s: the refusal named the wrong marker (wanted %s): %S" label
|
|
wants said;
|
|
List.iter (fun p -> try Sys.remove p with Sys_error _ -> ())
|
|
[ h; m1; o; e ]
|
|
in
|
|
(* An --x86 host handed an LLVM module: the pair the CLI can build today,
|
|
since [flan reload] has no --x86 spelling. *)
|
|
agg_cross "x86-host-llvm-module" x86 agg_llvm_mod "flan.abi.llvm";
|
|
(* And the reverse, which no command spells but [X86.redefinition] does. *)
|
|
agg_cross "llvm-host-x86-module" dev agg_x86_mod "flan.abi.x86";
|
|
|
|
(* The option-record guard, which is the older and narrower half of the
|
|
same answer: [Build.opts] is where the backend choice lives, so a
|
|
builder handed the *other* backend's option record refuses by name. It
|
|
catches a caller holding one option record and reaching for the wrong
|
|
builder. It cannot catch a caller holding two — the crossed runs above
|
|
pass both of these refusals — which is what the marker is for. See
|
|
docs/handoffs/HANDOFF-x86-aggregates.md and docs/handoffs/HANDOFF-x86-abi-marker.md. *)
|
|
(match Build.shared ~opts:x86 ~ir:"" ~out:(tmp "never.so") () with
|
|
| _ -> fail "Build.shared accepted an --x86 option record"
|
|
| exception Failure m when has m "--x86" -> ()
|
|
| exception Failure m -> fail "Build.shared refused for the wrong reason: %s" m);
|
|
(match Build.shared_x86 ~opts:dev ~asm:"" ~out:(tmp "never.so") () with
|
|
| _ -> fail "Build.shared_x86 accepted an LLVM option record"
|
|
| exception Failure m when has m "--x86" -> ()
|
|
| exception Failure m ->
|
|
fail "Build.shared_x86 refused for the wrong reason: %s" m);
|
|
|
|
(* The layout-drift guard, which needs a process of its own because what it
|
|
does is abort one. [extra] does not exist in the host: v3 introduced it
|
|
at run time, so flan_dev.c allocated its storage and recorded its size,
|
|
and every later module asking for that name is handed the same
|
|
allocation back. v5 asks for it as an i32. Handing back eight bytes for
|
|
a four-byte type is not an error anything downstream can detect — the
|
|
new body simply reads fields at offsets the allocation was never laid
|
|
out for — so the registry compares sizes and dies at the first chance
|
|
it has.
|
|
|
|
Asserted on the message as well as on the exit status: a process that
|
|
died for some other reason is not this guard firing, and the exit code
|
|
alone cannot tell the two apart. *)
|
|
let out5 = tmp "out5" and err5 = tmp "err5" in
|
|
let code5 =
|
|
Sys.command
|
|
(Printf.sprintf "%s %s %s %s > %s 2> %s" (Filename.quote host)
|
|
(Filename.quote so1) (Filename.quote so3) (Filename.quote so5)
|
|
(Filename.quote out5) (Filename.quote err5))
|
|
in
|
|
let said = In_channel.with_open_bin err5 In_channel.input_all in
|
|
if code5 = 0 then
|
|
fail "a global retyped across a reload was accepted (exit 0)";
|
|
if not (has said "size changed") then
|
|
fail "a retyped global did not stop on the size guard: %S" said;
|
|
|
|
(* The two fixed-size limits in flan_dev.c, which nothing had ever
|
|
reached: the 4K result buffer a renderer emits into, and the 4096-name
|
|
registry. Both are driven from dev_limits.c rather than from Flan,
|
|
because neither has a Flan spelling and a program that reached either
|
|
one by accident would be a program nobody wants in the corpus.
|
|
|
|
One process per mode. The name table never shrinks, so the two cases
|
|
would contaminate each other, and the overflow case ends in abort. *)
|
|
let limits = tmp "limits" in
|
|
ignore
|
|
(Build.executable ~opts:dev ~csrcs:[ "dev_limits.c" ] p1 ~out:limits);
|
|
let mode m =
|
|
let o = tmp ("limits-" ^ m ^ ".out") and e = tmp ("limits-" ^ m ^ ".err") in
|
|
let code =
|
|
Sys.command
|
|
(Printf.sprintf "%s %s > %s 2> %s" (Filename.quote limits) m
|
|
(Filename.quote o) (Filename.quote e))
|
|
in
|
|
let out = In_channel.with_open_bin o In_channel.input_all in
|
|
let err = In_channel.with_open_bin e In_channel.input_all in
|
|
List.iter (fun p -> try Sys.remove p with Sys_error _ -> ()) [ o; e ];
|
|
(code, out, err)
|
|
in
|
|
(* 6000 bytes emitted into 4096. The length is the cap itself, the three
|
|
dots are what says the value was cut rather than being that short, the
|
|
middle byte says the content before the cut is the content that was
|
|
emitted, and the generation moved exactly once — a reader waits on that
|
|
counter and a value published twice would be read half-formed. The last
|
|
line is the flag being cleared: a short value after a truncated one must
|
|
not inherit its ellipsis. *)
|
|
let code, out, _ = mode "cap" in
|
|
let want_cap = "len 4096\ntail ...\nmid b\nhead a\ngen 1\nagain 12\n" in
|
|
if code <> 0 || out <> want_cap then
|
|
fail "the 4K result cap\n got: %S (exit %d)\n wanted: %S"
|
|
out code want_cap;
|
|
(* 4096 distinct names fit; the next one stops the process. The table is
|
|
fixed and never moves, because a loaded module holds the address of a
|
|
cell in it, so growing is not available and overrunning is the only
|
|
other thing it could do. *)
|
|
let code, out, err = mode "names" in
|
|
if code = 0 then fail "the registry accepted a 4097th name (exit 0)";
|
|
if out <> "interned 4096\n" then
|
|
fail "the registry did not take 4096 names first: %S" out;
|
|
if not (has err "out of dev name slots") then
|
|
fail "the registry overflowed without saying so: %S" err;
|
|
|
|
Printf.printf
|
|
"reload: emit %.1fms llc %.1fms ld %.1fms (v2: emit %.1fms llc %.1fms ld %.1fms) host run %.1fms\n"
|
|
emit_ms t1.Build.llc_ms t1.Build.link_ms emit2_ms t2.Build.llc_ms
|
|
t2.Build.link_ms dlopen_ms;
|
|
print_string timings;
|
|
|
|
List.iter (fun p -> try Sys.remove p with Sys_error _ -> ())
|
|
[ host; limits; so1; so2; so3; so4; so5; out; out5; err5; tmp "err" ];
|
|
if !failures = 0 then print_endline "reload: all tests passed"
|
|
else begin
|
|
Printf.printf "\n%d failure(s)\n" !failures;
|
|
exit 1
|
|
end
|
|
| _ -> print_endline "reload: skipped (no clang or llc on PATH)"
|