tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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')