mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-25 14:01:43 +01:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
270 lines
10 KiB
Python
270 lines
10 KiB
Python
"""Generate instruction tests (Xenia's .s format) for instructions we added to
|
|
XenonRecomp that Xenia has no tests for, or too few.
|
|
|
|
Expected values come from an independent Python model of the PowerPC
|
|
semantics (big-endian element order, as written in the tests), never from the
|
|
recompiler, so a codegen bug can't agree with itself.
|
|
|
|
python3 tests/gen_instr_tests.py # writes tests/instr/nfsmw_*.s
|
|
"""
|
|
import os
|
|
import random
|
|
import struct
|
|
|
|
OUT = os.path.join(os.path.dirname(__file__), 'instr')
|
|
rng = random.Random(0x360)
|
|
|
|
|
|
# --- vector helpers: a vector is 4 big-endian words [w0, w1, w2, w3] ---------
|
|
|
|
def to_bytes(ws):
|
|
return b''.join(struct.pack('>I', w) for w in ws)
|
|
|
|
def from_bytes(b):
|
|
return [struct.unpack('>I', b[i:i + 4])[0] for i in range(0, 16, 4)]
|
|
|
|
def elems(ws, size, signed):
|
|
fmt = {1: 'b', 2: 'h', 4: 'i'}[size]
|
|
fmt = fmt if signed else fmt.upper()
|
|
return list(struct.unpack('>' + fmt * (16 // size), to_bytes(ws)))
|
|
|
|
def pack(values, size, signed):
|
|
fmt = {1: 'b', 2: 'h', 4: 'i'}[size]
|
|
fmt = fmt if signed else fmt.upper()
|
|
return from_bytes(struct.pack('>' + fmt * (16 // size), *values))
|
|
|
|
def sat(x, lo, hi):
|
|
return max(lo, min(hi, x))
|
|
|
|
def vec(ws):
|
|
return '[' + ', '.join(f'{w:08X}' for w in ws) + ']'
|
|
|
|
def rand_vec(size=None):
|
|
"""Random vector, biased toward edge values for the element size."""
|
|
if size is None or rng.random() < 0.3:
|
|
return [rng.getrandbits(32) for _ in range(4)]
|
|
n, bits = 16 // size, size * 8
|
|
edges = [0, 1, (1 << bits) - 1, 1 << (bits - 1), (1 << (bits - 1)) - 1]
|
|
vals = [rng.choice(edges) if rng.random() < 0.4 else rng.getrandbits(bits) for _ in range(n)]
|
|
return pack(vals, size, False)
|
|
|
|
|
|
# --- reference models --------------------------------------------------------
|
|
|
|
def shift_op(size, kind):
|
|
bits = size * 8
|
|
mask = (1 << bits) - 1
|
|
def f(a, b):
|
|
ua, sa, sh = elems(a, size, False), elems(a, size, True), elems(b, size, False)
|
|
out = []
|
|
for x, sx, s in zip(ua, sa, sh):
|
|
s &= bits - 1
|
|
if kind == 'sl': out.append((x << s) & mask)
|
|
elif kind == 'sr': out.append(x >> s)
|
|
elif kind == 'sra': out.append((sx >> s) & mask)
|
|
elif kind == 'rl': out.append(((x << s) | (x >> (bits - s))) & mask)
|
|
return pack(out, size, False)
|
|
return f
|
|
|
|
def sat_arith(size, signed, op):
|
|
bits = size * 8
|
|
lo, hi = (-(1 << (bits - 1)), (1 << (bits - 1)) - 1) if signed else (0, (1 << bits) - 1)
|
|
def f(a, b):
|
|
return pack([sat(op(x, y), lo, hi) for x, y in zip(elems(a, size, signed), elems(b, size, signed))], size, signed)
|
|
return f
|
|
|
|
def modular(size, op):
|
|
mask = (1 << (size * 8)) - 1
|
|
def f(a, b):
|
|
return pack([op(x, y) & mask for x, y in zip(elems(a, size, False), elems(b, size, False))], size, False)
|
|
return f
|
|
|
|
def elementwise(size, signed, op):
|
|
def f(a, b):
|
|
return pack([op(x, y) for x, y in zip(elems(a, size, signed), elems(b, size, signed))], size, signed)
|
|
return f
|
|
|
|
def pack_op(in_size, in_signed, out_signed):
|
|
out_bits = in_size * 4
|
|
lo, hi = (-(1 << (out_bits - 1)), (1 << (out_bits - 1)) - 1) if out_signed else (0, (1 << out_bits) - 1)
|
|
def f(a, b):
|
|
# vD = sat(vA elements) then sat(vB elements), in element order.
|
|
vals = [sat(x, lo, hi) for x in elems(a, in_size, in_signed) + elems(b, in_size, in_signed)]
|
|
return pack(vals, in_size // 2, out_signed)
|
|
return f
|
|
|
|
def compare(size, signed, op):
|
|
mask = (1 << (size * 8)) - 1
|
|
def f(a, b):
|
|
return pack([mask if op(x, y) else 0 for x, y in zip(elems(a, size, signed), elems(b, size, signed))], size, False)
|
|
return f
|
|
|
|
|
|
VECTOR_BINOPS = {
|
|
# name: (model, element size for biased inputs)
|
|
'vslh': (shift_op(2, 'sl'), 2),
|
|
'vsrh': (shift_op(2, 'sr'), 2),
|
|
'vsrah': (shift_op(2, 'sra'), 2),
|
|
'vrlh': (shift_op(2, 'rl'), 2),
|
|
'vsrab': (shift_op(1, 'sra'), 1),
|
|
'vaddsws': (sat_arith(4, True, lambda x, y: x + y), 4),
|
|
'vaddsbs': (sat_arith(1, True, lambda x, y: x + y), 1),
|
|
'vsubshs': (sat_arith(2, True, lambda x, y: x - y), 2),
|
|
'vsububm': (modular(1, lambda x, y: x - y), 1),
|
|
'vmaxsh': (elementwise(2, True, max), 2),
|
|
'vminsh': (elementwise(2, True, min), 2),
|
|
'vavguh': (elementwise(2, False, lambda x, y: (x + y + 1) >> 1), 2),
|
|
'vandc': (lambda a, b: [x & ~y & 0xFFFFFFFF for x, y in zip(a, b)], None),
|
|
'vpkswss': (pack_op(4, True, True), 4),
|
|
'vpkswus': (pack_op(4, True, False), 4),
|
|
'vpkshss': (pack_op(2, True, True), 2),
|
|
'vpkuhus': (pack_op(2, False, False), 2),
|
|
'vcmpgtsh': (compare(2, True, lambda x, y: x > y), 2),
|
|
'vcmpgtsw': (compare(4, True, lambda x, y: x > y), 4),
|
|
'vcmpequh': (compare(2, False, lambda x, y: x == y), 2),
|
|
'vcmpgtuh': (compare(2, False, lambda x, y: x > y), 2),
|
|
}
|
|
|
|
RECORD_FORMS = ['vcmpgtsh', 'vcmpgtsw', 'vcmpequh', 'vcmpgtuh']
|
|
|
|
|
|
def write(name, tests):
|
|
with open(os.path.join(OUT, f'nfsmw_{name}.s'), 'w') as f:
|
|
f.write(f'# Generated by tests/gen_instr_tests.py (independent Python model). Do not edit.\n\n')
|
|
for i, t in enumerate(tests, 1):
|
|
f.write(f'test_nfsmw_{name}_{i}:\n' + t + '\n')
|
|
|
|
|
|
def main():
|
|
os.makedirs(OUT, exist_ok=True)
|
|
for f in os.listdir(OUT):
|
|
if f.startswith('nfsmw_'):
|
|
os.remove(os.path.join(OUT, f))
|
|
|
|
for name, (model, size) in VECTOR_BINOPS.items():
|
|
tests = []
|
|
for _ in range(8):
|
|
a, b = rand_vec(size), rand_vec(size)
|
|
if name == 'vcmpequh' and rng.random() < 0.5:
|
|
b = list(a) # make equality actually happen
|
|
d = model(a, b)
|
|
tests.append(
|
|
f' #_ REGISTER_IN v4 {vec(a)}\n'
|
|
f' #_ REGISTER_IN v5 {vec(b)}\n'
|
|
f' {name} v3, v4, v5\n'
|
|
f' blr\n'
|
|
f' #_ REGISTER_OUT v3 {vec(d)}\n'
|
|
f' #_ REGISTER_OUT v4 {vec(a)}\n'
|
|
f' #_ REGISTER_OUT v5 {vec(b)}\n')
|
|
write(name, tests)
|
|
|
|
# Record forms: cr6.lt = all elements true, cr6.eq = none true. The harness
|
|
# can't read CR, so branch on it: r3 bit 0 = all, bit 1 = none.
|
|
for name in RECORD_FORMS:
|
|
model, size = VECTOR_BINOPS[name]
|
|
tests = []
|
|
cases = [(rand_vec(size), rand_vec(size)) for _ in range(4)]
|
|
a = rand_vec(size)
|
|
cases += [(a, a), (pack([0] * (16 // size), size, False), pack([0] * (16 // size), size, False))]
|
|
# all-true for the greater-than forms: a = b + 1 elementwise where possible
|
|
big = pack([(1 << (size * 8 - 1)) - 1] * (16 // size), size, False)
|
|
small = pack([0] * (16 // size), size, False)
|
|
cases += [(big, small), (small, big)]
|
|
for a, b in cases:
|
|
d = model(a, b)
|
|
all_true = all(w == 0xFFFFFFFF for w in d)
|
|
none_true = all(w == 0 for w in d)
|
|
r3 = (1 if all_true else 0) | (2 if none_true else 0)
|
|
tests.append(
|
|
f' #_ REGISTER_IN v4 {vec(a)}\n'
|
|
f' #_ REGISTER_IN v5 {vec(b)}\n'
|
|
f' #_ REGISTER_IN r3 0\n'
|
|
f' {name}. v3, v4, v5\n'
|
|
f' bge cr6, .+8\n'
|
|
f' ori r3, r3, 1\n'
|
|
f' bne cr6, .+8\n'
|
|
f' ori r3, r3, 2\n'
|
|
f' blr\n'
|
|
f' #_ REGISTER_OUT v3 {vec(d)}\n'
|
|
f' #_ REGISTER_OUT r3 {r3}\n')
|
|
write(name + '_rc', tests)
|
|
|
|
# vspltish: sign-extended 5-bit immediate splatted to every halfword.
|
|
tests = []
|
|
for imm in (-16, -1, 0, 1, 7, 15):
|
|
d = pack([imm & 0xFFFF] * 8, 2, False)
|
|
tests.append(f' vspltish v3, {imm}\n blr\n #_ REGISTER_OUT v3 {vec(d)}\n')
|
|
write('vspltish', tests)
|
|
|
|
# Summary-overflow branches after fcmpu: so = unordered (a NaN operand).
|
|
NAN = '0x7FF8000000000000'
|
|
tests = []
|
|
for mnemonic in ('bso', 'bns'):
|
|
for f2, unordered in (('2.0', False), (NAN, True), ('1.0', False)):
|
|
taken = unordered if mnemonic == 'bso' else not unordered
|
|
tests.append(
|
|
f' #_ REGISTER_IN f1 1.0\n'
|
|
f' #_ REGISTER_IN f2 {f2}\n'
|
|
f' #_ REGISTER_IN r3 1\n'
|
|
f' fcmpu cr1, f1, f2\n'
|
|
f' {mnemonic} cr1, .+8\n'
|
|
f' li r3, 2\n'
|
|
f' blr\n'
|
|
f' #_ REGISTER_OUT r3 {1 if taken else 2}\n')
|
|
for mnemonic in ('bsolr', 'bnslr'):
|
|
for f2, unordered in (('2.0', False), (NAN, True)):
|
|
returns = unordered if mnemonic == 'bsolr' else not unordered
|
|
tests.append(
|
|
f' #_ REGISTER_IN f1 1.0\n'
|
|
f' #_ REGISTER_IN f2 {f2}\n'
|
|
f' #_ REGISTER_IN r3 1\n'
|
|
f' fcmpu cr1, f1, f2\n'
|
|
f' {mnemonic} cr1\n'
|
|
f' li r3, 2\n'
|
|
f' blr\n'
|
|
f' #_ REGISTER_OUT r3 {1 if returns else 2}\n')
|
|
write('bso', tests)
|
|
|
|
# cror / crorc: cr7.eq = cr0.lt OR [NOT] cr1.gt (bits 30, 0, 5).
|
|
tests = []
|
|
for mnemonic in ('cror', 'crorc'):
|
|
for x, y in ((-1, 1), (-1, -1), (1, 1), (1, -1)):
|
|
lt0, gt1 = x < 0, y > 0
|
|
result = lt0 or (not gt1 if mnemonic == 'crorc' else gt1)
|
|
tests.append(
|
|
f' #_ REGISTER_IN r4 0x{x & 0xFFFFFFFFFFFFFFFF:X}\n'
|
|
f' #_ REGISTER_IN r5 0x{y & 0xFFFFFFFFFFFFFFFF:X}\n'
|
|
f' #_ REGISTER_IN r3 1\n'
|
|
f' cmpwi cr0, r4, 0\n'
|
|
f' cmpwi cr1, r5, 0\n'
|
|
f' cmpwi cr7, r5, 12345\n'
|
|
f' {mnemonic} 30, 0, 5\n'
|
|
f' beq cr7, .+8\n'
|
|
f' li r3, 2\n'
|
|
f' blr\n'
|
|
f' #_ REGISTER_OUT r3 {1 if result else 2}\n')
|
|
write('cror', tests)
|
|
|
|
# stfsu: store single, then update rA with the effective address.
|
|
tests = []
|
|
# Values are written as strings with a '.', which is how the harness tells
|
|
# a float input from an integer one.
|
|
for text, disp in (('1.5', 16), ('-2.25', 0), ('3.0e38', -8)):
|
|
value = float(text)
|
|
bits = struct.pack('>f', value)
|
|
ea = 0x1000 + disp
|
|
tests.append(
|
|
f' #_ REGISTER_IN f1 {text}\n'
|
|
f' #_ REGISTER_IN r4 0x1000\n'
|
|
f' stfsu f1, {disp}(r4)\n'
|
|
f' blr\n'
|
|
f' #_ REGISTER_OUT r4 0x{ea:X}\n'
|
|
f' #_ MEMORY_OUT {ea:08X} ' + ' '.join(f'{c:02X}' for c in bits) + '\n')
|
|
write('stfsu', tests)
|
|
|
|
print('wrote', sorted(f for f in os.listdir(OUT) if f.startswith('nfsmw_')))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|