#!/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 [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.`; the IR spells it @"flan." # 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=":" '$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"