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