tricu

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

native_math.py (7870B)


      1 #!/usr/bin/env python3
      2 """Opt-in native math: explicit portable compiler, exact independent tree oracles.
      3 No recovery compilation, compiler rebuilding, or alternative host execution.
      4 """
      5 import argparse
      6 import hashlib
      7 import json
      8 from pathlib import Path
      9 import random
     10 import re
     11 import subprocess
     12 import sys
     13 import time
     14 
     15 sys.dont_write_bytecode = True
     16 
     17 ROOT = Path(__file__).resolve().parent.parent
     18 p = argparse.ArgumentParser(description=__doc__)
     19 p.add_argument('tricu', type=Path)
     20 p.add_argument('--compiler-bundle', type=Path, required=True)
     21 p.add_argument('--directory', type=Path, required=True, help='fresh preparation directory')
     22 p.add_argument('--generation', type=Path, help='optional matching baseline cohort; enables bounded performance/lexer probes')
     23 p.add_argument('--counter-tricu', type=Path, help='optional existing investigation-only counted Haskell host; counts only, never timing evidence')
     24 a = p.parse_args()
     25 exe = str(a.tricu.resolve())
     26 root = a.directory.resolve()
     27 root.mkdir(parents=True, exist_ok=False)
     28 started = time.monotonic()
     29 records = []
     30 
     31 
     32 def run(args, executable=None):
     33     if time.monotonic() - started > 900:
     34         raise RuntimeError('15 minute initial pass budget exhausted')
     35     command = [executable or exe, *map(str, args)]
     36     begin = time.monotonic()
     37     try:
     38         result = subprocess.run(command, capture_output=True, timeout=120)
     39     except subprocess.TimeoutExpired:
     40         records.append(dict(command=command, timeout=120))
     41         save()
     42         raise
     43     records.append(dict(command=command, seconds=time.monotonic() - begin,
     44                         stderr=result.stderr.decode(), returncode=result.returncode))
     45     save()
     46     if result.returncode:
     47         raise RuntimeError(result.stderr.decode())
     48     return result.stdout.decode().strip()
     49 
     50 
     51 def save():
     52     (root / 'measurements.json').write_text(json.dumps(records, indent=2) + '\n')
     53 
     54 
     55 def import_bundle(path, store):
     56     reply = run(['arboricx', 'import', '--file', path, '--store', store, '--indexed'])
     57     return re.search(r'Created module alias .* -> ([0-9a-f]{64})', reply)[1]
     58 
     59 
     60 def number(n):
     61     return b'\0' if n == 0 else b'\2' + (b'\1\0' if n & 1 else b'\0') + number(n >> 1)
     62 
     63 
     64 def boolean(b):
     65     return b'\1\0' if b else b'\0'
     66 
     67 
     68 def tree_list(values):
     69     return b''.join(b'\2' + v for v in values) + b'\0'
     70 
     71 
     72 def check(name, values, expected, matches=True):
     73     stem = name if matches else name + '-wrong'
     74     payload = root / (stem + '.tree')
     75     payload.write_bytes(tree_list(values))
     76     (root / (stem + '.expected.tree')).write_bytes(tree_list(expected))
     77     output = run(['arboricx', 'run', '--module', pin, '--name', name,
     78                   '--store', root / 'tools', '--arg-tree-file', payload,
     79                   '--format', 'ternary', '--profile', '+RTS', '-N1', '-T', '-RTS'])
     80     (root / (stem + '.actual.ternary')).write_text(output + '\n')
     81     oracle = ''.join(str(b) for b in tree_list(expected))
     82     assert (output == oracle) == matches, (name, output[:160], oracle[:160])
     83     print(f'{name}: {len(values)} exact results' if matches else f'{name}: wrong-value control rejected', flush=True)
     84 
     85 
     86 def file_hash(path):
     87     return hashlib.sha256(Path(path).read_bytes()).hexdigest()
     88 
     89 
     90 (root / 'provenance.json').write_text(json.dumps(dict(
     91     executable=exe, executable_sha256=file_hash(exe),
     92     compiler=str(a.compiler_bundle.resolve()), compiler_sha256=file_hash(a.compiler_bundle),
     93     library_sha256=file_hash(ROOT / 'lib/math/native.tri'),
     94     lexer_sha256=file_hash(ROOT / 'lib/source/lexer.tri'),
     95     runner_sha256=file_hash(__file__),
     96     performance_runner_sha256=file_hash(ROOT / 'test/native_math_performance.py'),
     97     generation=str(a.generation.resolve()) if a.generation else None,
     98     counter=str(a.counter_tricu.resolve()) if a.counter_tricu else None,
     99     counter_sha256=file_hash(a.counter_tricu) if a.counter_tricu else None,
    100 ), indent=2) + '\n')
    101 tool = import_bundle(a.compiler_bundle.resolve(), root / 'tools')
    102 source = (ROOT / 'lib/math/native.tri').read_text()
    103 source += '''
    104 mapStep self xs f = triage t (_ : t) (h r : t (f h) (self r f)) xs
    105 mapValues xs f = fix mapStep xs f
    106 binary f row = triage t (_ : t) (a b : f a b) row
    107 '''
    108 unary = dict(zero=lambda n: n == 0, odd=lambda n: bool(n & 1), even=lambda n: not n & 1,
    109              ascii=lambda n: n <= 127, digit=lambda n: 48 <= n <= 57,
    110              letter=lambda n: 65 <= n <= 90 or 97 <= n <= 122,
    111              lowerHex=lambda n: 48 <= n <= 57 or 97 <= n <= 102,
    112              printable=lambda n: 32 <= n <= 126,
    113              successor=lambda n: n + 1, predecessor=lambda n: max(0, n - 1),
    114              leftBound=lambda n: 65 <= n, rightBound=lambda n: n <= 65,
    115              partialAdd=lambda n: n + 1)
    116 binary = dict(compare=lambda x, y: (x > y) - (x < y),
    117               equal=lambda x, y: x == y, lt=lambda x, y: x < y,
    118               lte=lambda x, y: x <= y, gt=lambda x, y: x > y, gte=lambda x, y: x >= y,
    119               minimum=min, maximum=max, add=lambda x, y: x + y,
    120               subtract=lambda x, y: max(0, x - y), multiply=lambda x, y: x * y)
    121 source += 'leftBound value = lte 65 value\nrightBound value = lte value 65\npartialAdd = add 1\n'
    122 for name in unary:
    123     source += f'check_{name} xs = mapValues xs {name}\n'
    124 for name in binary:
    125     source += f'check_{name} xs = mapValues xs (binary {name})\n'
    126 source += 'check_decimal xs = mapValues xs (binary decimal)\n'
    127 source += ('check_range xs = mapValues xs (row : triage t (_ : t) '
    128            '(value bounds : triage t (_ : t) (low high : inRange value low high) bounds) row)\n')
    129 source = ''.join(f'!export check_{name}\n' for name in [*unary, *binary, 'decimal', 'range']) + source
    130 (root / 'math.tri').write_text(source)
    131 (root / 'source.sha256').write_text(hashlib.sha256(source.encode()).hexdigest() + '\n')
    132 run(['arboricx', 'compile', '--file', root / 'math.tri', '--toolchain', tool,
    133      '--tool-store', root / 'tools', '--store', root / 'application',
    134      '--output', root / 'math.arboricx', '--compile-timeout', '120',
    135      '+RTS', '-N1', '-RTS'])
    136 pin = import_bundle(root / 'math.arboricx', root / 'tools')
    137 values = list(range(256)) + [256, 257, 65535, 65536, 2**64 - 1, 2**128]
    138 for name, oracle in unary.items():
    139     encode = number if name in ['successor', 'predecessor', 'partialAdd'] else boolean
    140     check('check_' + name, [number(n) for n in values], [encode(oracle(n)) for n in values])
    141 rng = random.Random(171)
    142 pairs = [(x, y) for x in range(16) for y in range(16)]
    143 bounds = sorted({n for k in [1, 2, 7, 8, 16, 32, 64, 128] for n in [2**k-1, 2**k, 2**k+1]})
    144 pairs += [(x, y) for x in [0, *bounds] for y in [0, *bounds]]
    145 pairs += [(rng.getrandbits(128), rng.getrandbits(128)) for _ in range(64)]
    146 for name, oracle in binary.items():
    147     encode = boolean if name in ['equal', 'lt', 'lte', 'gt', 'gte'] else number
    148     if name == 'compare':
    149         encode = lambda order: b'\0' if order == 0 else b'\1\0' if order < 0 else b'\2\0\0'
    150     check('check_' + name, [b'\2' + number(x) + number(y) for x, y in pairs],
    151           [encode(oracle(x, y)) for x, y in pairs])
    152 decimal_pairs = [(x, d) for x in values for d in range(10)]
    153 check('check_decimal', [b'\2' + number(x) + number(d) for x, d in decimal_pairs],
    154       [number(x * 10 + d) for x, d in decimal_pairs])
    155 ranges = [(v, lo, hi) for v in range(16) for lo in range(8) for hi in range(8)]
    156 check('check_range', [b'\2' + number(v) + b'\2' + number(lo) + number(hi) for v, lo, hi in ranges],
    157       [boolean(lo <= v <= hi) for v, lo, hi in ranges])
    158 check('check_zero', [number(0)], [boolean(False)], matches=False)
    159 print('PASS: native math; compilation/publication and exact canonical results', flush=True)
    160 if a.generation:
    161     from native_math_performance import probe
    162     probe(root, ROOT, a.generation.resolve(), tool, pin, run, import_bundle, number, boolean, tree_list,
    163           str(a.counter_tricu.resolve()) if a.counter_tricu else None)