native_math.py (7870B)
1 #!/usr/bin/env python3 2 """Opt-in native math: explicit portable compiler, exact independent tree oracles. 3 No recovery compilation, compiler rebuilding, or alternative host execution. 4 """ 5 import argparse 6 import hashlib 7 import json 8 from pathlib import Path 9 import random 10 import re 11 import subprocess 12 import sys 13 import time 14 15 sys.dont_write_bytecode = True 16 17 ROOT = Path(__file__).resolve().parent.parent 18 p = argparse.ArgumentParser(description=__doc__) 19 p.add_argument('tricu', type=Path) 20 p.add_argument('--compiler-bundle', type=Path, required=True) 21 p.add_argument('--directory', type=Path, required=True, help='fresh preparation directory') 22 p.add_argument('--generation', type=Path, help='optional matching baseline cohort; enables bounded performance/lexer probes') 23 p.add_argument('--counter-tricu', type=Path, help='optional existing investigation-only counted Haskell host; counts only, never timing evidence') 24 a = p.parse_args() 25 exe = str(a.tricu.resolve()) 26 root = a.directory.resolve() 27 root.mkdir(parents=True, exist_ok=False) 28 started = time.monotonic() 29 records = [] 30 31 32 def run(args, executable=None): 33 if time.monotonic() - started > 900: 34 raise RuntimeError('15 minute initial pass budget exhausted') 35 command = [executable or exe, *map(str, args)] 36 begin = time.monotonic() 37 try: 38 result = subprocess.run(command, capture_output=True, timeout=120) 39 except subprocess.TimeoutExpired: 40 records.append(dict(command=command, timeout=120)) 41 save() 42 raise 43 records.append(dict(command=command, seconds=time.monotonic() - begin, 44 stderr=result.stderr.decode(), returncode=result.returncode)) 45 save() 46 if result.returncode: 47 raise RuntimeError(result.stderr.decode()) 48 return result.stdout.decode().strip() 49 50 51 def save(): 52 (root / 'measurements.json').write_text(json.dumps(records, indent=2) + '\n') 53 54 55 def import_bundle(path, store): 56 reply = run(['arboricx', 'import', '--file', path, '--store', store, '--indexed']) 57 return re.search(r'Created module alias .* -> ([0-9a-f]{64})', reply)[1] 58 59 60 def number(n): 61 return b'\0' if n == 0 else b'\2' + (b'\1\0' if n & 1 else b'\0') + number(n >> 1) 62 63 64 def boolean(b): 65 return b'\1\0' if b else b'\0' 66 67 68 def tree_list(values): 69 return b''.join(b'\2' + v for v in values) + b'\0' 70 71 72 def check(name, values, expected, matches=True): 73 stem = name if matches else name + '-wrong' 74 payload = root / (stem + '.tree') 75 payload.write_bytes(tree_list(values)) 76 (root / (stem + '.expected.tree')).write_bytes(tree_list(expected)) 77 output = run(['arboricx', 'run', '--module', pin, '--name', name, 78 '--store', root / 'tools', '--arg-tree-file', payload, 79 '--format', 'ternary', '--profile', '+RTS', '-N1', '-T', '-RTS']) 80 (root / (stem + '.actual.ternary')).write_text(output + '\n') 81 oracle = ''.join(str(b) for b in tree_list(expected)) 82 assert (output == oracle) == matches, (name, output[:160], oracle[:160]) 83 print(f'{name}: {len(values)} exact results' if matches else f'{name}: wrong-value control rejected', flush=True) 84 85 86 def file_hash(path): 87 return hashlib.sha256(Path(path).read_bytes()).hexdigest() 88 89 90 (root / 'provenance.json').write_text(json.dumps(dict( 91 executable=exe, executable_sha256=file_hash(exe), 92 compiler=str(a.compiler_bundle.resolve()), compiler_sha256=file_hash(a.compiler_bundle), 93 library_sha256=file_hash(ROOT / 'lib/math/native.tri'), 94 lexer_sha256=file_hash(ROOT / 'lib/source/lexer.tri'), 95 runner_sha256=file_hash(__file__), 96 performance_runner_sha256=file_hash(ROOT / 'test/native_math_performance.py'), 97 generation=str(a.generation.resolve()) if a.generation else None, 98 counter=str(a.counter_tricu.resolve()) if a.counter_tricu else None, 99 counter_sha256=file_hash(a.counter_tricu) if a.counter_tricu else None, 100 ), indent=2) + '\n') 101 tool = import_bundle(a.compiler_bundle.resolve(), root / 'tools') 102 source = (ROOT / 'lib/math/native.tri').read_text() 103 source += ''' 104 mapStep self xs f = triage t (_ : t) (h r : t (f h) (self r f)) xs 105 mapValues xs f = fix mapStep xs f 106 binary f row = triage t (_ : t) (a b : f a b) row 107 ''' 108 unary = dict(zero=lambda n: n == 0, odd=lambda n: bool(n & 1), even=lambda n: not n & 1, 109 ascii=lambda n: n <= 127, digit=lambda n: 48 <= n <= 57, 110 letter=lambda n: 65 <= n <= 90 or 97 <= n <= 122, 111 lowerHex=lambda n: 48 <= n <= 57 or 97 <= n <= 102, 112 printable=lambda n: 32 <= n <= 126, 113 successor=lambda n: n + 1, predecessor=lambda n: max(0, n - 1), 114 leftBound=lambda n: 65 <= n, rightBound=lambda n: n <= 65, 115 partialAdd=lambda n: n + 1) 116 binary = dict(compare=lambda x, y: (x > y) - (x < y), 117 equal=lambda x, y: x == y, lt=lambda x, y: x < y, 118 lte=lambda x, y: x <= y, gt=lambda x, y: x > y, gte=lambda x, y: x >= y, 119 minimum=min, maximum=max, add=lambda x, y: x + y, 120 subtract=lambda x, y: max(0, x - y), multiply=lambda x, y: x * y) 121 source += 'leftBound value = lte 65 value\nrightBound value = lte value 65\npartialAdd = add 1\n' 122 for name in unary: 123 source += f'check_{name} xs = mapValues xs {name}\n' 124 for name in binary: 125 source += f'check_{name} xs = mapValues xs (binary {name})\n' 126 source += 'check_decimal xs = mapValues xs (binary decimal)\n' 127 source += ('check_range xs = mapValues xs (row : triage t (_ : t) ' 128 '(value bounds : triage t (_ : t) (low high : inRange value low high) bounds) row)\n') 129 source = ''.join(f'!export check_{name}\n' for name in [*unary, *binary, 'decimal', 'range']) + source 130 (root / 'math.tri').write_text(source) 131 (root / 'source.sha256').write_text(hashlib.sha256(source.encode()).hexdigest() + '\n') 132 run(['arboricx', 'compile', '--file', root / 'math.tri', '--toolchain', tool, 133 '--tool-store', root / 'tools', '--store', root / 'application', 134 '--output', root / 'math.arboricx', '--compile-timeout', '120', 135 '+RTS', '-N1', '-RTS']) 136 pin = import_bundle(root / 'math.arboricx', root / 'tools') 137 values = list(range(256)) + [256, 257, 65535, 65536, 2**64 - 1, 2**128] 138 for name, oracle in unary.items(): 139 encode = number if name in ['successor', 'predecessor', 'partialAdd'] else boolean 140 check('check_' + name, [number(n) for n in values], [encode(oracle(n)) for n in values]) 141 rng = random.Random(171) 142 pairs = [(x, y) for x in range(16) for y in range(16)] 143 bounds = sorted({n for k in [1, 2, 7, 8, 16, 32, 64, 128] for n in [2**k-1, 2**k, 2**k+1]}) 144 pairs += [(x, y) for x in [0, *bounds] for y in [0, *bounds]] 145 pairs += [(rng.getrandbits(128), rng.getrandbits(128)) for _ in range(64)] 146 for name, oracle in binary.items(): 147 encode = boolean if name in ['equal', 'lt', 'lte', 'gt', 'gte'] else number 148 if name == 'compare': 149 encode = lambda order: b'\0' if order == 0 else b'\1\0' if order < 0 else b'\2\0\0' 150 check('check_' + name, [b'\2' + number(x) + number(y) for x, y in pairs], 151 [encode(oracle(x, y)) for x, y in pairs]) 152 decimal_pairs = [(x, d) for x in values for d in range(10)] 153 check('check_decimal', [b'\2' + number(x) + number(d) for x, d in decimal_pairs], 154 [number(x * 10 + d) for x, d in decimal_pairs]) 155 ranges = [(v, lo, hi) for v in range(16) for lo in range(8) for hi in range(8)] 156 check('check_range', [b'\2' + number(v) + b'\2' + number(lo) + number(hi) for v, lo, hi in ranges], 157 [boolean(lo <= v <= hi) for v, lo, hi in ranges]) 158 check('check_zero', [number(0)], [boolean(False)], matches=False) 159 print('PASS: native math; compilation/publication and exact canonical results', flush=True) 160 if a.generation: 161 from native_math_performance import probe 162 probe(root, ROOT, a.generation.resolve(), tool, pin, run, import_bundle, number, boolean, tree_list, 163 str(a.counter_tricu.resolve()) if a.counter_tricu else None)