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