FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

GitHub Viewer

#!/usr/bin/env python3 """Compare bytecode between CPython and RustPython. Compiles all Python files under Lib/ with both interpreters and reports differences in the generated bytecode instructions. Usage: python scripts/compare_bytecode.py python scripts/compare_bytecode.py --detail python scripts/compare_bytecode.py --filter "asyncio/*.py" python scripts/compare_bytecode.py --summary-json report.json """ import argparse import fnmatch import json import os import random import subprocess import sys import tempfile from collections import defaultdict SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.dirname(SCRIPT_DIR) DIS_DUMP = os.path.join(SCRIPT_DIR, "dis_dump.py") DEFAULT_REPORT = os.path.join(PROJECT_ROOT, "compare_bytecode.report") DUMP_TIMEOUT = 600 def find_rustpython(): """Locate the RustPython binary, allowing release builds only.""" if "RUSTPYTHON" in os.environ: path = os.environ["RUSTPYTHON"] normalized = os.path.normpath(path) debug_fragment = os.path.join("target", "debug", "rustpython") if normalized.endswith(debug_fragment): raise ValueError( "RUSTPYTHON must point to a release binary, not target/debug/rustpython" ) return path path = os.path.join(PROJECT_ROOT, "target", "release", "rustpython") if os.path.isfile(path) and os.access(path, os.X_OK): return path return None def collect_targets(lib_dir, pattern=None): """Collect Python files to compare, relative to lib_dir.""" targets = [] for root, dirs, files in os.walk(lib_dir): dirs[:] = sorted( d for d in dirs if d != "__pycache__" and not d.startswith(".") ) for fname in sorted(files): if not fname.endswith(".py"): continue fpath = os.path.join(root, fname) relpath = os.path.relpath(fpath, lib_dir) if pattern and not fnmatch.fnmatch(relpath, pattern): continue targets.append((relpath, fpath)) return targets def _start_one(interpreter, targets, base_dir): """Start a single dis_dump.py subprocess.""" env = os.environ.copy() if interpreter != sys.executable: env["RUSTPYTHONPATH"] = base_dir files_file = None output_file = None try: files_file = tempfile.NamedTemporaryFile( mode="w", encoding="utf-8", delete=False, prefix="compare-bytecode-files-", ) output_file = tempfile.NamedTemporaryFile( mode="w", encoding="utf-8", delete=False, prefix="compare-bytecode-output-", ) for _, path in targets: files_file.write(path) files_file.write("\n") files_file.close() output_file.close() cmd = [ interpreter, DIS_DUMP, "--base-dir", base_dir, "--files-from", files_file.name, "--output", output_file.name, "--progress", "10", ] proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=None, # inherit stderr so progress dots appear on terminal env=env, cwd=PROJECT_ROOT, ) return { "proc": proc, "files_file": files_file.name, "output_file": output_file.name, "targets": targets, "interpreter": interpreter, "base_dir": base_dir, } except Exception: for handle in (files_file, output_file): if handle is None: continue try: handle.close() finally: if os.path.exists(handle.name): os.unlink(handle.name) raise def _load_dump_output(output_file): try: with open(output_file, encoding="utf-8") as f: content = f.read().strip() except OSError as e: print(" Failed to read dump output: %s" % e, file=sys.stderr) return None if not content: return {} try: return json.loads(content) except json.JSONDecodeError as e: print(" JSON parse error: %s" % e, file=sys.stderr) return None def _run_sync_dump(interpreter, targets, base_dir, timeout=DUMP_TIMEOUT): job = _start_one(interpreter, targets, base_dir) proc = job["proc"] stdout = b"" timed_out = False try: stdout = proc.communicate(timeout=timeout)[0] except subprocess.TimeoutExpired: proc.kill() proc.communicate() print(f" Timeout ({timeout}s)", file=sys.stderr) timed_out = True try: data = _load_dump_output(job["output_file"]) finally: for path in (job["files_file"], job["output_file"]): if os.path.exists(path): os.unlink(path) if timed_out: return {} if proc.returncode != 0: print(" Warning: exited with code %d" % proc.returncode, file=sys.stderr) stray = stdout.decode(errors="replace").strip() if stray: print(" Warning: unexpected stdout from dump helper", file=sys.stderr) return data def _rerun_missing_targets(interpreter, targets, base_dir): recovered = {} failed = [] empty = [] for target in targets: relpath = target[0] data = _run_sync_dump(interpreter, [target], base_dir) if data is None: failed.append(relpath) recovered[relpath] = { "status": "error", "error": "dump helper failed while rerunning target", } elif data: recovered.update(data) else: empty.append(relpath) recovered[relpath] = { "status": "error", "error": "dump helper produced no data while rerunning target", } if failed: print( " Warning: rerun failed for %d file(s): %s" % (len(failed), ", ".join(failed[:5])), file=sys.stderr, ) if empty: print( " Warning: rerun produced no data for %d file(s): %s" % (len(empty), ", ".join(empty[:5])), file=sys.stderr, ) return recovered def _finish_one(job, timeout=DUMP_TIMEOUT): """Wait for a single dis_dump.py process and return parsed JSON.""" proc = job["proc"] expected = {relpath for relpath, _ in job["targets"]} stdout = b"" try: stdout = proc.communicate(timeout=timeout)[0] except subprocess.TimeoutExpired: proc.kill() proc.communicate() print( f" Timeout ({timeout}s), retrying {len(job['targets'])} file(s) serially", file=sys.stderr, ) data = None else: data = _load_dump_output(job["output_file"]) finally: for path in (job["files_file"], job["output_file"]): if os.path.exists(path): os.unlink(path) if proc.returncode != 0: print(" Warning: exited with code %d" % proc.returncode, file=sys.stderr) stray = stdout.decode(errors="replace").strip() if stray: print(" Warning: unexpected stdout from dump helper", file=sys.stderr) if data is None: return _rerun_missing_targets( job["interpreter"], job["targets"], job["base_dir"] ) missing = [ target for target in job["targets"] if target[0] not in data and target[0] in expected ] if missing: print( " Re-running %d missing file(s) serially" % len(missing), file=sys.stderr, ) data.update( _rerun_missing_targets(job["interpreter"], missing, job["base_dir"]) ) return data def start_dump(interpreter, targets, base_dir, num_workers=1): """Start dis_dump.py under the given interpreter, split across workers.""" if num_workers 50: p(" ... and %d more" % (len(rp_error_files) - 50)) p() if diff_files: p("-" * 60) p(" Bytecode Differences") p("-" * 60) for fp, code_diffs in diff_files: p() p(" %s:" % fp) for code_path, diffs in code_diffs: shown = min(len(diffs), args.max_diffs) p(" %s: %d difference(s)" % (code_path, len(diffs))) for idx, cp_inst, rp_inst in diffs[:shown]: if idx == -1: p(" %s" % (cp_inst or rp_inst)) else: p(" [%3d] CPython: %s" % (idx, cp_inst)) p(" RustPython: %s" % rp_inst) if len(diffs) > shown: p(" ... and %d more" % (len(diffs) - shown)) p() else: list_limit = 0 if args.summary_json else max(args.list_limit, 0) if diff_summaries and list_limit: shown = min(list_limit, len(diff_summaries)) total = len(diff_summaries) p(f"Top differing files ({shown} shown of {total}):") top = sorted( diff_summaries, key=lambda item: ( item["diff_instructions"], item["diff_code_objects"], item["path"], ), reverse=True, )[:list_limit] for item in top: p( " %s (%d code objects, %d instruction diffs)" % ( item["path"], item["diff_code_objects"], item["diff_instructions"], ) ) p() p("Use --detail to see specific instruction differences.") p() # Summary JSON output if args.summary_json: summary = { "total": total, "sample": args.sample, "sample_seed": sample_seed, "match": match, "differ": differ, "rp_error": rp_err, "cp_error": cp_err, "both_error": both_err, "rp_missing": rp_miss, "match_pct": round(100.0 * match / total, 2) if total else 0, "diff_files": [fp for fp, _ in diff_files] if need_detailed_diffs else [item["path"] for item in diff_summaries], "top_diff_files": sorted( diff_summaries, key=lambda item: ( item["diff_instructions"], item["diff_code_objects"], item["path"], ), reverse=True, )[: min(20, len(diff_summaries))], "rp_error_files": [fp for fp, _ in rp_error_files], } with open(args.summary_json, "w") as f: json.dump(summary, f, indent=2) log("Summary JSON: %s" % args.summary_json) log("Done: %d match, %d differ, %d errors" % (match, differ, rp_err)) sys.exit(0 if differ == 0 and rp_err == 0 else 1) if __name__ == "__main__": main()

Back | FazBrowse Home | New Git URL