65816-llvm-mos/scripts/fuzzCompile.py
Scott Duensing 6d7eae0356 Checkpoint.
2026-04-30 01:29:16 -05:00

151 lines
5.1 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Generate small random C programs and compile them with the W65816
backend. Catches crashes / lowering gaps / verifier failures.
Each generated program is small (~10-50 lines), uses combinations of
features the compiler should handle:
- integer arithmetic (i8, i16, i32, i64)
- control flow (if, while, for, switch)
- structs and pointer derefs
- function calls (recursive, multi-arg)
- casts and bit operations
- arrays (small)
For each program, we just compile to .o. If clang exits non-zero or
crashes, we save the offending source for inspection.
Optionally MAME-runs each program for additional runtime checks (off
by default — slow).
Usage: fuzzCompile.py [-n COUNT] [-s SEED] [--keep-failures DIR]
"""
import argparse, os, random, subprocess, sys, tempfile, hashlib
from pathlib import Path
CLANG = Path(__file__).parent.parent / "tools/llvm-mos-build/bin/clang"
# --- generators ---
def gen_expr(rng, depth=0):
"""Generate a random arithmetic expression returning int."""
if depth > 3 or rng.random() < 0.3:
return rng.choice([
str(rng.randint(0, 100)),
f"({rng.randint(0, 5)} + {rng.randint(0, 5)})",
"x",
])
op = rng.choice(["+", "-", "*", "&", "|", "^", "<<", ">>"])
lhs = gen_expr(rng, depth + 1)
rhs = rng.choice(["1", "2", "3", "4", str(rng.randint(0, 10))])
if op in ("<<", ">>"):
rhs = str(rng.randint(0, 7))
return f"({lhs} {op} {rhs})"
def gen_stmt(rng, varCount, depth=0):
"""Generate a random statement."""
kind = rng.choice(["assign", "if", "while", "loop"])
if depth > 2:
kind = "assign"
if kind == "assign":
v = f"v{rng.randint(0, varCount - 1)}"
return f"{v} = {gen_expr(rng)};"
if kind == "if":
cond = f"{gen_expr(rng)} {rng.choice(['<', '>', '==', '!='])} {rng.randint(0, 30)}"
body = gen_stmt(rng, varCount, depth + 1)
return f"if ({cond}) {{ {body} }}"
if kind == "while":
cnt = rng.randint(2, 5)
body = gen_stmt(rng, varCount, depth + 1)
return f"{{ int j = {cnt}; while (j-- > 0) {{ {body} }} }}"
if kind == "loop":
v = f"v{rng.randint(0, varCount - 1)}"
return f"for (int i = 0; i < {rng.randint(2, 6)}; i++) {{ {v} += i; }}"
return ";"
def gen_function(rng, name, varCount):
"""Generate a function `int name(int x)` with random body."""
decls = "\n ".join(f"int v{i} = {rng.randint(0, 50)};" for i in range(varCount))
stmts = "\n ".join(gen_stmt(rng, varCount) for _ in range(rng.randint(3, 8)))
ret = "v0"
if varCount > 1:
ret = " + ".join(f"v{i}" for i in range(min(varCount, 3)))
return f"""int {name}(int x) {{
{decls}
{stmts}
return {ret};
}}"""
def gen_program(rng):
funcCount = rng.randint(1, 3)
parts = []
for i in range(funcCount):
varCount = rng.randint(1, 5)
parts.append(gen_function(rng, f"f{i}", varCount))
parts.append(f"int call_all(int x) {{ return " +
" + ".join(f"f{i}(x)" for i in range(funcCount)) + "; }")
return "\n\n".join(parts) + "\n"
# --- driver ---
def compile_one(source, keepDir=None, idx=0):
"""Compile source bytes; return (ok, msg)."""
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as f:
f.write(source); cFile = f.name
oFile = cFile + ".o"
try:
r = subprocess.run(
[str(CLANG), "-target", "w65816", "-O2",
"-ffunction-sections", "-c", cFile, "-o", oFile],
capture_output=True, timeout=60
)
if r.returncode != 0:
if keepDir:
tag = hashlib.sha256(source.encode()).hexdigest()[:8]
kept = Path(keepDir) / f"fail_{idx:03d}_{tag}.c"
kept.write_text(source)
kept.with_suffix(".c.stderr").write_bytes(r.stderr)
return False, r.stderr.decode("utf-8", errors="replace")
return True, ""
except subprocess.TimeoutExpired:
return False, "timeout (60s)"
finally:
for p in (cFile, oFile):
try: os.unlink(p)
except FileNotFoundError: pass
def main():
ap = argparse.ArgumentParser()
ap.add_argument("-n", "--count", type=int, default=20)
ap.add_argument("-s", "--seed", type=int, default=42)
ap.add_argument("--keep-failures", default=None,
help="directory to save sources of failing inputs")
ap.add_argument("-q", "--quiet", action="store_true")
args = ap.parse_args()
if args.keep_failures:
Path(args.keep_failures).mkdir(parents=True, exist_ok=True)
rng = random.Random(args.seed)
fails = 0
for i in range(args.count):
src = gen_program(rng)
ok, msg = compile_one(src, args.keep_failures, i)
if not ok:
fails += 1
if not args.quiet:
print(f"[fuzz] FAIL #{i}: {msg.splitlines()[0] if msg else '?'}")
elif not args.quiet:
print(f"[fuzz] OK #{i}")
print(f"fuzz: {args.count - fails}/{args.count} passed ({fails} fails)")
sys.exit(1 if fails else 0)
if __name__ == "__main__":
main()