tricu

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

native-modules.py (6737B)


      1 #!/usr/bin/env python3
      2 """Independent native CLI expectations for browser module semantics.
      3 
      4 Uses the shipped stable compiler and the site's exact complete-interface bundles
      5 for independent native checks. Run with a Nix-built tricu. Temporary indexed
      6 stores are removed on exit.
      7 """
      8 import argparse
      9 import hashlib
     10 import json
     11 from pathlib import Path
     12 import re
     13 import subprocess
     14 import sys
     15 import tempfile
     16 
     17 ROOT = Path(__file__).resolve().parents[3]
     18 SITE = ROOT / 'ext/demo-site'
     19 sys.path.insert(0, str(ROOT / 'test'))
     20 from bundle_dag import read
     21 from shipped_compiler import verify
     22 
     23 
     24 def main():
     25     parser = argparse.ArgumentParser(description=__doc__)
     26     parser.add_argument('tricu', type=Path)
     27     parser.add_argument('--capture', type=Path, help='Explicitly capture independent complete-tree digests')
     28     args = parser.parse_args()
     29     host = args.tricu.resolve()
     30     metadata = verify()
     31     releases = json.loads((SITE / 'packages.json').read_text())['releases']
     32     def library(name):
     33         # An immutable known release, not an expected value generated by a browser.
     34         return next(r for r in releases if r['name'] == 'tricu-lib-' + name and r['version'] == '0.1.0')
     35     math, base, io, modules = [library(n) for n in ['native-math', 'base', 'io', 'source-module']]
     36     with tempfile.TemporaryDirectory(prefix='tricu-browser-native-modules-') as temporary:
     37         work = Path(temporary)
     38         (work / 'tricu.workspace').write_text('tricu.workspace.v1\n')
     39         def run(*words, success=True):
     40             result = subprocess.run([str(host), *map(str, words), '+RTS', '-N1', '-M2048m', '-RTS'],
     41                                     cwd=work, capture_output=True, timeout=120)
     42             output = (result.stdout + result.stderr).decode()
     43             if (result.returncode == 0) != success:
     44                 raise AssertionError(output)
     45             return output.strip()
     46         def imported(file, store):
     47             text = run('arboricx', 'import', '--indexed', '--store', store, '--file', file)
     48             return re.search(r'Created module alias .* -> ([0-9a-f]{64})', text)[1]
     49         compiler = imported(ROOT / metadata['bundle'], work / 'tools')
     50         for release in [math, base, io, modules]:
     51             assert imported(SITE / 'bundles' / (release['hash'] + '.arboricx'), work / 'store') == release['manifest']
     52         def compile_source(source, success=True):
     53             (work / 'main.tri').write_text(source)
     54             output = work / 'out.arboricx'
     55             output.unlink(missing_ok=True)
     56             diagnostic = run('arboricx', 'compile', '--file', work / 'main.tri', '--store', work / 'store',
     57                             '--toolchain', compiler, '--tool-store', work / 'tools', '--output', output,
     58                             '--compile-timeout', '110', success=success)
     59             if not success:
     60                 assert not output.exists()
     61                 return diagnostic
     62             return imported(output, work / 'consumer'), list(read(output)[3])
     63         cases = [
     64             (f'!import {math["manifest"]} as N\n!export answer\nprivate = 99\nanswer = N.add 40 2', 'number', '42'),
     65             (f'!import {math["manifest"]} as N\n!export answer\nprivate = 99\nanswer = N.add (N.add 40 2) private', 'number', '141'),
     66             (f'!import {base["manifest"]} *\n!export answer\nanswer = add 20 22', 'number', '42'),
     67             (f'!import {base["manifest"]} *\n!export answer\ncheckedIdentity x =@nat? x\nanswer = checkedIdentity 42', 'number', '42'),
     68             (f'!import {math["manifest"]} as A\n!import {math["manifest"]} as B\n!export *\nanswer = A.add (B.add 10 11) 21', 'number', '42'),
     69             (f'!import {modules["manifest"]} as M\n!export answer\nanswer = M.sourceModuleQualified "N" "add"', 'string', 'N.add'),
     70             (f'!import {math["manifest"]} as N\n!export answer\nloop x = loop x\nanswer = N.add 40 2', 'number', '42'),
     71         ]
     72         for source, format, expected in cases:
     73             manifest, names = compile_source(source)
     74             assert names == ['answer'], names
     75             actual = run('arboricx', 'run', '--module', manifest, '--store', work / 'consumer',
     76                          '--name', 'answer', '--format', format)
     77             assert actual == (json.dumps(expected) if format == 'string' else expected), (format, actual, expected)
     78         manifest, names = compile_source('!export * from ' + io['manifest'])
     79         assert manifest == io['manifest']
     80         assert names == list(read(SITE / 'bundles' / (io['hash'] + '.arboricx'))[3])
     81         tree_cases = [
     82             ('keep', f'!import {base["manifest"]} as B\n!export keep\nkeep x = B.pair x x'),
     83             ('addTwo', f'!import {math["manifest"]} as N\n!export addTwo\naddTwo x = N.add x 2'),
     84         ]
     85         trees = []
     86         for name, source in tree_cases:
     87             manifest, names = compile_source(source)
     88             assert names == [name]
     89             tree = run('arboricx', 'run', '--module', manifest, '--store', work / 'consumer',
     90                        '--name', name, '--format', 'ternary')
     91             assert tree and set(tree) <= set('012')
     92             trees.append(dict(source=source, name=name, bytes=len(tree),
     93                               ternaryAsciiSha256=hashlib.sha256(tree.encode('ascii')).hexdigest()))
     94         evidence = dict(schema=1, nativeCompilerSha256=metadata['sha256'], trees=trees)
     95         if args.capture:
     96             args.capture.write_text(json.dumps(evidence, indent=2) + '\n')
     97         else:
     98             assert evidence == json.loads((SITE / 'test/module-goldens.json').read_text())
     99         failures = [
    100             (f'!import {math["manifest"]}\n!export answer\nanswer = 42', 'hash imports require an alias'),
    101             (f'!import {math["manifest"]} as N\n!import {math["manifest"]} as N\n!export answer\nanswer = 42', 'duplicate imported name'),
    102             (f'!import {math["manifest"]} as N\nanswer = 42', 'module requires an export'),
    103             ('!export absent\nanswer = 42', 'undeclared local export'),
    104             ('!export answer\n!export *\nanswer = 42', 'duplicate export'),
    105             (f'!export * from {io["manifest"]}\n!export * from {io["manifest"]}', 'duplicate'),
    106         ]
    107         for source, message in failures:
    108             diagnostic = compile_source(source, False)
    109             assert message in diagnostic, diagnostic
    110         for source in [
    111             f'!import {math["manifest"]} *\n!export add\nadd x y = x',
    112             f'!export * from {io["manifest"]}\n!export answer\nanswer = readFile',
    113         ]:
    114             compile_source(source, False)
    115     print('Native module oracle passed: values, full reexports, import/export collisions, lexical scope and failed publication')
    116 
    117 
    118 if __name__ == '__main__':
    119     main()