prepare-contracts.py (5005B)
1 #!/usr/bin/env python3 2 """Independent Haskell capture for the browser's existing packaged contract environment. 3 No compiler/library source builds or fixture relabeling. Run after nix build .#: 4 python3 ext/demo-site/test/prepare-contracts.py result/bin/tricu 5 """ 6 import importlib.util 7 import json 8 from pathlib import Path 9 import re 10 import subprocess 11 import sys 12 import tempfile 13 14 HERE = Path(__file__).resolve().parent 15 spec = importlib.util.spec_from_file_location('demo_prepare', HERE.parent / 'prepare.py') 16 p = importlib.util.module_from_spec(spec) 17 spec.loader.exec_module(p) 18 host = Path(sys.argv[1]).resolve() 19 compiler = p.reference_compiler() 20 source = (HERE.parent / 'src/contracts.ts').read_bytes() 21 bindings = {quoted or bare: exported for quoted, bare, exported in 22 re.findall(r"(?:'([^']+)'|([A-Za-z_]\w*)):\s*'(base\.[^']+)'", source.decode())} 23 assert len(bindings) == 38 24 cases = [ 25 dict(source='id =@nat? (x : x)', export='id', arguments=[], failure='not a natural number'), 26 dict(source='id x =@nat? x', export='id', arguments=[p.of_number(42)], expected=p.of_number(42)), 27 dict(source='id x =@nat? x', export='id', arguments=['10'], failure='not a natural number'), 28 dict(source='id x =@nat? x', export='id', arguments=['200'], failure='not a natural number'), 29 dict(source='id x@nat? =@nat? x', export='id', arguments=[p.of_number(7)], expected=p.of_number(7)), 30 dict(source='id x@nat? =@nat? x', export='id', arguments=[p.of_string('bad')], failure='not a natural number'), 31 dict(source='id @nat? =@nat? (x : x)', export='id', arguments=[p.of_number(9)], expected=p.of_number(9)), 32 dict(source='id @nat? =@nat? (x : x)', export='id', arguments=['10'], failure='not a natural number'), 33 dict(source='first x@nat? y@nat? =@nat? x', export='first', arguments=['10',p.of_number(2)], failure='not a natural number'), 34 dict(source='main = check nat? 42', export='main', arguments=[], expected=p.of_number(42)), 35 dict(source='main = check (listOf nat?) [1 2]', export='main', arguments=[], expected='2'+p.of_number(1)+'2'+p.of_number(2)+'0'), 36 ] 37 with tempfile.TemporaryDirectory(prefix='arboricx-contract-capture-') as directory: 38 temp = Path(directory) 39 store = temp / 'store' 40 def run(*args): 41 reply = subprocess.run([str(host), *map(str, args), '+RTS', '-M8192m', '-RTS'], capture_output=True, timeout=120, cwd=p.ROOT) 42 if reply.returncode: 43 raise RuntimeError(reply.stderr.decode() + reply.stdout.decode()) 44 return reply.stdout.decode() 45 def put(kind, payload): 46 pin = p.digest(kind.encode() + b'\0' + payload) 47 path = store / 'objects' / pin[:3] / pin 48 path.parent.mkdir(parents=True, exist_ok=True) 49 path.write_bytes(payload) 50 return pin 51 imported = run('arboricx','import','--file',compiler,'--store',store,'--indexed') 52 pin = re.search(r'Created module alias .* -> ([0-9a-f]{64})',imported)[1] 53 roots = {exported: run('arboricx','run','--module',pin,'--name',exported,'--store',store,'--format','ternary').strip() 54 for exported in set(bindings.values())} 55 environment = ''.join('22'+p.of_string(name)+roots[exported] for name,exported in bindings.items())+'0' 56 env_file = temp / 'environment.tree' 57 env_file.write_bytes(bytes(map(int,environment))) 58 for case in cases: 59 result = run('arboricx','run','--module',pin,'--name','source_unit.compileStableModuleDefinitions', 60 '--store',store,'--arg-tree-file',env_file,'--arg',case['source'],'--format','ternary').strip() 61 success,body=p.pair(result) 62 assert success=='10',(case['source'],result[:120]) 63 definitions,_=p.pair(body) 64 programs={} 65 while definitions!='0': 66 binding,definitions=p.pair(definitions) 67 name,tree=p.pair(binding) 68 programs[p.string(name)]=tree 69 case['definitions']=programs 70 tree_pin=put('arboricx.tree-term.v1',bytes(map(int,programs[case['export']]))) 71 manifest=put('arboricx.module-manifest.v1',f'arboricx.module-manifest.v1\nexport\tmain\tarboricx.tree-term.v1\t{tree_pin}\tarboricx.abi.tree.v1\n'.encode()) 72 arguments=[] 73 for i,tree in enumerate(case['arguments']): 74 file=temp/f'arg{i}.tree';file.write_bytes(bytes(map(int,tree)));arguments+=['--arg-tree-file',file] 75 case['result']=run('arboricx','run','--module',manifest,'--name','main','--store',store,'--format','ternary',*arguments).strip() 76 if 'failure' in case: 77 case['expected']='20'+'2'+p.of_string(case['failure'])+'0' 78 assert case['result']==case['expected'],case 79 print('PASS Haskell contracts',case['source'],flush=True) 80 payload=dict(compilerBundleSha256=p.digest(compiler.read_bytes()),hostSha256=p.digest(host.read_bytes()), 81 contractsSourceSha256=p.digest(source),bindings=bindings,backend='stable',cases=cases) 82 (HERE/'contracts-conformance.json').write_text(json.dumps(payload,indent=2)+'\n')