commit 7cd481b4a9c4280c56135562bf6f11d9e0242605
parent b49e05e66a98cf0a87c15ee7f08000f48ab4e5f3
Author: James Eversole <james@eversole.co>
Date: Fri, 16 Oct 2026 13:59:24 -0500
Verify cross-runtime fixed point and complete post-generation results
Frozen a3b1cc2: isolated H1/H2/H3/Z3 pass; H2=H3=Z3 across 931 objects, receipts/checkpoints and all three bundles. H3 590s / Z3 2789s within 32 GiB/3600s budgets. Twelve independent complete results pass on each runtime; eight verifier mutation controls reject and restored copy passes. Rolling retention still pending.
Diffstat:
5 files changed, 282 insertions(+), 13 deletions(-)
diff --git a/docs/compiler-rebuilding.md b/docs/compiler-rebuilding.md
@@ -78,9 +78,71 @@ 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.
+the shared runner on small closures. Ordinary pair acceptance and retention
+toolchain roles remain Haskell-only. Cross-host reproduction has the separate
+verifier below; selecting Zig execution does not bypass acceptance or retention.
+
+### Isolated cross-runtime fixed-point reproduction
+
+The opt-in Linux experiment builds two Haskell generations, then forks the second
+compiler into a Haskell control and a Zig-executed third generation:
+
+```text
+seed --Haskell--> H1 --Haskell--> H2 --Haskell--> H3
+ `----Zig----> Z3
+```
+
+```sh
+nix develop -c python3 test/bootstrap_cross_host.py \
+ --source-commit COMMITTED_SOURCE --directory "$SCRATCH/cross-runtime-new" \
+ --tricu "$TRICU" --zig-library "$ZIG/lib/libarboricx.so"
+```
+
+Select exact Nix runtime paths. The output directory must not exist. The source
+commit supplies the frozen map, sources, kit and inner generation runner. Each job
+uses a fresh store in a filesystem sandbox without previous stores or the live
+checkout. Only the selected authenticated compiler/adapter kit crosses between
+jobs. Haskell performs common transport and packing; the selected execution host
+performs all compilation and publication. Jobs run sequentially with 32 GiB and
+one hour per job; Haskell uses eight capabilities. No budget increases or backend
+fallbacks are automatic. See the [isolated probe](../ext/zig/README.md#isolated-mapped-library-probe)
+for namespace requirements and bounded validation before a full run.
+
+`experiment.json` freezes source/archive hashes, mapped inputs, runtime/Python
+identities, tooling and comparison boundaries. Logs, invocations and runtime
+provenance deliberately differ. The independent verifier requires ordinary
+H1/H2 and H2/H3 acceptance, verifies that Z3 used H2's exact compiler, and requires
+H2/H3/Z3 to match in complete object inventories, module checkpoints/receipts and
+all three packaged compiler artifacts. It does not trust recorded comparison
+booleans. `accepted-cross-host.json` records this reproduction result, not completed
+promotion or cleanup. `--verify-only` rechecks existing results without compilation.
+
+Post-generation validation includes the ordinary H2 smoke, then complete result
+observation on Zig:
+
+```sh
+nix develop -c python3 test/bootstrap_smoke.py "$TRICU" \
+ --generation "$EXPERIMENT/H2/generation" \
+ --acceptance "$EXPERIMENT/accepted-pair.json" \
+ --source-root "$EXPERIMENT/source" --directory "$EXPERIMENT/haskell-smoke"
+nix develop -c python3 test/bootstrap_cross_host_smoke.py "$EXPERIMENT" \
+ --library "$ZIG/lib/libarboricx.so" --directory "$EXPERIMENT/zig-program-smoke"
+nix develop -c python3 test/bootstrap_cross_host_controls.py "$EXPERIMENT" \
+ --directory "$SCRATCH/cross-host-controls-new"
+```
+
+The program smoke covers native imports, recursive lists, and invoice totals and
+decimal text against independent complete trees. Its program compilation is
+Haskell-executed using the bit-identical accepted compiler; result execution is
+checked on both hosts. Negative controls mutate independent copies only and reject
+false host/runtime/freshness/compiler claims, corrupt objects, changed packaged
+artifacts and additional valid objects. Restored copies must pass again.
+
+Finish ordinary rolling retention with H2 as the current Haskell toolchain and an
+independently useful rollback. Explicitly protect H3/Z3, frozen inputs, cross-host
+reports, and a registered Nix root for the Zig runtime as additional evidence.
+This milestone demonstrates cross-runtime reproduction, not Haskell-free setup,
+transport, packaging, or distribution.
### Explicit bootstrap kit
diff --git a/test/bootstrap_cross_host_controls.py b/test/bootstrap_cross_host_controls.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""Negative controls on a completed experiment; mutate only independent copies.
+No compiler execution, no publication and no changes to accepted stores.
+"""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import shutil
+import subprocess
+from bootstrap_acceptance import sha
+from bootstrap_cross_host import verify_cross
+from native_indexed_store import indexed_bytes
+from base_workloads import encode
+
+
+def main():
+ p=argparse.ArgumentParser(description=__doc__)
+ p.add_argument('experiment',type=Path)
+ p.add_argument('--directory',type=Path,required=True)
+ a=p.parse_args(); a.directory.mkdir(parents=True,exist_ok=False)
+ e=a.experiment.resolve(); definition=json.loads((e/'experiment.json').read_text())
+ runtime=Path(definition['runtime']['path']); library=Path(definition['zig_library']['path'])
+ first,second,control,original=(e/name/'generation' for name in ('H1','H2','H3','Z3'))
+ source=e/'source'; copy=a.directory/'zig-copy'
+ shutil.copytree(original,copy)
+ report=dict(complete=False,rejected=[],acceptance_sha256=sha(e/'accepted-cross-host.json'))
+ def reject(label,expected,zig=copy):
+ try: verify_cross(first,second,control,zig,runtime,library,source)
+ except (ValueError,AssertionError,subprocess.CalledProcessError) as error:
+ if expected and expected not in str(error): raise
+ report['rejected'].append(label)
+ (a.directory/(label+'.log')).write_text(str(error)+'\n'+str(getattr(error,'output','')))
+ else: raise AssertionError('invalid experiment accepted: '+label)
+ try:
+ reject('same-directory','four distinct',zig=control)
+ for key,value,message in [('execution_host','haskell','execution host mismatch'),
+ ('runtime_sha256','0'*64,'runtime identity mismatch'),
+ ('fresh_store',False,'fresh-store execution evidence'),
+ ('compiler_bundle_sha256','0'*64,'compiler bundle identity mismatch')]:
+ saved={name:(copy/name).read_bytes() for name in ('inputs.json','status.json')}
+ for name,payload in saved.items():
+ record=json.loads(payload); record[key]=value
+ (copy/name).write_text(json.dumps(record))
+ reject(key,message)
+ for name,payload in saved.items(): (copy/name).write_bytes(payload)
+ status=json.loads((copy/'status.json').read_text())
+ digest=status['artifacts']['build-stable.arboricx']['tree']
+ obj=copy/'cas/objects'/digest[:3]/digest
+ original_object=obj.read_bytes(); obj.write_bytes(b'corrupt')
+ reject('corrupt-object',None)
+ obj.write_bytes(original_object)
+ package=copy/'build-stable.arboricx'; original_package=package.read_bytes()
+ package.write_bytes(original_package[:-1]+bytes([original_package[-1]^1]))
+ status['artifacts']['build-stable.arboricx']['sha256']=sha(package)
+ (copy/'status.json').write_text(json.dumps(status))
+ reject('different-authenticated-package','cross-host packaged artifact mismatch')
+ package.write_bytes(original_package)
+ shutil.copyfile(original/'status.json',copy/'status.json')
+ extra=indexed_bytes(encode(987654321))
+ digest=hashlib.sha256(b'arboricx.indexed-tree.v1\0'+extra).hexdigest()
+ obj=copy/'cas/objects'/digest[:3]/digest
+ assert not obj.exists(); obj.parent.mkdir(exist_ok=True); obj.write_bytes(extra)
+ reject('additional-valid-object','complete canonical object/receipt inventory differs')
+ obj.unlink()
+ # The restored copy must pass, so failure is not caused merely by relocation.
+ verify_cross(first,second,control,copy,runtime,library,source)
+ report.update(complete=True,restored_copy_passes=True)
+ print('PASS: '+str(len(report['rejected']))+' cross-host rejection controls; restored copy accepted')
+ finally: (a.directory/'report.json').write_text(json.dumps(report,indent=2)+'\n')
+
+
+if __name__=='__main__': main()
diff --git a/test/bootstrap_cross_host_smoke.py b/test/bootstrap_cross_host_smoke.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+"""Observe the post-generation program on Zig against independent complete trees.
+Requires the Haskell post-acceptance smoke and cross-host reproduction evidence.
+Compilation of this program was Haskell-executed; compiler bundles are bit-identical.
+"""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import sys
+from bootstrap_smoke import smoke_cases
+from bootstrap_acceptance import sha
+from process_rss import run_guarded
+
+ROOT=Path(__file__).resolve().parent.parent
+
+
+def main():
+ p=argparse.ArgumentParser(description=__doc__)
+ p.add_argument('experiment',type=Path)
+ p.add_argument('--library',type=Path,required=True)
+ p.add_argument('--directory',type=Path,required=True)
+ a=p.parse_args(); a.directory.mkdir(parents=True,exist_ok=False)
+ accepted=json.loads((a.experiment/'accepted-cross-host.json').read_text())
+ hs=a.experiment/'haskell-smoke'
+ previous=json.loads((hs/'report.json').read_text())
+ assert accepted['accepted'] and previous['complete']
+ assert sha(a.library)==accepted['zig_inputs']['runtime_sha256']
+ assert previous['compiler_sha256']==accepted['artifacts']['build-stable.arboricx']
+ assert sha(hs/'program.arboricx')==previous['bundle_sha256']
+ report=dict(complete=False,compiler_sha256=previous['compiler_sha256'],
+ bundle_sha256=previous['bundle_sha256'],runtime_sha256=sha(a.library),
+ acceptance_sha256=sha(a.experiment/'accepted-cross-host.json'),
+ compilation_host='haskell',execution_host='zig',
+ observer_sha256=sha(ROOT/'test/zig_observe.py'),cases=[])
+ try:
+ for i,(export,argument,expected) in enumerate(smoke_cases()):
+ input_path=a.directory/(str(i)+'.input.tree'); input_path.write_bytes(argument)
+ output=a.directory/(str(i)+'.output.tree')
+ reply,peak=run_guarded([sys.executable,str(ROOT/'test/zig_observe.py'),
+ '--library',str(a.library),'--bundle',str(hs/'program.arboricx'),
+ '--export',export,'--argument',str(input_path),'--output',str(output)],
+ memory_mib=512,seconds=65)
+ (a.directory/(str(i)+'.log')).write_text(reply)
+ assert output.read_bytes()==expected, (export,i)
+ assert sha(output)==previous['cases'][i]['output_sha256']
+ report['cases'].append(dict(export=export,input_sha256=sha(input_path),output_sha256=sha(output),peak_kib=peak))
+ report.update(complete=True,wrong_output_rejected=(output.read_bytes()!=b'\0'))
+ assert report['wrong_output_rejected']
+ print('PASS: 12 complete results on Zig, including recursive lists and invoice totals/text')
+ finally: (a.directory/'report.json').write_text(json.dumps(report,indent=2)+'\n')
+
+
+if __name__=='__main__': main()
diff --git a/test/bootstrap_smoke.py b/test/bootstrap_smoke.py
@@ -46,6 +46,20 @@ def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
+def smoke_cases():
+ cases = [('imports', b'\0', n([23,0])), ('imports', b'\1\0', n([23,1])),
+ ('imports', b'\2\0\0', n([23,2]))]
+ for values in [[],[7],list(range(32))]:
+ cases += [('copy',n(values),n(values)),('reverseLocal',n(values),n(values[::-1]))]
+ for entries in [[],[(3,125),(2,49),(0,999),(1,1000)],[(9,9999),(11,12345)]]:
+ argument = b'\0'
+ for quantity,price in reversed(entries):
+ argument = pair(pair(n(quantity),n(price)),argument)
+ total = sum(q*p for q,p in entries)
+ cases.append(('invoice',argument,result(True,pair(n(total),n(list(str(total).encode()))))))
+ return cases
+
+
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('tricu', type=Path)
@@ -91,16 +105,7 @@ def main():
imported = runner.run('native-import', ['import','--file',bundle,'--store',runner.store])
pin = re.search(r'Created module alias .* -> ([0-9a-f]{64})', imported)[1]
assert pin == report['modules']['program']['manifest']
- cases = [('imports', b'\0', n([23,0])), ('imports', b'\1\0', n([23,1])),
- ('imports', b'\2\0\0', n([23,2]))]
- for values in [[],[7],list(range(32))]:
- cases += [('copy',n(values),n(values)),('reverseLocal',n(values),n(values[::-1]))]
- for entries in [[],[(3,125),(2,49),(0,999),(1,1000)],[(9,9999),(11,12345)]]:
- argument = b'\0'
- for quantity,price in reversed(entries):
- argument = pair(pair(n(quantity),n(price)),argument)
- total = sum(q*p for q,p in entries)
- cases.append(('invoice',argument,result(True,pair(n(total),n(list(str(total).encode()))))))
+ cases = smoke_cases()
for i,(export,argument,expected) in enumerate(cases):
path = root/'argument.tree'; path.write_bytes(argument)
actual = runner.invoke(str(i),pin,export,['--arg-tree-file',path])
diff --git a/test/zig_observe.py b/test/zig_observe.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+"""Bounded test observer: invoke a named bundle export and emit its entire tree.
+Flat test data only; this is not a production transport or diagnostic renderer.
+"""
+import argparse
+import ctypes as C
+from pathlib import Path
+import resource
+from executable_object import FLAT, verify_payload
+
+
+def main():
+ p=argparse.ArgumentParser(description=__doc__)
+ for key in ('library','bundle','argument','output'): p.add_argument('--'+key,type=Path,required=True)
+ p.add_argument('--export',required=True)
+ a=p.parse_args()
+ for kind,limit in [(resource.RLIMIT_AS,512*1024**2),(resource.RLIMIT_STACK,8*1024**2),
+ (resource.RLIMIT_CPU,60),(resource.RLIMIT_CORE,0)]:
+ resource.setrlimit(kind,(limit,limit))
+ lib=C.CDLL(str(a.library)); H=C.c_uint64; CTX=C.c_void_p
+ def bind(name,types,result=C.c_int):
+ f=getattr(lib,name); f.argtypes=types; f.restype=result; return f
+ init=bind('arboricx_init',[],CTX); free=bind('arboricx_free',[CTX],None)
+ load=bind('arb_load_bundle',[CTX,C.c_void_p,C.c_size_t,C.c_char_p,C.POINTER(H)])
+ leaf=bind('arb_leaf',[CTX,C.POINTER(H)])
+ stem=bind('arb_stem',[CTX,H,C.POINTER(H)])
+ fork=bind('arb_fork',[CTX,H,H,C.POINTER(H)])
+ app=bind('arb_app',[CTX,H,H,C.POINTER(H)])
+ reduce=bind('arb_reduce',[CTX,H,C.c_uint64,C.POINTER(H)])
+ release=bind('arb_release',[CTX,H])
+ is_leaf=bind('arb_is_leaf',[CTX,H,C.POINTER(C.c_int)])
+ is_stem=bind('arb_is_stem',[CTX,H,C.POINTER(C.c_int)])
+ child=bind('arb_get_stem_child',[CTX,H,C.POINTER(H)])
+ children=bind('arb_get_fork_children',[CTX,H,C.POINTER(H),C.POINTER(H)])
+ ctx=init()
+ if not ctx: raise MemoryError('context')
+ def check(status):
+ if status: raise RuntimeError('ABI status '+str(status))
+ def owned(f,*args):
+ out=H(); check(f(ctx,*args,C.byref(out))); return out.value
+ try:
+ data=a.argument.read_bytes(); verify_payload(FLAT,data)
+ cursor=0
+ def construct():
+ nonlocal cursor
+ tag=data[cursor]; cursor+=1
+ args=[construct() for _ in range(tag)]
+ value=owned((leaf,stem,fork)[tag],*args)
+ for h in args: check(release(ctx,h))
+ return value
+ argument=construct(); bundle=a.bundle.read_bytes()
+ worker=owned(load,bundle,len(bundle),a.export.encode())
+ term=owned(app,worker,argument)
+ check(release(ctx,worker)); check(release(ctx,argument))
+ todo=[term]; output=bytearray()
+ while todo:
+ h=todo.pop(); normalized=owned(reduce,h,10_000_000); check(release(ctx,h)); h=normalized
+ flag=C.c_int(); check(is_leaf(ctx,h,C.byref(flag)))
+ if flag.value: output.append(0)
+ else:
+ check(is_stem(ctx,h,C.byref(flag)))
+ if flag.value:
+ output.append(1); todo.append(owned(child,h))
+ else:
+ output.append(2); left,right=H(),H()
+ check(children(ctx,h,C.byref(left),C.byref(right)))
+ todo.extend((right.value,left.value))
+ check(release(ctx,h))
+ if len(output)>1024*1024: raise ValueError('test output boundary')
+ verify_payload(FLAT,output)
+ a.output.write_bytes(output)
+ finally: free(ctx)
+
+
+if __name__=='__main__': main()