stable_constructor_k.py (6860B)
1 #!/usr/bin/env python3 2 """Opt-in current-source constructor-K check; never publishes compiler artifacts. 3 4 The selected host must contain the authenticated packaged compiler. Unchanged 5 fixture dependencies are source-verified; only source_stable and source_unit 6 are rebuilt. Probe-only exports expose internal summaries to an independent 7 finite oracle without making them part of the production interface. 8 """ 9 import argparse 10 import hashlib 11 import json 12 from pathlib import Path 13 import re 14 import subprocess 15 import time 16 17 from bundle_dag import Intern, merge_bundles, read 18 from fixture_sources import snapshot_bytes, verify 19 from pattern_lowering import audit 20 from shipped_compiler import verify as verify_compiler 21 22 ROOT = Path(__file__).resolve().parent.parent 23 24 25 def main(): 26 p = argparse.ArgumentParser(description=__doc__) 27 p.add_argument('tricu', type=Path) 28 p.add_argument('--directory', type=Path, required=True) 29 p.add_argument('--expected-stable', type=Path, help='optional exact public-tree comparison against a prior prototype') 30 p.add_argument('--expected-unit', type=Path, help='optional exact source_unit comparison against a prior prototype') 31 args = p.parse_args() 32 root = args.directory.resolve() 33 root.mkdir(parents=True, exist_ok=False) 34 workspace = root / 'workspace' 35 workspace.mkdir() 36 (workspace / 'tricu.workspace').write_text('tricu.workspace.v1\n') 37 store = root / 'store' 38 host = args.tricu.resolve() 39 metadata = verify_compiler() 40 fixture_rows = {row[2]: row for row in verify(ROOT / 'test/fixtures')} 41 entries = {} 42 for row in (ROOT / 'bootstrap/compiler-source-map.v1').read_text().splitlines()[1:]: 43 _, alias, _, path, deps = row.split('\t') 44 entries[alias] = (path, [] if deps == '-' else deps.split(',')) 45 affected = {'source_stable', 'source_unit'} 46 needed = set() 47 48 def visit(alias): 49 if alias in needed: 50 return 51 needed.add(alias) 52 for dep in entries[alias][1]: 53 visit(dep) 54 visit('source_unit') 55 for alias in needed - affected: 56 assert snapshot_bytes(ROOT / 'test/fixtures', fixture_rows[alias][4]) == (ROOT / entries[alias][0]).read_bytes(), ('stale reuse', alias) 57 report = dict(complete=False, compiler_sha256=metadata['sha256'], 58 harness_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), 59 oracle_sha256=hashlib.sha256((ROOT / 'test/StableConstructorK.hs').read_bytes()).hexdigest(), 60 runtime_sha256=hashlib.sha256(host.read_bytes()).hexdigest(), commands=[], reused={}) 61 started = time.monotonic() 62 63 def command(argv, seconds=180): 64 result = subprocess.run(list(map(str, argv)), cwd=ROOT, capture_output=True, 65 text=True, timeout=seconds) 66 report['commands'].append(dict(argv=list(map(str, argv)), returncode=result.returncode, 67 stdout=result.stdout, stderr=result.stderr)) 68 (root / 'report.json').write_text(json.dumps(report, indent=2) + '\n') 69 if result.returncode: 70 raise RuntimeError(result.stdout[-4000:] + result.stderr[-4000:]) 71 return result.stdout 72 73 def run(options, seconds=180): 74 return command([host, 'arboricx', *options, '+RTS', '-N1', '-M4096m', '-RTS'], seconds) 75 76 active = affected | {dep for alias in affected for dep in entries[alias][1]} 77 for alias in sorted(active - affected): 78 cohort, _, _, _, source_pin = fixture_rows[alias] 79 fixture = ROOT / 'test/fixtures' / (cohort + '.arboricx') 80 bundle = root / (alias + '.arboricx') 81 merge_bundles(bundle, [(fixture, [(name, name[len(alias)+1:]) for name in read(fixture)[3] 82 if name.startswith(alias + '.')])], prune_unreachable=True) 83 pin = re.search(r'Created module alias .* -> ([0-9a-f]{64})', 84 run(['import', '--file', bundle, '--store', store]))[1] 85 (workspace / (alias + '.tri')).write_text('!export * from ' + pin + '\n') 86 report['reused'][alias] = dict(manifest=pin, source_pin=source_pin) 87 for alias in affected: 88 source = (ROOT / entries[alias][0]).read_bytes() 89 report[alias + '_source_sha256'] = hashlib.sha256(source).hexdigest() 90 (workspace / (alias + '.tri')).write_bytes(source) 91 probes = ['stableCachedFromPlain', 'stableCachedToPlain', 'stableCachedBracket'] 92 stable_path = workspace / 'source_stable.tri' 93 stable_path.write_text(''.join('!export ' + name + '\n' for name in probes) + stable_path.read_text()) 94 for alias in ['source_unit', 'source_stable']: 95 run(['compile', '--file', workspace / (alias + '.tri'), '--store', store, 96 '--output', root / (alias + '.arboricx'), '--compile-timeout', '280'], 300) 97 report['modules'] = audit(workspace, store, metadata['sha256'], report['reused']) 98 for alias, expected in [('source_stable', args.expected_stable), ('source_unit', args.expected_unit)]: 99 if expected: 100 intern = Intern() 101 def roots(path): 102 _, _, nodes, exports, _ = read(path) 103 ids = intern.graph(nodes) 104 return {name: ids[value] for name, value in exports.items()} 105 actual, before = roots(root / (alias + '.arboricx')), roots(expected) 106 selected = set(actual) - set(probes) 107 assert all(name in before and actual[name] == before[name] for name in selected), alias 108 report[alias + '_exact_public_exports'] = len(selected) 109 # GHC and all packages come from Nix. No cabal calls or ambient GHC. 110 expression = ('let f=builtins.getFlake "git+file://' + str(ROOT) + '";' 111 'p=f.inputs.nixpkgs.legacyPackages.x86_64-linux;' 112 'in p.haskellPackages.ghcWithPackages(h:[h.cryptonite h.memory h.base16-bytestring h.vector])') 113 command(['nix', 'shell', '--impure', '--expr', expression, '--command', 'ghc', '-O2', '-rtsopts', 114 '-XOverloadedStrings', '-XLambdaCase', '-XMultiWayIf', '-XScopedTypeVariables', 115 '-isrc', '-outputdir', root / 'objects', ROOT / 'test/StableConstructorK.hs', 116 '-o', root / 'controls'], 300) 117 command([root / 'controls', root / 'source_stable.arboricx', 118 ROOT / 'test/fixtures/compiler-library.arboricx', '+RTS', '-M2048m', '-RTS']) 119 command(['python3', ROOT / 'test/local_control.py', host, '--unit', root / 'source_unit.arboricx', 120 '--directory', root / 'local-controls'], 300) 121 report.update(complete=True, seconds=time.monotonic() - started) 122 (root / 'report.json').write_text(json.dumps(report, indent=2) + '\n') 123 print('PASS: constructor-K cached invariants, independent results/demand/diagnostics, local controls and provenance') 124 125 126 if __name__ == '__main__': 127 main()