prepare-compiler.py (6739B)
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 = ('), 49 dict(source='id = nope'), 50 ] 51 with tempfile.TemporaryDirectory(prefix='arboricx-browser-compiler-') as temporary: 52 temp = Path(temporary) 53 store = temp / 'store' 54 def run(*args, input=None): 55 reply = subprocess.run([str(host), *map(str, args), '+RTS', '-M8192m', '-RTS'], input=input, capture_output=True, timeout=60, cwd=p.ROOT) 56 if reply.returncode: 57 raise RuntimeError(reply.stderr.decode()) 58 return reply.stdout.decode() 59 def put(kind, payload): 60 pin = p.digest(kind.encode() + b'\0' + payload) 61 path = store / 'objects' / pin[:3] / pin 62 path.parent.mkdir(parents=True, exist_ok=True) 63 path.write_bytes(payload) 64 return pin 65 imported = run('arboricx', 'import', '--file', compiler, '--store', store, '--indexed') 66 pin = re.search(r'Created module alias .* -> ([0-9a-f]{64})', imported)[1] 67 empty = temp / 'empty.tree' 68 empty.write_bytes(b'\0') 69 for case in cases: 70 result = run('arboricx', 'run', '--module', pin, '--name', 'source_unit.compileStableModuleDefinitions', 71 '--store', store, '--arg-tree-file', empty, '--arg', case['source'], '--format', 'ternary') 72 tag, body = p.pair(result) 73 if tag == '0': 74 diagnostic, _ = p.pair(body) 75 code, tail = p.pair(diagnostic) 76 expected, fields = p.pair(tail) 77 _, location_fields = p.pair(fields) 78 location, _ = p.pair(location_fields) 79 where = '' 80 if location != '0': 81 line, rest = p.pair(location) 82 column, _ = p.pair(rest) 83 where = f' at {p.number(line)}:{p.number(column)}' 84 number = p.number(code) 85 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 '')) 86 case['diagnostic'] = f'Compiler diagnostic {number}{where}{label}' 87 assert 'export' not in case 88 continue 89 assert tag == '10' 90 definitions, _ = p.pair(body) 91 bindings = {} 92 while definitions != '0': 93 binding, definitions = p.pair(definitions) 94 name, tree = p.pair(binding) 95 bindings[p.string(name)] = tree 96 case['definitions'] = bindings 97 program = bindings[case['export']] 98 tree_pin = put('arboricx.tree-term.v1', bytes(map(int, program))) 99 manifest = put('arboricx.module-manifest.v1', 100 f'arboricx.module-manifest.v1\nexport\tmain\tarboricx.tree-term.v1\t{tree_pin}\tarboricx.abi.tree.v1\n'.encode()) 101 args = [] 102 for i, argument in enumerate(case['arguments']): 103 path = temp / f'arg{i}.tree' 104 path.write_bytes(bytes(map(int, argument))) 105 args += ['--arg-tree-file', path] 106 case['result'] = run('arboricx', 'run', '--module', manifest, '--name', 'main', '--store', store, '--format', 'ternary', *args, 107 *(['--io'] if case.get('mode') == 'io' else []), 108 input=(case['input'] + '\n').encode() if 'input' in case else None) 109 110 if 'expectedNumber' in case: 111 assert case['result'] == p.of_number(case['expectedNumber']) 112 113 payload = dict(compilerBundleSha256=p.digest(compiler.read_bytes()), hostSha256=p.digest(host.read_bytes()), 114 backend='stable', export='source_unit.compileStableModuleDefinitions', cases=cases) 115 (HERE / 'compiler-conformance.json').write_text(json.dumps(payload, indent=2) + '\n') 116 compiler_hash = payload['compilerBundleSha256'] 117 (HERE.parent / 'bundles' / f'{compiler_hash}.arboricx').write_bytes(compiler.read_bytes()) 118 descriptor['hash'] = compiler_hash 119 (HERE.parent / 'compiler-reference.v1').write_bytes(metadata) 120 (HERE.parent / 'compiler.json').write_text(json.dumps(descriptor, indent=2) + '\n') 121 print(f'Published verified compiler and {len(cases)} independent Haskell captures')