prepare-compiler.py (7679B)
1 #!/usr/bin/env python3 2 """Opt-in independent Haskell compiler/demo capture; no compiler/library rebuild.""" 3 import argparse 4 import importlib.util 5 import json 6 from pathlib import Path 7 import re 8 import subprocess 9 import sys 10 import tempfile 11 12 HERE = Path(__file__).resolve().parent 13 spec = importlib.util.spec_from_file_location('demo_prepare', HERE.parent / 'prepare.py') 14 p = importlib.util.module_from_spec(spec) 15 spec.loader.exec_module(p) 16 parser = argparse.ArgumentParser(description=__doc__) 17 parser.add_argument('tricu', type=Path) 18 parser.add_argument('--compiler-bundle', type=Path) 19 parser.add_argument('--reference-commit') 20 args = parser.parse_args() 21 host = args.tricu.resolve() 22 if bool(args.compiler_bundle) != bool(args.reference_commit): 23 parser.error('upgrades require both --compiler-bundle and --reference-commit') 24 descriptor = json.loads((HERE.parent / 'compiler.json').read_text()) 25 metadata = (HERE.parent / 'compiler-reference.v1').read_bytes() 26 compiler = p.reference_compiler() 27 if args.compiler_bundle: 28 compiler = args.compiler_bundle.resolve() 29 metadata = compiler.with_suffix('.v1').read_bytes() 30 commit = args.reference_commit 31 if not re.fullmatch('[0-9a-f]{40}', commit): 32 parser.error('reference commit must be a full Git identity') 33 # Authenticate origin independently, not just a self-consistent new descriptor. 34 for name, payload in [('compiler-library.arboricx', compiler.read_bytes()), ('compiler-library.v1', metadata)]: 35 expected = subprocess.check_output(['git', 'show', commit + ':test/fixtures/' + name], cwd=p.ROOT) 36 if payload != expected: 37 raise ValueError('Compiler fixture differs from recorded commit: ' + name) 38 descriptor['referenceCommit'] = commit 39 assert ('bundle\t' + p.digest(b'tricu.compiler-library-fixture.v1\0' + compiler.read_bytes())) in metadata.decode().splitlines() 40 41 cases = [ 42 dict(source='id = (x : x)', export='id', arguments=[p.of_number(42)]), 43 dict(source='pair a b = t a b', export='pair', arguments=[p.of_number(7), p.of_string('hi')]), 44 dict(source='id = (x : x)\nanswer = id 42', export='answer', arguments=[]), 45 dict(source='id = (x : x)\nanswer = id 42\nother = id 7\nmore = id 9\nextra = id 11\nlast = id 13', export='other', arguments=[]), 46 dict(source='io=t "tricuIO" (t 1 (t 11 t))', export='io', arguments=[], mode='io', input='λ雪🙂'), 47 dict(source='main xs = (let rec\n last [h] = h\n last [_ | rest] = last rest\n last _ = 0\n in case xs of\n [] = 0\n _ = last xs)', export='main', arguments=['2'+p.of_number(7)+'2'+p.of_number(42)+'0'], expectedNumber=42, extended=True), 48 dict(source='id x = x\nmain =\n id\n 42', export='main', arguments=[], expectedNumber=42, extended=True), 49 dict(source='main = case t of\n t = value\n _ = 0\nwhere value = 42', export='main', arguments=[], expectedNumber=42, extended=True), 50 dict(source='id = ('), 51 dict(source='id = nope'), 52 ] 53 with tempfile.TemporaryDirectory(prefix='arboricx-browser-compiler-') as temporary: 54 temp = Path(temporary) 55 store = temp / 'store' 56 def run(*args, input=None): 57 reply = subprocess.run([str(host), *map(str, args), '+RTS', '-M8192m', '-RTS'], input=input, capture_output=True, timeout=60, cwd=p.ROOT) 58 if reply.returncode: 59 raise RuntimeError(reply.stderr.decode()) 60 return reply.stdout.decode() 61 def put(kind, payload): 62 pin = p.digest(kind.encode() + b'\0' + payload) 63 path = store / 'objects' / pin[:3] / pin 64 path.parent.mkdir(parents=True, exist_ok=True) 65 path.write_bytes(payload) 66 return pin 67 imported = run('arboricx', 'import', '--file', compiler, '--store', store, '--indexed') 68 pin = re.search(r'Created module alias .* -> ([0-9a-f]{64})', imported)[1] 69 empty = temp / 'empty.tree' 70 empty.write_bytes(b'\0') 71 for case in cases: 72 result = run('arboricx', 'run', '--module', pin, '--name', 'source_unit.compileStableModuleDefinitions', 73 '--store', store, '--arg-tree-file', empty, '--arg', case['source'], '--format', 'ternary') 74 tag, body = p.pair(result) 75 if tag == '0': 76 diagnostic, _ = p.pair(body) 77 code, tail = p.pair(diagnostic) 78 expected, fields = p.pair(tail) 79 _, location_fields = p.pair(fields) 80 location, _ = p.pair(location_fields) 81 where = '' 82 if location != '0': 83 line, rest = p.pair(location) 84 column, _ = p.pair(rest) 85 where = f' at {p.number(line)}:{p.number(column)}' 86 number = p.number(code) 87 label = f': expected token kind {p.number(expected)}' if number == 2 else (f': unresolved name {json.dumps(p.string(expected), ensure_ascii=False)}' if number == 20 else (f': expected {json.dumps(p.string(expected), ensure_ascii=False)}' if number >= 30 else '')) 88 case['diagnostic'] = f'Compiler diagnostic {number}{where}{label}' 89 assert 'export' not in case 90 continue 91 assert tag == '10' 92 definitions, _ = p.pair(body) 93 bindings = {} 94 while definitions != '0': 95 binding, definitions = p.pair(definitions) 96 name, tree = p.pair(binding) 97 bindings[p.string(name)] = tree 98 case['definitions'] = bindings 99 program = bindings[case['export']] 100 tree_pin = put('arboricx.tree-term.v1', bytes(map(int, program))) 101 manifest = put('arboricx.module-manifest.v1', 102 f'arboricx.module-manifest.v1\nexport\tmain\tarboricx.tree-term.v1\t{tree_pin}\tarboricx.abi.tree.v1\n'.encode()) 103 args = [] 104 for i, argument in enumerate(case['arguments']): 105 path = temp / f'arg{i}.tree' 106 path.write_bytes(bytes(map(int, argument))) 107 args += ['--arg-tree-file', path] 108 if case.get('mode') == 'io': 109 # Historical framing remains a compilation/inspection specimen. 110 # Current native execution must reject it before terminal work. 111 rejected = subprocess.run([str(host), 'arboricx', 'run', '--module', manifest, 112 '--name', 'main', '--store', str(store), '--io'], 113 input=b'', capture_output=True, timeout=60, cwd=p.ROOT) 114 assert rejected.returncode != 0 and rejected.stdout == b'' 115 assert b'Unsupported tricuIO version' in rejected.stderr 116 case['compileOnly'] = True 117 case['executionRejected'] = 'Unsupported tricuIO version' 118 continue 119 case['result'] = run('arboricx', 'run', '--module', manifest, '--name', 'main', '--store', store, '--format', 'ternary', *args, 120 *(['--io'] if case.get('mode') == 'io' else []), 121 input=(case['input'] + '\n').encode() if 'input' in case else None) 122 123 if 'expectedNumber' in case: 124 assert case['result'] == p.of_number(case['expectedNumber']) 125 126 payload = dict(compilerBundleSha256=p.digest(compiler.read_bytes()), hostSha256=p.digest(host.read_bytes()), 127 backend='stable', export='source_unit.compileStableModuleDefinitions', cases=cases) 128 (HERE / 'compiler-conformance.json').write_text(json.dumps(payload, indent=2) + '\n') 129 compiler_hash = payload['compilerBundleSha256'] 130 (HERE.parent / 'bundles' / f'{compiler_hash}.arboricx').write_bytes(compiler.read_bytes()) 131 descriptor['hash'] = compiler_hash 132 (HERE.parent / 'compiler-reference.v1').write_bytes(metadata) 133 (HERE.parent / 'compiler.json').write_text(json.dumps(descriptor, indent=2) + '\n') 134 print(f'Published verified compiler and {len(cases)} independent Haskell captures')