zig-session-probe.mjs (9201B)
1 import {terminalHost} from './terminal-host.mjs'; 2 // Independent complete Haskell captures, exercised through Zig's coarse adapter. 3 const assert=(ok,message)=>{if(!ok)throw Error(message);}; 4 const equal=(a,b,label)=>assert(JSON.stringify(a)===JSON.stringify(b),label+': '+JSON.stringify(a).slice(0,160)); 5 async function rejects(fn,pattern){try{await fn();}catch(e){assert(pattern.test(e.message),'Wrong rejection: '+e.message);return;}throw Error('Expected '+pattern);} 6 export async function run(control) { 7 const phase=async name=>{if(control)await fetch(control+'/phase',{method:'POST',body:JSON.stringify({name})});}; 8 const base=new URL('../',document.querySelector('script[type=module][src]').src); 9 const {Terminal}=(await import(new URL('dist/probe-api.js',base))).ioV2Terminal; 10 const {create}=await import(new URL('runtime/zig/site-session.mjs',base)); 11 const fixtures=await(await fetch('/zig-fixtures.json')).json(); 12 const catalogue=await(await fetch(new URL('catalogue.json',base))).json(); 13 const releases=(await(await fetch(new URL('packages.json',base))).json()).releases; 14 const report={packages:0,applications:0,compiledDemos:0,checks:[],memory:[]}; 15 const host=input=>({before:async()=>{},after:()=>{},read:async()=>input,write:()=>{},running:()=>{}}); 16 const bytes=async hash=>new Uint8Array(await(await fetch('/_arboricx/bundle/hash/'+hash)).arrayBuffer()); 17 const s=await create({base,compiler:false}); 18 try { 19 for(const [name,demo] of Object.entries(fixtures.demos)) { 20 await phase("full golden "+name); 21 await s.load(await bytes(demo.hash),demo.hash);report.packages++; 22 for(const c of demo.haskellCases){ 23 let terminal='',before; 24 const hooks=demo.mode==='io'?terminalHost(Terminal,c.input):host(c.input); 25 hooks.before=async e=>{assert(!before,'Missing after');before=e;}; 26 hooks.after=e=>{assert(e.step===before?.step&&e.name===before.name,'Trace ordering/name');before=undefined;}; 27 if(demo.mode==='io'){const event=hooks.v2.event;hooks.v2.event=e=>{s.collect();event(e);};} 28 else {hooks.read=async()=>{s.collect();return c.input;};hooks.write=text=>{terminal+=text;};} 29 equal(await s.inspect(c.export,c.arguments.map(value=>({type:'tree',value})),demo.mode,hooks),c.result,'Full golden '+name); 30 equal(demo.mode==='io'?hooks.terminalText:terminal,c.terminal,'Terminal '+name);report.applications++; 31 } 32 } 33 const negatives=await(await fetch('/zig-negative.json')).json(); 34 { 35 const f=negatives['v1-output'];await s.load(new Uint8Array(f.bytes),f.hash); 36 let calls=0;const forbidden=()=>{calls++;throw Error('retired callback');}; 37 await rejects(()=>s.run('root',[],'tree','io',{...host(),before:forbidden,write:forbidden,read:forbidden}),/Unsupported tricuIO version/); 38 equal(calls,0,'Retired v1 output never invokes host'); 39 } 40 for(const [name,pattern] of [['version',/version/],['version-effect',/version/],['unsupported',/Unsupported/],['abort',/Unsupported tricuIO version/],['output',/Unsupported tricuIO version/],['actions',/Unsupported tricuIO version/]]){ 41 const f=negatives[name];await s.load(new Uint8Array(f.bytes),f.hash); 42 await rejects(()=>s.run('root',[],'tree','io',host()),pattern); 43 } 44 { 45 const f=negatives['version-effect'];await s.load(new Uint8Array(f.bytes),f.hash); 46 let callbacks=0;const forbidden=()=>{callbacks++;throw Error('version gate reached host callback');}; 47 await rejects(()=>s.run('root',[],'tree','io',{before:forbidden,after:forbidden,read:forbidden,write:forbidden,running:forbidden}),/version/); 48 equal(callbacks,0,'Unsupported version invoked host'); 49 report.checks.push('IO v2 rejected before host callbacks; no v1 retry'); 50 } 51 const shared=negatives.sharing;await s.load(new Uint8Array(shared.bytes),shared.hash); 52 const preview=(await s.run('root',[],'tree','pure',host())).value; 53 assert(!preview.truncated&&preview.nodes.length===41,'Physical sharing expanded'); 54 assert(preview.nodes.filter(n=>n.kind==='fork').every(n=>n.children[0]===n.children[1]),'Lost child aliases'); 55 const intro=fixtures.demos.intro;await s.load(await bytes(intro.hash),intro.hash); 56 const invoke=(arg,type)=>s.run('identity',[arg],type,'pure',host()); 57 for(const vector of fixtures.codecVectors){const type=vector.kind,value=type==='number'?String(vector.value):vector.value;equal((await invoke({type,value},type)).value,value,'Scalar codec');} 58 for(const value of [true,false])equal((await invoke({type:'bool',value},'bool')).value,value,'Boolean'); 59 equal((await invoke({type:'bytes',value:[0,1,127,128,255]},'bytes')).value,[0,1,127,128,255],'Bytes'); 60 await rejects(()=>invoke({type:'number',value:'256'},'bool'),/Boolean/); 61 await rejects(()=>invoke({type:'list',value:[{type:'number',value:'256'}]},'bytes'),/byte/); 62 await rejects(()=>invoke({type:'list',value:[{type:'number',value:'55296'}]},'string'),/status 5/); 63 await rejects(()=>invoke({type:'string',value:'\ud800'},'string'),/surrogate/); 64 const scalars={type:'list',value:[{type:'string',value:'λ🙂'},{type:'number',value:'1267650600228229401496703205376'}]}; 65 equal((await invoke(scalars,'values')).value,scalars.value,'Decoded list preserves text and exact big naturals'); 66 await rejects(()=>invoke({type:'list',value:[{type:'bool',value:true}]},'values'),/number/); 67 equal((await invoke({type:'bytes',value:Array(4096).fill(42)},'values')).value.length,4096,'Decoded lists are not capped at 32 preview items'); 68 equal((await invoke({type:'list',value:[{type:'number',value:'42'}]},'list')).value.length,1,'List'); 69 const original=await bytes(intro.hash); 70 await rejects(()=>s.load(original,'0'.repeat(64)),/SHA-256 mismatch/); 71 const mutable=original.slice(),loading=s.load(mutable,intro.hash);mutable.fill(0);await loading; 72 equal((await invoke({type:'number',value:'42'},'number')).value,'42','Load must authenticate an immutable snapshot'); 73 const baseline=s.diagnostics().owned; 74 for(let i=0;i<50;i++){ 75 equal((await invoke({type:'string',value:'λ雪🙂'},'string')).value,'λ雪🙂','Retained run'); 76 s.collect();equal(s.diagnostics().owned,baseline,'Temporary owned-handle leak'); 77 report.memory.push(s.diagnostics().linearMemoryBytes); 78 } 79 assert(Math.max(...report.memory.slice(25))<=Math.max(...report.memory.slice(0,25))+65536,'Memory failed to plateau'); 80 report.checks.push(`${report.applications} complete published goldens; v2 IO GC and retired framing rejection; scalar/list/tree codecs and negative controls; physical sharing; ownership and memory plateau`); 81 const greet=fixtures.demos.greet;await s.load(await bytes(greet.hash),greet.hash); 82 let entered;const paused=new Promise(resolve=>{entered=resolve;}); 83 const pausedHost=terminalHost(Terminal,'');pausedHost.v2.stepping=true;pausedHost.before=()=>{entered();return new Promise(()=>{});}; 84 const running=s.run(greet.haskellCases[0].export,[],'string','io',pausedHost); 85 running.catch(()=>{});await paused;s.dispose();await rejects(()=>running,/Disposed/);assert(s.disposed,'Disposed session reused'); 86 }finally{s.dispose();} 87 const c=await create({base,compiler:true}); 88 try { 89 for(const [name,demo] of Object.entries(fixtures.demos)){ 90 const entry=catalogue.find(x=>x.hash===demo.hash); 91 assert(entry||name==='intro'&&releases.some(r=>r.name==='intro'&&r.version==='0.2.0'&&r.hash===demo.hash),'Current catalogue pin or explicitly retained historical intro publication'); 92 await phase("source compile "+name); 93 await c.compile(demo.source,entry?.environment??'empty'); 94 const first=demo.haskellCases[0]; 95 equal(await c.inspect(first.export,first.arguments.map(value=>({type:'tree',value})),demo.mode,demo.mode==='io'?terminalHost(Terminal,first.input):host(first.input)),first.result,'Compiled '+name);report.compiledDemos++; 96 } 97 const intro=fixtures.demos.intro;await c.compile(intro.source,'empty'); 98 await rejects(()=>c.compile('name = unavailableName','empty'),/Compiler diagnostic/); 99 // Retired stale-definition retention: a failed replacement cannot expose the 100 // previous program or its imported scope. Runtime/IO errors still retain it. 101 await rejects(()=>c.run('answer',[],'number','pure',host()),/Missing definition/); 102 const greet=fixtures.demos.greet;await c.compile(greet.source,'empty'); 103 const failingHost=terminalHost(Terminal,'x');failingHost.v2.stepping=true;failingHost.before=async()=>{throw Error('host failed');}; 104 await rejects(()=>c.run(greet.haskellCases[0].export,[],'string','io',failingHost),/host failed/); 105 const first=greet.haskellCases[0];equal(await c.inspect(first.export,[],greet.mode,terminalHost(Terminal,first.input)),first.result,'Host-error cleanup preserves definitions'); 106 await c.compile('q x = x x\nlazy = t (q q) t\naborted = t \"tricuIO\" (t 2 (t 2 (q q)))','empty'); 107 const partial=await c.preview('lazy',1);assert(partial.nodes.length===1&&partial.truncated,'Preview forced excluded divergent child'); 108 await rejects(()=>c.run('aborted',[],'tree','io',terminalHost(Terminal,null)),/IO v2 failed/); 109 report.checks.push('current published browser source demos and retained historical fixtures; compile-error invalidation/host-error retention; nonforcing divergent preview/abort'); 110 }finally{c.dispose();} 111 return report; 112 }