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