local_control.py (13194B)
1 #!/usr/bin/env python3 2 """Artifact-only stable compiler probes for case expressions and explicit local rec. 3 4 No source compilation during setup and no fallback frontend. Select a genuinely 5 rebuilt source_unit bundle; each invocation compiles the supplied test program. 6 """ 7 import argparse 8 import json 9 from pathlib import Path 10 import time 11 from http_headers import LEAF, TRUE, byte_tree, number, pair, tree_list, expect, sha 12 from source_order import Runner, field, number as decode_number 13 from bundle_dag import merge_bundles 14 15 16 def cases(): 17 return [ 18 ('case-leaf', 'main = case t of\n t = 23\n _ = 24', number(23)), 19 ('case-pair', 'main = case (t 23 24) of\n (a, b) = [b a]\n _ = []', tree_list([number(24), number(23)])), 20 ('case-whole', 'main = case (t 23 24) of\n whole{a, b} = whole\n _ = t', pair(number(23), number(24))), 21 ('case-pin', 'wanted = 25\nmain = case 25 of\n ^wanted = 23\n _ = 24', number(23)), 22 ('case-capture-shadow', 'value = 99\nmain = case 23 of\n value = value', number(23)), 23 ('case-ordered', 'main = case t of\n first = 23\n t = 24\n _ = 25', number(23)), 24 ('case-nested', 'main = case (t 23) of\n (t value) = case value of\n 23 = 24\n _ = 25\n _ = 26', number(24)), 25 ('case-group-application', 'id x = x\nmain = id (case t of\n t = 23\n _ = 24)', number(23)), 26 ('case-list-atom', 'main = [(case t of\n t = 23\n _ = 24) 25]', tree_list([number(23), number(25)])), 27 ('case-do', 'pass value next = next value\nmain = do pass\n value <- case t of\n t = 23\n _ = 24\n case value of\n 23 = 25\n _ = 26', number(25)), 28 ('case-scope', 'main = (outside : case (t 23) of\n (t inside) = [outside inside]\n _ = []) 24', tree_list([number(24), number(23)])), 29 ('case-failed-capture', 'main = case (t 23 24) of\n (bound, t) = bound\n _ = 25', number(25)), 30 ('plain-let-shadow', 'f x = 23\nmain = let f x = f x in f t', number(23)), 31 ('rec-let', 'main = let rec\n walk [] = 23\n walk [_ | rest] = walk rest\n walk _ = 24\nin walk [1 2 3]', number(23)), 32 ('rec-where', 'main = walk [1 2] where rec\n walk [] = 23\n walk [_ | rest] = walk rest\n walk _ = 24', number(23)), 33 ('rec-capture', 'main = (outside : let rec\n walk [] = outside\n walk [_ | rest] = walk rest\n walk _ = 24\nin walk [1 2]) 23', number(23)), 34 ('rec-returned', 'make outside = let rec\n walk [] = outside\n walk [_ | rest] = walk rest\n walk _ = 24\nin walk\nmain = make 23 [1 2]', number(23)), 35 ('rec-shadows-outer', 'walk xs = 99\nmain = let rec\n walk [] = 23\n walk [_ | rest] = walk rest\n walk _ = 24\nin walk [1]', number(23)), 36 ('rec-parameter-shadow', 'main = let rec f f = f in f 23', number(23)), 37 ('rec-explicit-lambda', 'main = let rec f = (x : x) in f 23', number(23)), 38 ('rec-no-import-y', 'y = 99\nfix = 98\nmain = let rec\n walk [] = 23\n walk [_ | rest] = walk rest\n walk _ = 24\nin walk [1]', number(23)), 39 ('rec-with-case', 'main = let rec\n walk xs = case xs of\n [] = 23\n [_ | rest] = walk rest\n _ = 24\nin walk [1 2]', number(23)), 40 ('case-forward-body', 'main = case t of\n _ = later 23\nlater x = x', number(23)), 41 ('case-deeper-body', 'main = case t of\n t = (x : x)\n 23\n _ = 24', number(23)), 42 ('case-pin-shadow-scope', 'main = (wanted : case (t 23) of\n (t inside) = case inside of\n ^wanted = 24\n _ = 25\n _ = 26) 23', number(24)), 43 ('rec-nested-capture', 'main = (outside : let rec\n outer [] = outside\n outer [_ | rest] = let rec inner x = outer x in inner rest\n outer _ = 99\nin outer [1 2]) 23', number(23)), 44 ('rec-capture-shadow', 'main = let rec\n walk (t walk) = walk\n walk _ = 99\nin walk (t 23)', number(23)), 45 ('rec-plain-shadow-inside', 'main = let rec\n walk [] = 23\n walk [_ | rest] = let walk xs = walk xs in walk rest\n walk _ = 24\nin walk [1 2]', number(23)), 46 ('case-ignored-scrutinee', 'main = let rec loop x = loop x in case (loop t) of\n _ = 23', number(23)), 47 ('case-unused-loop', 'main = case t of\n t = 23\n _ = let rec loop x = loop x in loop t', number(23)), 48 ('case-failed-before-loop', 'main = case t of\n (t x) = let rec loop x = loop x in loop x\n _ = 23', number(23)), 49 ] 50 51 52 def failures(): 53 # Complete diagnostic fields are checked below; the compiler-private failure 54 # remainder is retained in the report but is not a language-level oracle. 55 return [ 56 ('case-catchall', 'main = case t of\n t = 23', 48, 'final irrefutable equation', '23'), 57 ('case-duplicate', 'main = case (t t t) of\n (x, x) = x\n _ = t', 48, 'unique capture', 'x', 2), 58 ('case-pin-collision', 'x = 23\nmain = case t of\n x{^x} = x\n _ = t', 48, 'pin outside equation captures', '^x', 1, 1), 59 ('case-subject-missing-first', 'main = case missing of\n _ = absent', 20, 'missing', 'missing'), 60 ('case-arm-missing', 'main = case t of\n _ = absent', 20, 'absent', 'absent'), 61 ('case-pin-missing', 'main = case t of\n ^missing = 23\n _ = 24', 20, 'missing', 'missing'), 62 ('case-empty', 'main = case t of\n', 40, 'indented case arms', None), 63 ('case-indent', 'main = case t of\nt = 23', 40, 'indented case arms', 't ='), 64 ('rec-value', 'main = let rec value = value in t', 48, 'recursive function requires parameters', 'value'), 65 ('rec-value-before-missing', 'main = let rec value = missing in t', 48, 'recursive function requires parameters', 'value'), 66 ('rec-value-leaf', 'main = let rec value = t in value', 48, 'recursive function requires parameters', 'value'), 67 ('rec-two-groups', 'main = let rec\n first x = x\n second x = x\nin first t', 48, 'one recursive local function', 'let'), 68 ('rec-no-mutual', 'main = let rec f x = g x in let rec g x = f x in f t', 20, 'g', 'g x'), 69 ('plain-local-still-not-recursive', 'main = let f x = f x in f t', 20, 'f', 'f x', 2), 70 ('rec-unused-missing', 'main = let rec f x = missing in t', 20, 'missing', 'missing'), 71 ] 72 73 74 def decode_text(tree): 75 chars = [] 76 while tree != LEAF: 77 chars.append(chr(decode_number(field(tree, 0)))) 78 tree = field(tree, 1) 79 return ''.join(chars) 80 81 82 def ast_cases(): 83 def build(source, recursive=False): 84 def pos(offset): 85 return pair(number(source[:offset].count('\n')+1), pair(number(offset-source.rfind('\n',0,offset)), number(offset))) 86 def span(text, occurrence=1): 87 at = -1 88 for _ in range(occurrence): at = source.index(text, at+1) 89 return pos(at), pos(at+len(text)) 90 def node(kind, payload, bounds): 91 return pair(number(kind), pair(payload, pair(*bounds))) 92 name = node(2, byte_tree(b'main'), span('main')) 93 if recursive: 94 binder = node(2, byte_tree(b'f'), span('f')) 95 value = node(4, pair(byte_tree(b'x'), node(2, byte_tree(b'x'), span('x', 2))), (span('x')[0], span('x',2)[1])) 96 body = node(3, pair(node(2, byte_tree(b'f'), span('f',2)), node(6, number(23), span('23'))), (span('f',2)[0], span('23')[1])) 97 expression = node(15, pair(binder, pair(value, body)), (span('let')[0], span('23')[1])) 98 else: 99 bounds = (span('case')[0], span('24')[1]) 100 subject = node(1, LEAF, span('t')) 101 leaf = node(13, pair(number(1), LEAF), span('t',2)) 102 wild = node(13, pair(number(0), LEAF), span('_')) 103 arms = tree_list([pair(tree_list([leaf]), node(6, number(23), span('23'))), 104 pair(tree_list([wild]), node(6, number(24), span('24')))]) 105 expression = node(14, pair(subject, node(12, arms, bounds)), bounds) 106 program = pair(tree_list([pair(name, expression)]), LEAF) 107 return pair(TRUE, pair(program, pair(LEAF, pair(TRUE, LEAF)))) 108 source = 'main = case t of\n t = 23\n _ = 24' 109 rec = 'main = let rec f x = x in f 23' 110 return [('case-ast', source, build(source)), ('rec-ast', rec, build(rec, True))] 111 112 113 def main(): 114 p = argparse.ArgumentParser(description=__doc__) 115 p.add_argument('tricu', type=Path) 116 p.add_argument('--unit', type=Path, required=True) 117 p.add_argument('--program', type=Path, help='also check full located parser replies') 118 p.add_argument('--directory', type=Path, required=True) 119 p.add_argument('--case', action='append') 120 args = p.parse_args() 121 unknown = set(args.case or []) - {row[0] for row in cases()+failures()} 122 if unknown: p.error('unknown cases: '+', '.join(sorted(unknown))) 123 root = args.directory.resolve(); root.mkdir(parents=True, exist_ok=False) 124 runner = Runner(args.tricu, root) 125 env = root/'environment.tree'; env.write_bytes(LEAF) 126 selected = root/'unit.arboricx'; selected.write_bytes(args.unit.read_bytes()) 127 report = dict(complete=False, unit_sha256=sha(selected.read_bytes()), 128 runtime_sha256=sha(runner.exe.read_bytes()), harness_sha256=sha(Path(__file__).read_bytes()), cases=[]) 129 try: 130 pin = runner.imported(selected, 'unit') 131 for row in cases()+failures(): 132 label, source = row[:2] 133 if args.case and label not in args.case: continue 134 path = root/(label+'.source.tree'); path.write_bytes(byte_tree(source.encode())) 135 answer = runner.invoke(label, pin, 'compileStableModuleDefinitions', 136 ['--arg-tree-file', env, '--arg-tree-file', path]) 137 if len(row) == 3: 138 expect(field(answer, 0), TRUE, label+' successful compilation') 139 bindings = field(answer, 1, 0); found = None 140 while bindings != LEAF: 141 binding = field(bindings, 0) 142 if decode_text(field(binding, 0)) == 'main': found = field(binding, 1) 143 bindings = field(bindings, 1) 144 expect(found, row[2], label+' independent value') 145 expect(field(answer, 1, 1), LEAF, label+' success remainder') 146 else: 147 expect(field(answer, 0), LEAF, label+' rejected') 148 diagnostic = field(answer, 1, 0) 149 code, expected, needle = row[2:5] 150 assert decode_number(field(diagnostic, 0)) == code, label 151 assert decode_text(field(diagnostic, 1, 0)) == expected, label 152 occurrence, shift = (row[5] if len(row)>5 else 1), (row[6] if len(row)>6 else 0) 153 location = LEAF 154 if needle: 155 offset = -1 156 for _ in range(occurrence): offset = source.index(needle, offset+1) 157 offset += shift 158 location = pair(number(source[:offset].count('\n')+1), pair(number(offset-source.rfind('\n',0,offset)), number(offset))) 159 found = LEAF 160 if label == 'case-duplicate' or label == 'case-pin-collision': found = byte_tree(b'x') 161 elif label.startswith('rec-value'): found = byte_tree(b'value') 162 elif label == 'case-indent': found = number(1) 163 expected_diagnostic = pair(number(code), pair(byte_tree(expected.encode()), pair(found, pair(location, LEAF)))) 164 expect(diagnostic, expected_diagnostic, label+' complete diagnostic') 165 report['cases'].append(dict(label=label, result_sha256=sha(answer))) 166 wrong = root/'wrong-unit.arboricx' 167 merge_bundles(wrong, [(selected, [('sourceModuleCheckShadows', 'compileStableModuleDefinitions')])], prune_unreachable=True) 168 wrong_pin = runner.imported(wrong, 'wrong-unit') 169 control_source = root/'control.tree'; control_source.write_bytes(byte_tree(cases()[0][1].encode())) 170 wrong_answer = runner.invoke('wrong-selected-unit', wrong_pin, 'compileStableModuleDefinitions', 171 ['--arg-tree-file', env, '--arg-tree-file', control_source]) 172 expected = pair(TRUE, pair(tree_list([pair(byte_tree(b'main'), number(23))]), LEAF)) 173 if wrong_answer == expected: raise ValueError('wrong selected compiler passed') 174 report['wrong_subject_rejected'] = True 175 if args.program: 176 program = root/'program.arboricx'; program.write_bytes(args.program.read_bytes()) 177 report['program_sha256'] = sha(program.read_bytes()) 178 program_pin = runner.imported(program, 'program') 179 for label, source, expected in ast_cases(): 180 path = root/(label+'.source.tree'); path.write_bytes(byte_tree(source.encode())) 181 answer = runner.invoke(label, program_pin, 'parseGateProgram', ['--arg-tree-file', path]) 182 expect(answer, expected, label+' full parser reply') 183 report['cases'].append(dict(label=label, result_sha256=sha(answer))) 184 report['complete'] = True 185 except Exception as error: 186 report['failure'] = str(error); raise 187 finally: 188 report['seconds'] = time.monotonic()-runner.started 189 (root/'report.json').write_text(json.dumps(report, indent=2)+'\n') 190 print(f'PASS: {len(report["cases"])} local control cases') 191 192 193 if __name__ == '__main__': main()