tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

haskell_generation_executor.py (6977B)


      1 #!/usr/bin/env python3
      2 """Fresh-process Haskell C-ABI executor for captured IO generation requests.
      3 
      4 The supervisor owns transport, process guards and independent store audits.
      5 This child authenticates the complete captured request before executing effects.
      6 """
      7 import argparse
      8 import hashlib
      9 import json
     10 from pathlib import Path
     11 import resource
     12 import subprocess
     13 import sys
     14 import time
     15 
     16 ROOT = Path(__file__).resolve().parent.parent
     17 BINDING = ROOT / 'ext/haskell/python/tricu.py'
     18 sys.path.insert(0, str(BINDING.parent))
     19 from tricu import Library, TricuError
     20 from generation_io import version, versions
     21 
     22 
     23 def sha(path):
     24     return hashlib.sha256(path.read_bytes()).hexdigest()
     25 
     26 
     27 def validate_library(path, io_version):
     28     if version(io_version) != 2:
     29         raise ValueError('Haskell embedding requires IO version 2; select a retained runtime for v1')
     30     # The supervisor never initializes an RTS. Validation and actual execution
     31     # have separate, freshly exec'd processes, with no inherited GHC contexts.
     32     result = subprocess.run([sys.executable, str(Path(__file__).resolve()), '--library', str(path),
     33                              '--validate-only'], capture_output=True, text=True, timeout=30)
     34     if result.returncode:
     35         raise ValueError('Haskell embedding library unavailable or incompatible; no fallback: ' + result.stderr[-2000:])
     36 
     37 
     38 def captured_request(output, library):
     39     recorded = json.loads((output / 'inputs.json').read_text())
     40     request = json.loads((output / 'execution-request.json').read_text())
     41     execution = version(request.get('execution_io_version'))
     42     if execution != 2 or execution != versions(recorded)[0]:
     43         raise ValueError('execution request IO version mismatch')
     44     if (recorded.get('execution_host') != 'haskell-ffi'
     45             or recorded.get('runtime_sha256') != sha(library)
     46             or recorded.get('executor_sha256') != sha(Path(__file__).resolve())
     47             or recorded.get('binding_sha256') != sha(BINDING)):
     48         raise ValueError('execution runtime/launcher/binding identity mismatch')
     49     if sha(output / 'source-map.v1') != recorded.get('map_sha256'):
     50         raise ValueError('execution map identity mismatch')
     51     expected = dict(execution_io_version=execution,
     52                     worker_export=recorded['worker_export'],
     53                     compiler_bundle_sha256=recorded['compiler_bundle_sha256'],
     54                     adapter_sha256=recorded['adapter_sha256'],
     55                     arguments=[recorded['source_root'], str(output / 'overlay'), str(output / 'cas'),
     56                                recorded['compiler_bundle_sha256'], (output / 'source-map.v1').read_text(), recorded['root']])
     57     if request != expected:
     58         raise ValueError('execution request identity/arguments mismatch')
     59     if any(not isinstance(text, str) or '\0' in text for text in request['arguments']):
     60         raise ValueError('invalid compiler arguments')
     61     compiler = (output / 'compiler.arboricx').read_bytes()
     62     adapter = (output / 'io-adapter.arboricx').read_bytes()
     63     if (hashlib.sha256(compiler).hexdigest() != request['compiler_bundle_sha256']
     64             or hashlib.sha256(adapter).hexdigest() != request['adapter_sha256']):
     65         raise ValueError('captured executable identity mismatch')
     66     return request, compiler, adapter
     67 
     68 
     69 def diagnostic(ctx, roots, value):
     70     # Observe only a bounded prefix of an ordinary compiler error. Terminal task
     71     # diagnostics never enter this function and remain opaque.
     72     characters, first = [], None
     73     try:
     74         while ctx.kind(value) == 2 and len(characters) < 240:
     75             head = roots.child(value, 0)
     76             number = ctx.to_number(head)
     77             if first is None:
     78                 first = number
     79             if number > 0x10ffff or 0xd800 <= number <= 0xdfff:
     80                 break
     81             characters.append(chr(number))
     82             value = roots.child(value, 1)
     83         if ctx.kind(value) == 0:
     84             return ''.join(characters)
     85         if len(characters) == 240:
     86             return ''.join(characters) + '…'
     87     except TricuError:
     88         pass
     89     return {'first_number': first} if first is not None else 'opaque compiler error'
     90 
     91 
     92 def child(args):
     93     resource.setrlimit(resource.RLIMIT_AS, (args.memory_mib * 1024**2,) * 2)
     94     resource.setrlimit(resource.RLIMIT_STACK, (8 * 1024**2,) * 2)
     95     resource.setrlimit(resource.RLIMIT_CPU, (args.seconds,) * 2)
     96     resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
     97     started = time.monotonic()
     98     request, compiler, adapter = captured_request(args.output, args.library)
     99     library = Library(args.library)
    100     with library.context() as ctx, ctx.scope() as roots:
    101         worker = roots.load_bundle(compiler, request['worker_export'])
    102         wrapper = roots.load_bundle(adapter, 'main')
    103         current = roots.apply_many(wrapper, worker, *(roots.string(text) for text in request['arguments']))
    104         prepared = time.monotonic()
    105         program = roots.reduce(current)
    106         forced = time.monotonic()
    107         outcome, result, cleanup = roots.run_io(program)
    108         ran = time.monotonic()
    109         message, ok = None, False
    110         if outcome != 0:
    111             message = 'opaque terminal task failure/cancellation'
    112         else:
    113             if ctx.kind(cleanup) != 0 or ctx.kind(result) != 2:
    114                 raise ValueError('invalid compiler Result/cleanup shape')
    115             ok = ctx.to_bool(roots.child(result, 0))
    116             payload = roots.child(result, 1)
    117             if ctx.kind(payload) != 2 or ctx.kind(roots.child(payload, 1)) != 0:
    118                 raise ValueError('invalid compiler Result remainder')
    119             if not ok:
    120                 message = diagnostic(ctx, roots, roots.child(payload, 0))
    121         record = dict(ok=ok, diagnostic=message, task_outcome=outcome, execution_io_version=2,
    122                       prepare_seconds=prepared-started, initial_reduce_seconds=forced-prepared,
    123                       io_seconds=ran-forced, total_seconds=time.monotonic()-started)
    124         (args.output / 'execution.json').write_text(json.dumps(record, indent=2) + '\n')
    125         if not ok:
    126             raise RuntimeError(message)
    127         print('ok: Haskell shared-library map-builder completed', flush=True)
    128         print(json.dumps(record), flush=True)
    129 
    130 
    131 if __name__ == '__main__':
    132     parser = argparse.ArgumentParser(description=__doc__)
    133     parser.add_argument('--library', type=Path, required=True)
    134     parser.add_argument('--output', type=Path)
    135     parser.add_argument('--validate-only', action='store_true')
    136     parser.add_argument('--memory-mib', type=int, default=1024)
    137     parser.add_argument('--seconds', type=int, default=30)
    138     args = parser.parse_args()
    139     if not 0 < args.memory_mib <= 65536 or not 0 < args.seconds <= 7200:
    140         parser.error('invalid resource boundaries')
    141     if args.validate_only:
    142         Library(args.library)
    143     elif args.output is None:
    144         parser.error('--output is required for execution')
    145     else:
    146         child(args)