`flan emit --x86` printed a three-line header and then nothing but .byte blobs. The information was all there and none of it was written down. Each run of bytes is now headed by the Flan form that produced it, with the position it was written at, indented by how deeply the form nests. The headings are queued rather than written, so a form that emits nothing does not leave its heading on the next form's bytes; atoms queue none at all, because a literal operand would otherwise steal the heading standing above the imul that consumes it. Above each function is a frame map, which is the half no disassembly recovers: every value in this backend lives in a frame temporary, so -0x20(%rbp) is the whole vocabulary of the listing and nothing says what it means. It is read out of what emit_fn already keeps, so it cannot drift. Beside it, where the arguments arrived and whether there is a hidden sret. And the bookkeeping is named where it appears -- the transfer guard, the bounds triple, the arithmetic guards, rep movsb, the dev indirection cell -- with each explained once in a legend at the top rather than at every site. Always on for `emit --x86`, which exists to be read, and never for a build, whose .s is a temp file handed to clang. spike/x86/annot.sh is the check that this costs no byte: emit both ways, assemble both, compare every section. 342 SAME / 0 DIFFER over the corpus in default, --dev and --debug. dump.sh now shows the annotated listing beside objdump's disassembly -- why beside what, which is the pairing that answers the mnemonics question. survey.sh has not been run on this; see the handoff.
93 lines
4.1 KiB
Bash
Executable File
93 lines
4.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# Every lowering of one program, side by side: the LLVM IR the frontend emits,
|
|
# what LLVM makes of it at -O0 and at -O2, and what the hand-written backend
|
|
# emits -- the last of those twice, as the annotated listing it writes and as
|
|
# the disassembly of the object that listing assembles to. Reading one against
|
|
# another is the only way to check a lowering by eye, and the whole reason the
|
|
# second backend is trustworthy is that the two agree.
|
|
#
|
|
# $ spike/x86/dump.sh file.flan [name]
|
|
#
|
|
# With a name, each output is narrowed to that function -- which is almost
|
|
# always what you want, because the prelude is emitted too and a two-line
|
|
# program is ten thousand lines of IR. The name is the Flan one: `twice`, not
|
|
# `flan.twice`.
|
|
#
|
|
# Output goes to a directory printed at the end and is not cleaned up; these are
|
|
# files you are going to read several times and diff against each other.
|
|
#
|
|
# FLAN overrides the compiler; the default is the one dune just built. Flags
|
|
# after the name are passed to every stage that understands them, so
|
|
# `dump.sh f.flan twice --dev` compares the dev lowerings rather than the
|
|
# release ones.
|
|
set -e
|
|
here=$(cd "$(dirname "$0")" && pwd)
|
|
root=$(cd "$here/../.." && pwd)
|
|
FLAN=${FLAN:-$root/_build/default/bin/main.exe}
|
|
case $FLAN in /*) ;; *) FLAN=$(cd "$(dirname "$FLAN")" && pwd)/$(basename "$FLAN") ;; esac
|
|
|
|
[ -n "$1" ] || { echo "usage: $0 <file.flan> [name] [flags...]" >&2; exit 2; }
|
|
src=$1; shift
|
|
case $1 in ''|-*) fn= ;; *) fn=$1; shift ;; esac
|
|
|
|
out=$(mktemp -d "${TMPDIR:-/tmp}/flan-dump.XXXXXX")
|
|
base=$(basename "$src" .flan)
|
|
|
|
"$FLAN" emit "$src" "$@" > "$out/$base.ll"
|
|
llc -O0 "$out/$base.ll" -o "$out/$base.O0.s"
|
|
llc -O2 "$out/$base.ll" -o "$out/$base.O2.s"
|
|
# Two files out of the backend, and they answer different questions.
|
|
#
|
|
# The .s is what it emits, and it is annotated: a frame map above each
|
|
# function saying which rbp displacement is which parameter and which is which
|
|
# named local, the Flan form above each run of bytes that produced it, and a
|
|
# name for each piece of bookkeeping the compiler added. That is the *why*, and
|
|
# it is the file to read when the question is where eleven instructions came
|
|
# from.
|
|
#
|
|
# The disassembly is the *what*. The backend writes machine code rather than
|
|
# mnemonics -- .byte blobs, so that every offset stays exactly known -- so
|
|
# assembling and disassembling is the only way to see the instructions, and it
|
|
# is also a check that the bytes are well formed, which reading them never
|
|
# would be.
|
|
"$FLAN" emit --x86 "$src" "$@" > "$out/$base.x86.s"
|
|
as --64 -o "$out/$base.x86.o" "$out/$base.x86.s"
|
|
objdump -d --no-show-raw-insn "$out/$base.x86.o" > "$out/$base.x86.dis"
|
|
|
|
if [ -n "$fn" ]; then
|
|
# LLVM names a Flan function `flan.<name>`; the IR spells it @"flan.<name>"
|
|
# when it needs quoting, and the assembler output as a plain label.
|
|
awk -v f="flan.$fn" '
|
|
$0 ~ "^define .*@\"?" f "\"?\\(" {p=1} p; p && /^}/ {p=0}' \
|
|
"$out/$base.ll" > "$out/$base.fn.ll" || true
|
|
for o in O0 O2; do
|
|
awk -v f="flan.$fn" '
|
|
$0 ~ "^\"?" f "\"?:" {p=1} p; p && /\.size/ {p=0}' \
|
|
"$out/$base.$o.s" > "$out/$base.fn.$o.s" || true
|
|
done
|
|
awk -v f="<flan.$fn>:" '$0 ~ f {p=1} p; p && /^$/ {p=0}' \
|
|
"$out/$base.x86.dis" > "$out/$base.fn.x86" || true
|
|
# The annotated listing for the same function, which is the half of the four
|
|
# that says *why*. It has to carry the frame map with it -- the map sits above
|
|
# the .globl, and without it every displacement in the body is anonymous -- so
|
|
# this keeps the most recent run of comment lines in hand and prints it when
|
|
# the function's label arrives.
|
|
awk -v f="flan.$fn" '
|
|
/^#/ { if (!inc) buf = ""; inc = 1; buf = buf $0 "\n"; next }
|
|
{ inc = 0 }
|
|
$0 ~ "^\"?" f "\"?:" { printf "%s", buf; p = 1 }
|
|
p
|
|
p && /\.size/ { p = 0 }' \
|
|
"$out/$base.x86.s" > "$out/$base.fn.x86.s" || true
|
|
|
|
for f in "$out/$base.fn.ll" "$out/$base.fn.O0.s" "$out/$base.fn.O2.s" \
|
|
"$out/$base.fn.x86.s" "$out/$base.fn.x86"; do
|
|
[ -s "$f" ] || { echo "nothing for '$fn' in $(basename "$f")" >&2; continue; }
|
|
echo "=== $(basename "$f") ==="
|
|
cat "$f"
|
|
echo
|
|
done
|
|
fi
|
|
|
|
echo "all of them in $out"
|