tricu

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

bootstrap_generation_cli.py (7745B)


      1 #!/usr/bin/env python3
      2 """Opt-in bounded generation checks using retained compiler and adapter artifacts.
      3 No bootstrap compilation and no full compiler generation.
      4 """
      5 import argparse
      6 import json
      7 from pathlib import Path
      8 import subprocess
      9 import sys
     10 import tempfile
     11 from generation_backend import BACKENDS, DEFAULT_BACKEND
     12 
     13 REPO = Path(__file__).resolve().parent.parent
     14 
     15 
     16 def main():
     17     parser = argparse.ArgumentParser(description=__doc__)
     18     parser.add_argument('previous', type=Path)
     19     parser.add_argument('--tricu', type=Path, default=REPO / 'result/bin/tricu')
     20     parser.add_argument('--directory', type=Path)
     21     parser.add_argument('--backend', choices=BACKENDS, default=DEFAULT_BACKEND,
     22                         help='generation lowering backend (default: %(default)s)')
     23     parser.add_argument('--worker-bundle', type=Path)
     24     parser.add_argument('--adapter', type=Path, help='explicit standard adapter for transition probes')
     25     args = parser.parse_args()
     26     directory = args.directory or Path(tempfile.mkdtemp(prefix='tricu-generation-check-'))
     27     directory = directory.resolve()
     28     source = directory / 'source'
     29     source.mkdir(parents=True, exist_ok=True)
     30     map_file = directory / 'map.v1'
     31     map_file.write_text('arboricx.source-map.v1\n'
     32         'unit\tdep\tprobe.dep\tdep.tri\t-\n'
     33         'unit\tapp\tprobe.app\tapp.tri\tdep\n')
     34     (source / 'dep.tri').write_text('answer = 41\n')
     35     (source / 'app.tri').write_text('!import "probe.dep" !Local\nmain = answer\n')
     36     output = directory / 'run'
     37     # Transport allowlist proves setup never calls the recovery frontend.
     38     transport_log = directory / 'transport.jsonl'
     39     transport_log.write_text('')
     40     transport = directory / 'artifact-only-tricu'
     41     transport.write_text('#!' + sys.executable + '\nimport json, os, sys\n'
     42         'assert sys.argv[1] == "arboricx" and sys.argv[2] in ("export", "import", "run"), sys.argv\n'
     43         f'with open({str(transport_log)!r}, "a") as log: log.write(json.dumps(sys.argv[1:]) + "\\n")\n'
     44         f'os.execv({str(args.tricu.resolve())!r}, [{str(args.tricu.resolve())!r}, *sys.argv[1:]])\n')
     45     transport.chmod(0o755)
     46     command = [sys.executable, str(REPO / 'test/bootstrap_generations.py'),
     47         str(args.previous.resolve()), str(output), '--tricu', str(transport),
     48         '--map', str(map_file), '--source-root', str(source), '--root', 'app', '--backend', args.backend]
     49     if args.worker_bundle:
     50         command += ['--worker-bundle', str(args.worker_bundle.resolve())]
     51     if args.adapter:
     52         command += ['--adapter', str(args.adapter.resolve())]
     53     def run(extra=(), success=True):
     54         reply = subprocess.run([*command, *extra], cwd=REPO, capture_output=True, text=True, timeout=120)
     55         assert (reply.returncode == 0) == success, (reply.stdout, reply.stderr)
     56         return reply
     57     def checkpoints():
     58         return {name: (output / 'cas/cache/source-build' / name).read_bytes() for name in ('dep', 'app')}
     59     def manifest(name):
     60         pin = (output / 'cas/cache/source-build' / name).read_text().split('\t')[2]
     61         return pin, (output / 'cas/objects' / pin[:3] / pin).read_text()
     62     def result(number):
     63         pin, _ = manifest('app')
     64         reply = subprocess.run([str(args.tricu.resolve()), 'arboricx', 'run', '--store', str(output / 'cas'),
     65             '--module', pin, '--name', 'main', '--format', 'number'], capture_output=True, text=True, check=True)
     66         assert reply.stdout.strip() == str(number), reply.stdout
     67     run()
     68     assert json.loads((output / 'status.json').read_text())['complete']
     69     result(41)
     70     cold = checkpoints()
     71     run()
     72     assert checkpoints() == cold
     73     print('cold publication and verified warm reuse passed', flush=True)
     74     # Unrelated stale checkpoints must not establish closure completeness.
     75     cache = output / 'cas/cache/source-build'
     76     (cache / 'unrelated').write_text('unrelated stale metadata')
     77     (source / 'dep.tri').write_text('answer = 42\n')
     78     run()
     79     changed = checkpoints()
     80     assert all(changed[name] != cold[name] for name in cold)
     81     result(42)
     82     print('changed dependency invalidates both modules; old map receipts tolerated', flush=True)
     83     # Remove the root checkpoint, retaining the completed dependency/object CAS.
     84     (cache / 'app').unlink()
     85     run(('--memory-mib', '1'), success=False)
     86     assert not json.loads((output / 'status.json').read_text())['complete']
     87     assert (cache / 'dep').read_bytes() == changed['dep']
     88     run()
     89     assert checkpoints() == changed
     90     result(42)
     91     print('guarded interruption and restart passed', flush=True)
     92     # Matching local cache corruption must fail closed, not silently compile.
     93     original = (cache / 'app').read_bytes()
     94     (cache / 'app').write_text('corrupt')
     95     run(success=False)
     96     assert not json.loads((output / 'status.json').read_text())['complete']
     97     (cache / 'app').write_bytes(original)
     98     run()
     99     print('matching checkpoint corruption rejected; restored restart passed', flush=True)
    100     # No implicit source compilation when adapter is unavailable.
    101     reply = run(('--adapter', str(directory / 'missing.arboricx')), success=False)
    102     assert 'precompiled adapter required' in reply.stderr
    103     # Header/map mismatch is caught before any subprocess/artifact preparation.
    104     (source / 'app.tri').write_text('main = 42\n')
    105     reply = run(('--validate-only',), success=False)
    106     assert 'header mismatch' in reply.stderr
    107     print('missing adapter and stale header rejected without bootstrap fallback', flush=True)
    108     # Both profiles reuse the same source/CAS directory, but never each other's
    109     # receipt key. Literal executable trees may coincide; keys must not.
    110     (source / 'app.tri').write_text('!import "probe.dep" !Local\nmain = answer\n')
    111     run(('--backend', 'optimized'))
    112     optimized = checkpoints()
    113     run(('--backend', 'stable'))
    114     stable = checkpoints()
    115     assert all(stable[name] != optimized[name] for name in stable)
    116     status = json.loads((output / 'status.json').read_text())
    117     assert status['backend'] == 'stable' and status['worker_export'] == 'sourceMapBuildStable'
    118     result(42)
    119     run(('--backend', 'stable'))
    120     assert checkpoints() == stable
    121     # Lie about the selected profile in both provenance records: independently
    122     # checked module options still expose this (not just metadata consistency).
    123     saved = {}
    124     for filename in ('status.json', 'inputs.json'):
    125         path = output / filename
    126         saved[filename] = path.read_bytes()
    127         data = json.loads(saved[filename])
    128         data.update(backend='optimized', worker_export=BACKENDS['optimized'][0],
    129                     backend_options=BACKENDS['optimized'][1])
    130         path.write_text(json.dumps(data))
    131     audit = subprocess.run([sys.executable, str(REPO / 'test/bootstrap_store_check.py'), str(output)],
    132                            capture_output=True, text=True)
    133     assert audit.returncode != 0
    134     for filename, payload in saved.items():
    135         (output / filename).write_bytes(payload)
    136     (source / 'app.tri').write_text('!import "probe.dep" !Local\n'
    137         'walk t = answer\nwalk (t x) = walk x\nwalk input = 9\nmain = walk (t (t t))\n')
    138     run(('--backend', 'stable'))
    139     result(42)
    140     reply = run(('--backend', 'optimized'), success=False)
    141     assert not json.loads((output / 'status.json').read_text())['complete']
    142     run(('--backend', 'stable'))
    143     result(42)
    144     print('backend switch, stable warm reuse, false-profile audit, direct recursion and no fallback passed', flush=True)
    145     assert transport_log.read_text()
    146     print('artifact-only transport allowlist passed (export/import/run only)', flush=True)
    147     print('Artifacts: ' + str(directory), flush=True)
    148 
    149 
    150 if __name__ == '__main__':
    151     main()