tricu

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

commit cbf109e13134a62f66fb11f9af2e28eb78e3e3d8
parent 5b6448bd3a42ca510bda2ad0719e369f3d08a10c
Author: James Eversole <james@eversole.co>
Date:   Fri, 16 Oct 2026 12:19:03 -0500

Validate bounded cross-host compiler compatibility and failure controls

Both hosts reproduce complete objects/receipts through imports, reexports, multi-export syntax and lazy/recursive programs. Verify cache invalidation, corruption rejection, compiler diagnostic tag, resource interruption/restart and no fallback. Full nix flake check passes; no full generation yet.

Diffstat:
Mext/zig/README.md | 10++++++++++
Mtest/cross_host_generation.py | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtest/zig_generation_executor.py | 16+++++++++++++++-
3 files changed, 130 insertions(+), 3 deletions(-)

diff --git a/ext/zig/README.md b/ext/zig/README.md @@ -86,6 +86,16 @@ performance benchmark. Unsupported actions during a pending IO continuation produce an error result identifying the action tag rather than a successful compilation. +Add `--compatibility` for reexports/multiple exports, direct and local recursion, +patterns, sequencing, lazy discard, UTF-8 comments, dependency changes, warm +reuse, compiler error tags, corrupt checkpoints/executables, guarded interruption +and restart, and missing-input/no-fallback controls. BOM/header failures are +supervisor preflight checks, not evidence of executing the compiler's source +reader. Source/indexed IO permission denial and malformed-input handling are +covered separately by the runtime's Debug/ReleaseSafe tests. The execution child +records string diagnostics or a bounded numeric prefix for structured diagnostics; +it is not a replacement for a complete diagnostic renderer. + ### Indexed tree transport The existing IO actions `readIndexedTree` (50), `hashIndexedTree` (51), and diff --git a/test/cross_host_generation.py b/test/cross_host_generation.py @@ -8,6 +8,7 @@ import hashlib import json from pathlib import Path import sys +import subprocess from process_rss import run_guarded ROOT = Path(__file__).resolve().parent.parent @@ -18,6 +19,7 @@ def main(): p.add_argument('--tricu', type=Path, required=True) p.add_argument('--library', type=Path, required=True) p.add_argument('--directory', type=Path, required=True) + p.add_argument('--compatibility', action='store_true', help='also check broader syntax, invalidation and negative controls') args = p.parse_args() directory = args.directory.resolve() directory.mkdir(parents=True, exist_ok=False) @@ -30,9 +32,20 @@ def main(): report = dict(complete=False, stages=[]) def save(): (directory / 'report.json').write_text(json.dumps(report, indent=2)+'\n') - def run(label, command): + def run(label, command, success=True): try: output, peak = run_guarded(list(map(str,command)), memory_mib=1024, seconds=150) + if not success: + raise AssertionError('unexpected success: '+label) + except subprocess.CalledProcessError as error: + (directory / (label+'.log')).write_text(error.output) + if success: + report['failure'] = label + save() + raise + report['stages'].append(dict(label=label, expected_failure=True)) + save() + return error.output except Exception as error: (directory / (label+'.log')).write_text(str(getattr(error,'output',''))+'\n'+str(error)) report['failure'] = label @@ -45,6 +58,7 @@ def main(): def checkpoints(host): return {name:(directory/host/'cas/cache/source-build'/name).read_bytes() for name in ('dep','app')} save() + commands = {} for host in ('haskell','zig'): output = directory / host command = [sys.executable, ROOT/'test/bootstrap_generations.py', output, @@ -53,6 +67,7 @@ def main(): '--backend','stable','--memory-mib','1024','--wall-seconds','120', '--initial-seconds','60','--extended-seconds','100'] if host == 'zig': command += ['--zig-library',args.library.resolve()] + commands[host] = command 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'] @@ -73,9 +88,97 @@ def main(): # 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' + if args.compatibility: + def same_objects(): + assert objects('haskell') == objects('zig'), 'published object sets differ' + def stage(label, success=True, diagnostic=None): + for host, command in commands.items(): + reply = run(host+'-'+label, command, success=success) + status = json.loads((directory/host/'status.json').read_text()) + assert status['complete'] == success + if diagnostic: + assert diagnostic in reply, (host,label,reply[-3000:]) + same_objects() + def result(host, name, expected): + pin = (directory/host/'cas/cache/source-build/app').read_text().split('\t')[2] + reply = run(host+'-result-'+name,[args.tricu,'arboricx','run','--store',directory/host/'cas', + '--module',pin,'--name',name,'--format','number', + '+RTS','-N1','-M1024m','-RTS']) + assert reply.strip()==str(expected), reply + dep = '!export answer\n!export extra\nanswer = 42\nextra = 99\n-- preserved UTF-8: λ 😀\n' + (source/'dep.tri').write_text(dep) + (source/'bridge.tri').write_text('!export * from dep\n') + source_map.write_text('arboricx.source-map.v1\nnative\tdep\tdep\tdep.tri\t-\n' + 'native\tbridge\tbridge\tbridge.tri\tdep\n' + 'native\tapp\tapp\tapp.tri\tbridge\n') + app = '!import bridge as D\n!export main\n!export local\n!export pinned\n!export sequenced\n!export lazy\n!export partial\nwalk t = D.answer\nwalk (t x) = walk x\nwalk _ = 0\nmain = walk (t (t t))\nlocal = (let rec\n copy [] = []\n copy [x | xs] = t x (copy xs)\n copy _ = []\n in (x : case x of\n [head | _] = head\n _ = 0) (copy [42 8]))\nchoose ^D.extra = 42\nchoose _ = 0\npinned = choose 99\npass value next = next value\nsequenced = (do pass\n value <- 42\n (x : x) value)\nlazy = (x : 42) ((x : x x) (x : x x))\nfirst x y = x\npartial = first 42 0\n' + (source/'app.tri').write_text(app) + stage('features-and-change') + for host in commands: + for name in ('main','local','pinned','sequenced','lazy','partial'): + result(host,name,42) + before = {host:checkpoints(host) for host in commands} + stage('features-warm') + assert before == {host:checkpoints(host) for host in commands} + # Invalid source, not merely a supervisor preflight error. + (source/'app.tri').write_text('!import bridge as D\n!export main\nmain = missingName\n') + stage('undefined-name',success=False) + assert json.loads((directory/'zig/execution.json').read_text())['diagnostic']=={'first_number':20} + assert 'error: [20, "missingName"' in (directory/'haskell-undefined-name.log').read_text() + (source/'app.tri').write_text(app) + stage('restore-source') + assert before == {host:checkpoints(host) for host in commands} + # A matching corrupt local checkpoint must fail closed, not recompile. + saved = {} + for host in commands: + path = directory/host/'cas/cache/source-build/app' + saved[host] = path.read_bytes() + path.write_text('corrupt') + stage('corrupt-checkpoint',success=False) + for host in commands: + (directory/host/'cas/cache/source-build/app').write_bytes(saved[host]) + # Corrupt a referenced executable while keeping its digest/path. + for host in commands: + pin = saved[host].decode().split('\t')[2] + manifest = (directory/host/'cas/objects'/pin[:3]/pin).read_text() + digest = next(row.split('\t')[3] for row in manifest.splitlines()[1:] if row.split('\t')[1]=='main') + path = directory/host/'cas/objects'/digest[:3]/digest + saved[host] = (path,path.read_bytes()) + path.write_bytes(b'corrupt') + stage('corrupt-executable',success=False) + for path,payload in saved.values(): path.write_bytes(payload) + stage('restore-object') + # An interrupted preparation must invalidate success; completed dependency + # publications survive and the next invocation reconstructs the root. + for host,command in commands.items(): + (directory/host/'cas/cache/source-build/app').unlink() + run(host+'-resource-stop',command+['--memory-mib','1'],success=False) + assert not json.loads((directory/host/'status.json').read_text())['complete'] + # Also stop a running Zig invocation, not only Haskell transport setup. + run('zig-execution-deadline',commands['zig']+['--wall-seconds','0.5'],success=False) + stopped = json.loads((directory/'zig/status.json').read_text()) + assert not stopped['complete'] and stopped['reason']=='whole-job wall-time guard', stopped + assert 'code' in stopped, 'guard must reach the execution subprocess' + stage('restart') + assert before == {host:checkpoints(host) for host in commands} + # Header preflight rejects initial BOM before an invocation starts; + # existing status remains historical, not evidence of this failed attempt. + (source/'app.tri').write_bytes(b'\xef\xbb\xbf'+app.encode()) + for host, command in commands.items(): + reply = run(host+'-source-bom',command,success=False) + assert 'header mismatch' in reply + (source/'app.tri').write_text(app) + stage('final-restored') + for host,command in commands.items(): + reply = run(host+'-missing-dependency',command+['--root','absent'],success=False) + assert 'undeclared map dependency' in reply + reply = run('zig-missing-library',commands['zig']+['--zig-library',str(directory/'missing.so')],success=False) + assert 'no fallback' in reply + report['compatibility'] = True report['complete'] = True save() - print('Shared-runner cold/warm parity, exact receipts/objects and result 41 passed: '+str(directory)) + print('Shared-runner cold/warm parity and exact receipts/objects passed; compatibility=' + +str(args.compatibility)+': '+str(directory)) if __name__ == '__main__': diff --git a/test/zig_generation_executor.py b/test/zig_generation_executor.py @@ -44,6 +44,8 @@ def child(args): 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)]) + number = bind('arb_to_number', [ctx_t, handle, C.POINTER(C.c_uint64)]) + pair = bind('arb_get_fork_children', [ctx_t, handle, C.POINTER(handle), C.POINTER(handle)]) 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)] @@ -94,7 +96,19 @@ def child(args): 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})' + # Preserve a useful bounded prefix of structured diagnostics. + # This recognizes only a generic list prefix, not compiler tags. + prefix, tail = handle(), handle() + tag = C.c_uint64() + normalized = owned(reduce, value.value, 1_000_000) + if pair(ctx, normalized, C.byref(prefix), C.byref(tail)) == 0: + if number(ctx, prefix, C.byref(tag)) == 0: + diagnostic = dict(first_number=tag.value) + check(release(ctx, prefix)) + check(release(ctx, tail)) + check(release(ctx, normalized)) + if diagnostic is None: + 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)