#!/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" <= 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