zig-session-probe.mjs (7087B)
1 // Independent complete Haskell captures, exercised through Zig's coarse adapter. 2 const assert=(ok,message)=>{if(!ok)throw Error(message);}; 3 const equal=(a,b,label)=>assert(JSON.stringify(a)===JSON.stringify(b),label+': '+JSON.stringify(a).slice(0,160)); 4 async function rejects(fn,pattern){try{await fn();}catch(e){assert(pattern.test(e.message),'Wrong rejection: '+e.message);return;}throw Error('Expected '+pattern);} 5 export async function run() { 6 const base=new URL('../',document.querySelector('script[type=module][src]').src); 7 const {create}=await import(new URL('runtime/zig/site-session.mjs',base)); 8 const fixtures=await(await fetch('/zig-fixtures.json')).json(); 9 const catalogue=await(await fetch(new URL('catalogue.json',base))).json(); 10 const report={packages:0,applications:0,compiledDemos:0,checks:[],memory:[]}; 11 const host=input=>({before:async()=>{},after:()=>{},read:async()=>input,write:()=>{},running:()=>{}}); 12 const bytes=async hash=>new Uint8Array(await(await fetch('/_arboricx/bundle/hash/'+hash)).arrayBuffer()); 13 const s=await create({base,compiler:false}); 14 try { 15 for(const [name,demo] of Object.entries(fixtures.demos)) { 16 await s.load(await bytes(demo.hash),demo.hash);report.packages++; 17 for(const c of demo.haskellCases){ 18 let terminal='',before; 19 const hooks=host(c.input); 20 hooks.before=async e=>{assert(!before,'Missing after');before=e;}; 21 hooks.after=e=>{assert(e.step===before?.step&&e.name===before.name,'Trace ordering/name');before=undefined;}; 22 hooks.read=async()=>{s.collect();return c.input;};hooks.write=text=>{terminal+=text;}; 23 equal(await s.inspect(c.export,c.arguments.map(value=>({type:'tree',value})),demo.mode,hooks),c.result,'Full golden '+name); 24 equal(terminal,c.terminal,'Terminal '+name);report.applications++; 25 } 26 } 27 const negatives=await(await fetch('/zig-negative.json')).json(); 28 for(const [name,pattern] of [['version',/version/],['unsupported',/Unsupported/],['abort',/aborted/],['output',/output limit/],['actions',/action limit/]]){ 29 const f=negatives[name];await s.load(new Uint8Array(f.bytes),f.hash); 30 await rejects(()=>s.run('root',[],'tree','io',host()),pattern); 31 } 32 const shared=negatives.sharing;await s.load(new Uint8Array(shared.bytes),shared.hash); 33 const preview=(await s.run('root',[],'tree','pure',host())).value; 34 assert(!preview.truncated&&preview.nodes.length===41,'Physical sharing expanded'); 35 assert(preview.nodes.filter(n=>n.kind==='fork').every(n=>n.children[0]===n.children[1]),'Lost child aliases'); 36 const intro=fixtures.demos.intro;await s.load(await bytes(intro.hash),intro.hash); 37 const invoke=(arg,type)=>s.run('identity',[arg],type,'pure',host()); 38 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');} 39 for(const value of [true,false])equal((await invoke({type:'bool',value},'bool')).value,value,'Boolean'); 40 equal((await invoke({type:'bytes',value:[0,1,127,128,255]},'bytes')).value,[0,1,127,128,255],'Bytes'); 41 await rejects(()=>invoke({type:'number',value:'256'},'bool'),/Boolean/); 42 await rejects(()=>invoke({type:'list',value:[{type:'number',value:'256'}]},'bytes'),/byte/); 43 await rejects(()=>invoke({type:'list',value:[{type:'number',value:'55296'}]},'string'),/status 5/); 44 await rejects(()=>invoke({type:'string',value:'\ud800'},'string'),/surrogate/); 45 const record={type:'list',value:[{type:'list',value:[{type:'string',value:'__proto__'},{type:'number',value:'0'},{type:'string',value:'safe'}]}]}; 46 const decoded=(await invoke(record,'record')).value;assert(Object.hasOwn(decoded,'__proto__')&&decoded.__proto__==='safe','Prototype-safe record'); 47 await rejects(()=>invoke({type:'list',value:[record.value[0],record.value[0]]},'record'),/unique/); 48 equal((await invoke({type:'list',value:[{type:'number',value:'42'}]},'list')).value.length,1,'List'); 49 const original=await bytes(intro.hash); 50 await rejects(()=>s.load(original,'0'.repeat(64)),/SHA-256 mismatch/); 51 const mutable=original.slice(),loading=s.load(mutable,intro.hash);mutable.fill(0);await loading; 52 equal((await invoke({type:'number',value:'42'},'number')).value,'42','Load must authenticate an immutable snapshot'); 53 const baseline=s.diagnostics().owned; 54 for(let i=0;i<50;i++){ 55 equal((await invoke({type:'string',value:'λ雪🙂'},'string')).value,'λ雪🙂','Retained run'); 56 s.collect();equal(s.diagnostics().owned,baseline,'Temporary owned-handle leak'); 57 report.memory.push(s.diagnostics().linearMemoryBytes); 58 } 59 assert(Math.max(...report.memory.slice(25))<=Math.max(...report.memory.slice(0,25))+65536,'Memory failed to plateau'); 60 report.checks.push('117 complete published goldens; IO GC/trace ordering/limits; seven codecs/negative controls; physical sharing; ownership and memory plateau'); 61 const greet=fixtures.demos.greet;await s.load(await bytes(greet.hash),greet.hash); 62 let entered;const paused=new Promise(resolve=>{entered=resolve;}); 63 const running=s.run(greet.haskellCases[0].export,[],'string','io',{...host(''),before:()=>{entered();return new Promise(()=>{});}}); 64 running.catch(()=>{});await paused;s.dispose();await rejects(()=>running,/Disposed/);assert(s.disposed,'Disposed session reused'); 65 }finally{s.dispose();} 66 const c=await create({base,compiler:true}); 67 try { 68 for(const [name,demo] of Object.entries(fixtures.demos)){ 69 const entry=catalogue.find(x=>x.hash===demo.hash);assert(entry,'Catalogue fixture pin'); 70 await c.compile(demo.source,entry.environment); 71 const first=demo.haskellCases[0]; 72 equal(await c.inspect(first.export,first.arguments.map(value=>({type:'tree',value})),demo.mode,host(first.input)),first.result,'Compiled '+name);report.compiledDemos++; 73 } 74 const intro=fixtures.demos.intro;await c.compile(intro.source,'empty'); 75 await rejects(()=>c.compile('name = unavailableName','empty'),/Compiler diagnostic/); 76 // Retired stale-definition retention: a failed replacement cannot expose the 77 // previous program or its imported scope. Runtime/IO errors still retain it. 78 await rejects(()=>c.run('answer',[],'number','pure',host()),/Missing definition/); 79 const greet=fixtures.demos.greet;await c.compile(greet.source,'empty'); 80 await rejects(()=>c.run(greet.haskellCases[0].export,[],'string','io',{...host('x'),before:async()=>{throw Error('host failed');}}),/host failed/); 81 const first=greet.haskellCases[0];equal(await c.inspect(first.export,[],greet.mode,host(first.input)),first.result,'Host-error cleanup preserves definitions'); 82 await c.compile('q x = x x\nlazy = t (q q) t\naborted = t \"tricuIO\" (t 1 (t 2 (q q)))','empty'); 83 const partial=await c.preview('lazy',1);assert(partial.nodes.length===1&&partial.truncated,'Preview forced excluded divergent child'); 84 await rejects(()=>c.run('aborted',[],'tree','io',host()),/aborted/); 85 report.checks.push('ten complete native source demos; compile-error invalidation/host-error retention; nonforcing divergent preview/abort'); 86 }finally{c.dispose();} 87 return report; 88 }