tricu

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

collections_batch.py (29681B)


      1 #!/usr/bin/env python3
      2 """Opt-in current-source collection/contract/IO checks; no fixture publication.
      3 
      4 Prepare an explicit native workspace against a selected accepted stable compiler.
      5 All library dependencies in this workspace are current source, not old fixture
      6 facades. Complete expected trees are authored independently in Python. Historical
      7 fixtures are only negative controls for intentional corrections.
      8 """
      9 import argparse
     10 import hashlib
     11 import json
     12 from pathlib import Path
     13 import re
     14 import statistics
     15 import time
     16 
     17 from base_workloads import encode
     18 from bundle_dag import merge_bundles, read
     19 from fixture_sources import verify
     20 from http_headers import LEAF, TRUE, pair, result, byte_tree, tree_list, expect
     21 from http_pipeline import action, script, finished
     22 from source_order import Runner
     23 from native_header import native_header
     24 from native_library_migration import digest
     25 from repair_support_fixture import checked_receipt
     26 from executable_object import KINDS, verify_payload
     27 
     28 ROOT = Path(__file__).resolve().parent.parent
     29 REFERENCE_FIXTURES = ROOT/'test/fixtures'
     30 SOURCES = {name: 'lib/' + name + '.tri' for name in
     31            ['base', 'bytes', 'conversions', 'prelude', 'patterns', 'io', 'parser', 'socket', 'http']}
     32 SOURCES.update(intensional='lib/intensionalContracts.tri', recursion='lib/recursion/list.tri',
     33                native_math='lib/math/native.tri', tests_httpEffects='test/httpEffects.tri',
     34                tests_registryEffects='test/registryEffects.tri',
     35                demo_patterns='demos/patternMatching.tri', demo_contracts='demos/contractEffects.tri')
     36 DRIVER = '''!import prelude as B
     37 !import patterns as P
     38 !import intensional as C
     39 !import io as I
     40 !import recursion as R
     41 !import bytes as Bytes
     42 !import tests_registryEffects as E
     43 !import socket as S
     44 !import http as H
     45 !import demo_patterns as DP
     46 !import demo_contracts as DC
     47 !export *
     48 spin x = spin x
     49 demoPattern x = DP.matchExample x
     50 demoEffects x = B.runM (B.handleM "missing" (_ _ : B.pureM 2) (DC.pipeline x))
     51 appendProbe x = B.append x [9]
     52 reverseProbe x = B.reverse x
     53 joinProbe x = B.intercalate [44] x
     54 terminateProbe x = B.joinSuffix [10] x
     55 memberProbe x = B.lExist? 2 x
     56 foldProbe x = B.foldr (acc item : B.pair item acc) [] x
     57 lengthProbe x = B.length x
     58 snocProbe x = B.snoc 9 x
     59 countProbe x = B.count 2 x
     60 lastProbe x = B.last x
     61 allProbe x = B.all? (B.equal? 2) x
     62 anyProbe x = B.any? (B.equal? 2) x
     63 nthProbe x = B.nth 1 x
     64 lastMaybeProbe x = B.lastMaybe x
     65 nthMaybeProbe x = B.nthMaybe 1 x
     66 takeProbe x = B.take 2 x
     67 dropProbe x = B.drop 2 x
     68 concatProbe x = B.concatMap (item : [item item]) x
     69 findProbe x = B.find (B.equal? 2) x
     70 partitionProbe x = B.partition (B.equal? 2) x
     71 prefixProbe x = B.startsWith? [1 2] x
     72 containsProbe x = B.contains? [2 3] x
     73 zipProbe x = B.zipWith B.pair x [7 8]
     74 bytesProbe x = [(Bytes.bytesHead x) (Bytes.bytesTail x)]
     75 listContract x = B.listOf B.anyC x 7
     76 nonemptyContract x = B.nonEmptyListOf B.anyC x 7
     77 pairContract x = B.pairOf B.anyC B.anyC x 7
     78 natContract x = B.nat? x 7
     79 nonzeroContract x = B.nonZero? x 7
     80 nonemptyTree x = C.nonEmptyTree? x 7
     81 fullTree x = C.fullTree? x 7
     82 powerTwo x = C.powerOfTwo? x 7
     83 evenContract x = C.evenC? x 7
     84 oddContract x = C.oddC? x 7
     85 asciiContract x = C.asciiString? x 7
     86 sortedContract x = C.sortedList? B.anyC x 7
     87 transformC value rest = B.ok (B.succ value) 99
     88 transformedList x = B.listOf transformC x 7
     89 listProxy x = (B.bindResult (B.listOf (B.fnContract B.nat? B.nat?) [B.id] 8)
     90   (functions _ : B.head functions x 7))
     91 listProxyResult x = (B.bindResult (B.listOf (B.fnContract B.anyC (B.neverC "result")) [B.id] 8)
     92   (functions _ : B.head functions x 7))
     93 listFailedSuffix x = B.listOf (item rest : B.ok (spin x) rest) (B.pair x (t t)) 7
     94 listIgnoredChecked x = B.fst (B.listOf (item rest : B.ok (spin x) rest) [x] 7)
     95 sortedTransformed x = C.sortedList? (B.mapC (_ : 0) B.anyC) x 7
     96 transformedPair x = B.pairOf transformC transformC x 7
     97 andContract x = B.andC transformC transformC x 7
     98 orContract x = B.orC (B.neverC "first") transformC x 7
     99 notContract x = B.notC (B.neverC "first") x 7
    100 mapContract x = B.mapC B.succ transformC x 7
    101 bindContract x = B.bindC transformC (checked original rest : B.ok (B.pair checked original) rest) x 7
    102 proxy x = (B.bindResult (B.fnContract B.nat? B.nat? B.succ 8)
    103   (function _ : function x 7))
    104 proxyTwo x = (B.bindResult (B.fn2 B.nat? B.nat? B.nat? B.add 8)
    105   (first _ : B.bindResult (first x 6) (second _ : second 3 7)))
    106 proxySecondBad x = (B.bindResult (B.fn2 B.nat? B.nat? B.anyC (spin x) 8)
    107   (first _ : B.bindResult (first 2 6) (second _ : second (t t) 7)))
    108 proxyTransforms x = (B.bindResult (B.fnContract transformC transformC B.id 8)
    109   (function _ : function x 7))
    110 proxyBadResult x = (B.bindResult (B.fnContract B.anyC (B.neverC "result") B.id 8)
    111   (function _ : function x 7))
    112 proxyRejected x = (B.bindResult (B.fnContract (B.neverC "argument") B.anyC (spin x) 8)
    113   (function _ : function (spin x) 7))
    114 matchProbe x = P.match x [[(B.equal? 2) B.succ] [P.otherwise B.id]]
    115 matchMalformed x = P.match x [[P.otherwise B.id B.succ]]
    116 matchLazy x = P.match x (B.pair [(B.const (B.pair t t)) B.id] (spin x))
    117 condProbe x = P.cond [(P.guard (_ : t) (_ : spin x)) (P.guard (_ : B.pair t t) (_ : x))]
    118 condLazy x = P.cond (B.pair (P.guard (_ : B.true) (_ : x)) (spin x))
    119 findLazy x = B.find (_ : B.true) (B.pair x (spin x))
    120 allLazy x = B.all? (_ : t) (B.pair (spin x) (spin x))
    121 anyLazy x = B.any? (_ : B.true) (B.pair (spin x) (spin x))
    122 memberLazy x = B.lExist? x (B.pair x (spin x))
    123 orLazy x = B.or? (B.pair t t) (spin x)
    124 takeZeroLazy x = B.take 0 (spin x)
    125 takePrefixLazy x = B.take 1 (B.pair x (spin x))
    126 containsEmptyLazy x = B.contains? [] (spin x)
    127 foldLazy x = B.foldr (_ item : item) t (B.pair x (spin x))
    128 contractLazy x = B.listOf (B.neverC "first") (B.pair (spin x) (spin x)) 7
    129 pairLazy x = B.pairOf (B.neverC "first") (spin x) (B.pair (spin x) (spin x)) 7
    130 emptyContractLazy x = B.listOf (spin x) [] 7
    131 invalidListLazy x = B.listOf (spin x) (t t) 7
    132 fullLazy x = C.fullTree? (t (t t) (spin x)) 7
    133 foldUntil x = R.listFoldUntil x [] (item acc : R.listContinue (B.pair item acc))
    134 stopUntil x = R.listFoldUntil x 0 (item acc : R.listStop 42)
    135 malformedDecision x = R.listFoldUntil x 7 (_ _ : B.pair (B.pair t t) (spin x))
    136 stopLazy x = R.listFoldUntil (B.pair x (spin x)) 0 (_ _ : R.listStop 42)
    137 -- Inspect only the state; serializing the retained divergent suffix would force it.
    138 stopLazyState x = B.fst (stopLazy x)
    139 ordinaryFinally script = E.call I.finally [(I.putStr "use") (I.putStr "cleanup")] script
    140 acquired script = E.call I.bracket [(I.readFile "acquire") (resource : I.putStr resource) (resource : I.writeFile "use" resource)] script
    141 onOkBoth script = E.call I.onOk [(I.readFile "x") (value rest : I.pure (B.pair value rest))] script
    142 onOkValue script = E.call I.onOk_ [(I.readFile "x") I.pure] script
    143 onResultValue script = E.call I.onResult_ [(I.readFile "x") I.pure I.pure] script
    144 abortFinally script = E.call I.finally [(I.abortIO "stop") (I.putStr "cleanup")] script
    145 abortBracket script = E.call I.bracket [(I.readFile "acquire") (resource : I.putStr resource) (_ : I.abortIO "stop")] script
    146 foreverAbort script = E.call I.forever [(I.abortIO "stop")] script
    147 socketConnect script = E.call S.connectTo ["127.0.0.1" 9050] script
    148 httpResponse body = H.okResponse body
    149 httpRespond script = E.call H.respondAndClose [7 "reply"] script
    150 '''
    151 
    152 
    153 def cases():
    154     n = encode
    155     ok = lambda value: result(True, value, n(7))
    156     bad = lambda message: result(False, byte_tree(message.encode()), n(7))
    157     rows = []
    158     def add(name, value, expected, label=None):
    159         rows.append((label or name + '-' + str(len(rows)), name, value, expected))
    160     for xs in [[], [1], [1, 2, 3], [2, 2, 2], [1, 3]]:
    161         data = n(xs)
    162         for name, value in [
    163             ('appendProbe', n(xs + [9])), ('foldProbe', data), ('lengthProbe', n(len(xs))),
    164             ('snocProbe', n(xs + [9])), ('countProbe', n(xs.count(2))),
    165             ('memberProbe', TRUE if 2 in xs else LEAF),
    166             ('allProbe', TRUE if all(x == 2 for x in xs) else LEAF),
    167             ('anyProbe', TRUE if 2 in xs else LEAF),
    168             ('nthProbe', n(xs[1]) if len(xs) > 1 else LEAF),
    169             ('lastMaybeProbe', b'\1' + n(xs[-1]) if xs else LEAF),
    170             ('nthMaybeProbe', b'\1' + n(xs[1]) if len(xs) > 1 else LEAF),
    171             ('takeProbe', n(xs[:2])), ('dropProbe', n(xs[2:])),
    172             ('concatProbe', n([x for x in xs for _ in range(2)])),
    173             ('findProbe', b'\1' + n(2) if 2 in xs else LEAF),
    174             ('partitionProbe', pair(n([x for x in xs if x == 2]), n([x for x in xs if x != 2]))),
    175             ('prefixProbe', TRUE if xs[:2] == [1, 2] else LEAF),
    176             ('containsProbe', TRUE if any(xs[i:i+2] == [2, 3] for i in range(len(xs))) else LEAF),
    177             ('zipProbe', tree_list([pair(n(a), n(b)) for a, b in zip(xs, [7, 8])])),
    178             ('listContract', ok(data)),
    179             ('nonemptyContract', ok(data) if xs else bad('empty list')),
    180             ('foldUntil', pair(n(xs[::-1]), LEAF)),
    181             ('stopUntil', pair(n(42 if xs else 0), data)),
    182             ('malformedDecision', pair(n(7), data)),
    183         ]:
    184             add(name, data, value)
    185         if xs:
    186             add('lastProbe', data, n(xs[-1]))
    187         add('bytesProbe', data, tree_list([b'\1' + n(xs[0]), b'\1' + n(xs[1:])]) if xs else n([[], []]))
    188     for chunks in [[], [[]], [[1], [2]], [[1], [], [2]], [[1, 2]]]:
    189         joined = [value for i, chunk in enumerate(chunks) for value in ([44] if i else []) + chunk]
    190         add('joinProbe', n(chunks), n(joined))
    191         add('terminateProbe', n(chunks), n([value for chunk in chunks for value in chunk+[10]]))
    192     stem = b'\1\0'
    193     malformed = pair(n(1), stem)
    194     for value in [stem, malformed]:
    195         add('listContract', value, bad('not a list'))
    196         add('nonemptyContract', value, bad('not a list'))
    197     for value in [LEAF, stem]:
    198         add('pairContract', value, bad('not a pair'))
    199     add('pairContract', pair(n(2), n(3)), ok(pair(n(2), n(3))))
    200     add('foldUntil', malformed, pair(n([1]), stem))
    201     for value in [0, 1, 2, 3, 4, 8, 16, 17, 127, 128]:
    202         add('natContract', n(value), ok(n(value)))
    203         add('powerTwo', n(value), ok(n(value)) if value > 0 and value & (value-1) == 0 else bad('not a power of two'))
    204         add('evenContract', n(value), ok(n(value)) if value % 2 == 0 else bad('not even'))
    205         add('oddContract', n(value), ok(n(value)) if value % 2 else bad('not odd'))
    206     for value in [stem, pair(b'\1\1\0', LEAF), pair(pair(LEAF, LEAF), LEAF)]:
    207         for name in ['natContract', 'powerTwo', 'evenContract', 'oddContract']:
    208             add(name, value, bad('not a natural number'))
    209     # Redundant high zeros disagree with zero tests/arithmetic: reject, don't normalize.
    210     for redundant in [pair(LEAF, LEAF), pair(TRUE, pair(LEAF, LEAF)), pair(LEAF, pair(LEAF, LEAF))]:
    211         add('natContract', redundant, bad('not a natural number'))
    212         add('powerTwo', redundant, bad('not a natural number'))
    213         add('nonzeroContract', redundant, bad('non-zero'))
    214     add('nonzeroContract', LEAF, bad('non-zero'))
    215     add('nonzeroContract', n(1), ok(n(1)))
    216     for value in [stem, pair(stem, LEAF), pair(LEAF, LEAF)]:
    217         add('nonemptyTree', value, ok(value))
    218     add('nonemptyTree', LEAF, bad('empty tree'))
    219     add('fullTree', LEAF, ok(LEAF))
    220     add('fullTree', pair(LEAF, LEAF), ok(pair(LEAF, LEAF)))
    221     add('fullTree', stem, bad('not a full binary tree'))
    222     for xs, expected in [([], ok(n([]))), ([0, 65, 127], ok(n([0, 65, 127]))), ([128], bad('non-ascii byte'))]:
    223         add('asciiContract', n(xs), expected)
    224     add('asciiContract', tree_list([stem]), bad('not a natural number'))
    225     for xs in [[], [1], [1, 2, 2, 3], [2, 1]]:
    226         add('sortedContract', n(xs), ok(n(xs)) if xs == sorted(xs) else bad('not sorted'))
    227     add('sortedContract', tree_list([stem]), bad('not sorted'))
    228     add('transformedList', n([1, 2]), ok(n([2, 3])))
    229     add('listProxy', b'\1\0', bad('not a natural number'))
    230     add('listProxy', n(2), ok(n(2)))
    231     add('listProxyResult', n(2), bad('result'))
    232     add('listFailedSuffix', n(2), bad('not a list'))
    233     add('listIgnoredChecked', n(2), TRUE)
    234     add('sortedTransformed', n([2, 1]), ok(n([0, 0])))
    235     add('transformedPair', pair(n(1), n(2)), ok(pair(n(2), n(3))))
    236     for name, expected in [('andContract', result(True, n(4), n(99))), ('orContract', result(True, n(3), n(99))),
    237                            ('notContract', ok(n(2))), ('mapContract', ok(n(4))),
    238                            ('bindContract', ok(pair(n(3), n(2)))), ('proxy', ok(n(3))),
    239                            ('proxyTwo', ok(n(5))), ('proxySecondBad', bad('not a natural number')),
    240                            ('proxyTransforms', ok(n(4))), ('proxyBadResult', bad('result')),
    241                            ('proxyRejected', bad('argument'))]:
    242         add(name, n(2), expected)
    243     add('proxy', stem, bad('not a natural number'))
    244     # First-stage failure belongs to the first call's remainder, not the second.
    245     add('proxyTwo', stem, result(False, byte_tree(b'not a natural number'), n(6)))
    246     for value in [1, 2, 3]:
    247         add('matchProbe', n(value), n(3 if value == 2 else value))
    248     add('matchMalformed', n(2), LEAF)
    249     for name, expected in [('matchLazy', n(42)), ('condProbe', n(42)), ('condLazy', n(42)),
    250                            ('findLazy', b'\1'+n(42)), ('allLazy', LEAF), ('anyLazy', TRUE),
    251                            ('memberLazy', TRUE), ('orLazy', TRUE), ('takeZeroLazy', LEAF),
    252                            ('takePrefixLazy', n([42])), ('containsEmptyLazy', TRUE), ('foldLazy', n(42)), ('contractLazy', bad('first')),
    253                            ('pairLazy', bad('first')), ('emptyContractLazy', ok(LEAF)),
    254                            ('invalidListLazy', bad('not a list')), ('fullLazy', bad('not a full binary tree')),
    255                            ('stopLazyState', n(42))]:
    256         add(name, n(42), expected)
    257     add('demoPattern', n(3), byte_tree(b'three'))
    258     add('demoPattern', n(11), byte_tree(b'I ran out of fingers!'))
    259     add('demoEffects', n([10, 20, 30]), result(True, n(10)))
    260     add('demoEffects', LEAF, result(False, byte_tree(b'expected non-empty list')))
    261     for body in [b'', b'123456789', b'1234567890', b'x'*255, b'x'*1024]:
    262         expected = (b'HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: '
    263                     + str(len(body)).encode() + b'\r\nConnection: close\r\n\r\n' + body)
    264         add('httpResponse', byte_tree(body), byte_tree(expected))
    265     return rows
    266 
    267 
    268 def effects():
    269     n = encode
    270     success = result(True, byte_tree(b'resource'), n(23))
    271     failure = result(False, byte_tree(b'denied'), n(29))
    272     cleanup_fail = result(False, byte_tree(b'cleanup'), n(31))
    273     acquire = action(20, byte_tree(b'acquire'))
    274     use = action(21, pair(byte_tree(b'use'), byte_tree(b'resource')))
    275     release = action(10, byte_tree(b'resource'))
    276     abort = action(2, byte_tree(b'stop'))
    277     rows = []
    278     def add(label, name, replies, value, trace, final_label=b'done'):
    279         rows.append((label, name, script(replies), finished(value, trace, label=final_label)))
    280     for value in [success, failure, LEAF]:
    281         add('finally-' + hashlib.sha256(value).hexdigest()[:8], 'ordinaryFinally', [(10, value), (10, cleanup_fail)], value,
    282             [action(10, byte_tree(b'use')), action(10, byte_tree(b'cleanup'))])
    283     add('acquire-failure', 'acquired', [(20, failure)], failure, [acquire])
    284     for use_result in [success, failure]:
    285         for closed in [success, cleanup_fail]:
    286             add('acquired-' + hashlib.sha256(use_result+closed).hexdigest()[:8], 'acquired',
    287                 [(20, success), (21, use_result), (10, closed)], use_result, [acquire, use, release])
    288     for name, expected in [('onOkBoth', pair(byte_tree(b'resource'), n(23))),
    289                            ('onOkValue', byte_tree(b'resource')), ('onResultValue', byte_tree(b'resource'))]:
    290         add(name, name, [(20, success)], expected, [action(20, byte_tree(b'x'))])
    291     for name in ['onOkBoth', 'onOkValue', 'onResultValue']:
    292         add(name+'-failure', name, [(20, failure)], byte_tree(b'denied') if name == 'onResultValue' else failure,
    293             [action(20, byte_tree(b'x'))])
    294     add('abort-finally', 'abortFinally', [], abort, [abort], b'abort')
    295     add('abort-bracket', 'abortBracket', [(20, success)], abort, [acquire, abort], b'abort')
    296     add('forever-abort', 'foreverAbort', [], abort, [abort], b'abort')
    297     sock = n(7)
    298     add('socket-connect-failure', 'socketConnect', [(70, result(True, sock)), (75, failure), (71, cleanup_fail)], failure,
    299         [action(70, LEAF), action(75, pair(sock, pair(byte_tree(b'127.0.0.1'), n(9050)))), action(71, sock)])
    300     add('http-send-failure', 'httpRespond', [(77, failure), (71, cleanup_fail)], failure,
    301         [action(77, pair(sock, byte_tree(b'reply'))), action(71, sock)])
    302     return rows
    303 
    304 
    305 REMOVED_BASE = set('append_ lExist?_ foldr_ length_ snoc_ count_ last_ all?_ any?_ nth_ lastMaybe_ nthMaybe_ take_ drop_ concatMap_ find_ partition_ startsWith?_ contains?_ zipWith_'.split())
    306 
    307 
    308 def audit_workspace(workspace, store, compiler):
    309     """Authenticate all staged source/receipt/manifest edges, not just the driver."""
    310     def obj(domain, pin):
    311         assert re.fullmatch('[0-9a-f]{64}', pin)
    312         payload = (store/'objects'/pin[:3]/pin).read_bytes()
    313         assert digest(domain, payload) == pin, 'object digest mismatch'
    314         return payload
    315     modules = {}
    316     for source in sorted(workspace.glob('*.tri')):
    317         alias = source.stem
    318         fields = (store/'cache/source-build'/alias).read_text().split('\t')
    319         assert len(fields) == 4 and fields[0] == 'arboricx.source-build-cache.v1'
    320         key, manifest, receipt = [x.strip() for x in fields[1:]]
    321         rows = checked_receipt(obj('arboricx.build-receipt.v1', receipt), source.read_bytes(), manifest, compiler)
    322         payload = obj('arboricx.module-manifest.v1', manifest)
    323         exports = []
    324         export_hashes = {}
    325         for line in payload.decode().splitlines()[1:]:
    326             tag, name, domain, pin, abi = line.split('\t')
    327             assert tag == 'export' and domain in KINDS and abi == 'arboricx.abi.tree.v1'
    328             assert name not in export_hashes, 'duplicate module export'
    329             verify_payload(domain, obj(domain, pin))
    330             exports.append(name)
    331             export_hashes[name] = pin
    332         expected, _ = native_header(source.read_text())
    333         dependencies = [row for row in rows if row[0] == 'local']
    334         assert [row[1] for row in dependencies] == expected
    335         modules[alias] = dict(manifest=manifest, receipt=receipt, exports=exports, export_hashes=export_hashes, dependencies=dependencies,
    336                               pins=[row for row in rows if row[0]=='pin'],
    337                               source_sha256=hashlib.sha256(source.read_bytes()).hexdigest())
    338     for module in modules.values():
    339         for _, alias, manifest, receipt in module['dependencies']:
    340             assert (manifest, receipt) == (modules[alias]['manifest'], modules[alias]['receipt']), 'stale candidate dependency'
    341         expected_pins = {('pin', alias, manifest, name, pin)
    342                          for _, alias, manifest, _ in module['dependencies']
    343                          for name, pin in modules[alias]['export_hashes'].items()}
    344         assert {tuple(row) for row in module['pins']} == expected_pins, 'stale candidate export pin'
    345     return modules
    346 
    347 
    348 def check_surface(modules):
    349     fixtures = [REFERENCE_FIXTURES/'compiler-library.arboricx', REFERENCE_FIXTURES/'support-library.arboricx']
    350     previous = {n for p in fixtures for n in read(p)[3]}
    351     removed = {}
    352     for alias, module in modules.items():
    353         old = {n.split('.', 1)[1] for n in previous if n.startswith(alias+'.')}
    354         if not old:
    355             continue
    356         new = set(module['exports'])
    357         expected = REMOVED_BASE if alias in ['base', 'prelude'] else {'match_', 'cond_'} if alias=='patterns' else {'isSorted_'} if alias=='intensional' else set()
    358         retired_formatters = {'incDecRev', 'showNumberRev_'}
    359         if alias in ['conversions', 'prelude']:
    360             expected = expected | retired_formatters
    361         added = {'divMod'} if alias in ['base', 'prelude'] else {'divMod', 'quotient', 'remainder', 'power'} if alias=='native_math' else set()
    362         assert old - new == expected and new - old == added, 'unexpected surface change: ' + alias
    363         removed[alias] = sorted(expected)
    364     return removed
    365 
    366 
    367 def historical_controls(runner, root):
    368     """Old authenticated kernels must not satisfy the explicitly corrected cases."""
    369     baseline = root/'historical.arboricx'
    370     merge_bundles(baseline, [
    371         (REFERENCE_FIXTURES/'compiler-library.arboricx', [('base.'+n, n) for n in ['listOf', 'pairOf', 'nat?', 'anyC']]),
    372         (REFERENCE_FIXTURES/'support-library.arboricx', [('intensional.'+n, n) for n in ['nonEmptyTree?', 'powerOfTwo?']])
    373     ], prune_unreachable=True)
    374     pin = runner.imported(baseline, 'historical')
    375     old = []
    376     for name, contract_count, value, expected in [
    377         ('listOf', 1, b'\1\0', result(False, byte_tree(b'not a list'), encode(7))),
    378         ('pairOf', 2, LEAF, result(False, byte_tree(b'not a pair'), encode(7))),
    379         ('nat?', 0, pair(b'\1\1\0', LEAF), result(False, byte_tree(b'not a natural number'), encode(7))),
    380         ('nonEmptyTree?', 0, b'\1\0', result(True, b'\1\0', encode(7))),
    381         ('powerOfTwo?', 0, encode(2), result(True, encode(2), encode(7))),
    382     ]:
    383         argv = ['--arg-export', pin+':anyC'] * contract_count
    384         for i, arg in enumerate([value, encode(7)]):
    385             path = root/f'historical-arg-{i}.tree';path.write_bytes(arg);argv += ['--arg-tree-file', path]
    386         actual = runner.invoke('historical-'+name, pin, name, argv)
    387         assert actual != expected, 'historical implementation unexpectedly satisfies correction: '+name
    388         old.append(dict(export=name, previous_sha256=hashlib.sha256(actual).hexdigest(),
    389                         corrected_sha256=hashlib.sha256(expected).hexdigest()))
    390     return dict(bundle_sha256=hashlib.sha256(baseline.read_bytes()).hexdigest(), rejected=old)
    391 
    392 
    393 def workloads(runner, root, workspace, tool):
    394     """Small practical cost screen, not a statistical speedup claim."""
    395     baseline = root/'reference-base.arboricx'
    396     fixture = REFERENCE_FIXTURES/'compiler-library.arboricx'
    397     merge_bundles(baseline, [(fixture, [(n, n.split('.', 1)[1]) for n in read(fixture)[3] if n.startswith('base.')])], prune_unreachable=True)
    398     candidate = root/'candidate-base.arboricx'
    399     runner.run('base-export', ['compile', '--file', workspace/'base.tri', '--toolchain', tool,
    400         '--tool-store', runner.store, '--store', runner.store, '--output', candidate, '--compile-timeout', '180'], 210)
    401     pins = {name: runner.imported(path, name+'-base') for name, path in [('reference', baseline), ('candidate', candidate)]}
    402     samples = []
    403     for size in [128, 2048]:
    404         xs = [i % 16 for i in range(size)];data = encode(xs)
    405         for name, arguments, expected in [
    406             ('append', [data, encode([9])], encode(xs+[9])),
    407             ('count', [encode(2), data], encode(xs.count(2))),
    408             ('length', [data], encode(size)),
    409             ('listOf', [data, encode(7)], result(True, data, encode(7))),
    410         ]:
    411             argv = []
    412             for i, value in enumerate(arguments):
    413                 path = root/f'work-arg-{i}.tree';path.write_bytes(value);argv += ['--arg-tree-file', path]
    414             for repetition in range(4):
    415                 for role in (['reference', 'candidate'] if repetition % 2 == 0 else ['candidate', 'reference']):
    416                     args = (['--arg-export', pins[role]+':anyC'] if name == 'listOf' else []) + argv
    417                     label = f'work-{name}-{size}-{repetition}-{role}'
    418                     expect(runner.invoke(label, pins[role], name, args), expected, label)
    419                     row = runner.records[-1]
    420                     samples.append(dict(role=role, workload=name, size=size, warmup=repetition==0,
    421                                         invoke_seconds=row['invoke_seconds'], allocated_bytes=row['allocated_bytes']))
    422     summary = []
    423     for size in [128, 2048]:
    424         for name in ['append', 'count', 'length', 'listOf']:
    425             values = {role: {key: statistics.median(r[key] for r in samples if r['role']==role and r['workload']==name and r['size']==size and not r['warmup'])
    426                              for key in ['invoke_seconds', 'allocated_bytes']} for role in pins}
    427             summary.append(dict(workload=name, size=size, medians=values))
    428     return dict(samples=samples, summary=summary, baseline_sha256=hashlib.sha256(baseline.read_bytes()).hexdigest(),
    429                 candidate_sha256=hashlib.sha256(candidate.read_bytes()).hexdigest())
    430 
    431 
    432 class ProbeRunner(Runner):
    433     def __init__(self, exe, root, seconds, *, memory_mib=8192):
    434         super().__init__(exe, root, memory_mib=memory_mib)
    435         self.deadline = time.monotonic() + seconds
    436 
    437     def run(self, label, args, seconds=30):
    438         remaining = self.deadline - time.monotonic()
    439         if remaining <= 0:
    440             raise TimeoutError('candidate job budget exhausted')
    441         return super().run(label, args, seconds=min(seconds, remaining))
    442 
    443 
    444 def main():
    445     global REFERENCE_FIXTURES
    446     p = argparse.ArgumentParser(description=__doc__)
    447     p.add_argument('tricu', type=Path)
    448     p.add_argument('--reference-fixtures', type=Path, required=True, help='authenticated pre-rewrite fixture directory for historical controls/API deltas')
    449     p.add_argument('--compiler', type=Path, required=True)
    450     p.add_argument('--directory', type=Path, required=True)
    451     p.add_argument('--seconds', type=int, default=1200)
    452     p.add_argument('--resume', action='store_true', help='explicit diagnostic reuse; native receipts invalidate changed inputs')
    453     args = p.parse_args()
    454     REFERENCE_FIXTURES = args.reference_fixtures.resolve()
    455     root = args.directory.resolve();root.mkdir(parents=True, exist_ok=args.resume)
    456     workspace = root/'workspace';workspace.mkdir(exist_ok=args.resume)
    457     verify(REFERENCE_FIXTURES)
    458     report = dict(complete=False, sources={}, cases=[], compiler_sha256=hashlib.sha256(args.compiler.read_bytes()).hexdigest(),
    459                   runtime_sha256=hashlib.sha256(args.tricu.read_bytes()).hexdigest(),
    460                   harness_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest())
    461     (root/'harness.py').write_bytes(Path(__file__).read_bytes())
    462     for alias, path in SOURCES.items():
    463         raw = (ROOT/path).read_bytes();(workspace/(alias+'.tri')).write_bytes(raw)
    464         report['sources'][path] = hashlib.sha256(raw).hexdigest()
    465     (workspace/'recursion_support.tri').write_text('!import base as B\n!export pair\npair = B.pair\n')
    466     (workspace/'driver.tri').write_text(DRIVER)
    467     runner = ProbeRunner(args.tricu, root, args.seconds)
    468     try:
    469         tool = runner.imported(args.compiler, 'tool')
    470         bundle = root/'driver.arboricx'
    471         runner.run('compile', ['compile', '--file', workspace/'driver.tri', '--toolchain', tool,
    472                    '--tool-store', runner.store, '--store', runner.store, '--output', bundle, '--compile-timeout', '300'], 330)
    473         pin = runner.imported(bundle, 'driver')
    474         report['bundle_sha256'] = hashlib.sha256(bundle.read_bytes()).hexdigest()
    475         for label, name, argument, expected in cases() + effects():
    476             argument_path = root/'argument.tree';argument_path.write_bytes(argument)
    477             actual = runner.invoke(label, pin, name, ['--arg-tree-file', argument_path])
    478             expect(actual, expected, label)
    479             report['cases'].append(dict(label=label, export=name, input_sha256=hashlib.sha256(argument).hexdigest(),
    480                                        result_sha256=hashlib.sha256(actual).hexdigest()))
    481         wrong = root/'wrong.arboricx'
    482         merge_bundles(wrong, [(bundle, [('reverseProbe', 'appendProbe')])], prune_unreachable=True)
    483         wrong_pin = runner.imported(wrong, 'wrong')
    484         (root/'argument.tree').write_bytes(encode([1, 2, 3]))
    485         assert runner.invoke('wrong-subject', wrong_pin, 'appendProbe', ['--arg-tree-file', root/'argument.tree']) != encode([1, 2, 3, 9])
    486         # Indexed execution and native dependency manifests have distinct pins.
    487         imported = runner.run('driver-native-manifest', ['import', '--file', bundle, '--store', runner.store])
    488         native_manifest = re.search(r'Created module alias .* -> ([0-9a-f]{64})', imported)[1]
    489         tool_manifest = (runner.store/'objects'/tool[:3]/tool).read_bytes()
    490         assert digest('arboricx.module-manifest.v1', tool_manifest) == tool
    491         compiler = next(row.split('\t')[3] for row in tool_manifest.decode().splitlines()[1:] if row.split('\t')[1]=='sourceBuildGraph')
    492         report['modules'] = audit_workspace(workspace, runner.store, compiler)
    493         assert native_manifest == report['modules']['driver']['manifest']
    494         report['removed_exports'] = check_surface(report['modules'])
    495         report['historical_controls'] = historical_controls(runner, root)
    496         report['workloads'] = workloads(runner, root, workspace, tool)
    497         for alias, path in SOURCES.items():
    498             assert (ROOT/path).read_bytes() == (workspace/(alias+'.tri')).read_bytes(), 'source moved during probe: ' + path
    499         report.update(complete=True, wrong_subject_rejected=True, seconds=time.monotonic()-runner.started)
    500         print(f'PASS: {len(report["cases"])} complete-result/demand/effect cases; {report["seconds"]:.2f}s')
    501     finally:
    502         (root/'report.json').write_text(json.dumps(report, indent=2)+'\n')
    503 
    504 
    505 if __name__ == '__main__':
    506     main()