import argparse
import re
import subprocess
import sys
from collections import OrderedDict
HEADER_RE = re.compile(r"\[generated bytecode for function: (.*?) \(")
INSTR_RE = re.compile(r"@\s+(\d+) : ((?:[0-9a-f]{2} )*[0-9a-f]{2})\s+(\S.*)$")
META_RE = re.compile(r"^(Bytecode length|Parameter count|Register count|Frame size|OSR nesting level|Bytecode Age):?\s*(.*)$")
def parse_blocks(text):
blocks = []
cur = None
for line in text.splitlines():
m = HEADER_RE.search(line)
if m:
name = m.group(1).strip()
cur = {"name": name, "meta": {}, "instrs": [], "consts": []}
blocks.append(cur)
continue
if cur is None:
continue
mm = META_RE.match(line.strip())
if mm:
cur["meta"][mm.group(1)] = mm.group(2).strip()
continue
im = INSTR_RE.search(line)
if im:
bytes_ = " ".join(im.group(2).split())
body = im.group(3).strip()
body = re.sub(
r"\((?:0x[0-9a-fA-F]+|jscd) @ (\d+)\)",
lambda mm: f"(@ {mm.group(1)})",
body,
)
cur["instrs"].append((int(im.group(1)), bytes_, body))
continue
if line.strip().startswith("Constant pool"):
cur["consts"].append(line.strip())
elif cur["consts"] and line.strip():
cur["consts"].append(line.strip())
return blocks
def run(cmd, **kw):
r = subprocess.run(cmd, capture_output=True, text=True, **kw)
return r.stdout + r.stderr
def main():
ap = argparse.ArgumentParser()
ap.add_argument("fixture")
ap.add_argument("corpus")
ap.add_argument("node_version")
ap.add_argument("--jscd", default="target/debug/jscd")
ap.add_argument("--show", type=int, default=3, help="每个差异函数展示的差异行数")
args = ap.parse_args()
node_out = run([
"mise", "exec", f"node@{args.node_version}", "--", "node",
"--no-lazy", "--no-flush-bytecode", "--print-bytecode", args.fixture,
])
jscd_out = run([args.jscd, "disasm", args.corpus])
node_blocks = parse_blocks(node_out)
jscd_blocks = parse_blocks(jscd_out)
node_by_name = {}
for b in node_blocks:
node_by_name.setdefault(b["name"], []).append(b)
jscd_by_name = {}
for b in jscd_blocks:
jscd_by_name.setdefault(b["name"], []).append(b)
ok, diff, missing, extra, ambiguous = 0, [], [], [], []
def block_key(b):
return (
b["meta"].get("Bytecode length"),
b["meta"].get("Parameter count"),
b["meta"].get("Register count"),
b["meta"].get("Frame size"),
tuple((i, by, body) for i, by, body in b["instrs"]),
)
anon_node = {block_key(b): b for b in node_blocks if not b["name"]}
anon_matched = 0
anon_unmatched = []
for name, jlist in jscd_by_name.items():
if not name:
for jb in jlist:
if block_key(jb) in anon_node:
anon_matched += 1
else:
anon_unmatched.append(jb)
continue
if name not in node_by_name:
extra.append(name)
continue
nlist = node_by_name[name]
used = set()
for i, jb in enumerate(jlist):
def same_meta(cand):
return all(
cand["meta"].get(key) == jb["meta"].get(key)
for key in ("Bytecode length", "Parameter count", "Register count", "Frame size")
)
def same_instrs(cand):
return list(cand["instrs"]) == list(jb["instrs"])
head = jb["instrs"][0][1] if jb["instrs"] else ""
free = [k for k in range(len(nlist)) if k not in used]
nb = None
for k in free:
if same_instrs(nlist[k]):
nb, _hit = nlist[k], used.add(k)
break
if nb is None:
for k in free:
cand = nlist[k]
if same_meta(cand) and cand["instrs"] and cand["instrs"][0][1] == head:
nb, _hit = cand, used.add(k)
break
if nb is None:
cands = [k for k in free if same_meta(nlist[k])]
if len(cands) == 1 and not nlist[cands[0]]["instrs"]:
nb, _hit = nlist[cands[0]], used.add(cands[0])
problems = []
for key in ("Bytecode length", "Parameter count", "Register count", "Frame size"):
if nb["meta"].get(key) != jb["meta"].get(key):
problems.append(f"{key}: node={nb['meta'].get(key)} jscd={jb['meta'].get(key)}")
if len(nb["instrs"]) != len(jb["instrs"]):
problems.append(f"instr count: node={len(nb['instrs'])} jscd={len(jb['instrs'])}")
else:
for k, (ni, ji) in enumerate(zip(nb["instrs"], jb["instrs"])):
if ni[0] != ji[0] or ni[1] != ji[1] or ni[2] != ji[2]:
problems.append(
f"@{k} offset {ni[0]}/{ji[0]}\n node: {ni[1]} {ni[2]}\n jscd: {ji[1]} {ji[2]}"
)
if len(problems) > args.show:
break
if problems:
diff.append((f"{name}#{i}", problems))
else:
ok += 1
for name, nlist in node_by_name.items():
if name and name not in jscd_by_name:
missing.append(f"{name} x{len(nlist)}")
print(f"=== {args.fixture} (node {args.node_version}) ===")
print(
f"matched: {ok} (+{anon_matched} anonymous by-content) mismatched: {len(diff)} "
f"jscd-extra: {len(extra)} ambiguous: {len(ambiguous)} node-only: {len(missing)}"
)
for name, problems in diff[:5]:
print(f"\n--- MISMATCH: {name!r}")
for p in problems:
print(f" {p}")
if extra:
print(f"\njscd-only functions (expected: they are inside our corpus but node printed under a different name/shape):")
for n in extra[:10]:
print(f" {n!r}")
return 1 if diff else 0
if __name__ == "__main__":
sys.exit(main())