tricu

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

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