tricu

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

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