tricu

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

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:
Mdocs/compiler-rebuilding.md | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Atest/bootstrap_cross_host_controls.py | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/bootstrap_cross_host_smoke.py | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/bootstrap_smoke.py | 25+++++++++++++++----------
Atest/zig_observe.py | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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()