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