use std::path::{Path, PathBuf};
use std::process::Command;
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn xpile_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_xpile"))
}
struct FixtureCfg {
file: &'static str,
entry: &'static str,
args: &'static [(i64, i64)],
overflow_args: &'static [(i64, i64)],
}
const FIXTURES: &[FixtureCfg] = &[
FixtureCfg {
file: "factorial.py",
entry: "factorial",
args: &[(0, 12)],
overflow_args: &[(21, 30)],
},
FixtureCfg {
file: "fib.py",
entry: "fib",
args: &[(0, 30)],
overflow_args: &[],
},
FixtureCfg {
file: "abs_val.py",
entry: "abs_val",
args: &[(-1_000_000, 1_000_000)],
overflow_args: &[],
},
FixtureCfg {
file: "sign.py",
entry: "sign",
args: &[(-1_000_000_000, 1_000_000_000)],
overflow_args: &[],
},
FixtureCfg {
file: "sum_to.py",
entry: "sum_to",
args: &[(0, 65_535)],
overflow_args: &[],
},
FixtureCfg {
file: "for_sum.py",
entry: "for_sum",
args: &[(0, 65_535)],
overflow_args: &[],
},
FixtureCfg {
file: "countdown.py",
entry: "factorial_iter",
args: &[(0, 12)],
overflow_args: &[(21, 30)],
},
FixtureCfg {
file: "gcd.py",
entry: "gcd",
args: &[(0, 1_000_000), (0, 1_000_000)],
overflow_args: &[],
},
FixtureCfg {
file: "multi_branch.py",
entry: "range_size",
args: &[
(-1_000_000_000, 1_000_000_000),
(-1_000_000_000, 1_000_000_000),
],
overflow_args: &[],
},
FixtureCfg {
file: "bits.py",
entry: "bits",
args: &[
(-i64::pow(2, 61) + 1, i64::pow(2, 61) - 1),
(-i64::pow(2, 61) + 1, i64::pow(2, 61) - 1),
],
overflow_args: &[],
},
];
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Lcg(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0
}
fn next_i64_in(&mut self, lo: i64, hi: i64) -> i64 {
assert!(lo <= hi);
let span = (hi - lo) as u64 + 1;
let r = self.next_u64() % span;
lo + r as i64
}
}
fn have_python_and_rustc() -> bool {
let py = Command::new("python3").arg("--version").output().is_ok();
let rs = Command::new("rustc").arg("--version").output().is_ok();
py && rs
}
fn build_rust_binary(
fixture_path: &Path,
entry: &str,
arity: usize,
out_dir: &Path,
) -> Result<PathBuf, String> {
let out = Command::new(xpile_bin())
.args([
"transpile",
fixture_path.to_str().unwrap(),
"--target",
"rust",
])
.output()
.map_err(|e| format!("spawn xpile: {e}"))?;
if !out.status.success() {
return Err(format!(
"xpile transpile failed: {}",
String::from_utf8_lossy(&out.stderr)
));
}
let transpiled = String::from_utf8(out.stdout).map_err(|e| format!("utf8: {e}"))?;
let uses_bigint = transpiled.contains("xpile_bigint::BigInt");
if uses_bigint {
build_rust_binary_bigint(&transpiled, entry, arity, out_dir)
} else {
build_rust_binary_i64(&transpiled, entry, arity, out_dir)
}
}
fn build_rust_binary_i64(
transpiled: &str,
entry: &str,
arity: usize,
out_dir: &Path,
) -> Result<PathBuf, String> {
let call_args: Vec<String> = (0..arity).map(|i| format!("argv[{i}]")).collect();
let call = format!("{entry}({})", call_args.join(", "));
let driver = format!(
r#"
fn main() {{
let argv: Vec<i64> = std::env::args()
.skip(1)
.map(|s| s.parse::<i64>().expect("parse i64"))
.collect();
assert_eq!(argv.len(), {arity}, "expected {arity} args");
println!("{{}}", {call});
}}
"#
);
let merged = format!("{transpiled}\n{driver}\n");
let rs_file = out_dir.join(format!("{entry}.rs"));
std::fs::write(&rs_file, &merged).map_err(|e| format!("write rs: {e}"))?;
let bin_path = out_dir.join(entry);
let compile = Command::new("rustc")
.args([
"--edition=2021",
"-O",
"-o",
bin_path.to_str().unwrap(),
rs_file.to_str().unwrap(),
])
.output()
.map_err(|e| format!("spawn rustc: {e}"))?;
if !compile.status.success() {
return Err(format!(
"rustc failed:\n=== source ===\n{merged}\n=== stderr ===\n{}",
String::from_utf8_lossy(&compile.stderr)
));
}
Ok(bin_path)
}
fn build_rust_binary_bigint(
transpiled: &str,
entry: &str,
arity: usize,
out_dir: &Path,
) -> Result<PathBuf, String> {
let call_args: Vec<String> = (0..arity).map(|i| format!("argv[{i}].clone()")).collect();
let call = format!("{entry}({})", call_args.join(", "));
let driver = format!(
r#"
fn main() {{
let argv: Vec<xpile_bigint::BigInt> = std::env::args()
.skip(1)
.map(|s| {{
let n: i64 = s.parse().expect("parse i64");
xpile_bigint::BigInt::from(n)
}})
.collect();
assert_eq!(argv.len(), {arity}, "expected {arity} args");
println!("{{}}", {call});
}}
"#
);
let merged = format!("{transpiled}\n{driver}\n");
let xpile_bigint_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("crates/")
.join("xpile-bigint");
let pkg_dir = out_dir.join(format!("{entry}-cargo"));
let src_dir = pkg_dir.join("src");
std::fs::create_dir_all(&src_dir).map_err(|e| format!("create dir: {e}"))?;
let cargo_toml = format!(
r#"[package]
name = "{entry}-bin"
version = "0.0.0"
edition = "2021"
[[bin]]
name = "{entry}"
path = "src/main.rs"
[dependencies]
xpile-bigint = {{ path = "{}" }}
"#,
xpile_bigint_dir.display()
);
std::fs::write(pkg_dir.join("Cargo.toml"), &cargo_toml)
.map_err(|e| format!("write Cargo.toml: {e}"))?;
std::fs::write(src_dir.join("main.rs"), &merged).map_err(|e| format!("write main.rs: {e}"))?;
let target_dir = pkg_dir.join("target");
let build = Command::new("cargo")
.args([
"build",
"--release",
"--quiet",
"--manifest-path",
pkg_dir.join("Cargo.toml").to_str().unwrap(),
"--target-dir",
target_dir.to_str().unwrap(),
])
.output()
.map_err(|e| format!("spawn cargo: {e}"))?;
if !build.status.success() {
return Err(format!(
"cargo build (BigInt path) failed:\n=== source ===\n{merged}\n=== stderr ===\n{}",
String::from_utf8_lossy(&build.stderr)
));
}
Ok(target_dir.join("release").join(entry))
}
fn run_rust(bin: &Path, args: &[i64]) -> Result<String, String> {
let mut cmd = Command::new(bin);
for a in args {
cmd.arg(a.to_string());
}
let out = cmd.output().map_err(|e| format!("spawn rust bin: {e}"))?;
if !out.status.success() {
return Err(format!(
"rust bin exited non-zero (overflow? input out of declared range?):\n stderr: {}",
String::from_utf8_lossy(&out.stderr)
));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
#[derive(Debug)]
enum OverflowOutcome {
Promoted(String),
DocumentedGap,
OffContractCrash(String),
}
fn run_rust_expecting_overflow(bin: &Path, args: &[i64]) -> OverflowOutcome {
let mut cmd = Command::new(bin);
for a in args {
cmd.arg(a.to_string());
}
let out = match cmd.output() {
Ok(o) => o,
Err(e) => return OverflowOutcome::OffContractCrash(format!("spawn failed: {e}")),
};
if out.status.success() {
return OverflowOutcome::Promoted(String::from_utf8_lossy(&out.stdout).trim().to_string());
}
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("C-PY-INT-ARITH") {
OverflowOutcome::DocumentedGap
} else {
OverflowOutcome::OffContractCrash(stderr.to_string())
}
}
fn run_python(fixture_path: &Path, entry: &str, args: &[i64]) -> Result<String, String> {
let src_path = fixture_path.to_str().ok_or("non-utf8 fixture path")?;
let call_args: Vec<String> = args.iter().map(|a| a.to_string()).collect();
let prog = format!(
"exec(open(r'{src_path}').read()); print({entry}({}))",
call_args.join(", ")
);
let out = Command::new("python3")
.args(["-c", &prog])
.output()
.map_err(|e| format!("spawn python: {e}"))?;
if !out.status.success() {
return Err(format!(
"python failed: {}",
String::from_utf8_lossy(&out.stderr)
));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
const INPUTS_PER_FIXTURE: usize = 10;
const LCG_SEED: u64 = 0x00C0_FFEE_FACE_FEEDu64;
#[test]
fn differential_execution_cpython_vs_transpiled_rust() {
if !have_python_and_rustc() {
eprintln!(
"warning: skipping XPILE-DIFF-001 — python3 and/or rustc not on PATH. \
CI environments with both will still run this gate."
);
return;
}
let out_dir = std::env::temp_dir().join("xpile-diff-exec");
let _ = std::fs::remove_dir_all(&out_dir);
std::fs::create_dir_all(&out_dir).expect("create temp dir");
let mut rng = Lcg::new(LCG_SEED);
let mut total_checks = 0;
let mut mismatches: Vec<(String, Vec<i64>, String, String)> = Vec::new();
let mut overflow_checks = 0;
let mut promotion_gaps = 0;
let mut overflow_promoted_ok = 0;
let mut overflow_promoted_mismatches: Vec<(String, Vec<i64>, String, String)> = Vec::new();
let mut off_contract_crashes: Vec<(String, Vec<i64>, String)> = Vec::new();
for cfg in FIXTURES {
let py_path = fixture(cfg.file);
let bin = match build_rust_binary(&py_path, cfg.entry, cfg.args.len(), &out_dir) {
Ok(b) => b,
Err(e) => {
panic!(
"build failed for fixture `{}` entry `{}`:\n {e}",
cfg.file, cfg.entry
);
}
};
for _ in 0..INPUTS_PER_FIXTURE {
let args: Vec<i64> = cfg
.args
.iter()
.map(|(lo, hi)| rng.next_i64_in(*lo, *hi))
.collect();
let py = run_python(&py_path, cfg.entry, &args)
.unwrap_or_else(|e| panic!("python {}({args:?}): {e}", cfg.file));
let rs = run_rust(&bin, &args)
.unwrap_or_else(|e| panic!("rust {}({args:?}): {e}", cfg.file));
total_checks += 1;
if py != rs {
mismatches.push((cfg.file.to_string(), args, py, rs));
}
}
if cfg.overflow_args.is_empty() {
continue;
}
assert_eq!(
cfg.overflow_args.len(),
cfg.args.len(),
"fixture `{}`: overflow_args length must match arity",
cfg.file
);
for _ in 0..INPUTS_PER_FIXTURE {
let args: Vec<i64> = cfg
.overflow_args
.iter()
.map(|(lo, hi)| rng.next_i64_in(*lo, *hi))
.collect();
let py = run_python(&py_path, cfg.entry, &args)
.unwrap_or_else(|e| panic!("python {}({args:?}): {e}", cfg.file));
overflow_checks += 1;
match run_rust_expecting_overflow(&bin, &args) {
OverflowOutcome::DocumentedGap => {
promotion_gaps += 1;
}
OverflowOutcome::Promoted(rs) => {
if py == rs {
overflow_promoted_ok += 1;
} else {
overflow_promoted_mismatches.push((cfg.file.to_string(), args, py, rs));
}
}
OverflowOutcome::OffContractCrash(stderr) => {
off_contract_crashes.push((cfg.file.to_string(), args, stderr));
}
}
}
}
let mut fatal = String::new();
if !mismatches.is_empty() {
fatal.push_str(&format!(
"Differential execution disagreement (XPILE-DIFF-001/002):\n\
{} of {} fast-path input-comparisons diverged between CPython and the transpiled \
Rust binary. Either the codegen miscompiles the construct OR the fixture's \
declared input range needs tightening to stay inside the C-PY-INT-ARITH fast-path \
domain.\n\n",
mismatches.len(),
total_checks
));
for (fx, args, py, rs) in &mismatches {
fatal.push_str(&format!(
" - {fx} args={args:?}\n python: {py}\n rust: {rs}\n"
));
}
}
if !overflow_promoted_mismatches.is_empty() {
fatal.push_str(&format!(
"\nOverflow-phase silent miscompile (XPILE-DIFF-003):\n\
{} input(s) where Rust returned a value that diverged from Python's BigInt-promoted \
result. This is worse than the documented promotion gap — Rust produced a *wrong* \
answer instead of panicking.\n\n",
overflow_promoted_mismatches.len(),
));
for (fx, args, py, rs) in &overflow_promoted_mismatches {
fatal.push_str(&format!(
" - {fx} args={args:?}\n python: {py}\n rust: {rs}\n"
));
}
}
if !off_contract_crashes.is_empty() {
fatal.push_str(&format!(
"\nOff-contract crashes (XPILE-DIFF-003):\n\
{} input(s) where Rust panicked but the panic message did NOT cite \
`C-PY-INT-ARITH`. Either codegen regressed (dropped the contract citation) or the \
panic comes from an unrelated path. Either way, the gate can't classify the \
outcome as a documented gap.\n\n",
off_contract_crashes.len(),
));
for (fx, args, stderr) in &off_contract_crashes {
fatal.push_str(&format!(
" - {fx} args={args:?}\n stderr: {}\n",
stderr.trim().lines().next().unwrap_or("(no stderr line)")
));
}
}
if !fatal.is_empty() {
panic!("{fatal}");
}
eprintln!(
"XPILE-DIFF-001/002: {total_checks} fast-path differential checks across {} fixtures — \
all green.",
FIXTURES.len()
);
if overflow_checks > 0 {
let n_overflow_fixtures = FIXTURES
.iter()
.filter(|c| !c.overflow_args.is_empty())
.count();
eprintln!(
"XPILE-DIFF-003: {overflow_checks} overflow-phase checks across {n_overflow_fixtures} \
fixture(s) — {promotion_gaps} documented promotion gaps, {overflow_promoted_ok} \
promoted-and-agreed (BigInt-mode would land here)."
);
}
}
#[test]
fn lcg_is_deterministic_with_seed() {
let mut rng = Lcg::new(LCG_SEED);
let a = rng.next_u64();
let b = rng.next_u64();
let c = rng.next_u64();
let mut rng2 = Lcg::new(LCG_SEED);
assert_eq!(rng2.next_u64(), a);
assert_eq!(rng2.next_u64(), b);
assert_eq!(rng2.next_u64(), c);
let mut rng3 = Lcg::new(LCG_SEED);
for _ in 0..1000 {
let v = rng3.next_i64_in(-100, 100);
assert!(
(-100..=100).contains(&v),
"LCG produced {v} outside [-100, 100]"
);
}
}