65816-llvm-mos/scripts/probeLocals.sh
Scott Duensing da095402ec Updated
2026-06-02 23:17:57 -05:00

214 lines
7.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# probeLocals.sh - end-to-end validation harness for pc2line.py --locals.
#
# Phase 3.2 slice 2 smoke probe:
# 1. Compile a probe C file (4 i16 locals + a sentinel store at $025000)
# with -O0 -g.
# 2. Link with crt0+libgcc, produce DWARF sidecar + map.
# 3. Load the .bin into MAME, run until the sentinel store fires
# (poll bank-2 $5000 for 0xC0DE).
# 4. Snapshot the S register at that point and the stack memory
# around it.
# 5. Call pc2line.py --locals with the captured S; verify each
# reported ADDR= holds the expected constant (0xABCD/0x1234/0x5678).
#
# Exit 0 if at least the first variable's resolved ADDR yields the
# expected value (matches the smoke gate the plan asks for: "asserts
# at least one of x/y/z resolves correctly"). Exit non-zero on any
# build/link failure or MAME read mismatch.
#
# Usage: probeLocals.sh [--verbose]
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
VERBOSE=0
if [ "${1:-}" = "--verbose" ]; then
VERBOSE=1
fi
CLANG="$ROOT/tools/llvm-mos-build/bin/clang"
LLVMMC="$ROOT/tools/llvm-mos-build/bin/llvm-mc"
LINK="$ROOT/tools/link816"
if [ ! -x "$CLANG" ] || [ ! -x "$LLVMMC" ] || [ ! -x "$LINK" ]; then
echo "probeLocals: missing toolchain (clang/llvm-mc/link816)" >&2
exit 2
fi
if ! command -v mame >/dev/null 2>&1; then
echo "probeLocals: mame not on PATH; skipping" >&2
exit 77 # autotools-style "skip"
fi
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
CFILE="$WORK/loctest.c"
OFILE="$WORK/loctest.o"
OCRT0="$WORK/crt0.o"
OLIBGCC="$WORK/libgcc.o"
BIN="$WORK/loctest.bin"
MAP="$WORK/loctest.map"
DWARF="$WORK/loctest.dwarf"
LUA="$WORK/loctest.lua"
OUT="$WORK/loctest.out"
cat > "$CFILE" <<'EOF'
int main(void) {
int x = 0xABCD;
int y = 0x1234;
int z = 0x5678;
*(volatile unsigned short *)0x025000 = 0xC0DE;
while (1) { }
return 0;
}
EOF
"$CLANG" --target=w65816 -O0 -g -ffunction-sections \
-c "$CFILE" -o "$OFILE" 2>/dev/null
"$LLVMMC" -arch=w65816 -filetype=obj \
"$ROOT/runtime/src/crt0.s" -o "$OCRT0" 2>/dev/null
"$LLVMMC" -arch=w65816 -filetype=obj \
"$ROOT/runtime/src/libgcc.s" -o "$OLIBGCC" 2>/dev/null
"$LINK" -o "$BIN" --text-base 0x1000 \
--map "$MAP" --debug-out "$DWARF" \
"$OCRT0" "$OFILE" "$OLIBGCC" >/dev/null 2>&1 || true
[ -s "$BIN" ] || { echo "probeLocals: link produced empty .bin"; exit 1; }
[ -s "$DWARF" ] || { echo "probeLocals: link produced empty DWARF sidecar"; exit 1; }
MAIN_PC=$(awk '$2 == "main" { print $1; exit }' "$MAP")
[ -n "$MAIN_PC" ] || { echo "probeLocals: no 'main' symbol in map"; exit 1; }
# Lua: load .bin at $001000, kick PC, then poll bank-2 $5000 for the
# sentinel value 0xC0DE. When the sentinel fires, snapshot S + PC and
# the stack memory in the surrounding 64-byte window. Print everything
# on MAME- prefixed lines so the host script can grep them.
cat > "$LUA" <<EOF
local frame = 0
local loaded = false
local captured = false
emu.register_frame_done(function()
frame = frame + 1
if frame == 30 and not loaded then
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local f = io.open("$BIN", "rb"); local data = f:read("*all"); f:close()
for i = 1, #data do
local addr = 0x001000 + i - 1
if not (addr >= 0x00C000 and addr < 0x00D000) then
mem:write_u8(addr, data:byte(i))
end
end
cpu.state["PC"].value = 0x1000
cpu.state["PB"].value = 0
cpu.state["DB"].value = 0
cpu.state["D"].value = 0
cpu.state["P"].value = 0x34
cpu.state["E"].value = 0
cpu.state["S"].value = 0x01FF
loaded = true
print("MAME-LOADED bytes=" .. #data)
end
if loaded and not captured and frame > 35 then
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local sentinel = mem:read_u16(0x025000)
if sentinel == 0xC0DE then
local sp = cpu.state["S"].value
local pc = cpu.state["PC"].value
print(string.format("MAME-SENTINEL val=0x%04x", sentinel))
print(string.format("MAME-S val=0x%04x", sp))
print(string.format("MAME-PC val=0x%06x", pc))
-- Dump 64 bytes of stack around S (sp+0 .. sp+63) as u16
-- words. pc2line.py addresses we'll evaluate land in this
-- window (fbreg offsets are at most 24 for this probe).
for ofs = 0, 32 do
local addr = sp + ofs
local v = mem:read_u16(addr)
print(string.format("MAME-STACK addr=0x%06x val=0x%04x",
addr, v))
end
captured = true
manager.machine:exit()
end
end
if frame >= 240 then
print("MAME-TIMEOUT")
manager.machine:exit()
end
end)
EOF
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout 30 \
mame apple2gs -rompath "$ROOT/tools/mame/roms" \
-plugins -autoboot_script "$LUA" \
-video none -sound none -nothrottle -seconds_to_run 6 2>&1 \
| grep "^MAME-" > "$OUT" || true
if [ "$VERBOSE" -eq 1 ]; then
cat "$OUT" >&2
fi
if grep -q "^MAME-TIMEOUT$" "$OUT"; then
echo "probeLocals: timed out before sentinel fired" >&2
exit 1
fi
SP_HEX=$(awk -F= '/^MAME-S val=/ {print $NF; exit}' "$OUT")
PC_HEX=$(awk -F= '/^MAME-PC val=/ {print $NF; exit}' "$OUT")
SENTINEL=$(awk -F= '/^MAME-SENTINEL val=/ {print $NF; exit}' "$OUT")
if [ -z "$SP_HEX" ] || [ -z "$PC_HEX" ]; then
echo "probeLocals: no S/PC snapshot captured" >&2
exit 1
fi
if [ "$SENTINEL" != "0xc0de" ]; then
echo "probeLocals: sentinel mismatch ($SENTINEL)" >&2
exit 1
fi
echo "probeLocals: sentinel fired, S=$SP_HEX PC=$PC_HEX"
# Call pc2line.py --locals with the captured S. Expect three variables
# (the DW_TAG_variable DIEs for x, y, z) each with an ADDR= field.
LOCALS=$(python3 "$HERE/pc2line.py" --sidecar "$DWARF" --map "$MAP" \
--locals --sp "$SP_HEX" "$PC_HEX")
echo "$LOCALS"
# For each ADDR= line, read the stored value from the snapshot and
# compare against the set of expected constants. At least one must
# match (the slice gate).
EXPECTED=(abcd 1234 5678)
hits=0
while IFS= read -r line; do
addr_hex=$(echo "$line" | sed -nE 's/.* ADDR=0x([0-9a-fA-F]+).*/\1/p')
if [ -z "$addr_hex" ]; then
continue
fi
addr_norm=$(printf "0x%06x" "0x$addr_hex" 2>/dev/null || echo "")
# Find that addr in MAME-STACK lines.
snap=$(awk -F= -v want="$addr_norm" '
/^MAME-STACK addr=/ {
split($2, parts, " ")
a = parts[1]
v = $NF
if (a == want) { print v; exit }
}' "$OUT")
if [ -z "$snap" ]; then
continue
fi
snap_lc=$(echo "$snap" | tr 'A-Z' 'a-z' | sed 's/^0x//')
for exp in "${EXPECTED[@]}"; do
if [ "$snap_lc" = "$exp" ]; then
echo "probeLocals: HIT addr=$addr_norm value=0x$snap_lc"
hits=$((hits + 1))
break
fi
done
done <<< "$LOCALS"
if [ "$hits" -lt 1 ]; then
echo "probeLocals: FAIL: no variable's --locals ADDR resolved to a known constant" >&2
exit 1
fi
echo "probeLocals: OK ($hits/3 variables resolved correctly)"
exit 0