staging_experiment.py (8464B)
1 #!/usr/bin/env python3 2 """Opt-in runtime IR staging experiment; no canonical compiler/source changes.""" 3 import argparse 4 import hashlib 5 import json 6 from pathlib import Path 7 import re 8 import struct 9 import subprocess 10 import tempfile 11 import time 12 13 ROOT = Path(__file__).resolve().parent.parent 14 parser = argparse.ArgumentParser(description=__doc__) 15 parser.add_argument('tricu', type=Path) 16 parser.add_argument('--prepared', type=Path, action='append', required=True) 17 parser.add_argument('--compiler-bundle', type=Path, required=True) 18 parser.add_argument('--skip-measurements', action='store_true') 19 parser.add_argument('--directory', type=Path) 20 args = parser.parse_args() 21 tricu = str(args.tricu.resolve()) 22 23 24 from research_session import ResearchSession 25 26 def run(command, seconds=90): 27 return session.run(command, seconds) 28 29 30 def stats(path): 31 data = path.read_bytes() 32 start = next(struct.unpack_from('>Q', data, offset + 12)[0] 33 for offset in range(32, 96, 32) 34 if struct.unpack_from('>I', data, offset)[0] == 2) 35 nodes = struct.unpack_from('>Q', data, start)[0] 36 return {'nodes': nodes, 'bytes': len(data)} 37 38 39 manager = tempfile.TemporaryDirectory(prefix='.staging-', dir=ROOT / 'test') if not args.directory else None 40 try: 41 root = Path(manager.name if manager else args.directory).resolve() 42 assert root.is_relative_to(ROOT), 'retained directory must be inside checkout' 43 root.mkdir(parents=True, exist_ok=True) 44 session = ResearchSession(tricu, root, args.prepared, args.compiler_bundle) 45 store = session.store 46 47 def compile(source, names, output, seconds=1200): 48 return session.compile(source, names, output, seconds) 49 50 def imported(bundle, indexed=True): 51 reply, _ = run([tricu, 'arboricx', 'import', '--file', str(bundle), '--store', str(store), 52 *(['--indexed'] if indexed else ['--flat'])]) 53 pin = re.search(rb'Created module alias .* -> ([0-9a-f]{64})', reply)[1].decode() 54 return pin 55 56 def host(bundle, name, fixture=None, seconds=90): 57 pin = imported(bundle) 58 command = [tricu, 'arboricx', 'run', '--module', pin, '--name', name, 59 '--store', str(store), '--format', 'tree'] 60 if fixture: 61 fixture_pin = imported(fixture) 62 manifest = (store / 'objects' / fixture_pin[:3] / fixture_pin).read_text() 63 name = manifest.splitlines()[1].split('\t')[1] 64 command += ['--arg-export', fixture_pin + ':' + name] 65 return run(command, seconds) 66 67 lower = root / 'lower.arboricx' 68 elapsed = compile(ROOT / 'lib/source/staging-experiment.tri', ['stageBody', 'stageList', 'stageBranch', 'stageRecur'], lower) 69 lower_pin = imported(lower) 70 # Select from the authenticated prepared fixture module. Never copy it 71 # to an unrecognized path and recompile it once per selected export. 72 source = ROOT / 'test/stagingExperiment.tri' 73 runner = root / 'runner.arboricx' 74 compile(source, ['matchesFixture'], runner) 75 names = re.findall(r'^(fixture\w+) =', source.read_text(), re.M) 76 divergent_names = [name for name in names if name.endswith('Diverge')] 77 successful = [name for name in names if name != 'fixtureWrong' and name not in divergent_names] 78 for name in successful + ['fixtureWrong']: 79 fixture = root / f'{name}.arboricx' 80 compile(source, [name], fixture) 81 h, _ = host(runner, 'matchesFixture', fixture) 82 expected = name != 'fixtureWrong' 83 assert h == (b't t' if expected else b't'), (name, h) 84 print(f'{name}: Haskell passes', flush=True) 85 divergence = {} 86 for name in divergent_names: 87 divergent = root / f'{name}.arboricx' 88 compile(source, [name], divergent) 89 try: 90 host(runner, 'matchesFixture', divergent, seconds=2) 91 raise AssertionError('selected divergent work returned') 92 except subprocess.TimeoutExpired: 93 pass 94 divergence[name] = {'Haskell': 'bounded timeout'} 95 print(f'{name}: bounded Haskell timeout', flush=True) 96 97 # Negative construction control: an ordinary unused thunk binder does 98 # not protect a closed divergent operand under an unknown condition. 99 unsafe = root / 'unsafe.tri' 100 unsafe.write_text('!import "prelude" !Local\n' 101 'spin self input = triage 7 (_ : 7) (left right : self input) input\n' 102 'loop input = y spin input\n' 103 'unsafe condition = lazyBool (_ : loop [1]) (_ : 23) condition\n') 104 try: 105 compile(unsafe, ['unsafe'], root / 'unsafe.arboricx', seconds=2) 106 raise AssertionError('plain unused thunk unexpectedly protected closed work') 107 except subprocess.TimeoutExpired: 108 negative_control = 'portable authoring budget stop; phase not isolated' 109 110 artifacts = {} 111 for name in ['compiledAccept', 'acceptReference', 'compiledSlice', 'sliceReference', 'compiledKnown', 'compiledLazy', 'compiledClosed']: 112 bundle = root / f'{name}.arboricx' 113 elapsed = compile(source, [name], bundle) 114 artifacts[name] = {**stats(bundle), 'preparation_seconds': elapsed, 'preparation': 'selected prepared exports packed'} 115 tree_equal = {} 116 for name in ['acceptTreeEqual', 'sliceTreeEqual']: 117 bundle = root / f'{name}.arboricx' 118 compile(source, [name], bundle) 119 h, _ = host(bundle, name) 120 tree_equal[name] = h == b't t' 121 print(f'{name}: Haskell={h.decode()}', flush=True) 122 # Direct compiled-code measurements: no IR lowering in the timed process. 123 # Fresh processes prevent cross-sample reuse; startup, verification, argument 124 # loading, application, equality and Boolean rendering are all included. 125 compare_source = root / 'compare.tri' 126 compare_source.write_text('!import "prelude" !Local\n' 127 'compareApplied function fixture = equal? ' 128 '(foldl (f argument : f argument) function (fst fixture)) (snd fixture)\n') 129 compare = root / 'compare.arboricx' 130 compile(compare_source, ['compareApplied'], compare) 131 compare_pin = imported(compare) 132 def argument_pin(bundle): 133 pin = imported(bundle) 134 name = (store / 'objects' / pin[:3] / pin).read_text().splitlines()[1].split('\t')[1] 135 return pin + ':' + name 136 samples = {} 137 for size in ([] if args.skip_measurements else [256, 1024]): 138 text = '0123456789abcdef' * (size // 16) 139 input_source = root / 'input.tri' 140 input_source.write_text(f'input = t [{json.dumps(text)} (t 32 96)] {json.dumps(text[32:96])}\n') 141 input_bundle = root / 'input.arboricx' 142 compile(input_source, ['input'], input_bundle) 143 input_pin = argument_pin(input_bundle) 144 for name in ['compiledSlice', 'sliceReference']: 145 bundle = root / f'{name}.arboricx' 146 tree_pin = argument_pin(bundle) 147 observations = {'Haskell': []} 148 for _ in range(3): 149 h, elapsed = run([tricu, 'arboricx', 'run', '--module', compare_pin, 150 '--name', 'compareApplied', '--store', str(store), '--format', 'tree', 151 '--arg-export', tree_pin, '--arg-export', input_pin]) 152 assert h == b't t', (name, size, h) 153 observations['Haskell'].append(elapsed) 154 samples[f'{name}/{size}'] = observations 155 report = {'lowerer': stats(lower), 'artifacts': artifacts, 156 'tree_equal_to_reference': tree_equal, 157 'source_sha256': {name: hashlib.sha256((ROOT / name).read_bytes()).hexdigest() 158 for name in ['lib/source/staging-experiment.tri', 'test/stagingExperiment.tri', 159 'test/staging_experiment.py']}, 160 'executables': {'Haskell': tricu}, 161 'limits': {'direct_child_rss_gib': 64, 'job_seconds_max': 1200, 'harness_seconds_max': 1200}, 162 'lowerer_preparation': 'verified completed portable artifacts; selected exports repacked', 163 'runtime_seconds_including_startup': samples, 164 'plain_thunk_negative_control': negative_control, 165 'runtime_cases': len(successful), 'wrong_output_control': True, 166 'selected_divergence': divergence, 167 'scope': 'trusted explicit IR, no canonical compiler generation'} 168 (root / 'measurements.json').write_text(json.dumps(report, indent=2) + '\n') 169 print(json.dumps(report, indent=2), flush=True) 170 finally: 171 if manager: 172 manager.cleanup()