tricu

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

stable_constructor_k.py (6860B)


      1 #!/usr/bin/env python3
      2 """Opt-in current-source constructor-K check; never publishes compiler artifacts.
      3 
      4 The selected host must contain the authenticated packaged compiler. Unchanged
      5 fixture dependencies are source-verified; only source_stable and source_unit
      6 are rebuilt. Probe-only exports expose internal summaries to an independent
      7 finite oracle without making them part of the production interface.
      8 """
      9 import argparse
     10 import hashlib
     11 import json
     12 from pathlib import Path
     13 import re
     14 import subprocess
     15 import time
     16 
     17 from bundle_dag import Intern, merge_bundles, read
     18 from fixture_sources import snapshot_bytes, verify
     19 from pattern_lowering import audit
     20 from shipped_compiler import verify as verify_compiler
     21 
     22 ROOT = Path(__file__).resolve().parent.parent
     23 
     24 
     25 def main():
     26     p = argparse.ArgumentParser(description=__doc__)
     27     p.add_argument('tricu', type=Path)
     28     p.add_argument('--directory', type=Path, required=True)
     29     p.add_argument('--expected-stable', type=Path, help='optional exact public-tree comparison against a prior prototype')
     30     p.add_argument('--expected-unit', type=Path, help='optional exact source_unit comparison against a prior prototype')
     31     args = p.parse_args()
     32     root = args.directory.resolve()
     33     root.mkdir(parents=True, exist_ok=False)
     34     workspace = root / 'workspace'
     35     workspace.mkdir()
     36     (workspace / 'tricu.workspace').write_text('tricu.workspace.v1\n')
     37     store = root / 'store'
     38     host = args.tricu.resolve()
     39     metadata = verify_compiler()
     40     fixture_rows = {row[2]: row for row in verify(ROOT / 'test/fixtures')}
     41     entries = {}
     42     for row in (ROOT / 'bootstrap/compiler-source-map.v1').read_text().splitlines()[1:]:
     43         _, alias, _, path, deps = row.split('\t')
     44         entries[alias] = (path, [] if deps == '-' else deps.split(','))
     45     affected = {'source_stable', 'source_unit'}
     46     needed = set()
     47 
     48     def visit(alias):
     49         if alias in needed:
     50             return
     51         needed.add(alias)
     52         for dep in entries[alias][1]:
     53             visit(dep)
     54     visit('source_unit')
     55     for alias in needed - affected:
     56         assert snapshot_bytes(ROOT / 'test/fixtures', fixture_rows[alias][4]) == (ROOT / entries[alias][0]).read_bytes(), ('stale reuse', alias)
     57     report = dict(complete=False, compiler_sha256=metadata['sha256'],
     58                   harness_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
     59                   oracle_sha256=hashlib.sha256((ROOT / 'test/StableConstructorK.hs').read_bytes()).hexdigest(),
     60                   runtime_sha256=hashlib.sha256(host.read_bytes()).hexdigest(), commands=[], reused={})
     61     started = time.monotonic()
     62 
     63     def command(argv, seconds=180):
     64         result = subprocess.run(list(map(str, argv)), cwd=ROOT, capture_output=True,
     65                                 text=True, timeout=seconds)
     66         report['commands'].append(dict(argv=list(map(str, argv)), returncode=result.returncode,
     67                                        stdout=result.stdout, stderr=result.stderr))
     68         (root / 'report.json').write_text(json.dumps(report, indent=2) + '\n')
     69         if result.returncode:
     70             raise RuntimeError(result.stdout[-4000:] + result.stderr[-4000:])
     71         return result.stdout
     72 
     73     def run(options, seconds=180):
     74         return command([host, 'arboricx', *options, '+RTS', '-N1', '-M4096m', '-RTS'], seconds)
     75 
     76     active = affected | {dep for alias in affected for dep in entries[alias][1]}
     77     for alias in sorted(active - affected):
     78         cohort, _, _, _, source_pin = fixture_rows[alias]
     79         fixture = ROOT / 'test/fixtures' / (cohort + '.arboricx')
     80         bundle = root / (alias + '.arboricx')
     81         merge_bundles(bundle, [(fixture, [(name, name[len(alias)+1:]) for name in read(fixture)[3]
     82                                        if name.startswith(alias + '.')])], prune_unreachable=True)
     83         pin = re.search(r'Created module alias .* -> ([0-9a-f]{64})',
     84                         run(['import', '--file', bundle, '--store', store]))[1]
     85         (workspace / (alias + '.tri')).write_text('!export * from ' + pin + '\n')
     86         report['reused'][alias] = dict(manifest=pin, source_pin=source_pin)
     87     for alias in affected:
     88         source = (ROOT / entries[alias][0]).read_bytes()
     89         report[alias + '_source_sha256'] = hashlib.sha256(source).hexdigest()
     90         (workspace / (alias + '.tri')).write_bytes(source)
     91     probes = ['stableCachedFromPlain', 'stableCachedToPlain', 'stableCachedBracket']
     92     stable_path = workspace / 'source_stable.tri'
     93     stable_path.write_text(''.join('!export ' + name + '\n' for name in probes) + stable_path.read_text())
     94     for alias in ['source_unit', 'source_stable']:
     95         run(['compile', '--file', workspace / (alias + '.tri'), '--store', store,
     96              '--output', root / (alias + '.arboricx'), '--compile-timeout', '280'], 300)
     97     report['modules'] = audit(workspace, store, metadata['sha256'], report['reused'])
     98     for alias, expected in [('source_stable', args.expected_stable), ('source_unit', args.expected_unit)]:
     99         if expected:
    100             intern = Intern()
    101             def roots(path):
    102                 _, _, nodes, exports, _ = read(path)
    103                 ids = intern.graph(nodes)
    104                 return {name: ids[value] for name, value in exports.items()}
    105             actual, before = roots(root / (alias + '.arboricx')), roots(expected)
    106             selected = set(actual) - set(probes)
    107             assert all(name in before and actual[name] == before[name] for name in selected), alias
    108             report[alias + '_exact_public_exports'] = len(selected)
    109     # GHC and all packages come from Nix. No cabal calls or ambient GHC.
    110     expression = ('let f=builtins.getFlake "git+file://' + str(ROOT) + '";'
    111                   'p=f.inputs.nixpkgs.legacyPackages.x86_64-linux;'
    112                   'in p.haskellPackages.ghcWithPackages(h:[h.cryptonite h.memory h.base16-bytestring h.vector])')
    113     command(['nix', 'shell', '--impure', '--expr', expression, '--command', 'ghc', '-O2', '-rtsopts',
    114              '-XOverloadedStrings', '-XLambdaCase', '-XMultiWayIf', '-XScopedTypeVariables',
    115              '-isrc', '-outputdir', root / 'objects', ROOT / 'test/StableConstructorK.hs',
    116              '-o', root / 'controls'], 300)
    117     command([root / 'controls', root / 'source_stable.arboricx',
    118              ROOT / 'test/fixtures/compiler-library.arboricx', '+RTS', '-M2048m', '-RTS'])
    119     command(['python3', ROOT / 'test/local_control.py', host, '--unit', root / 'source_unit.arboricx',
    120              '--directory', root / 'local-controls'], 300)
    121     report.update(complete=True, seconds=time.monotonic() - started)
    122     (root / 'report.json').write_text(json.dumps(report, indent=2) + '\n')
    123     print('PASS: constructor-K cached invariants, independent results/demand/diagnostics, local controls and provenance')
    124 
    125 
    126 if __name__ == '__main__':
    127     main()