flan/spike/x86/annot.sh
Joseph Ferano 238f65db59 A listing says which form it came from, and what each slot is
`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.
2026-09-14 11:54:36 +07:00

102 lines
3.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Does annotation change a single byte of what the backend emits?
#
# `flan emit --x86` annotates: a comment per Flan form, a frame map per
# function, and a name for each piece of bookkeeping the compiler adds. All of
# that is comments, plus the splitting of one long `.byte` directive into
# several. Both are supposed to be invisible to the assembler -- and "supposed
# to be" is exactly the kind of claim this repo measures rather than asserts,
# because the whole licence for annotating at all is that the bytes are the
# bytes.
#
# So: emit every program in the corpus both ways, assemble both, and compare
# each section of the two objects byte for byte. No linking and no running --
# survey.sh is what says the programs still behave, and this says nothing they
# are built from moved.
#
# Three settings, because they are three different emitters. The default; --dev,
# which adds the indirection cells and the ABI marker; and --debug, which adds
# the line table, the labels its rows hang off, and the CFI directives. The
# debug case is the sharp one: a `.debug_line` row is an address expressed as a
# label, and annotation emits no labels precisely so that those cannot move.
#
# Usage: spike/x86/annot.sh [name-substring ...]
set -u
orig=$(pwd)
here=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$here/../.." && pwd)
cd "$root" || exit 1
if [ -n "${FLAN:-}" ]; then
case $FLAN in /*) flan=$FLAN;; *) flan=$orig/$FLAN;; esac
else
dune build --root . bin/main.exe 2>&1 | head -30
flan=$root/_build/default/bin/main.exe
fi
test -x "$flan" || { echo "build failed"; exit 1; }
corpus=${SURVEY_CORPUS:-$root}
tmp=$(mktemp -d "${TMPDIR:-/tmp}/flan-annot.XXXXXX")
trap 'rm -rf "$tmp"' EXIT
same=0; differ=0; skip=0
# Every section either object has, not a fixed list: a section that exists on
# one side and not the other is itself a difference, and comparing a named list
# would miss one that annotation invented.
sections () {
objdump -h "$1" | awk '$1 ~ /^[0-9]+$/ { print $2 }'
}
check () {
src=$1; shift
name=$(basename "$src" .flan)
tag="$name${*:+ $*}"
a=$tmp/a.s; b=$tmp/b.s
if ! "$flan" emit --x86 "$@" "$src" > "$a" 2>"$tmp/err"; then
skip=$((skip + 1)); echo "SKIP $tag"; return
fi
if ! "$flan" emit --x86 --no-annotate "$@" "$src" > "$b" 2>/dev/null; then
skip=$((skip + 1)); echo "SKIP $tag"; return
fi
if ! as --64 -o "$tmp/a.o" "$a" 2>"$tmp/err"; then
differ=$((differ + 1))
echo "BADASM $tag -- the annotated listing does not assemble"
head -3 "$tmp/err"; return
fi
if ! as --64 -o "$tmp/b.o" "$b" 2>/dev/null; then
skip=$((skip + 1)); echo "SKIP $tag"; return
fi
bad=
for sec in $(sections "$tmp/a.o"; sections "$tmp/b.o"); do
case " $bad " in *" $sec "*) continue;; esac
objcopy -O binary --only-section="$sec" "$tmp/a.o" "$tmp/a.bin" 2>/dev/null
objcopy -O binary --only-section="$sec" "$tmp/b.o" "$tmp/b.bin" 2>/dev/null
cmp -s "$tmp/a.bin" "$tmp/b.bin" || bad="$bad $sec"
done
if [ -z "$bad" ]; then
same=$((same + 1)); echo "SAME $tag"
else
differ=$((differ + 1)); echo "DIFFER $tag --$bad"
fi
}
for src in "$corpus"/test/programs/*.flan "$corpus"/spike/x86/*.flan; do
[ -f "$src" ] || continue
if [ $# -gt 0 ]; then
hit=
for pat in "$@"; do case $src in *"$pat"*) hit=1;; esac; done
[ -n "$hit" ] || continue
fi
check "$src"
check "$src" --dev
check "$src" --debug
done
echo
echo "$same SAME / $differ DIFFER / $skip SKIP"
[ "$differ" -eq 0 ]