diff options
Diffstat (limited to 'tests/drivers/fuzz_compiler.py')
| -rw-r--r-- | tests/drivers/fuzz_compiler.py | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/tests/drivers/fuzz_compiler.py b/tests/drivers/fuzz_compiler.py new file mode 100644 index 0000000..512ec26 --- /dev/null +++ b/tests/drivers/fuzz_compiler.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Deterministic grammar fuzzing for the parser, runtime, and ownership paths. + +Usage: python3 tests/drivers/fuzz_compiler.py [path/to/zy] [path/to/zym] [--js] +""" +import random +import subprocess +import sys + + +args = [arg for arg in sys.argv[1:] if arg != "--js"] +js = "--js" in sys.argv[1:] +zy = args[0] if len(args) > 0 else "bin/zy" +zym = args[1] if len(args) > 1 else "bin/zym" +rng = random.Random(0x5A17) +values = [] +library_values = [] + + +def expr(depth): + if depth == 0 or rng.randrange(4) == 0: + value = rng.randrange(10) + return str(value), value + left, a = expr(depth - 1) + right, b = expr(depth - 1) + op = rng.choice(("+", "-", "*")) + if op == "+": + value = a + b + elif op == "-": + value = a - b + else: + value = a * b + return f"({left} {op} {right})", value + + +lines = ["include std;", "fn churn(n) {", " int i = 0;", " while (i < n) {", " { int* p = new(int); *p = i; }", " i++;", " }", " return 0;", "}", "fn stable() { return 73; }", "fn main() {", " let base = live_objects();"] +for _ in range(128): + source, value = expr(4) + lines.append(f" put_n({source}); put_c(32);") + values.append(value) +for _ in range(32): + a = "".join(rng.choice("abc") for _ in range(rng.randrange(11))) + b = "".join(rng.choice("abc") for _ in range(rng.randrange(6))) + lines.append(f' put_n(Text.len("{a}")); put_c(32); put_n(Text.eq("{a}", "{b}")); put_c(32);') + lines.append(f' put_n(Text.starts("{a}", "{b}")); put_c(32); put_n(Text.ends("{a}", "{b}")); put_c(32); put_n(Text.find("{a}", "{b}")); put_c(32);') + lines.append(f' put_n(Bytes.len(b"{a}")); put_c(32); put_n(Bytes.eq(b"{a}", b"{b}")); put_c(32);') + lines.append(f' put_n(Bytes.starts(b"{a}", b"{b}")); put_c(32); put_n(Bytes.ends(b"{a}", b"{b}")); put_c(32); put_n(Bytes.find(b"{a}", b"{b}")); put_c(32);') + library_values += [len(a), int(a == b), int(a.startswith(b)), int(a.endswith(b)), a.find(b), len(a.encode()), int(a == b), int(a.startswith(b)), int(a.endswith(b)), a.find(b)] +lines += [ + " churn(300); gc(); put_n(live_objects() - base); put_c(32);", + ' char* source = "fn f() { return 7; }"; let parsed = tokens(source);', + ' put_n(parsed[0] > 0); put_c(32); put_n(eq_w(source, "fn f() { return 7; }")); put_c(32);', + ' char* eval_source = "return 42;"; int e = 0;', + ' while (e < 16) { let q = tokens(eval_source); put_n(eval(q)); put_c(32); e++; }', + ' put_n(stable()); put_c(32); put_n(eq_w(eval_source, "return 42;")); put_c(10);', + " return 0;", + "}", +] +program = "\n".join(lines) + "\n" +expected = " ".join(str(x) for x in values + library_values + [0, 1, 1, *([42] * 16), 73, 1]) + "\n" + + +def run(binary): + result = subprocess.run(binary, input=program, text=True, capture_output=True, timeout=30) + if result.returncode or result.stdout != expected: + actual_words = result.stdout.split() + expected_words = expected.split() + mismatch = next((i for i, pair in enumerate(zip(actual_words, expected_words)) if pair[0] != pair[1]), min(len(actual_words), len(expected_words))) + raise SystemExit( + f"{' '.join(binary)} failed deterministic fuzz seed 0x5a17\n" + f"exit={result.returncode}, token mismatch at {mismatch}\n" + f"expected={expected_words[max(0, mismatch - 3):mismatch + 4]!r}\n" + f"stdout={actual_words[max(0, mismatch - 3):mismatch + 4]!r}\n" + f"stderr={result.stderr!r}" + ) + return result.stdout + + +hosts = [(zy, [zy]), (zym, [zym])] +if js: + hosts.append(("javascript", ["node", "src/js/tinyvm.js"])) +outputs = [run(command) for _, command in hosts] +if any(output != outputs[0] for output in outputs[1:]): + raise SystemExit("compiler hosts disagreed on deterministic fuzz output") +print("ok compiler/runtime/stdlib fuzz: 128 generated expressions, 300 scoped allocations and GC, " + "16 eval recompiles, unchanged source strings, and 32 randomized Text/Bytes cases on " + + ", ".join(name for name, _ in hosts)) |
