Files
totk-pc-recomp/tests/diff/common.py
T
Valiantsin DzerakhandClaude Opus 5.5 27056b212a Keep NZCV in a local of each generated function instead of c->nzcv
Every flag-setting instruction packed N, Z, C and V into c->nzcv and every reader unpacked them. recomp/flags.py
now runs a reaching-definitions pass per function: a compare stores its operands in a local Flags F, and a reader
whose definitions are all of one kind computes its condition from them, so cmp and b.cond become one host compare.
CCMP, FCMP, MSR NZCV and ADCS write the packed form, which readers reached by several kinds also use. Calls and
returns publish nothing (AAPCS64 neither passes nor preserves NZCV); tail branches, falls into the next function and
br through rt_dispatch publish c->nzcv, since discovery can start a function inside another one. Functions with an
unresolved computed br keep flags in c->nzcv.

1.4.3 main: 492k direct and 89k packed flag reads, .text 342 -> 299 MiB. Fight bench (bench-fight2-2026-10-27b,
frames 5400-7500, uncapped) 13.15 and 13.63 ms vs 13.9 before. tests/flags_check.py checks the new helpers against
the packed ones; tests/diff/func.py no longer compares NZCV, which generated code leaves untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:07:58 +03:00

276 lines
12 KiB
Python

"""Shared parts of the differential tests: Unicorn reference, operand generators, build and run of harness.c.
Adapted from LAS Recomp tools/test_lift.py and tools/test_simd.py @ da35b83 (MIT, Copyright (c) 2026 Peiikai).
"""
import os
import shutil
import struct
import subprocess
import sys
from pathlib import Path
import capstone
import unicorn as uc
from unicorn import arm64_const as A
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'recomp'))
BUF, BUF_SIZE = 0x7200000000, 0x40000
UC_INVALID = (uc.UC_ERR_INSN_INVALID, uc.UC_ERR_EXCEPTION)
# Record layouts of struct In / struct Out in harness.c.
IN_FMT = '<QQ32QQQ'
IN_TAIL = '<QQ'
OUT_SIZE = 32 * 8 + 4 * 8 + 32 * 16
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
F32 = [0, 0x80000000, 0x7f800000, 0xff800000, 0x7fc00000, 0xffc00000, 0x7fa00001, 0xff812345, 0x7fd00abc,
0x00000001, 0x80000001, 0x007fffff, 0x807fffff, 0x00400000, 0x00800000, 0x80800000, 0x00800001, 0x7f7fffff,
0xff7fffff, 0x3f800000, 0xbf800000, 0x3f000000, 0xbf000000, 0x40000000, 0x40400000, 0x3fc00000, 0xbfc00000,
0x40200000, 0x40100000, 0x4f000000, 0xcf000000, 0x4f800000, 0x5f000000, 0xdf000000, 0x5f800000, 0x4effffff,
0x4f7fffff, 0x5effffff, 0x5f7fffff, 0x3effffff, 0x3f7fffff, 0x7effffff, 0x01000000, 0x3e800000, 0x7e800000,
0x7e7fffff, 0x00200000, 0x001fffff, 0x00200001, 0x47800000, 0x477fe000, 0x477ff000, 0x38800000, 0x33800000,
0x33000000, 0x387fc000, 0x3f7ff000, 0x46fffe00, 0x80000000 | 0x00200000, 0x7f000000, 0x7f7ffffe]
F64 = [0, 1 << 63, 0x7ff0000000000000, 0xfff0000000000000, 0x7ff8000000000000, 0xfff8000000000000,
0x7ff4000000000001, 0xfff0000000012345, 0x7ffc00000abcdef0, 1, (1 << 63) | 1, 0x000fffffffffffff,
0x0008000000000000, 0x0010000000000000, 0x8010000000000000, 0x7fefffffffffffff, 0xffefffffffffffff,
0x3ff0000000000000, 0xbff0000000000000, 0x3fe0000000000000, 0x4000000000000000, 0x4008000000000000,
0x3ff8000000000000, 0xbff8000000000000, 0x4004000000000000, 0x41e0000000000000, 0xc1e0000000000000,
0x41f0000000000000, 0x43e0000000000000, 0xc3e0000000000000, 0x43f0000000000000, 0x41dfffffffc00000,
0x41efffffffe00000, 0x43dfffffffffffff, 0x43efffffffffffff, 0x3fdfffffffffffff, 0x7fdfffffffffffff,
0x7fd0000000000000, 0x0004000000000000, 0x0003ffffffffffff, 0x47efffffe0000000, 0x47efffff00000000,
0x36a0000000000000, 0x3810000000000000, 0x380fffffffffffff, 0x47f0000000000000, 0x3e70000000000000,
0x3f0ffc0000000000, 0x40effc0000000000, 0x40effe0000000000, 0x3e6ffffffff00000, 0x369fffffffffffff,
0x3810000010000000, 0x381000000fffffff, 0x3e80000000000000]
F16 = [0, 0x8000, 0x7c00, 0xfc00, 0x7e00, 0xfe00, 0x7d00, 0x7e55, 0x0001, 0x8001, 0x03ff, 0x0400, 0x7bff, 0xfbff,
0x3c00, 0xbc00, 0x3800, 0x4000, 0x3e00, 0x7fff, 0xffff]
_md = capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM)
def dis(w, pc=0):
d = next(_md.disasm(struct.pack('<I', w), pc), None)
return f'{d.mnemonic} {d.op_str}' if d else '?'
def mnemonic(w):
return dis(w).split(' ')[0]
def rand_lane(r, sz):
"""Random interesting integer lane of 8 << sz bits."""
bits_ = 8 << sz
k = r.random()
if k < 0.35:
return r.choice([0, 1, 2, 3, (1 << bits_) - 1, (1 << bits_) - 2, 1 << (bits_ - 1), (1 << (bits_ - 1)) - 1,
(1 << (bits_ - 1)) + 1, 0x80 % (1 << bits_), 0x7f, bits_ - 1, bits_, bits_ + 1,
(1 << bits_) - bits_, 1 << (bits_ // 2)])
if k < 0.5:
return r.getrandbits(r.randint(1, bits_))
if k < 0.6:
return ((1 << bits_) - r.getrandbits(r.randint(1, bits_ - 1))) & ((1 << bits_) - 1)
return r.getrandbits(bits_)
def rand_f32(r):
k = r.random()
if k < 0.4:
return r.choice(F32)
if k < 0.8:
e = r.choice([r.randint(110, 145), r.randint(0, 255), r.randint(120, 135)])
return (r.getrandbits(1) << 31) | (e << 23) | (r.getrandbits(23) if r.random() < 0.7 else
r.getrandbits(4) << 19)
return struct.unpack('<I', struct.pack('<f', r.choice([r.uniform(-4, 4), float(r.randint(-300, 300)) + 0.5,
float(r.randint(-5, 5)), r.uniform(-1e6, 1e6)])))[0]
def rand_f64(r):
k = r.random()
if k < 0.4:
return r.choice(F64)
if k < 0.8:
e = r.choice([r.randint(1000, 1050), r.randint(0, 2047), r.randint(1020, 1030), r.randint(0, 40),
r.randint(2000, 2047), r.randint(860, 920)])
return (r.getrandbits(1) << 63) | (e << 52) | (r.getrandbits(52) if r.random() < 0.7 else
r.getrandbits(6) << 46)
return struct.unpack('<Q', struct.pack('<d', r.choice([r.uniform(-4, 4), float(r.randint(-300, 300)) + 0.5,
r.uniform(-1e12, 1e12)])))[0]
def rand_v(r):
k = r.random()
out = 0
if k < 0.3:
for i in range(4):
out |= rand_f32(r) << (32 * i)
elif k < 0.55:
for i in range(2):
out |= rand_f64(r) << (64 * i)
elif k < 0.6:
for i in range(8):
out |= (r.choice(F16) if r.random() < 0.5 else r.getrandbits(16)) << (16 * i)
elif k < 0.9:
sz = r.randrange(4)
for i in range(16 >> sz):
out |= rand_lane(r, sz) << ((8 << sz) * i)
else:
out = r.getrandbits(128)
if r.random() < 0.15: # upper half zero, like most 64-bit results
out &= (1 << 64) - 1
return out
def rand_x(r):
k = r.random()
if k < 0.3:
return r.choice([0, 1, 2, 0xFFFFFFFFFFFFFFFF, 0x7FFFFFFFFFFFFFFF, 0x8000000000000000, 0xFFFFFFFF,
0x7FFFFFFF, 0x80000000, 0x100000000, 0xFFFFFFFF80000000, 0x8000000000000001, 0xFFFFFF,
0x1000001, 0x20000000000001, 0xFFFFFFFFFF000001, 63, 64, 31, 32, 0xFF, 0xFFFF])
if k < 0.45:
return rand_f32(r) | (r.getrandbits(32) << 32 if r.random() < 0.3 else 0)
if k < 0.6:
return rand_f64(r)
if k < 0.75:
return r.getrandbits(r.randint(1, 64))
if k < 0.8:
return (-r.getrandbits(r.randint(1, 63))) & 0xFFFFFFFFFFFFFFFF
return r.getrandbits(64)
def rand_fpcr(r):
k = r.random()
if k < 0.55:
return 0
v = 0
if r.random() < 0.4:
v |= 1 << 25 # DN
if r.random() < 0.4:
v |= 1 << 24 # FZ
if r.random() < 0.4:
v |= r.randrange(4) << 22 # RMode
if r.random() < 0.15:
v |= 1 << 26 # AHP
return v
class Emu:
"""Unicorn as a Cortex-A57, the Switch's CPU, unless another model is given (LSE atomics need
UC_CPU_ARM64_MAX). Images are (base, bytes) pairs mapped at their guest address."""
def __init__(self, images, model=A.UC_CPU_ARM64_A57):
mu = uc.Uc(uc.UC_ARCH_ARM64, uc.UC_MODE_ARM)
mu.ctl_set_cpu_model(model)
mu.mem_map(BUF, BUF_SIZE)
for base, data in images:
mu.mem_map(base, (len(data) + 0xFFFF) & ~0xFFFF)
mu.mem_write(base, bytes(data))
mu.reg_write(A.UC_ARM64_REG_CPACR_EL1, 0x300000) # enable FP/SIMD
self.mu = mu
self.xregs = [A.UC_ARM64_REG_X0 + i for i in range(29)] + [A.UC_ARM64_REG_X29, A.UC_ARM64_REG_X30]
self.qregs = [A.UC_ARM64_REG_Q0 + i for i in range(32)]
self.out_regs = self.xregs + [A.UC_ARM64_REG_SP, A.UC_ARM64_REG_NZCV, A.UC_ARM64_REG_PC] + self.qregs
def run(self, pc, fpcr, x, sp, nzcv, v, count=1):
"""Run count instructions from pc. Returns (x[31], sp, nzcv, pc, v[32])."""
mu = self.mu
mu.reg_write_batch(list(zip(self.xregs, x[:31])) + list(zip(self.qregs, v)) +
[(A.UC_ARM64_REG_SP, sp), (A.UC_ARM64_REG_NZCV, nzcv), (A.UC_ARM64_REG_FPCR, fpcr)])
mu.emu_start(pc, pc + 4 * count + 4, count=count)
o = mu.reg_read_batch(self.out_regs)
return list(o[:31]), o[31], o[32] & 0xF0000000, o[33], list(o[34:])
def compiler():
cc = os.environ.get('CC')
if cc:
return cc
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
return str(MSYS_BIN / 'clang.exe')
return shutil.which('clang') or 'clang'
def build_harness(work, bodies, jobs, cflags):
"""Compile one C function per test body plus harness.c into work/harness[.exe]."""
cc = compiler()
env = dict(os.environ)
if os.name == 'nt' and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
flags = ['-O1', '-fno-strict-aliasing', '-Wno-unused-label', '-I', str(ROOT / 'runtime')] + cflags.split()
n = max(1, min(jobs, len(bodies) // 200 + 1))
per = (len(bodies) + n - 1) // n
srcs = []
for j in range(n):
path = work / f'tests_{j}.c'
with open(path, 'w') as f:
f.write('#include "cpu.h"\n')
for k in range(j * per, min(len(bodies), (j + 1) * per)):
f.write(f'void t{k}(Cpu *c) {{ {bodies[k]} }}\n')
srcs.append(path)
with open(work / 'table.c', 'w') as f:
f.write('#include "cpu.h"\n')
f.write(''.join(f'void t{k}(Cpu *c);\n' for k in range(len(bodies))))
f.write('GuestFn tests[] = {' + ','.join(f't{k}' for k in range(len(bodies))) + '};\n')
f.write(f'int ntests = {len(bodies)};\n')
srcs += [work / 'table.c', ROOT / 'tests' / 'diff' / 'harness.c']
objs, procs = [], []
for s in srcs:
o = work / (s.stem + '.o')
objs.append(o)
procs.append(subprocess.Popen([cc] + flags + ['-c', str(s), '-o', str(o)], env=env))
if any(p.wait() for p in procs):
raise SystemExit('compile failed')
exe = work / ('harness.exe' if os.name == 'nt' else 'harness')
subprocess.check_call([cc, '-fuse-ld=lld', '-o', str(exe)] + [str(o) for o in objs], env=env)
return exe
def write_cases(path, cases, stubs=False):
"""cases: (k, fpcr, x[31], sp, nzcv, v[32], memchk). stubs: calls go to harness.c's call stub (rt_stub)."""
with open(path, 'wb') as f:
for k, fpcr, x, sp, nzcv, v, memchk in cases:
f.write(struct.pack(IN_FMT, k, fpcr, *(list(x[:31]) + [0]), sp, nzcv))
for q in v:
f.write(q.to_bytes(16, 'little'))
f.write(struct.pack(IN_TAIL, int(memchk), int(stubs)))
def run_harness(exe, work, buf0, cases, images, stubs=False):
"""Run the lifted tests. Returns (records, stderr tail or None). images: (base, path) pairs."""
with open(work / 'buf.bin', 'wb') as f:
f.write(buf0)
write_cases(work / 'cases.bin', cases, stubs)
out = work / 'out.bin'
args = [str(exe), str(work / 'buf.bin'), str(work / 'cases.bin'), str(out)]
for base, path in images:
args += [str(path), f'{base:x}']
p = subprocess.run(args, capture_output=True)
err = None if p.returncode == 0 else f'exit {p.returncode:#x}: {p.stderr.decode(errors="replace")[-2000:]}'
data = out.read_bytes() if out.exists() else b''
recs = []
for i in range(len(data) // OUT_SIZE):
rec = data[i * OUT_SIZE:(i + 1) * OUT_SIZE]
x = struct.unpack_from('<31Q', rec, 0)
sp, nzcv, pc, mem = struct.unpack_from('<4Q', rec, 256)
v = [int.from_bytes(rec[288 + 16 * j:304 + 16 * j], 'little') for j in range(32)]
recs.append((x, sp, nzcv, pc, mem, v))
return recs, err
def diff_state(got, want, check_pc, check_mem, check_nzcv=True):
"""got: harness record; want: (x, sp, nzcv, pc, v, mem). Returns a list of mismatches."""
hx, hsp, hnz, hpc, hmem, hv = got
ex, esp, enz, epc, ev, emem = want
d = [f'x{j}: got {hx[j]:x} want {ex[j]:x}' for j in range(31) if hx[j] != ex[j]]
if hsp != esp:
d.append(f'sp: got {hsp:x} want {esp:x}')
if check_nzcv and hnz != enz:
d.append(f'nzcv: got {hnz:x} want {enz:x}')
if check_pc and hpc != epc:
d.append(f'pc: got {hpc:x} want {epc:x}')
if check_mem and hmem != emem:
d.append('memory differs')
d += [f'v{j}: got {hv[j]:032x} want {ev[j]:032x}' for j in range(32) if hv[j] != ev[j]]
return d