tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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