Every lowering of one program, side by side

This commit is contained in:
Joseph Ferano 2026-09-14 11:05:35 +07:00
parent 7f44cc3c8f
commit f77216212e
2 changed files with 94 additions and 1 deletions

View File

@ -423,6 +423,33 @@ let () =
"flan emit: --target is refused — the emitted IR carries no triple and \
no datalayout, and the target is chosen at build.";
exit 2
(* --x86 prints the hand-written backend's assembly where the default prints
LLVM IR. The two are the same act here is what this program compiles to,
before an assembler or an optimiser has touched it and reading one
against the other is the only way to check a lowering by eye. --sanitize
is refused rather than ignored: ASan is an LLVM pass and this backend has
no arm for it, so honouring the flag is impossible and dropping it
silently would print something that is not what --sanitize builds. *)
| _ :: "emit" :: args
when List.mem x86_flag args && List.exists (fun a -> not (is_flag a)) args ->
if List.mem sanitize_flag args then begin
prerr_endline
"flan emit --x86: --sanitize is an LLVM pass and this backend has no \
arm for it emit without --x86 to see what a sanitized build compiles.";
exit 2
end;
let checks = not (List.mem no_checks_flag args) in
let dev = List.mem dev_flag args in
let debug = List.mem debug_flag args in
let files = List.filter (fun a -> not (is_flag a)) args in
List.iter
(fun path ->
with_errors path (fun () ->
load path |> fun l ->
Flan.Check.program_all l.decls
|> Flan.X86.program ~checks ~dev ~debug
|> print_string))
files
| _ :: "emit" :: args when List.exists (fun a -> not (is_flag a)) args ->
let checks = not (List.mem no_checks_flag args) in
let dev = List.mem dev_flag args in
@ -582,7 +609,7 @@ let () =
exit code)
| _ ->
prerr_endline
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n\
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n flan emit <file.flan> [--x86] [--dev] [--debug] [--no-bounds-checks]\n\
\ flan import-c <header.h> [package.flan...] [clang flags...]\n\
\ flan generate-c <package-dir>\n\
\ flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] \

66
spike/x86/dump.sh Executable file
View File

@ -0,0 +1,66 @@
#!/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. 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 four dev lowerings rather than the
# four 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"
# The backend writes machine code, not mnemonics, so its .s is .byte blobs.
# Assembling and disassembling is what makes it readable -- 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
for f in "$out/$base.fn.ll" "$out/$base.fn.O0.s" "$out/$base.fn.O2.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 four in $out"