tricu

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

effect_interfaces.py (10678B)


      1 #!/usr/bin/env python3
      2 """Opt-in source experiment, not a public effects API or compiler acceptance.
      3 
      4 nix build .#
      5 python3 test/effect_interfaces.py result/bin/tricu --directory /tmp/effect-comparison
      6 
      7 Creates a fresh isolated store. Complete output trees are checked against Python
      8 oracles, not against the other implementation. Retain samples.json and report.json
      9 outside docs/. Benchmark scope includes interpretation and trace rendering; it is
     10 not a measurement of handler dispatch alone. C uses explicit scope nodes and a
     11 linear routing environment; D fuses routing and execution without changing lookup.
     12 Application functions and physical adapters are shared. Every invocation is fresh.
     13 """
     14 import argparse
     15 import hashlib
     16 import json
     17 import os
     18 from pathlib import Path
     19 import statistics
     20 import re
     21 
     22 from http_headers import number, pair, result, tree_list
     23 from source_order import Runner, end, field
     24 
     25 ROOT = Path(__file__).resolve().parent.parent
     26 
     27 
     28 def tree(value):
     29     if isinstance(value, bytes):
     30         return value
     31     if isinstance(value, int):
     32         return number(value)
     33     return tree_list([tree(v) for v in value])
     34 
     35 
     36 def ok(value):
     37     return result(True, tree(value))
     38 
     39 
     40 def error(value):
     41     return result(False, tree(value))
     42 
     43 
     44 def publication(store=11, artifact=7):
     45     return [([store, 1, artifact], ok(0)), ([store, 2, artifact], ok(1)),
     46             ([20, 3, 0], ok(42)), ([30, 4, [artifact, 42]], ok(1))]
     47 
     48 
     49 def main():
     50     parser = argparse.ArgumentParser(description=__doc__)
     51     parser.add_argument('tricu', type=Path)
     52     parser.add_argument('--directory', type=Path, required=True)
     53     parser.add_argument('--benchmark', action='store_true')
     54     args = parser.parse_args()
     55     if os.geteuid() == 0:
     56         parser.error("OS permission-denial controls require a non-root user")
     57     directory = args.directory.resolve()
     58     directory.mkdir(parents=True, exist_ok=False)
     59     runner = Runner(args.tricu, directory)
     60     source = ROOT / 'test/effect_interfaces/workflow.tri'
     61     bundle = directory / 'workflow.arboricx'
     62     runner.run('compile', ['compile', '--file', source, '--store', runner.store,
     63                            '--output', bundle])
     64     pin = runner.imported(bundle, 'import')
     65     passed = []
     66 
     67     def check(label, export, script, expected, *, remaining=(), trace=None, prefix=()):
     68         script = [list(row) for row in script]
     69         if trace is None:
     70             trace = [event for event, reply in script]
     71         arguments = []
     72         for index, value in enumerate([*prefix, script]):
     73             path = directory / f'{label}-{index}.tree'
     74             path.write_bytes(tree(value))
     75             arguments += ['--arg-tree-file', path]
     76         actual = runner.invoke(label, pin, export, arguments)
     77         wanted = tree([expected, list(remaining), trace])
     78         if actual != wanted:
     79             (directory / f'{label}-expected.tree').write_bytes(wanted)
     80             raise AssertionError(f'{label}: complete output differs (see .tree files)')
     81         passed.append(label)
     82 
     83     for variant in 'abcd':
     84         check(variant+'-publish', variant+'Publish', publication(), ok(1))
     85         check(variant+'-exists', variant+'Publish', [([11, 1, 7], ok(1))], ok(0))
     86         check(variant+'-logged', variant+'Logged', [
     87             ([30, 4, [1, 7]], ok(1)), *publication()[:1],
     88             ([30, 4, [2, 7]], ok(1)), *publication()[1:]], ok(1))
     89         check(variant+'-copy', variant+'Copy', [([11, 5, 7], ok(7)),
     90                                                ([12, 2, 7], ok(1))], ok(1))
     91         check(variant+'-override', variant+'Override', publication(12), ok(1))
     92         check(variant+'-pinned', variant+'Pinned', publication(11), ok(1))
     93         check(variant+'-second-write-fails', variant+'Twice', [*publication(),
     94               ([11, 1, 8], ok(0)), ([11, 2, 8], error([80, 11, 2]))], error([80, 11, 2]))
     95         # Shape-malformed and domain-invalid responses: diagnostic deliberately
     96         # names physical service identity for A, logical identity for B.
     97         for label, reply in [('shape', number(42)), ('domain', ok(9))]:
     98             check(variant+'-bad-'+label, variant+'Publish', [([11, 1, 7], reply)],
     99                   error([90, 11 if variant == 'a' else 1, 1]))
    100         # An unconsumed sentinel ensures failure really stops subsequent effects.
    101         script = [([11, 1, 7], error([80, 11, 1])), ([11, 2, 7], ok(1))]
    102         check(variant+'-read-fails', variant+'Publish', script, error([80, 11, 1]),
    103               remaining=[list(script[1])], trace=[[11, 1, 7]])
    104     check('a-missing', 'aMissing', [], error([91, 1, 1]))
    105     check('a-malformed-bundle', 'aMalformedBundle', [], error([91, 1, 1]))
    106     check('b-missing', 'bMissing', [], error([95, [1, 1, 7]]), trace=[])
    107     check('c-missing', 'cMissing', [], error([95, [1, 1, 7]]), trace=[])
    108     check('d-missing', 'dMissing', [], error([95, [1, 1, 7]]), trace=[])
    109     # Negative controls: no implementation may ignore exhaustion or mismatches.
    110     for variant in 'abcd':
    111         check(variant+'-exhausted-script', variant+'Publish', [],
    112               error([93, [11, 1, 7]]), trace=[[11, 1, 7]])
    113         wrong = [([12, 1, 7], ok(0))]
    114         check(variant+'-wrong-script', variant+'Publish', wrong,
    115               error([92, [11, 1, 7], [12, 1, 7]]),
    116               remaining=[list(wrong[0])], trace=[[11, 1, 7]])
    117 
    118     for variant in 'bcd':
    119         check(variant+'-escaped', variant+'Escaped', [([11, 1, 7], ok(0))], ok(0))
    120         check(variant+'-restore', variant+'Restore',
    121               [([12, 1, 7], ok(0)), ([11, 1, 8], ok(1))], ok(1))
    122         check(variant+'-emission', variant+'Emission', [([11, 1, 7], ok(0))], ok(0))
    123         check(variant+'-chain', variant+'Chain', [([12, 1, 7], ok(0))], ok(0))
    124         check(variant+'-no-reentry', variant+'NoReentry', [([11, 1, 7], ok(0))], ok(0))
    125         check(variant+'-outer-log', variant+'OuterLog', publication(), ok(1))
    126         check(variant+'-forward', variant+'Forward',
    127               [([11, 1, 7], ok(0)), ([12, 1, 8], ok(1))], ok(1))
    128         sentinel = [([11, 1, 7], ok(0))]
    129         script = [([30, 4, [1, 7]], error([80, 30, 4])), *sentinel]
    130         check(variant+'-handler-failure', variant+'Logged', script, error([80, 30, 4]),
    131               remaining=[list(row) for row in sentinel], trace=[[30, 4, [1, 7]]])
    132 
    133     # Real filesystem with a fixed clock; same adapter for both implementations.
    134     for variant in 'abcd':
    135         fs = directory / ('filesystem-' + variant)
    136         fs.mkdir()
    137         artifact, log = fs / 'artifact', fs / 'log'
    138         def filesystem(label):
    139             output = runner.run(label, ['run', '--module', pin, '--name', variant+'Filesystem',
    140                 '--store', runner.store, '--io', '--format', 'ternary',
    141                 '--arg', str(artifact), '--arg', str(log)])
    142             text = next(re.match(r'^([012]+)', line)[1] for line in output.splitlines()
    143                         if re.match(r'^[012]+', line))
    144             raw = bytes(map(int, text))
    145             return raw[:end(raw)]
    146         # OS traversal denial replaces the retired source-path grant policy.
    147         fs.chmod(0)
    148         try:
    149             denied = filesystem(variant+'-filesystem-denied')
    150         finally:
    151             fs.chmod(0o700)
    152         assert field(denied, 0) == b'\0', 'permission denial must remain a failure'
    153         assert not artifact.exists() and not log.exists()
    154         assert filesystem(variant+'-filesystem-new') == ok(1)
    155         assert artifact.read_bytes() == b'\x07'
    156         assert log.read_text() == 'published 7 at 42\n'
    157         log.write_text('sentinel: existing artifacts must not log again\n')
    158         assert filesystem(variant+'-filesystem-existing') == ok(0)
    159         assert artifact.read_bytes() == b'\x07'
    160         assert log.read_text() == 'sentinel: existing artifacts must not log again\n'
    161         passed.extend([variant+'-filesystem-denied', variant+'-filesystem-new',
    162                        variant+'-filesystem-existing'])
    163 
    164     benchmarks = []
    165     if args.benchmark:
    166         for workload, export in [('clock', 'Bench'), ('publication', 'PublishBench')]:
    167             for count in (10, 100):
    168                 items = [0]*count if workload == 'clock' else list(range(7, 7+count))
    169                 script = ([([20, 3, 0], ok(42))]*count if workload == 'clock' else
    170                           [row for artifact in items for row in publication(artifact=artifact)])
    171                 for depth in (0, 4, 16):
    172                     rows = {variant: [] for variant in 'acd'}
    173                     # Rotate order across repeats to reduce systematic timing bias.
    174                     for repeat, order in enumerate(('acd', 'cda', 'dac')):
    175                         for variant in order:
    176                             label = f'bench-{workload}-{variant}-{count}-{depth}-{repeat}'
    177                             check(label, variant+export, script, ok(0),
    178                                   prefix=(items, list(range(1000, 1000+depth))))
    179                             rows[variant].append(runner.records[-1])
    180                     for variant, samples in rows.items():
    181                         benchmarks.append(dict(workload=workload, variant=variant,
    182                             count=count, depth=depth,
    183                             seconds=statistics.median(r['invoke_seconds'] for r in samples),
    184                             allocated_bytes=statistics.median(r['allocated_bytes'] for r in samples)))
    185     report = dict(passed=passed, benchmarks=benchmarks,
    186                   source_sha256=hashlib.sha256(source.read_bytes()).hexdigest(),
    187                   bundle_sha256=hashlib.sha256(bundle.read_bytes()).hexdigest(),
    188                   host=str(args.tricu.resolve()),
    189                   inputs={str(path.relative_to(ROOT)): hashlib.sha256(path.read_bytes()).hexdigest()
    190                           for path in [Path(__file__).resolve(), ROOT / 'test/effect_interfaces/tricu.workspace',
    191                                        ROOT / 'lib/base.tri',
    192                                        ROOT / 'lib/io.tri', ROOT / 'bootstrap/build-stable.arboricx']},
    193                   limits=['finite scripted clock/read codecs; filesystem smoke uses fixed clock',
    194                           'no concurrency or resource scopes in the experimental effect protocol; physical files use IO scopes',
    195                           'benchmark includes trace construction and rendering',
    196                           'A depth is a control: captured services bypass unrelated scopes',
    197                           'C/D use the same linear environment lookup; B retained only for semantic checks'])
    198     (directory / 'report.json').write_text(json.dumps(report, indent=2)+'\n')
    199     print(json.dumps(report, indent=2))
    200 
    201 
    202 if __name__ == '__main__':
    203     main()