269 lines
9.7 KiB
Bash
Executable File
269 lines
9.7 KiB
Bash
Executable File
#!/bin/sh
|
|
# M3 differential TI-compiler control-flow/Xlib check.
|
|
#
|
|
# scripts/tests/m3/diff/check.sh
|
|
#
|
|
# The stored ti-branch.s/ti-xhello.s are one-time outputs of the 1987 TI
|
|
# compiler (GSP C Compiler 2.01) under DOSBox. CI never runs DOSBox: this
|
|
# script greps the stored golden files and compares their ABI shape with the
|
|
# assembly our compiler emits for the equivalent C.
|
|
#
|
|
# The oracle is the ABI shape, not instruction selection. Each requirement
|
|
# accepts the TI spellings and ours:
|
|
#
|
|
# sumto a conditional branch, an unconditional loop back-edge, and a last
|
|
# RETS of RETS 2
|
|
# max2 a conditional branch, and a last RETS of RETS 2
|
|
# xhello nine argument pushes before _XCreateSimpleWindow, and the ROM bit
|
|
# displacements 0x480 (Display.screens) / 0x40 (Screen.root)
|
|
#
|
|
# This check does not verify the result register, the argument order, or the
|
|
# "background first, display last" sequence: the MAME gate checks the returned
|
|
# values, and dis-roundtrip.sh checks the push order.
|
|
#
|
|
# TI emits JRLE/JRUC where we emit JAxx/JAUC; both are accepted. TI prints the
|
|
# displacements as decimal 1152/64, so 480h/40h are accepted too. A
|
|
# displacement only counts inside the load that uses it: a bare 64/1152
|
|
# elsewhere in the body (an unrelated local at *FP(64), the constant 0x400,
|
|
# an 0x4800 that merely shares a prefix) is not evidence.
|
|
#
|
|
# Our branch.c is built exactly like the M3 gate: tms-clang -O2 with the
|
|
# -replexitval=never optimizer (that rewrite would turn sumto's loop into an
|
|
# i33 multiply, which the target cannot select), then llc -O0. Dropping
|
|
# -replexitval=never makes this check fail to compile the loop, not pass.
|
|
#
|
|
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC TI_BRANCH TI_XHELLO
|
|
set -eu
|
|
|
|
ROOT=$(cd "$(dirname "$0")/../../../.." && pwd)
|
|
DIR=$ROOT/scripts/tests/m3/diff
|
|
SRC=$ROOT/scripts/m3
|
|
|
|
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
|
|
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
|
|
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
|
|
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
|
|
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
|
|
TI_BRANCH=${TI_BRANCH:-$DIR/ti-branch.s}
|
|
TI_XHELLO=${TI_XHELLO:-$DIR/ti-xhello.s}
|
|
export TMS_CLANG TMS_OPT TMS_BITADDR
|
|
|
|
fail() {
|
|
echo "M3 diff: FAIL: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
|
|
[ -x "$tool" ] || fail "missing tool: $tool"
|
|
done
|
|
[ -f "$TI_BRANCH" ] || fail "stored TI golden missing: $TI_BRANCH"
|
|
[ -f "$TI_XHELLO" ] || fail "stored TI golden missing: $TI_XHELLO"
|
|
|
|
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m3-diff.XXXXXX")
|
|
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
|
|
|
# branch.c is generated through a wrapper so only this file keeps its loop.
|
|
OPT_NOREPLEXIT=$TMP/opt-noreplexit
|
|
printf '#!/bin/sh\nexec "%s" -replexitval=never "$@"\n' "$TMS_OPT" > "$OPT_NOREPLEXIT"
|
|
chmod +x "$OPT_NOREPLEXIT"
|
|
|
|
echo "== compile our branch.s and xhello.s"
|
|
TMS_OPT=$OPT_NOREPLEXIT "$ROOT/scripts/tms-clang" -O2 -I "$SRC" \
|
|
-o "$TMP/branch.ll" "$SRC/branch.c"
|
|
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm "$TMP/branch.ll" -o "$TMP/branch.s"
|
|
"$ROOT/scripts/tms-clang" -O2 -I "$SRC" \
|
|
-o "$TMP/xhello.ll" "$SRC/xhello.c"
|
|
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm "$TMP/xhello.ll" -o "$TMP/xhello.s"
|
|
echo " branch.s: $(grep -c . "$TMP/branch.s") lines"
|
|
echo " xhello.s: $(grep -c . "$TMP/xhello.s") lines"
|
|
|
|
# Our llc output has `;` end-of-line comments (`; %bb.0`). Strip them so a
|
|
# comment cannot satisfy a check; TI asm has none.
|
|
for f in "$TMP/branch.s" "$TMP/xhello.s"; do
|
|
sed 's/;.*//' "$f" > "$f.code"
|
|
done
|
|
|
|
# body FILE FUNC: from `_func:` to the next .globl/.end.
|
|
body() {
|
|
awk -v want="_$2:" '
|
|
$1 == want { infn = 1; next }
|
|
infn && ($1 == ".globl" || $1 == ".end") { exit }
|
|
infn { print }
|
|
' "$1"
|
|
}
|
|
|
|
require() {
|
|
_file=$1
|
|
_pat=$2
|
|
_what=$3
|
|
if grep -Eiq "$_pat" "$_file"; then
|
|
echo " ok $_what"
|
|
return 0
|
|
fi
|
|
echo "M3 diff: FAIL: missing $_what" >&2
|
|
echo " pattern: $_pat" >&2
|
|
sed 's/^/ /' "$_file" >&2
|
|
exit 1
|
|
}
|
|
|
|
# require_last_rets2 FILE WHAT: the last RETS instruction in the body (the
|
|
# epilogue) is RETS 2. Matching any RETS 2 elsewhere would let a wrong
|
|
# epilogue through.
|
|
require_last_rets2() {
|
|
if awk '
|
|
/^[[:space:]]*RETS([[:space:]]|$)/ { last = $0; seen = 1 }
|
|
END {
|
|
if (!seen)
|
|
exit 1
|
|
exit (last ~ /^[[:space:]]*RETS[[:space:]]+2[[:space:]]*$/) ? 0 : 1
|
|
}
|
|
' "$1"; then
|
|
echo " ok $2"
|
|
return 0
|
|
fi
|
|
echo "M3 diff: FAIL: $2 (last RETS is not RETS 2)" >&2
|
|
sed 's/^/ /' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# A conditional branch: JA/JR plus a condition suffix. JAUC/JRUC/JRU do not
|
|
# match because their suffix is U/UC.
|
|
COND='^[[:space:]]*J[A-Z]*(EQ|NE|LT|LE|GT|GE|V|NV|P|N|Z|NZ|PL|MI)[[:space:]]'
|
|
# An unconditional branch: our JAUC, TI's JRUC (and bare JRU).
|
|
UNCOND='^[[:space:]]*J(AUC|RUC|RU|UMP)[[:space:]]'
|
|
|
|
# has_backedge FILE: true when a branch targets a label defined earlier in the
|
|
# same function body. TI closes the loop with the conditional JRLE L4; we
|
|
# close it with the unconditional JAUC LBB0_1, so either form counts.
|
|
has_backedge() {
|
|
awk '
|
|
/^[A-Za-z_][A-Za-z0-9_]*:/ {
|
|
lbl = $1
|
|
sub(/:$/, "", lbl)
|
|
pos[lbl] = NR
|
|
next
|
|
}
|
|
/^[[:space:]]*J/ {
|
|
if ($2 in pos && pos[$2] < NR)
|
|
found = 1
|
|
}
|
|
END { exit found ? 0 : 1 }
|
|
' "$1"
|
|
}
|
|
|
|
check_branch() { # check_branch FILE LABEL
|
|
_f=$1
|
|
body "$_f" sumto > "$TMP/sumto.body"
|
|
body "$_f" max2 > "$TMP/max2.body"
|
|
require "$TMP/sumto.body" "$COND" "$2: sumto has a conditional branch"
|
|
require "$TMP/sumto.body" "$UNCOND" "$2: sumto has an unconditional branch"
|
|
if ! has_backedge "$TMP/sumto.body"; then
|
|
echo "M3 diff: FAIL: $2: sumto has no loop back-edge" >&2
|
|
sed 's/^/ /' "$TMP/sumto.body" >&2
|
|
exit 1
|
|
fi
|
|
echo " ok $2: sumto branches back to the loop"
|
|
require_last_rets2 "$TMP/sumto.body" "$2: sumto ends with RETS 2"
|
|
require "$TMP/max2.body" "$COND" "$2: max2 has a conditional branch"
|
|
require_last_rets2 "$TMP/max2.body" "$2: max2 ends with RETS 2"
|
|
}
|
|
|
|
echo "== ABI shape: TI golden"
|
|
check_branch "$TI_BRANCH" "TI"
|
|
echo "== ABI shape: our compiler"
|
|
check_branch "$TMP/branch.s.code" "ours"
|
|
|
|
# Count the argument pushes since the previous CALLA, then require nine for
|
|
# XCreateSimpleWindow. Both compilers push the last argument first: display,
|
|
# root, x, y, width, height, border_width, border, background.
|
|
count_xcreate_pushes() {
|
|
awk '
|
|
/^_xhello:/ { infn = 1; n = 0; next }
|
|
!infn { next }
|
|
/CALLA[[:space:]]+_XCreateSimpleWindow/ { print n; exit }
|
|
/CALLA/ { n = 0; next }
|
|
/MOVE[[:space:]]+[^,]+,[[:space:]]*\*(STK|A14)\+/ { n++ }
|
|
' "$1"
|
|
}
|
|
|
|
for pair in "$TI_XHELLO:TI" "$TMP/xhello.s.code:ours"; do
|
|
f=${pair%%:*}
|
|
who=${pair##*:}
|
|
pushes=$(count_xcreate_pushes "$f")
|
|
[ -n "$pushes" ] || fail "$who xhello has no CALLA _XCreateSimpleWindow"
|
|
[ "$pushes" = "9" ] \
|
|
|| fail "$who XCreateSimpleWindow has $pushes argument pushes, want 9"
|
|
echo " ok $who: XCreateSimpleWindow takes nine argument pushes"
|
|
done
|
|
|
|
# TI: Display.screens is the indexed load `MOVE *A7(1152),A7,1`; Screen.root
|
|
# is loaded by `MOVE *A7(64),*FP(32),1`. The closing paren and the comma keep
|
|
# the boundary exact, so 0x4800/0x400 and decimal 11520/640 do not match; the
|
|
# register names may vary.
|
|
TI_SCREENS='^[[:space:]]*MOVE[[:space:]]+\*(A[0-9]+|FP|SP)\((1152|0x480|480h)\),'
|
|
TI_ROOT='^[[:space:]]*MOVE[[:space:]]+\*(A[0-9]+|FP|SP)\((64|0x40|40h)\),[[:space:]]*\*(A[0-9]+|FP|SP)\([0-9]+\)'
|
|
|
|
# has_addi_load FILE screens|root: our displacement is materialised by
|
|
# `ADDI <disp>, A<n>` and then used by a load through that same register
|
|
# (`MOVE *A<n>,...`). Only that pairing counts, not any ADDI in the body.
|
|
has_addi_load() {
|
|
awk -v kind="$2" '
|
|
function disp_ok(d) {
|
|
if (kind == "screens")
|
|
return d == "1152" || d == "0x480" || d == "480h"
|
|
return d == "64" || d == "0x40" || d == "40h"
|
|
}
|
|
/^[[:space:]]*ADDI[[:space:]]/ {
|
|
line = $0
|
|
sub(/^[[:space:]]*ADDI[[:space:]]+/, "", line)
|
|
split(line, f, /[[:space:]]*,[[:space:]]*/)
|
|
disp = f[1]
|
|
reg = f[2]
|
|
if (getline nxt <= 0)
|
|
next
|
|
while (nxt ~ /^[[:space:]]*$/) {
|
|
if (getline nxt <= 0)
|
|
next
|
|
}
|
|
if (disp_ok(disp) &&
|
|
nxt ~ ("^[[:space:]]*MOVE[[:space:]]+\\*" reg "([[:space:],]|$)"))
|
|
found = 1
|
|
}
|
|
END { exit found ? 0 : 1 }
|
|
' "$1"
|
|
}
|
|
|
|
check_offsets() { # check_offsets FILE WHO ti|ours
|
|
_f=$1
|
|
_who=$2
|
|
_side=$3
|
|
body "$_f" xhello > "$TMP/xhello.body"
|
|
if [ "$_side" = ti ]; then
|
|
require "$TMP/xhello.body" "$TI_SCREENS" \
|
|
"$_who: Display.screens is loaded at bit 0x480 (*A?(1152))"
|
|
require "$TMP/xhello.body" "$TI_ROOT" \
|
|
"$_who: Screen.root is loaded at bit 0x40 (*A?(64),*A?(...))"
|
|
return 0
|
|
fi
|
|
if has_addi_load "$TMP/xhello.body" screens; then
|
|
echo " ok $_who: Display.screens is loaded at bit 0x480 (ADDI 1152 + load)"
|
|
else
|
|
echo "M3 diff: FAIL: missing $_who: Display.screens load at bit 0x480" >&2
|
|
sed 's/^/ /' "$TMP/xhello.body" >&2
|
|
exit 1
|
|
fi
|
|
if has_addi_load "$TMP/xhello.body" root; then
|
|
echo " ok $_who: Screen.root is loaded at bit 0x40 (ADDI 64 + load)"
|
|
else
|
|
echo "M3 diff: FAIL: missing $_who: Screen.root load at bit 0x40" >&2
|
|
sed 's/^/ /' "$TMP/xhello.body" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
echo "== ROM bit displacements"
|
|
check_offsets "$TI_XHELLO" "TI" ti
|
|
check_offsets "$TMP/xhello.s.code" "ours" ours
|
|
|
|
echo "M3 diff: PASS"
|