#!/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()
