numeric_batch.py (10505B)
1 #!/usr/bin/env python3 2 """Opt-in numeric candidates: independent Python oracles, no fixture publication.""" 3 import argparse 4 import hashlib 5 import json 6 from pathlib import Path 7 import random 8 import re 9 import time 10 11 from collections_batch import ProbeRunner, audit_workspace, check_surface 12 from base_workloads import encode as n 13 from bundle_dag import merge_bundles 14 from fixture_sources import verify 15 from http_headers import LEAF, TRUE, pair, result, byte_tree, tree_list, expect 16 from native_library_migration import digest 17 from bootstrap_generations import source_map, snapshot 18 19 ROOT = Path(__file__).resolve().parent.parent 20 REFERENCE_FIXTURES = ROOT/'test/fixtures' 21 SOURCES = dict(base='lib/base.tri', bytes='lib/bytes.tri', conversions='lib/conversions.tri', 22 prelude='lib/prelude.tri', native_math='lib/math/native.tri') 23 DRIVER = '''!import prelude as B 24 !import native_math as N 25 !export * 26 spin x = spin x 27 nativeBinary (a, b) = [(N.add a b) (N.subtract a b) (N.multiply a b) 28 (N.compare a b) (N.equal a b) (N.lt a b) (N.lte a b) (N.gt a b) (N.gte a b) 29 (N.minimum a b) (N.maximum a b) (N.divMod a b) (N.quotient a b) (N.remainder a b)] 30 nativeBinary _ = t 31 wrongBinary (a, b) = nativeBinary (t b a) 32 wrongBinary _ = t 33 checkedBinary (a, b) = [(B.add a b) (B.sub a b) (B.mul a b) 34 (B.lt? a b) (B.lte? a b) (B.gt? a b) (B.gte? a b) 35 (B.min a b) (B.max a b) (B.divMod a b) (B.div a b) (B.mod a b)] 36 checkedBinary _ = t 37 unary x = [(N.successor x) (N.predecessor x) (N.zero x) (N.odd x) (N.even x) 38 (B.succ x) (B.pred x) (B.isZero? x) (B.odd? x) (B.even? x) (N.decimal x 7) (N.decimal x 0) (N.decimal x 9)] 39 classes x = [(N.ascii x) (N.digit x) (N.letter x) (N.lowerHex x) (N.printable x) (N.inRange x 0 0) (N.inRange x 0 255) (N.inRange x 10 5)] 40 powers (a, b) = [(N.power a b) (B.pow a b)] 41 powers _ = t 42 format x = B.showNumber x 43 badFirst x = [(B.add x (spin x)) (B.div x (spin x)) (B.pow x (spin x)) (B.showNumber x)] 44 badSecond x = [(B.add 2 x) (B.mul 2 x) (B.div 2 x) (B.mod 2 x) (B.divMod 2 x) (B.pow 2 x)] 45 zeroPowerIgnoresBase x = N.power (spin x) 0 46 zeroQuotientIgnoresDividend x = N.quotient (spin x) 0 47 invalidExponent x = B.pow 2 x 48 ''' 49 50 51 def boolean(value): 52 return TRUE if value else LEAF 53 54 55 def error(message): 56 return result(False, byte_tree(message.encode())) 57 58 59 def cases(): 60 rows = [] 61 def add(name, argument, expected): 62 rows.append((f'{name}-{len(rows)}', name, argument, expected)) 63 rng = random.Random(83471) 64 pairs = [(a,b) for a in range(10) for b in range(10)] 65 for bits in [4, 8, 16, 32, 64, 128]: 66 p = 1 << bits 67 pairs += [(p-1, p), (p, p-1), (p+1, p), (p+1, 1), (p, 10), (p, p), (0, p)] 68 pairs += [(rng.getrandbits(bits), rng.getrandbits(bits)) for _ in range(4)] 69 for a,b in pairs: 70 q,r = divmod(a,b) if b else (0,a) 71 order = LEAF if a==b else TRUE if a<b else pair(LEAF,LEAF) 72 arithmetic = [n(a+b),n(max(a-b,0)),n(a*b)] 73 comparisons = [boolean(a<b),boolean(a<=b),boolean(a>b),boolean(a>=b)] 74 limits = [n(min(a,b)),n(max(a,b))] 75 qr = [pair(n(q),n(r)),n(q),n(r)] 76 add('nativeBinary', pair(n(a),n(b)), tree_list(arithmetic+[order,boolean(a==b)]+comparisons+limits+qr)) 77 add('checkedBinary', pair(n(a),n(b)), tree_list(arithmetic+comparisons+limits+(qr if b else [error('non-zero')]*3))) 78 for x in [0,1,2,3,9,10,99,100,255,256,1023,1024,2**32-1,2**64,2**127-1]: 79 values = [n(x+1),n(max(0,x-1)),boolean(x==0),boolean(x%2),boolean(x%2==0)] 80 add('unary', n(x), tree_list(values+values+[n(x*10+7),n(x*10),n(x*10+9)])) 81 for x in range(258): 82 add('classes', n(x), tree_list([boolean(p) for p in 83 [x<=127,48<=x<=57,65<=x<=90 or 97<=x<=122,48<=x<=57 or 97<=x<=102,32<=x<=126,x==0,x<=255,False]])) 84 for a,b in [(a,b) for a in range(6) for b in range(9)]+[(65537,16),(2,255),(0,2**64),(1,2**64)]: 85 add('powers', pair(n(a),n(b)), n([a**b,a**b])) 86 for x in [0,1,9,10,11,99,100,101,999,1000,1001,255,256,2**64-1,10**30,10**30-1,2**255-1]: 87 add('format', n(x), byte_tree(str(x).encode())) 88 for x in [TRUE,pair(LEAF,LEAF),pair(TRUE,pair(LEAF,LEAF)),pair(pair(LEAF,LEAF),LEAF)]: 89 add('badFirst', x, tree_list([error('not a natural number')]*4)) 90 add('badSecond', x, tree_list([error('not a natural number')]*2+[error('non-zero')]*3+[error('not a natural number')])) 91 add('zeroPowerIgnoresBase', LEAF, n(1)) 92 add('zeroQuotientIgnoresDividend', LEAF, n(0)) 93 return rows 94 95 96 def screen(runner, root, modules): 97 baseline = REFERENCE_FIXTURES/'compiler-library.arboricx' 98 bundle = root/'baseline.arboricx' 99 exports = [('base.add','add'),('base.div','div'),('conversions.showNumber','format'), 100 ('native_math.add','nativeAdd'),('native_math.multiply','nativeMultiply'),('native_math.compare','nativeCompare')] 101 merge_bundles(bundle, [(baseline, exports)], prune_unreachable=True) 102 old = runner.imported(bundle,'baseline') 103 samples = [] 104 jobs = [(name, values, expected, module, symbol) for size in [255,4095] for name,values,expected,module,symbol in 105 [('add',[size,37],n(size+37),'base','add'),('div',[size,7],n(size//7),'base','div'), 106 ('format',[size],byte_tree(str(size).encode()),'conversions','showNumber')]] 107 for bits in [16,128]: 108 a,b = (1<<bits)-1,1<<(bits-1) 109 jobs += [('nativeAdd',[a,b],n(a+b),'native_math','add'), 110 ('nativeMultiply',[a,b],n(a*b),'native_math','multiply'), 111 ('nativeCompare',[a,b],pair(LEAF,LEAF),'native_math','compare')] 112 for job,(name,values,expected,module,symbol) in enumerate(jobs): 113 for repeat in range(3): 114 for role in (['reference','candidate'] if repeat%2==0 else ['candidate','reference']): 115 pin = old if role=='reference' else modules[module] 116 export = name if role=='reference' else symbol 117 argv=[] 118 for i,v in enumerate(values): 119 f=root/f'screen-{i}.tree';f.write_bytes(n(v));argv += ['--arg-tree-file',f] 120 label=f'screen-{job}-{role}-{repeat}' 121 expect(runner.invoke(label,pin,export,argv),expected,label) 122 record=runner.records[-1] 123 samples.append(dict(operation=name,arguments=values,role=role,warmup=repeat==0, 124 seconds=record['invoke_seconds'],allocated_bytes=record['allocated_bytes'])) 125 for name in ['div']: 126 argv=[] 127 for i,v in enumerate([7,0]): 128 f=root/f'negative-{i}.tree';f.write_bytes(n(v));argv += ['--arg-tree-file',f] 129 assert runner.invoke('historical-zero-'+name,old,name,argv)!=error('non-zero') 130 131 return samples 132 133 134 def main(): 135 global REFERENCE_FIXTURES 136 p=argparse.ArgumentParser(description=__doc__) 137 p.add_argument('tricu',type=Path);p.add_argument('--compiler',type=Path,required=True) 138 p.add_argument('--directory',type=Path,required=True);p.add_argument('--resume',action='store_true') 139 p.add_argument('--seconds',type=int,default=900) 140 p.add_argument('--reference-fixtures',type=Path,required=True,help='authenticated pre-rewrite fixtures') 141 args=p.parse_args() 142 REFERENCE_FIXTURES = args.reference_fixtures.resolve() 143 import collections_batch 144 collections_batch.REFERENCE_FIXTURES = REFERENCE_FIXTURES 145 root=args.directory.resolve();root.mkdir(parents=True,exist_ok=args.resume) 146 workspace=root/'workspace';workspace.mkdir(exist_ok=args.resume) 147 verify(REFERENCE_FIXTURES) 148 text,entries,required=source_map(ROOT/'bootstrap/compiler-source-map.v1',ROOT,'source_map') 149 assert set(entries)==required 150 report=dict(complete=False,map=snapshot(text,entries,ROOT),cases=[], 151 compiler_sha256=hashlib.sha256(args.compiler.read_bytes()).hexdigest(), 152 runtime_sha256=hashlib.sha256(args.tricu.read_bytes()).hexdigest(), 153 harness_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest()) 154 (root/'harness.py').write_bytes(Path(__file__).read_bytes()) 155 for alias,path in SOURCES.items(): 156 (workspace/(alias+'.tri')).write_bytes((ROOT/path).read_bytes()) 157 (workspace/'driver.tri').write_text(DRIVER) 158 runner=ProbeRunner(args.tricu,root,args.seconds) 159 try: 160 tool=runner.imported(args.compiler,'tool');bundle=root/'driver.arboricx' 161 runner.run('compile',['compile','--file',workspace/'driver.tri','--toolchain',tool, 162 '--tool-store',runner.store,'--store',runner.store,'--output',bundle,'--compile-timeout','240'],270) 163 pin=runner.imported(bundle,'driver') 164 for label,name,argument,expected in cases(): 165 f=root/'argument.tree';f.write_bytes(argument) 166 actual=runner.invoke(label,pin,name,['--arg-tree-file',f]);expect(actual,expected,label) 167 report['cases'].append(dict(label=label,input_sha256=hashlib.sha256(argument).hexdigest(),result_sha256=hashlib.sha256(actual).hexdigest())) 168 f=root/'argument.tree';f.write_bytes(pair(n(2),n(3))) 169 expected = next(expected for _,name,arg,expected in cases() if name=='nativeBinary' and arg==pair(n(2),n(3))) 170 assert runner.invoke('wrong-subject',pin,'wrongBinary',['--arg-tree-file',f])!=expected 171 tool_payload=(runner.store/'objects'/tool[:3]/tool).read_bytes() 172 assert digest('arboricx.module-manifest.v1',tool_payload)==tool 173 compiler=next(row.split('\t')[3] for row in tool_payload.decode().splitlines()[1:] if row.split('\t')[1]=='sourceBuildGraph') 174 report['modules']=audit_workspace(workspace,runner.store,compiler) 175 native=runner.run('native-driver',['import','--file',bundle,'--store',runner.store]) 176 assert re.search(r'Created module alias .* -> ([0-9a-f]{64})',native)[1]==report['modules']['driver']['manifest'] 177 report['bundle_sha256']=hashlib.sha256(bundle.read_bytes()).hexdigest() 178 report['removed_exports']=check_surface(report['modules']) 179 report['screen']=screen(runner,root,{name:row['manifest'] for name,row in report['modules'].items()}) 180 for alias,path in SOURCES.items(): 181 assert (workspace/(alias+'.tri')).read_bytes()==(ROOT/path).read_bytes() 182 report.update(complete=True,wrong_subject_rejected=True,seconds=time.monotonic()-runner.started) 183 print(f'PASS: {len(report["cases"])} numeric/format/domain cases; {report["seconds"]:.2f}s') 184 finally: 185 (root/'report.json').write_text(json.dumps(report,indent=2)+'\n') 186 187 if __name__=='__main__': 188 main()