browser-parity.mjs (11032B)
1 import {CompilerWorker} from './client.mjs'; 2 import {decodeBundle} from './site/bundle.js'; 3 import {snapshotBundle,exportedTree} from './bridge.mjs'; 4 const assert=(value,message)=>{if(!value)throw Error(message);}; 5 const equal=(a,b,message)=>assert(JSON.stringify(a)===JSON.stringify(b),message+': '+JSON.stringify(a).slice(0,200)); 6 async function rejects(action,pattern){try{await action();}catch(e){assert(pattern.test(e.message),'Unexpected rejection: '+e.message);return;}throw Error('Expected rejection '+pattern);} 7 export async function runParity(){ 8 const index=await(await fetch('./parity/index.json')).json(),demos=await(await fetch('./parity/demos.json')).json(); 9 const report={checks:[],packages:0,applications:0,ioRuns:0,compiledDemos:0}; 10 const w=new CompilerWorker(); 11 const request=async m=>JSON.parse((await w.request(m)).text); 12 const load=async name=>{ 13 const bytes=await(await fetch('./parity/'+name+'.arboricx')).arrayBuffer(); 14 const names=await request({type:'load',bytes,hash:index[name]}); 15 return {bytes,names}; 16 }; 17 async function runIO(name,args,input){ 18 let event=await request({type:'io-start',name,arguments:args}),terminal='',steps=0; 19 const trace=[];let awaitingAfter=null; 20 while(event.kind!=='done'){ 21 assert(++steps<600,'IO event loop failed to finish'); 22 trace.push(event.kind+':'+(event.name??'')); 23 if(event.kind==='before'){assert(awaitingAfter===null,'Missing after trace');awaitingAfter={step:event.step,name:event.name};} 24 if(event.kind==='after'){assert(awaitingAfter&&event.step===awaitingAfter.step&&event.name===awaitingAfter.name&&typeof event.value==='string','After trace contract');awaitingAfter=null;} 25 if(event.kind==='write')terminal+=event.text; 26 const old=event.token; 27 if(event.kind==='read'){ 28 await rejects(()=>request({type:'io-resume',token:old,value:'bad\nline'}),/one bounded line/); 29 await w.request({type:'collect'}); // Paused continuations survive a major GC. 30 } 31 event=await request({type:'io-resume',token:old,value:event.kind==='read'?(input??''):''}); 32 await rejects(()=>request({type:'io-resume',token:old,value:''}),/stale|no IO interaction/); 33 } 34 const snapshot=await request({type:'io-snapshot'}); 35 const tree=exportedTree(snapshotBundle(snapshot,'result'),'result','ternary').text; 36 report.ioRuns++; 37 return {terminal,tree,trace}; 38 } 39 try{ 40 const ready=await w.request({type:'init',compiler:false}); 41 assert(ready.compilerHash===null,'Package-only initialization claims a compiler'); 42 const stats=await w.request({type:'stats'}); 43 equal(JSON.parse(stats.text),{linearMemoryBytes:stats.linearBytes},'Stats must report only actual linear memory'); 44 await rejects(()=>w.request({type:'compile',source:'id x = x'}),/not initialized/); 45 const identity=await load('identity'),id=identity.names[0]; 46 await rejects(()=>request({type:'load',bytes:identity.bytes,hash:'0'.repeat(64)}),/SHA-256 mismatch/); 47 await rejects(()=>load('wrong-profile'),/manifest semantics/); 48 equal((await request({type:'evaluate',name:id,arguments:[{type:'number',value:'42'}],output:'number'})).value,'42','Failed load changed prior package'); 49 for(const vector of demos.codecVectors){ 50 const type=vector.kind,value=type==='number'?String(vector.value):vector.value; 51 const result=await request({type:'evaluate',name:id,arguments:[{type,value}],output:type}); 52 equal(result.value,value,'Scalar codec'); 53 const observed=await w.request({type:'observe',name:id,arguments:[vector.tree]}); 54 equal(observed.text,vector.tree,'Canonical scalar tree'); 55 } 56 for(const value of [true,false])equal((await request({type:'evaluate',name:id,arguments:[{type:'bool',value}],output:'bool'})).value,value,'Boolean codec'); 57 equal((await request({type:'evaluate',name:id,arguments:[{type:'bytes',value:[0,1,127,128,255]}],output:'bytes'})).value,[0,1,127,128,255],'Byte codec'); 58 await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'number',value:'256'}],output:'bool'}),/Boolean/); 59 await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'list',value:[{type:'number',value:'256'}]}],output:'bytes'}),/Invalid byte/); 60 await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'list',value:[{type:'number',value:'55296'}]}],output:'string'}),/Surrogate/); 61 const record={type:'list',value:[ 62 {type:'list',value:[{type:'string',value:'__proto__'},{type:'number',value:'0'},{type:'string',value:'safe'}]}, 63 {type:'list',value:[{type:'string',value:'n'},{type:'number',value:'1'},{type:'number',value:'42'}]}, 64 {type:'list',value:[{type:'string',value:'b'},{type:'number',value:'2'},{type:'bool',value:true}]} 65 ]}; 66 const decoded=(await request({type:'evaluate',name:id,arguments:[record],output:'record'})).value; 67 assert(Object.hasOwn(decoded,'__proto__')&&decoded.__proto__==='safe'&&decoded.n===42&&decoded.b===true,'Record codec / prototype key'); 68 await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'list',value:[record.value[0],record.value[0]]}],output:'record'}),/unique/); 69 const list=(await request({type:'evaluate',name:id,arguments:[{type:'list',value:[{type:'number',value:'42'},{type:'bool',value:true}]}],output:'list'})).value; 70 equal(list.length,2,'List codec');assert(list.every(v=>Array.isArray(v.nodes)),'List previews'); 71 report.checks.push('authenticated arbitrary bundle load; failed loads preserve previous exports; all seven site codecs and negative controls'); 72 await load('exponential'); 73 const preview=await request({type:'preview',name:'root',limit:8}); 74 assert(preview.truncated&&preview.nodes.length===8,'Bounded preview must truncate'); 75 const snapshot=await request({type:'snapshot',name:'root',limit:100}); 76 assert(!snapshot.truncated&&snapshot.nodes.length<=41,'Snapshot expanded sharing'); 77 const runtimeDag=exportedTree(snapshotBundle(snapshot,'root'),'root','dag'); 78 assert(runtimeDag.bytes<10000,'Runtime DAG export expanded sharing'); 79 const immutableDag=await request({type:'export',name:'root',format:'dag'}); 80 assert(immutableDag.bytes<10000,'Stored DAG export expanded sharing'); 81 await rejects(()=>request({type:'export',name:'root',format:'ternary'}),/Expanded tree/); 82 await rejects(()=>Promise.resolve(exportedTree(snapshotBundle(preview,'root'),'root','dag')),/partial export/); 83 report.checks.push('40-level exponential sharing preserved in runtime snapshot and immutable DAG export; bounded preview and expansion rejection'); 84 for(const [name,demo] of Object.entries(demos.demos)){ 85 await load(name);report.packages++; 86 for(const c of demo.haskellCases){ 87 const args=c.arguments.map(value=>({type:'tree',value})); 88 if(demo.mode==='io'){ 89 const actual=await runIO(c.export,args,c.input); 90 equal(actual.tree,c.result,'IO result '+name);equal(actual.terminal,c.terminal,'IO terminal '+name); 91 }else equal((await w.request({type:'observe',name:c.export,arguments:c.arguments})).text,c.result,'Published program '+name); 92 report.applications++; 93 } 94 const stored=await request({type:'export',name:demo.haskellCases[0].export,format:'dag'}); 95 assert(stored.extension==='dag'&&stored.bytes>0,'Stored definition export'); 96 } 97 report.checks.push('all curated published packages and Haskell result/terminal goldens'); 98 for(const [name,pattern] of [['bad-version',/version/],['unsupported-io',/Unsupported browser IO action/],['abort-io',/aborted/],['output-limit',/output limit/],['action-limit',/action limit/]]){ 99 await load(name); 100 await rejects(()=>runIO('root',[],''),pattern); 101 await request({type:'io-cancel'}); 102 } 103 await load('greet'); 104 let event=await request({type:'io-start',name:demos.demos.greet.haskellCases[0].export,arguments:[]}); 105 await rejects(()=>load('identity'),/pending/); 106 await request({type:'io-cancel'}); 107 await rejects(()=>request({type:'io-resume',token:event.token,value:''}),/no IO interaction/); 108 await load('identity'); 109 equal((await request({type:'evaluate',name:id,arguments:[{type:'string',value:'recovered'}],output:'string'})).value,'recovered','IO errors poisoned runtime'); 110 report.checks.push('IO envelope/version/action/output limits; abort without forcing huge payload; stale-token rejection; GC while awaiting input; explicit cancellation and recovery'); 111 }finally{w.dispose();} 112 // A compiler-capable session exercises all profiles without package loading 113 // replacing the retained compiler/environment. 114 const c=new CompilerWorker(); 115 try{ 116 const ready=await c.request({type:'init'},Infinity); 117 equal(ready.compilerHash,demos.compilerBundleSha256,'Ready compiler pin'); 118 const environments=await(await fetch('./parity/environments.json')).json(); 119 const profile=environments.profiles.practical; 120 const support=await(await fetch('./parity/environment-practical.arboricx')).arrayBuffer(); 121 for(const [name,demo] of Object.entries(demos.demos)){ 122 const environment=demo.supportSourceSha256?{bytes:support,hash:profile.hash,names:profile.names}:null; 123 await c.request({type:'compile',source:demo.source,environment,base:new URL('./parity/',location.href).href},60000); 124 const first=demo.haskellCases[0]; 125 if(demo.mode!=='io')equal((await c.request({type:'observe',name:first.export,arguments:first.arguments})).text,first.result,'Compiled demo '+name); 126 const exported=JSON.parse((await c.request({type:'export',name:first.export,format:'dag'})).text); 127 assert(exported.bytes>0,'Compiled DAG export'); 128 report.compiledDemos++; 129 } 130 await rejects(()=>c.request({type:'compile',source:'x = t',environment:{bytes:support,hash:'0'.repeat(64),names:profile.names}}),/SHA-256 mismatch/); 131 const previous=Object.values(demos.demos).at(-1).haskellCases[0].export; 132 await rejects(()=>c.request({type:'observe',name:previous,arguments:[]}),/missing export/); 133 await c.request({type:'compile',source:'q x = x x\nlazy = t (q q) t\naborted = t "tricuIO" (t 1 (t 2 (q q)))'},null); 134 const partial=JSON.parse((await c.request({type:'preview',name:'lazy',limit:1},2000)).text); 135 assert(partial.nodes.length===1&&partial.truncated,'Preview forced excluded divergent child'); 136 const before=JSON.parse((await c.request({type:'io-start',name:'aborted'},2000)).text); 137 assert(before.kind==='before'&&before.name==='abortIO','Abort before event'); 138 await rejects(()=>c.request({type:'io-resume',token:before.token,value:''},2000),/aborted/); 139 await c.request({type:'io-cancel'}); 140 report.checks.push('all ten native demo sources compile with explicit imports; compiled graph-preserving export; environment authentication; ready compiler pin'); 141 report.checks.push('preview and abort trace do not force excluded divergent payloads; null/Infinity disable request timer'); 142 }finally{c.dispose();} 143 return report; 144 }