browser-parity.mjs (10880B)
1 import {CompilerWorker} from './client.mjs'; 2 import {drive} from './site/io-driver.js'; 3 import {Terminal} from './site/io-terminal.js'; 4 import {treeResult} from './site/tree-result.js'; 5 import {terminalHost} from './terminal-host.mjs'; 6 import {snapshotBundle,exportedTree} from './bridge.mjs'; 7 const assert=(value,message)=>{if(!value)throw Error(message);}; 8 const equal=(a,b,message)=>assert(JSON.stringify(a)===JSON.stringify(b),message+': '+JSON.stringify(a).slice(0,200)); 9 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);} 10 export async function runParity(){ 11 const index=await(await fetch('./parity/index.json')).json(),demos=await(await fetch('./parity/demos.json')).json(); 12 const report={checks:[],packages:0,applications:0,ioRuns:0,compiledDemos:0}; 13 const w=new CompilerWorker(); 14 const request=async m=>JSON.parse((await w.request(m)).text); 15 const load=async name=>{ 16 const bytes=await(await fetch('./parity/'+name+'.arboricx')).arrayBuffer(); 17 const names=await request({type:'load',bytes,hash:index[name]}); 18 return {bytes,names}; 19 }; 20 async function runIO(name,args,input,worker=w){ 21 const call=async m=>JSON.parse((await worker.request(m)).text); 22 const selected=await call({type:'io-start',selectIO:true,name,arguments:args}); 23 assert(selected.kind==='v2','Only v2 selection'); 24 const host=terminalHost(Terminal,input),run=selected.run; 25 try { 26 const value=await drive({run,signal:worker.signal,discard:()=>worker.dispose(), 27 turn:command=>call({type:'io-session-turn',...command}), 28 view:async()=>treeResult((await call({type:'io-public-view',run,output:'ternary'})).value,'ternary'), 29 },host); 30 report.ioRuns++; 31 return {terminal:host.terminalText,tree:value.value}; 32 }finally{if(!worker.disposed)await call({type:'io-public-clear',run});} 33 } 34 try{ 35 const ready=await w.request({type:'init',compiler:false}); 36 assert(ready.compilerHash===null,'Package-only initialization claims a compiler'); 37 const stats=await w.request({type:'stats'}); 38 equal(JSON.parse(stats.text),{linearMemoryBytes:stats.linearBytes},'Stats must report only actual linear memory'); 39 await rejects(()=>w.request({type:'compile',source:'id x = x'}),/not initialized/); 40 const identity=await load('identity'),id=identity.names[0]; 41 await rejects(()=>request({type:'load',bytes:identity.bytes,hash:'0'.repeat(64)}),/SHA-256 mismatch/); 42 await rejects(()=>load('wrong-profile'),/manifest semantics/); 43 equal((await request({type:'evaluate',name:id,arguments:[{type:'number',value:'42'}],output:'number'})).value,'42','Failed load changed prior package'); 44 for(const vector of demos.codecVectors){ 45 const type=vector.kind,value=type==='number'?String(vector.value):vector.value; 46 const result=await request({type:'evaluate',name:id,arguments:[{type,value}],output:type}); 47 equal(result.value,value,'Scalar codec'); 48 const observed=await w.request({type:'observe',name:id,arguments:[vector.tree]}); 49 equal(observed.text,vector.tree,'Canonical scalar tree'); 50 } 51 for(const value of [true,false])equal((await request({type:'evaluate',name:id,arguments:[{type:'bool',value}],output:'bool'})).value,value,'Boolean codec'); 52 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'); 53 await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'number',value:'256'}],output:'bool'}),/Boolean/); 54 await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'list',value:[{type:'number',value:'256'}]}],output:'bytes'}),/Invalid byte/); 55 await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'list',value:[{type:'number',value:'55296'}]}],output:'string'}),/Surrogate/); 56 const scalars={type:'list',value:[{type:'string',value:'λ🙂'},{type:'number',value:'1267650600228229401496703205376'}]}; 57 equal((await request({type:'evaluate',name:id,arguments:[scalars],output:'values'})).value,scalars.value,'Decoded list preserves scalar values and big naturals'); 58 await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'list',value:[{type:'bool',value:true}]}],output:'values'}),/number/); 59 equal((await request({type:'evaluate',name:id,arguments:[{type:'bytes',value:Array(4096).fill(42)}],output:'values'})).value.length,4096,'Decoded list reaches 4096 elements independently of graph preview bound'); 60 const list=(await request({type:'evaluate',name:id,arguments:[{type:'list',value:[{type:'number',value:'42'},{type:'bool',value:true}]}],output:'list'})).value; 61 equal(list.length,2,'List codec');assert(list.every(v=>Array.isArray(v.nodes)),'List previews'); 62 report.checks.push('authenticated arbitrary bundle load; failed loads preserve previous exports; scalar/list/tree site codecs and negative controls'); 63 await load('exponential'); 64 const preview=await request({type:'preview',name:'root',limit:8}); 65 assert(preview.truncated&&preview.nodes.length===8,'Bounded preview must truncate'); 66 const snapshot=await request({type:'snapshot',name:'root',limit:100}); 67 assert(!snapshot.truncated&&snapshot.nodes.length<=41,'Snapshot expanded sharing'); 68 const runtimeDag=exportedTree(snapshotBundle(snapshot,'root'),'root','dag'); 69 assert(runtimeDag.bytes<10000,'Runtime DAG export expanded sharing'); 70 const immutableDag=await request({type:'export',name:'root',format:'dag'}); 71 assert(immutableDag.bytes<10000,'Stored DAG export expanded sharing'); 72 await rejects(()=>request({type:'export',name:'root',format:'ternary'}),/Expanded tree/); 73 await rejects(()=>Promise.resolve(exportedTree(snapshotBundle(preview,'root'),'root','dag')),/partial export/); 74 report.checks.push('40-level exponential sharing preserved in runtime snapshot and immutable DAG export; bounded preview and expansion rejection'); 75 for(const [name,demo] of Object.entries(demos.demos)){ 76 await load(name);report.packages++; 77 for(const c of demo.haskellCases){ 78 const args=c.arguments.map(value=>({type:'tree',value})); 79 if(demo.mode==='io'){ 80 const actual=await runIO(c.export,args,c.input); 81 equal(actual.tree,c.result,'IO result '+name);equal(actual.terminal,c.terminal,'IO terminal '+name); 82 }else equal((await w.request({type:'observe',name:c.export,arguments:c.arguments})).text,c.result,'Published program '+name); 83 report.applications++; 84 } 85 const stored=await request({type:'export',name:demo.haskellCases[0].export,format:'dag'}); 86 assert(stored.extension==='dag'&&stored.bytes>0,'Stored definition export'); 87 } 88 report.checks.push('all curated published packages and Haskell result/terminal goldens'); 89 for(const [name,pattern] of [['bad-version',/version/],['unsupported-io',/IO failed/],['abort-io',/IO failed/],['retired-output',/version/],['retired-actions',/version/]]){ 90 await load(name); 91 await rejects(()=>runIO('root',[],''),pattern); 92 } 93 await load('version-effect'); 94 await rejects(()=>request({type:'io-start',selectIO:true,name:'root',arguments:[]}),/version/); 95 report.checks.push('unsupported and retired IO versions reject before events; no retry'); 96 await load('greet'); 97 const selected=await request({type:'io-start',selectIO:true,name:demos.demos.greet.haskellCases[0].export,arguments:[]}); 98 await rejects(()=>load('identity'),/pending/); 99 await rejects(()=>request({type:'io-session-turn',run:'0',kind:'poll'}),/Stale/); 100 const host=terminalHost(Terminal,null),open=host.structured.open; 101 host.structured.open=(run,command)=>{open(run,command);command({run,kind:'stop'});}; 102 await rejects(()=>drive({run:selected.run,signal:w.signal,discard:()=>w.dispose(), 103 turn:command=>request({type:'io-session-turn',...command}), 104 view:async()=>{throw Error('Cancelled result was decoded');}, 105 },host),/cancelled/); 106 await request({type:'io-public-clear',run:selected.run}); 107 await rejects(()=>request({type:'io-session-turn',run:selected.run,kind:'poll'}),/Stale/); 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('v2 failed diagnostics; stale run rejection; acknowledged cancellation and recovery; legacy limit fixtures reject inertly'); 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 2 (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 await rejects(()=>runIO('aborted',[],'',c),/IO failed/); 137 report.checks.push('all native demo sources compile with explicit imports; compiled graph-preserving export; environment authentication; ready compiler pin'); 138 report.checks.push('preview and terminal failure do not force excluded divergent payloads; null/Infinity disable request timer'); 139 }finally{c.dispose();} 140 return report; 141 }