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:
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)