import json, os, pathlib, re, subprocess, sys, collections
ROOT = pathlib.Path(__file__).resolve().parent.parent
SAMPLES = ROOT / 'work' / 'samples'
OUT = ROOT / 'work' / 'out'
VERSIONS_ALL = ['8.17.0', '10.24.1', '12.22.12', '14.21.3', '16.20.2', '18.20.8', '20.20.2', '22.12.0', '24.12.0']
OURS = re.compile(r'\b(__[A-Za-z0-9_]*|_anon_[0-9]+|ctx[0-9]+|phi[0-9]+|r[0-9]+)\b')
_NODE_BIN = {}
def node_bin(version):
if version not in _NODE_BIN:
r = subprocess.run(['mise', 'exec', f'node@{version}', '--', 'which', 'node'],
capture_output=True, text=True, cwd=ROOT)
path = (r.stdout or '').strip().splitlines()[-1] if r.stdout else ''
if not path:
r2 = subprocess.run(['mise', 'where', f'node@{version}'], capture_output=True, text=True, cwd=ROOT)
base = (r2.stdout or '').strip().splitlines()[-1] if r2.stdout else ''
path = f'{base}/bin/node' if base else ''
_NODE_BIN[version] = path or 'node'
return _NODE_BIN[version]
def mise(version, *args, timeout=60):
return subprocess.run([node_bin(version), *args],
capture_output=True, text=True, timeout=timeout, cwd=ROOT)
def run(version, files, quiet=False):
OUT.mkdir(parents=True, exist_ok=True)
stats = collections.Counter()
failures = []
ro = ROOT / 'workspace' / 'ro' / f'ro-map-{version}.json'
for src in files:
jsc = OUT / f'{src.stem}-{version}.jsc'
dec = OUT / f'{src.stem}-{version}.js'
flags = []
if int(version.split('.')[0]) >= 10:
flags.append('--experimental-vm-modules')
r = mise(version, *flags, str(ROOT / 'scripts' / 'mkcorpus.js'), str(src), str(jsc))
if r.returncode != 0 or not jsc.exists():
stats['compile-fail'] += 1
failures.append((src.name, 'compile-env', r.stderr.strip().splitlines()[0][:80] if r.stderr.strip() else ''))
continue
cmd = [str(ROOT / 'target' / 'release' / 'jscd'), 'decompile', str(jsc), '--runtime']
if ro.exists():
cmd += ['--ro-map', str(ro)]
d = subprocess.run(cmd, capture_output=True, text=True, cwd=ROOT, timeout=120)
if d.returncode != 0:
stats['decompile-fail'] += 1
failures.append((src.name, 'decompile', (d.stderr.strip() or d.stdout.strip())[:90]))
continue
dec.write_text(d.stdout)
c = mise(version, '--check', str(dec))
if c.returncode != 0:
stats['syntax-fail'] += 1
first = [l for l in c.stderr.splitlines() if l.strip()][:2]
failures.append((src.name, 'syntax', ' | '.join(first)[:110]))
continue
harness = (
"const fs=require('fs'),vm=require('vm');"
"const src=fs.readFileSync(process.argv[1],'utf8');"
"const sandbox={console,print(){},assert:Object.assign(function(){},)"
"{sameValue(){},notSameValue(){},throws(){},fail(){}},$DONE(){},$262:{},}"
"Symbol,Object,Array,String,Number,Boolean,Math,JSON,Error,TypeError,RangeError"
" ,SyntaxError,ReferenceError,RegExp,Date,Map,Set,WeakMap,WeakSet,Promise,Proxy,Reflect"
" ,parseInt,parseFloat,isNaN,isFinite,decodeURI,encodeURI,undefined,NaN,Infinity,globalThis"
" ,ArrayBuffer,DataView,Uint8Array,Int8Array,Float64Array,BigInt,Function,eval,Infinity};"
"sandbox.globalThis=sandbox;vm.createContext(sandbox);"
"try{vm.runInContext(src,sandbox,{timeout:3000});process.stdout.write('OK');}"
"catch(e){process.stdout.write((e&&e.constructor&&e.constructor.name||'Err')+':'+String(e&&e.message).slice(0,120));}"
)
(OUT / 'harness.js').write_text(harness)
h = mise(version, str(OUT / 'harness.js'), str(dec))
out = (h.stdout or '').strip()
if out.startswith('OK'):
stats['ok'] += 1
elif out.startswith('ReferenceError') and OURS.search(out):
stats['ref-error(ours)'] += 1
failures.append((src.name, 'ref-ours', out[:110]))
else:
stats['ran(other)'] += 1
return stats, failures
def main():
argv = sys.argv[1:]
limit = None
offset = 0
quiet = '--quiet' in argv
if '--limit' in argv:
i = argv.index('--limit')
limit = int(argv[i + 1])
argv = argv[:i] + argv[i + 2:]
if '--offset' in argv:
i = argv.index('--offset')
offset = int(argv[i + 1])
argv = argv[:i] + argv[i + 2:]
samples = SAMPLES
if '--samples' in argv:
i = argv.index('--samples')
samples = ROOT / argv[i + 1]
argv = argv[:i] + argv[i + 2:]
versions = [a for a in argv if not a.startswith('--')] or VERSIONS_ALL
files = sorted(samples.glob('*.js'))
if offset:
files = files[offset:]
if limit:
files = files[:limit]
total = collections.Counter()
for v in versions:
stats, failures = run(v, files, quiet)
total.update(stats)
print(f'--- node {v}: ' + ' '.join(f'{k}={n}' for k, n in sorted(stats.items())))
if not quiet:
for name, kind, msg in failures[:12]:
print(f' {kind:14s} {name:46s} {msg}')
print('==== 合计 ' + ' '.join(f'{k}={n}' for k, n in sorted(total.items())))
if __name__ == '__main__':
main()