tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

arboricx_server_inspection.py (6924B)


      1 #!/usr/bin/env python3
      2 """Haskell-only server alias experiment; compiles/inspects but never runs IO.
      3 Usage: python3 test/arboricx_server_inspection.py result/bin/tricu --directory /tmp/server-inspection
      4 The output directory must not exist. Keeps bundles, outlines and measurements.
      5 """
      6 import argparse
      7 import json
      8 import os
      9 from pathlib import Path
     10 import re
     11 import subprocess
     12 import time
     13 
     14 ROOT = Path(__file__).resolve().parent.parent
     15 DEMO = ROOT / 'demos/arboricx-inspection'
     16 
     17 
     18 def main():
     19     parser = argparse.ArgumentParser(description=__doc__)
     20     parser.add_argument('tricu', type=Path)
     21     parser.add_argument('--directory', type=Path, required=True)
     22     args = parser.parse_args()
     23     host = args.tricu.resolve()
     24     output = args.directory.resolve()
     25     output.mkdir(parents=True, exist_ok=False)
     26     store = output / 'compiler-store'
     27 
     28     def run(*arguments):
     29         p = subprocess.run([str(host), *map(str, arguments), '+RTS', '-N1', '-M8192m', '-RTS'],
     30                            cwd=output, capture_output=True, text=True, timeout=930)
     31         assert p.returncode == 0, (arguments, p.stdout, p.stderr)
     32         return p.stdout
     33 
     34     measurements = {}
     35 
     36     def compile_source(label, source, *extra):
     37         bundle = output / (label + '.arboricx')
     38         start = time.monotonic()
     39         run('arboricx', 'compile', '--file', source, '--store', store,
     40             '--output', bundle, '--compile-timeout', '900', *extra)
     41         measurements[label] = {'bytes': bundle.stat().st_size,
     42                                'compileSeconds': time.monotonic() - start}
     43         return bundle
     44 
     45     def inspect(label, bundle, name='main'):
     46         report = json.loads(run('arboricx', 'inspect', '--file', bundle, '--name', name, '--as', 'io', '--json'))
     47         (output / (label + '.json')).write_text(json.dumps(report, indent=2) + '\n')
     48         (output / (label + '.txt')).write_text(run('arboricx', 'inspect', '--file', bundle, '--name', name, '--as', 'io'))
     49         assert not report['occurrenceLimitReached']
     50         return report
     51 
     52     baseline = compile_source('baseline', ROOT / 'tricu-apps/arboricxServer.tri')
     53     plain = inspect('baseline', baseline)
     54     named = compile_source('named', DEMO / 'main.tri')
     55     report = inspect('named', named)
     56     selected = compile_source('named-main-only', DEMO / 'main.tri', '--name', 'main')
     57     assert selected.read_bytes() == baseline.read_bytes(), 'inspection exports changed the application bundle'
     58 
     59     # Independently compare the imported indexed main objects, so identity is
     60     # checked for the full named bundle too, not just compile's filtered output.
     61     def main_pin(bundle):
     62         cas = output / 'comparison-store'
     63         reply = run('arboricx', 'import', '--file', bundle, '--store', cas)
     64         pin = re.search(r'Created module alias .* -> ([0-9a-f]{64})', reply)[1]
     65         rows = (cas / 'objects' / pin[:3] / pin).read_text().splitlines()[1:]
     66         exports = {r.split('\t')[1]: r.split('\t')[3] for r in rows}
     67         return exports['main']
     68 
     69     assert main_pin(baseline) == main_pin(named), 'full bundle main tree changed'
     70     expected = ['main', 'startupAction', 'startupBody']
     71     assert len(report['occurrences']) == len(plain['occurrences']) == len(expected)
     72     for before, after, alias in zip(plain['occurrences'], report['occurrences'], expected):
     73         assert alias in after['aliases'], (alias, after)
     74         for key in ('path', 'role', 'summary', 'status'):
     75             assert before[key] == after[key], (key, before, after)
     76     assert [o['summary'] for o in report['occurrences']] == [
     77         'tricuIO', 'Scope', 'scope callback (not applied)']
     78     assert report['occurrences'][-1]['status'] == 'opaque'
     79     create = inspect('create-view', named, 'createView')
     80     assert [o['summary'] for o in create['occurrences']] == [
     81         'tricuIO', 'Facility directory@1/create',
     82         'arguments (not validated; backend availability not inferred)']
     83     assert 'createRootDirectory' in create['occurrences'][1]['aliases']
     84     assert 'storeRoot' in create['occurrences'][2]['aliases']
     85     assert create['occurrences'][2]['status'] == 'opaque'
     86     initialize = inspect('initialize-view', named, 'initializeView')
     87     assert any(o['summary'] == 'Facility directory@1/create' for o in initialize['occurrences'])
     88     assert all(o['status'] == 'opaque' for o in initialize['occurrences'] if o['role'] == 'resume')
     89     assert not (output / 'store').exists(), 'inspection executed store initialization'
     90 
     91     # Negative/measurement cohort: normal exports, with no structural projections.
     92     mappings = []
     93     for row in (DEMO / 'tricu.workspace').read_text().splitlines()[1:]:
     94         name, path = row.split('\t')
     95         mappings.append(name + '\t' + os.path.relpath((DEMO / path).resolve(), output))
     96     (output / 'tricu.workspace').write_text('tricu.workspace.v1\n' + '\n'.join(mappings) + '\n')
     97     candidates = {
     98         'storeRoot': '"./store"',
     99         'createRootDirectory': 'IO.createDirectory storeRoot',
    100         'initializeStore': 'H.runIO (Artifacts.prepareFilesystem storeRoot)',
    101         'rawPrepare': 'Artifacts.prepareFilesystem',
    102         'rawListen': 'IO.listenTCP',
    103         'rawScope': 'IO.scope',
    104         'rawPure': 'IO.pure',
    105     }
    106     source = output / 'ordinary.tri'
    107     source.write_text('!import arboricx_app as App\n!import handler_artifacts as Artifacts\n'
    108                       '!import handlers as H\n!import io as IO\n!export main\n'
    109                       + ''.join('!export ' + name + '\n' for name in candidates)
    110                       + 'main = App.main\n'
    111                       + ''.join(name + ' = ' + value + '\n' for name, value in candidates.items()))
    112     ordinary = compile_source('ordinary', source)
    113     observed = inspect('ordinary', ordinary)
    114     hits = {alias for o in observed['occurrences'] for alias in o['aliases']} - {'main'}
    115     assert not hits, ('scope callback must remain opaque', hits)
    116     assert main_pin(ordinary) == main_pin(baseline), 'ordinary aliases changed main'
    117     assert not (output / 'store').exists(), 'ordinary inspection ran IO'
    118     measurements['ordinary']['matchedCandidateExports'] = sorted(hits)
    119     measurements['ordinary']['unmatchedCandidateExports'] = sorted(candidates.keys() - hits)
    120     for label, view in [('baseline', plain), ('ordinary', observed), ('named', report)]:
    121         measurements[label]['storedNodes'] = view['storedNodes']
    122         measurements[label]['aliasedOccurrences'] = sum(bool(o['aliases']) for o in view['occurrences'])
    123         measurements[label]['occurrences'] = len(view['occurrences'])
    124     measurements['mainTreeUnchanged'] = True
    125     (output / 'measurements.json').write_text(json.dumps(measurements, indent=2) + '\n')
    126     print(json.dumps(measurements, indent=2))
    127     print('PASS: exact main identity, named v2 scope, opaque callbacks/continuations, separate initialization views; no application IO executed')
    128 
    129 
    130 if __name__ == '__main__':
    131     main()