tricu

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

commit 5b6448bd3a42ca510bda2ad0719e369f3d08a10c
parent 95c819bd37c5b4b3e4bbe17a3224707bc7195965
Author: James Eversole <james@eversole.co>
Date:   Fri, 16 Oct 2026 12:09:22 -0500

Separate generation execution host from Haskell artifact transport

Shared runner passes both-host cold/warm two-module execution, independent audits, complete receipt/object equality and result 41. Preserve publication/resource guards and record executor/library versus transport provenance. Full-generation acceptance remains unchanged.

Diffstat:
Mdocs/compiler-rebuilding.md | 25+++++++++++++++++++++++++
Mext/zig/README.md | 10++++++----
Mtest/bootstrap_generations.py | 24+++++++++++++++++++++++-
Atest/cross_host_generation.py | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/zig_compiler_smoke.py | 200-------------------------------------------------------------------------------
Atest/zig_generation_executor.py | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 255 insertions(+), 205 deletions(-)

diff --git a/docs/compiler-rebuilding.md b/docs/compiler-rebuilding.md @@ -57,6 +57,31 @@ uses `sourceMapBuildStable` or `sourceMapBuild`, while application selection use the chosen application bundle. Neither path silently substitutes another backend or the recovery frontend. +### Experimental execution-host selection + +`--execution-host zig --zig-library /nix/store/.../lib/libarboricx.so` selects +Zig execution through the owned ABI. `--tricu` still selects the Haskell runtime +for authenticated input transport and output packaging; compilation and +publication execute only on Zig. The default execution host remains Haskell. +The lowering `--backend` is an independent selection and is never inferred from +the execution host. Missing libraries/exports fail without backend fallback. + +Provenance records the execution runtime and transport runtime separately. +Historical records lacking `execution_host` describe Haskell execution. Zig's +recorded runtime is the shared library, not the Python execution launcher; the +launcher is separately fingerprinted. The normal shared supervisor retains source +consistency checks, independent store audits, publication deadlines, whole-job +limits, and incomplete status on failure. Zig additionally uses an address-space +ceiling, CPU ceiling, and an 8 MiB native stack; it receives no GHC RTS flags. +Both generation execution routes grant trusted compiler jobs broad file IO; +these runners are not an untrusted-program filesystem sandbox. + +Bounded cross-host validation uses `test/cross_host_generation.py`, documented +in the [Zig runtime guide](../ext/zig/README.md#small-compiler-probe). This validates +the shared runner on small closures, not full compiler reproduction. Existing +pair acceptance and retention remain Haskell-only until explicitly generalized; +selecting Zig execution does not bypass either gate. + ### Explicit bootstrap kit The repository includes the accepted kit at `bootstrap/rebuild/`. It works from diff --git a/ext/zig/README.md b/ext/zig/README.md @@ -60,7 +60,8 @@ nix develop -c python3 ext/zig/tests/performance_smoke.py \ ## Small compiler probe -`test/zig_compiler_smoke.py` runs only a two-module native map-builder job using +`test/cross_host_generation.py` runs a two-module native map-builder job through +the shared generation runner on both Haskell and Zig, using the authenticated `bootstrap/rebuild` kit. It does not rebuild the compiler, modify retained toolchains, or promote a generation. Haskell authenticates the captured input bundles; Zig applies the retained compiler and adapter and drives @@ -71,13 +72,14 @@ IO. Successful publication must pass the independent store audit and produce nix build .#tricu-zig .# --no-link ZIG=$(nix path-info .#tricu-zig) HASKELL=$(nix path-info .#) -nix develop -c python3 test/zig_compiler_smoke.py \ +nix develop -c python3 test/cross_host_generation.py \ --library "$ZIG/lib/libarboricx.so" --tricu "$HASKELL/bin/tricu" \ --directory /tmp/zig-compiler-probe-new ``` -The output directory must not exist. Default limits are 1 GiB and 120 seconds -per subprocess, with an 8 MiB stack for Zig. Inputs, runtime hashes, logs and +The output directory must not exist. Limits are 1 GiB per child and 120 seconds +per generation invocation, with an 8 MiB stack for Zig. Cold and warm runs must +publish identical complete receipt and object sets across hosts. Inputs, runtime hashes, logs and incomplete/success status are recorded there. This is an opt-in compatibility probe, not a compiler generation, bootstrap acceptance, or a comparative performance benchmark. Unsupported actions during a pending IO continuation diff --git a/test/bootstrap_generations.py b/test/bootstrap_generations.py @@ -83,6 +83,8 @@ def main(): parser.add_argument('previous', type=Path, nargs='?', help='completed generation; omit when selecting --bootstrap-kit') parser.add_argument('output', type=Path) parser.add_argument('--tricu', default='result/bin/tricu') + parser.add_argument('--execution-host', choices=('haskell', 'zig'), default='haskell') + parser.add_argument('--zig-library', type=Path, help='explicit Nix-built owned-ABI library; required for Zig execution') parser.add_argument('--backend', choices=BACKENDS, default=DEFAULT_BACKEND, help='generation lowering backend (default: %(default)s); never inferred from previous') parser.add_argument('--worker-bundle', type=Path, help='explicit pinned transition worker exporting the selected backend entrypoint; no fallback') @@ -103,6 +105,10 @@ def main(): args = parser.parse_args() if not 0 < args.memory_mib <= 65536 or args.capabilities <= 0 or not 0 < args.wall_seconds <= 7200 or not 0 < args.initial_seconds <= args.extended_seconds: parser.error('invalid resource boundaries') + if (args.execution_host == 'zig') != (args.zig_library is not None): + parser.error('--zig-library is required exactly when --execution-host zig is selected') + if args.zig_library and not args.zig_library.is_file(): + parser.error('selected Zig library is unavailable; no fallback') previous = args.previous.resolve() if args.previous else None output = args.output.resolve() from bootstrap_retention_lock import protect_generations @@ -211,7 +217,13 @@ def main(): bootstrap_kit_sha256=kit_digest, compiler_bundle_sha256=compiler, compiler_manifest=compiler_manifest, adapter_sha256=hashlib.sha256(adapter_bytes).hexdigest(), - runtime_sha256=hashlib.sha256(Path(tricu).read_bytes()).hexdigest(), **inputs) + execution_host=args.execution_host, + runtime_path=str(args.zig_library.resolve()) if args.zig_library else tricu, + runtime_sha256=hashlib.sha256((args.zig_library or Path(tricu)).read_bytes()).hexdigest(), + transport_host='haskell', transport_runtime_path=tricu, + transport_runtime_sha256=hashlib.sha256(Path(tricu).read_bytes()).hexdigest(), + executor_sha256=hashlib.sha256((REPO / 'test/zig_generation_executor.py').read_bytes()).hexdigest() if args.zig_library else None, + **inputs) (output / 'inputs.json').write_text(json.dumps(provenance, indent=2) + '\n') (output / 'source-map.v1').write_text(map_text) cmd = [tricu, 'arboricx', 'run', '--store', str(output / 'compiler-cas'), @@ -221,6 +233,16 @@ def main(): '--arg', compiler, '--arg', map_text, '--arg', args.root, '--io', '--unsafe-io', '--result', '--format', 'decode', '+RTS', '-N' + str(args.capabilities), '-M' + str(args.memory_mib) + 'm', '-T', '-s', '-RTS'] + if args.execution_host == 'zig': + request = dict(worker_export=worker_export, compiler_bundle_sha256=compiler, + adapter_sha256=provenance['adapter_sha256'], + arguments=[str(source_root), str(output / 'overlay'), str(output / 'cas'), + compiler, map_text, args.root]) + (output / 'execution-request.json').write_text(json.dumps(request, indent=2) + '\n') + import math + cmd = [sys.executable, str(REPO / 'test/zig_generation_executor.py'), + '--library', str(args.zig_library.resolve()), '--output', str(output), + '--memory-mib', str(args.memory_mib), '--seconds', str(math.ceil(remaining()))] (output / 'invocation.json').write_text(json.dumps(cmd, indent=2)) log = output / f'native-{time.time_ns()}.log' (output / 'status.json').write_text(json.dumps(dict(complete=False, diff --git a/test/cross_host_generation.py b/test/cross_host_generation.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Opt-in bounded parity checks through the shared generation runner. +No compiler generation or toolchain promotion. Haskell transports and verifies; +only the selected execution host may perform compilation and publication. +""" +import argparse +import hashlib +import json +from pathlib import Path +import sys +from process_rss import run_guarded + +ROOT = Path(__file__).resolve().parent.parent + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--tricu', type=Path, required=True) + p.add_argument('--library', type=Path, required=True) + p.add_argument('--directory', type=Path, required=True) + args = p.parse_args() + directory = args.directory.resolve() + directory.mkdir(parents=True, exist_ok=False) + source = directory / 'source' + source.mkdir() + (source / 'dep.tri').write_text('!export answer\nanswer = 41\n') + (source / 'app.tri').write_text('!import dep as D\n!export main\nmain = D.answer\n') + source_map = directory / 'source-map.v1' + source_map.write_text('arboricx.source-map.v1\nnative\tdep\tdep\tdep.tri\t-\nnative\tapp\tapp\tapp.tri\tdep\n') + report = dict(complete=False, stages=[]) + def save(): + (directory / 'report.json').write_text(json.dumps(report, indent=2)+'\n') + def run(label, command): + try: + output, peak = run_guarded(list(map(str,command)), memory_mib=1024, seconds=150) + except Exception as error: + (directory / (label+'.log')).write_text(str(getattr(error,'output',''))+'\n'+str(error)) + report['failure'] = label + save() + raise + (directory / (label+'.log')).write_text(output) + report['stages'].append(dict(label=label,peak_kib=peak)) + save() + return output + def checkpoints(host): + return {name:(directory/host/'cas/cache/source-build'/name).read_bytes() for name in ('dep','app')} + save() + for host in ('haskell','zig'): + output = directory / host + command = [sys.executable, ROOT/'test/bootstrap_generations.py', output, + '--bootstrap-kit', ROOT/'bootstrap/rebuild', '--tricu',args.tricu.resolve(), + '--execution-host',host,'--source-root',source,'--map',source_map,'--root','app', + '--backend','stable','--memory-mib','1024','--wall-seconds','120', + '--initial-seconds','60','--extended-seconds','100'] + if host == 'zig': command += ['--zig-library',args.library.resolve()] + run(host+'-cold',command+['--fresh']) + status = json.loads((output/'status.json').read_text()) + assert status['complete'] and status['execution_host']==host and status['fresh_store'] + assert status['transport_host']=='haskell' + runtime = args.tricu if host == 'haskell' else args.library + assert status['runtime_sha256']==hashlib.sha256(runtime.read_bytes()).hexdigest() + invocation = json.loads((output/'invocation.json').read_text()) + assert ('+RTS' in invocation) == (host == 'haskell') + cold = checkpoints(host) + run(host+'-warm',command) + assert cold == checkpoints(host) + pin = cold['app'].decode().split('\t')[2] + answer = run(host+'-result',[args.tricu,'arboricx','run','--store',output/'cas', + '--module',pin,'--name','main','--format','number', + '+RTS','-N1','-M1024m','-RTS']) + assert answer.strip()=='41', answer + assert checkpoints('haskell') == checkpoints('zig'), 'complete module receipts differ' + # Exact independently audited object sets, not just root result equality. + objects = lambda host: {p.name:p.read_bytes() for p in (directory/host/'cas/objects').glob('*/*')} + assert objects('haskell') == objects('zig'), 'published object sets differ' + report['complete'] = True + save() + print('Shared-runner cold/warm parity, exact receipts/objects and result 41 passed: '+str(directory)) + + +if __name__ == '__main__': + main() diff --git a/test/zig_compiler_smoke.py b/test/zig_compiler_smoke.py @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -"""Bounded Zig execution of a tiny retained map-builder job; NOT a generation. - -The Haskell host authenticates input transport and runs the published result. -The existing independent store auditor checks the complete two-module closure. -No source compiler is built and no toolchain is promoted or retained here. -Run through nix develop; select Nix-built runtime/library paths explicitly. -""" -import argparse -import ctypes as C -import hashlib -import json -from pathlib import Path -import resource -import subprocess -import sys -import time - -from bootstrap_inputs import load_kit -from bootstrap_generations import source_map, snapshot -from generation_backend import BACKENDS -from process_rss import run_guarded - -ROOT = Path(__file__).resolve().parent.parent - - -def sha(path): - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def save(path, value): - path.write_text(json.dumps(value, indent=2) + '\n') - - -def child(args): - # No asynchronous subprocesses in the Zig IO driver; the parent additionally - # guards this whole process group with a wall deadline and RSS sampling. - resource.setrlimit(resource.RLIMIT_AS, (args.memory_mib * 1024**2,) * 2) - resource.setrlimit(resource.RLIMIT_STACK, (8 * 1024**2,) * 2) - resource.setrlimit(resource.RLIMIT_CPU, (args.seconds, args.seconds)) - resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) - lib = C.CDLL(str(args.library)) - handle, ctx_t = C.c_uint64, C.c_void_p - def bind(name, types, result=C.c_int): - f = getattr(lib, name) - f.argtypes, f.restype = types, result - return f - init = bind('arboricx_init', [], ctx_t) - free = bind('arboricx_free', [ctx_t], None) - version = bind('arb_abi_version', [], C.c_uint32) - assert version() == 2 - load = bind('arb_load_bundle', [ctx_t, C.c_void_p, C.c_size_t, C.c_char_p, C.POINTER(handle)]) - string = bind('arb_of_string', [ctx_t, C.c_char_p, C.POINTER(handle)]) - app = bind('arb_app', [ctx_t, handle, handle, C.POINTER(handle)]) - release = bind('arb_release', [ctx_t, handle]) - reduce = bind('arb_reduce', [ctx_t, handle, C.c_uint64, C.POINTER(handle)]) - unwrap = bind('arb_unwrap_result', [ctx_t, handle, C.POINTER(C.c_int), C.POINTER(handle), C.POINTER(handle)]) - to_string = bind('arb_to_string', [ctx_t, handle, C.POINTER(C.c_void_p), C.POINTER(C.c_size_t)]) - free_buf = bind('arboricx_free_buf', [ctx_t, C.c_void_p, C.c_size_t], None) - class Perms(C.Structure): - _fields_ = [('read', C.c_int), ('write', C.c_int)] - run_io = bind('arb_run_io', [ctx_t, handle, C.POINTER(Perms), C.POINTER(handle)]) - def check(status): - if status != 0: - raise RuntimeError(f'ABI status {status}') - ctx = init() - if not ctx: - raise MemoryError('context initialization') - def owned(f, *arguments): - out = handle() - check(f(ctx, *arguments, C.byref(out))) - return out.value - def apply(f, x): - result = owned(app, f, x) - check(release(ctx, f)); check(release(ctx, x)) - return result - run = args.directory / 'run' - started = time.monotonic() - try: - compiler_bytes = (run / 'compiler.arboricx').read_bytes() - adapter_bytes = (run / 'io-adapter.arboricx').read_bytes() - compiler = owned(load, compiler_bytes, len(compiler_bytes), b'sourceMapBuildStable') - adapter = owned(load, adapter_bytes, len(adapter_bytes), b'main') - current = apply(adapter, compiler) - for argument in [str(args.directory / 'source'), str(run / 'overlay'), str(run / 'cas'), - sha(run / 'compiler.arboricx'), (run / 'source-map.v1').read_text(), 'app']: - current = apply(current, owned(string, argument.encode())) - prepared = time.monotonic() - print(json.dumps({'stage': 'loaded-and-applied', 'seconds': prepared - started}), flush=True) - program = owned(reduce, current, 1_000_000_000) - check(release(ctx, current)) - forced = time.monotonic() - print(json.dumps({'stage': 'io-program', 'seconds': forced - prepared}), flush=True) - result = owned(run_io, program, C.byref(Perms(1, 1))) - check(release(ctx, program)) - ran = time.monotonic() - ok, value, rest = C.c_int(), handle(), handle() - check(unwrap(ctx, result, C.byref(ok), C.byref(value), C.byref(rest))) - diagnostic = None - if not ok.value: - ptr, length = C.c_void_p(), C.c_size_t() - status = to_string(ctx, value, C.byref(ptr), C.byref(length)) - if status == 0: - diagnostic = C.string_at(ptr, length.value).decode('utf-8', 'replace') - free_buf(ctx, ptr, length.value) - else: - diagnostic = f'non-string diagnostic (decode status {status})' - record = dict(ok=bool(ok.value), diagnostic=diagnostic, - prepare_seconds=prepared-started, initial_reduce_seconds=forced-prepared, - io_seconds=ran-forced, total_seconds=time.monotonic()-started) - save(run / 'execution.json', record) - print(json.dumps(record), flush=True) - if not ok.value: - raise RuntimeError(diagnostic) - finally: - free(ctx) - - -def main(args): - if not 0 < args.memory_mib <= 2048 or not 0 < args.seconds <= 180: - raise ValueError('smoke limits: at most 2 GiB and 180 seconds per invocation') - kit_bytes, payloads, kit_digest = load_kit(args.kit, 'stable') - args.directory.mkdir(parents=True, exist_ok=False) - source, run = args.directory / 'source', args.directory / 'run' - source.mkdir(); run.mkdir() - (source / 'dep.tri').write_text('!export answer\nanswer = 41\n') - (source / 'app.tri').write_text('!import dep as D\n!export main\nmain = D.answer\n') - map_path = run / 'source-map.v1' - map_path.write_text('arboricx.source-map.v1\nnative\tdep\tdep\tdep.tri\t-\nnative\tapp\tapp\tapp.tri\tdep\n') - text, entries, required = source_map(map_path, source, 'app') - assert required == {'dep', 'app'} - (run / 'compiler.arboricx').write_bytes(payloads['compiler']) - (run / 'io-adapter.arboricx').write_bytes(payloads['adapter']) - (run / 'bootstrap-kit.json').write_bytes(kit_bytes) - export, options = BACKENDS['stable'] - provenance = dict(scope='zig-small-compiler-smoke', root='app', backend='stable', - backend_options=options, worker_export=export, source_root=str(source), - compiler_bundle_sha256=sha(run / 'compiler.arboricx'), - adapter_sha256=sha(run / 'io-adapter.arboricx'), kit_sha256=kit_digest, - zig_library=str(args.library), zig_library_sha256=sha(args.library), - haskell=str(args.tricu), haskell_sha256=sha(args.tricu), - limits=dict(memory_mib=args.memory_mib, seconds=args.seconds, stack_mib=8), - **snapshot(text, entries, source)) - save(run / 'inputs.json', provenance) - report = dict(complete=False, **provenance) - save(run / 'status.json', report) - def guarded(label, argv): - started = time.monotonic() - try: - output, peak = run_guarded(list(map(str, argv)), memory_mib=args.memory_mib, seconds=args.seconds) - except Exception as error: - output = getattr(error, 'output', '') or str(error) - (run / (label + '.log')).write_text(str(output)) - report['failure'] = dict(stage=label, error=str(error), seconds=time.monotonic()-started) - save(run / 'status.json', report) - raise - (run / (label + '.log')).write_text(output) - report.setdefault('stages', {})[label] = dict(seconds=time.monotonic()-started, peak_kib=peak) - save(run / 'status.json', report) - print(label + ': ' + output.strip(), flush=True) - return output - # The C loader is not yet an authenticated transport boundary. Have the - # mature host validate the exact captured kit payloads before Zig loads them. - for filename in ('compiler.arboricx', 'io-adapter.arboricx'): - guarded('authenticate-' + filename, [args.tricu, 'arboricx', 'import', '--file', run / filename, - '--store', args.directory / 'transport', '--indexed', '+RTS', '-N1', '-RTS']) - command = [sys.executable, Path(__file__).resolve(), '--child', '--library', args.library, - '--tricu', args.tricu, '--directory', args.directory, - '--memory-mib', args.memory_mib, '--seconds', args.seconds] - guarded('zig-cold', command) - guarded('audit-cold', [sys.executable, ROOT / 'test/bootstrap_store_check.py', run]) - assert snapshot(text, entries, source) == {k: provenance[k] for k in ('map_sha256', 'sources')} - checkpoints = {name: (run / 'cas/cache/source-build' / name).read_bytes() for name in required} - manifest = checkpoints['app'].decode().split('\t')[2] - answer = guarded('run-result', [args.tricu, 'arboricx', 'run', '--store', run / 'cas', - '--module', manifest, '--name', 'main', '--format', 'number', '+RTS', '-N1', '-RTS']) - assert answer.strip() == '41', answer - report['answer'] = 41 - # Fresh Zig context, same store: demonstrate actual verified warm reuse. - guarded('zig-warm', command) - assert checkpoints == {name: (run / 'cas/cache/source-build' / name).read_bytes() for name in required} - guarded('audit-warm', [sys.executable, ROOT / 'test/bootstrap_store_check.py', run]) - report['complete'] = True - save(run / 'status.json', report) - print(json.dumps(report, indent=2)) - - -if __name__ == '__main__': - p = argparse.ArgumentParser(description=__doc__) - p.add_argument('--library', type=Path, required=True) - p.add_argument('--tricu', type=Path, required=True) - p.add_argument('--directory', type=Path, required=True) - p.add_argument('--kit', type=Path, default=ROOT / 'bootstrap/rebuild') - p.add_argument('--memory-mib', type=int, default=1024) - p.add_argument('--seconds', type=int, default=120) - p.add_argument('--child', action='store_true', help=argparse.SUPPRESS) - args = p.parse_args() - for key in ('library', 'tricu', 'directory', 'kit'): - setattr(args, key, getattr(args, key).resolve()) - child(args) if args.child else main(args) diff --git a/test/zig_generation_executor.py b/test/zig_generation_executor.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Execution-only child for bootstrap_generations; no transport or compiler policy. + +The supervisor authenticates/imports captured bundles, supplies the exact six +arguments and independently audits all publications. This child rechecks captured +bundle digests, runs the selected entrypoint via the owned ABI, and never falls +back to Haskell. IO permissions match the generation runner's unsafe-IO scope; +this is for trusted compiler jobs, not an untrusted-program sandbox. +""" +import argparse +import ctypes as C +import hashlib +import json +from pathlib import Path +import resource +import time + + +def save(path, value): + path.write_text(json.dumps(value, indent=2) + '\n') + + +def child(args): + # No asynchronous subprocesses in the Zig IO driver; the parent additionally + # guards this whole process group with a wall deadline and RSS sampling. + resource.setrlimit(resource.RLIMIT_AS, (args.memory_mib * 1024**2,) * 2) + resource.setrlimit(resource.RLIMIT_STACK, (8 * 1024**2,) * 2) + resource.setrlimit(resource.RLIMIT_CPU, (args.seconds, args.seconds)) + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + lib = C.CDLL(str(args.library)) + handle, ctx_t = C.c_uint64, C.c_void_p + def bind(name, types, result=C.c_int): + f = getattr(lib, name) + f.argtypes, f.restype = types, result + return f + init = bind('arboricx_init', [], ctx_t) + free = bind('arboricx_free', [ctx_t], None) + version = bind('arb_abi_version', [], C.c_uint32) + assert version() == 2 + load = bind('arb_load_bundle', [ctx_t, C.c_void_p, C.c_size_t, C.c_char_p, C.POINTER(handle)]) + string = bind('arb_of_string', [ctx_t, C.c_char_p, C.POINTER(handle)]) + app = bind('arb_app', [ctx_t, handle, handle, C.POINTER(handle)]) + release = bind('arb_release', [ctx_t, handle]) + reduce = bind('arb_reduce', [ctx_t, handle, C.c_uint64, C.POINTER(handle)]) + unwrap = bind('arb_unwrap_result', [ctx_t, handle, C.POINTER(C.c_int), C.POINTER(handle), C.POINTER(handle)]) + to_string = bind('arb_to_string', [ctx_t, handle, C.POINTER(C.c_void_p), C.POINTER(C.c_size_t)]) + free_buf = bind('arboricx_free_buf', [ctx_t, C.c_void_p, C.c_size_t], None) + class Perms(C.Structure): + _fields_ = [('read', C.c_int), ('write', C.c_int)] + run_io = bind('arb_run_io', [ctx_t, handle, C.POINTER(Perms), C.POINTER(handle)]) + def check(status): + if status != 0: + raise RuntimeError(f'ABI status {status}') + ctx = init() + if not ctx: + raise MemoryError('context initialization') + def owned(f, *arguments): + out = handle() + check(f(ctx, *arguments, C.byref(out))) + return out.value + def apply(f, x): + result = owned(app, f, x) + check(release(ctx, f)); check(release(ctx, x)) + return result + run = args.output + request = json.loads((run / 'execution-request.json').read_text()) + started = time.monotonic() + try: + compiler_bytes = (run / 'compiler.arboricx').read_bytes() + adapter_bytes = (run / 'io-adapter.arboricx').read_bytes() + if hashlib.sha256(compiler_bytes).hexdigest() != request['compiler_bundle_sha256'] or hashlib.sha256(adapter_bytes).hexdigest() != request['adapter_sha256']: + raise ValueError('captured executable identity mismatch') + compiler = owned(load, compiler_bytes, len(compiler_bytes), request['worker_export'].encode()) + adapter = owned(load, adapter_bytes, len(adapter_bytes), b'main') + current = apply(adapter, compiler) + for argument in request['arguments']: + if '\0' in argument: + raise ValueError('NUL in compiler argument') + current = apply(current, owned(string, argument.encode())) + prepared = time.monotonic() + program = owned(reduce, current, 1_000_000_000) + check(release(ctx, current)) + forced = time.monotonic() + result = owned(run_io, program, C.byref(Perms(1, 1))) + check(release(ctx, program)) + ran = time.monotonic() + ok, value, rest = C.c_int(), handle(), handle() + check(unwrap(ctx, result, C.byref(ok), C.byref(value), C.byref(rest))) + diagnostic = None + if not ok.value: + ptr, length = C.c_void_p(), C.c_size_t() + status = to_string(ctx, value, C.byref(ptr), C.byref(length)) + if status == 0: + diagnostic = C.string_at(ptr, length.value).decode('utf-8', 'replace') + free_buf(ctx, ptr, length.value) + else: + diagnostic = f'non-string diagnostic (decode status {status})' + record = dict(ok=bool(ok.value), diagnostic=diagnostic, + prepare_seconds=prepared-started, initial_reduce_seconds=forced-prepared, + io_seconds=ran-forced, total_seconds=time.monotonic()-started) + save(run / 'execution.json', record) + if not ok.value: + raise RuntimeError(diagnostic) + print('ok: Zig map-builder completed', flush=True) + print(json.dumps(record), flush=True) + finally: + free(ctx) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--library', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--memory-mib', type=int, required=True) + parser.add_argument('--seconds', type=int, required=True) + args = parser.parse_args() + if not 0 < args.memory_mib <= 65536 or not 0 < args.seconds <= 7200: + parser.error('invalid resource boundaries') + child(args)