FnAddr (Fnval n) emitted the symbol, which is right for a whole-program build
and wrong the instant anything is redefined into it. It now reads the cell,
and so does every direct call, which is what emit.ml's body_of does and is the
half that matters: a redefinition is one store, and it has to reach call sites
that already exist.
What is emitted, all of it behind dev:
- one cell per function in .data, .globl, initialised to the body this build
compiled. Spelled exactly as Emit.cellname spells it, because the point of
having one here is that an LLVM-built module binds
@"flan.cell.<n>" = external global ptr against it. nm -D over the two
builds of the same program gives identical sets of 68 cell symbols.
- the cell load placed after the arguments, which emit.ml has as a
load-bearing comment: a redefinition landing between two calls must not
land in the middle of one. CallPtr stays the other way round.
- the flan_dev_reg_enable constructor, which arms the allocation registry.
Not emitted: Emit.cellptr, the deeper spelling for a name the host was never
built with. It cannot arise in a whole-program build and belongs with the
redefinition module that would introduce one.
The --x86 --dev refusal is relaxed, and the argument is that flan dev never
reaches this fork: --x86 is read only by flan build, and the daemon builds
host and modules through Build.executable / Build.shared without it. So the
flag means a host whose call sites are redefinable, and nothing claims the
module that would redefine through them exists.
Two things were needed to believe any of that. First, the corpus with --dev on
both sides: 97 MATCH, 0 DIFFER, same as without it. Before the constructor was
added that read 96/1 — registry.flan asks (live? ...) and got four zeroes,
which is the whole of what a dev host does differently besides the cells.
Second, and the corpus cannot do this one: a dev build starts with every cell
pointing at the body this build compiled, so it prints what a release build
prints whether anything reads the cell or not. spike/x86/cells.sh preloads a
shared object whose constructor dlsyms flan.cell.twice and stores a different
body there -- the one store a redefinition ends in, done from outside, no
compiler involved. Both dev builds then print the new answer for a direct call
and for a function value, and both release builds are unchanged, which is what
says the change came from the indirection and not from symbol interposition.
One thing the later lane inherits, now written in both headers rather than
left to be discovered. x86.ml licenses its own calling convention on the
grounds that a dev build is compiled entirely here and a release build
entirely by LLVM, so the two never meet in one process. A cell an LLVM-built
module can store into is the first thing that could make that false: the
conventions agree on scalars and disagree on every aggregate, so an
Emit.redefinition module dlopened into an --x86 host would be right until the
first redefined function took or returned a struct. The answer is a
redefinition emitter here, not a classifier.
60 lines
2.0 KiB
Bash
Executable File
60 lines
2.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Does a --x86 --dev build actually call through the indirection cell?
|
|
#
|
|
# survey.sh cannot answer this and no program can. A dev build begins with
|
|
# every cell pointing at the body this build compiled, so it prints exactly
|
|
# what a release build prints whether the call reads the cell or ignores it.
|
|
# The only way to tell is to change what a cell holds and see whether anything
|
|
# notices.
|
|
#
|
|
# So: cell-override.c is preloaded, and its constructor looks up
|
|
# flan.cell.twice with dlsym and stores a different body there. No compiler is
|
|
# involved and nothing is redefined in the language's sense -- this is just the
|
|
# one store a redefinition ends in, done from outside.
|
|
#
|
|
# Four builds, and the two controls are half the test:
|
|
#
|
|
# llvm --dev cell is read -> 22 22
|
|
# x86 --dev cell is read -> 22 22 (this lane's claim)
|
|
# llvm no cell -> 42 42 (dlsym answers NULL)
|
|
# x86 no cell -> 42 42
|
|
#
|
|
# The release rows are what say the change came from the indirection and not
|
|
# from ordinary symbol interposition.
|
|
set -u
|
|
here=$(cd "$(dirname "$0")" && pwd)
|
|
root=$(cd "$here/../.." && pwd)
|
|
cd "$root" || exit 1
|
|
|
|
dune build --root . bin/main.exe 2>&1 | head -30
|
|
flan=$root/_build/default/bin/main.exe
|
|
test -x "$flan" || { echo "build failed"; exit 1; }
|
|
|
|
out=$(mktemp -d); trap 'rm -rf "$out"' EXIT
|
|
cc -shared -fPIC -o "$out/override.so" "$here/cell-override.c" || exit 1
|
|
|
|
src=$here/p8-cell.flan
|
|
fail=0
|
|
|
|
run() { # run <label> <expected> <build flags...>
|
|
local label=$1 want=$2; shift 2
|
|
if ! "$flan" build "$src" "$@" -o "$out/p8" >"$out/build.err" 2>&1; then
|
|
echo "FAIL $label: build failed"; head -3 "$out/build.err"; fail=1; return
|
|
fi
|
|
local got
|
|
got=$(cd "$out" && LD_PRELOAD="$out/override.so" ./p8 | tr '\n' ' ')
|
|
got=${got% }
|
|
if [ "$got" = "$want" ]; then
|
|
echo "ok $label: $got"
|
|
else
|
|
echo "FAIL $label: expected '$want', got '$got'"; fail=1
|
|
fi
|
|
}
|
|
|
|
run "llvm --dev" "22 22" --dev
|
|
run "x86 --dev" "22 22" --dev --x86
|
|
run "llvm " "42 42"
|
|
run "x86 " "42 42" --x86
|
|
|
|
exit $fail
|