tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

browser-page.mjs (8503B)


      1 // Driven by browser.mjs over CDP; every operation has a main-thread timeout.
      2 const assert = (ok,message) => { if (!ok) throw new Error(message); };
      3 const bytes = async path => { const r=await fetch(path); if(!r.ok)throw new Error(path); return r.arrayBuffer(); };
      4 const digest = async b => Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',b)),x=>x.toString(16).padStart(2,'0')).join('');
      5 function client() {
      6   const worker = new Worker(new URL('./worker.mjs',import.meta.url),{type:'module'});
      7   let id=0; const waiting=new Map(), unsolicited=[];
      8   const terminate = () => { worker.terminate(); for(const w of waiting.values()){clearTimeout(w.timer);w.reject(new Error('Terminated'));} waiting.clear(); };
      9   worker.onmessage=({data})=>{
     10     const w=waiting.get(data.id);
     11     if(!w){unsolicited.push(data);return;}
     12     waiting.delete(data.id);clearTimeout(w.timer);
     13     if(data.type==='error')w.reject(new Error(data.message));else w.resolve(data);
     14   };
     15   worker.onerror=e=>{for(const w of waiting.values()){clearTimeout(w.timer);w.reject(new Error(e.message));}waiting.clear();};
     16   return {terminate,unsolicited,send:m=>new Promise((resolve,reject)=>{
     17     const request=++id;
     18     const timer=setTimeout(()=>{waiting.delete(request);terminate();reject(new Error('Worker deadline'));},30_000);
     19     waiting.set(request,{resolve,reject,timer});worker.postMessage({...m,id:request});
     20   })};
     21 }
     22 // Only used to validate small, complete snapshots against independent goldens.
     23 function snapshotTree(snapshot) {
     24   assert(!snapshot.truncated,'Unexpected truncated fixture preview');
     25   const nodes=new Map(snapshot.nodes.map(n=>[n.id,n]));
     26   const aliases=new Map((snapshot.aliases??[]).map(a=>[a.from,a.to]));
     27   const pending=[snapshot.root], chars=[];
     28   while(pending.length) {
     29     assert(chars.length<10000,'Preview fixture expansion limit');
     30     let id=pending.pop();
     31     for(let i=0;aliases.has(id);i++){assert(i<256,'Alias cycle');id=aliases.get(id);}
     32     const node=nodes.get(id);assert(node,'Missing preview node');
     33     chars.push(String(node.children.length));pending.push(...[...node.children].reverse());
     34   }
     35   return chars.join('');
     36 }
     37 export async function run() {
     38   const wasm=await bytes('/arboricx.wasm'), identity=await bytes('/test/fixtures/id.arboricx');
     39   const goldens=await (await fetch('/ext/demo-site/test/compiler-conformance.json')).json();
     40   const compiler=await bytes(`/ext/demo-site/bundles/${goldens.compilerBundleSha256}.arboricx`);
     41   const init=(host,bundle,hash,name)=>({type:'init',backend:host==='wasm'?'zig-wasm':'js',bundle,hash,name,artifacts:{wasm}});
     42   const idHash=await digest(identity), samples=[], controls=[];
     43   let ticks=0;const heartbeat=setInterval(()=>ticks++,10);
     44   try {
     45     for(const host of ['js','wasm']) {
     46       const c=client();
     47       try {
     48         // A rejected hash never constructs the evaluator. Retry initialization.
     49         await c.send(init(host,identity,'0'.repeat(64))).then(()=>{throw new Error('Accepted wrong hash');},e=>assert(/hash mismatch/.test(e.message),'Wrong hash error: '+e.message));
     50         const malformed=new Uint8Array(identity.byteLength+1);malformed.set(new Uint8Array(identity));
     51         let invalidRejected=false;
     52         try{await c.send(init(host,malformed.buffer,await digest(malformed)));}catch{invalidRejected=true;}
     53         assert(invalidRejected,'Accepted malformed bundle with matching hash');
     54         const ready=await c.send(init(host,identity,idHash));
     55         assert(ready.backend===(host==='wasm'?'zig-wasm':'js'),'Wrong selected backend');
     56         await c.send(init(host==='wasm'?'js':'wasm',identity,idHash)).then(
     57           ()=>{throw new Error('Switched live session backend');},
     58           e=>assert(/Already initialized/.test(e.message),'Wrong session-switch error'));
     59         let surrogateRejected=false;
     60         try{await c.send({type:'text',value:'\ud800'});}catch{surrogateRejected=true;}
     61         assert(surrogateRejected,'Accepted unpaired surrogate');
     62         const unicode='\ufeff\0λ雪🙂\uffff\u{10ffff}';
     63         for(let i=0;i<10;i++)assert((await c.send({type:'text',value:unicode})).value===unicode,'Unicode mismatch');
     64       } finally {c.terminate();}
     65       controls.push(`${host}: hash/profile rejection, invalid surrogate recovery and Unicode`);
     66     }
     67     // Alternate fresh Worker order. Capture compilation (lazy definition handles)
     68     // separately from independent full definition/result validation.
     69     for(let round=0;round<3;round++)for(const host of round%2?['wasm','js']:['js','wasm']) {
     70       const c=client();
     71       try {
     72         await c.send(init(host,compiler,goldens.compilerBundleSha256,goldens.export));
     73         let repeatStats;
     74         for(let index=0;index<4;index++) {
     75           const golden=goldens.cases[index];
     76           const compiled=await c.send({type:'compile',source:golden.source});
     77           assert(JSON.stringify([...compiled.names].sort())===JSON.stringify(Object.keys(golden.definitions).sort()),'Definition names mismatch');
     78           const observeStart=performance.now();
     79           for(const [name,value] of Object.entries(golden.definitions))
     80             assert((await c.send({type:'inspect',name,arguments:[]})).value===value,`Definition mismatch ${name}`);
     81           assert((await c.send({type:'inspect',name:golden.export,arguments:golden.arguments})).value===golden.result,'Applied result mismatch');
     82           const observedMs=performance.now()-observeStart;
     83           let previewMs=0;
     84           for(const [name,value] of Object.entries(golden.definitions)) {
     85             const preview=await c.send({type:'preview',name,limit:256});
     86             assert(snapshotTree(preview.value)===value,`Preview mismatch ${name}`);
     87             previewMs+=preview.ms;
     88           }
     89           const {stats}=await c.send({type:'collect'});
     90           samples.push({host,round,case:index,compileMs:compiled.ms,observeRoundTripMs:observedMs,previewMs,stats});
     91         }
     92         // Replace retained exports, then verify compiler-only/definition roots
     93         // stabilize live counts after warm-up and explicit collection.
     94         for(let i=0;i<8;i++) {
     95           await c.send({type:'compile',source:goldens.cases[0].source});
     96           assert((await c.send({type:'inspect',name:'id',arguments:goldens.cases[0].arguments})).value===goldens.cases[0].result,'Retained compiler changed');
     97           const {stats}=await c.send({type:'collect'});
     98           if(host==='wasm' && repeatStats)assert(stats.owned===repeatStats.owned && stats.liveAfterCollect===repeatStats.liveAfterCollect,'Retained-session leak: '+JSON.stringify({stats,repeatStats}));
     99           repeatStats=stats;
    100         }
    101         for(const bad of goldens.cases.filter(x=>x.diagnostic)) {
    102           let diagnostic;
    103           try {await c.send({type:'compile',source:bad.source});} catch(error) {diagnostic=error.message;}
    104           assert(diagnostic?.includes(bad.diagnostic),'Compiler diagnostic mismatch: '+diagnostic);
    105           assert((await c.send({type:'inspect',name:'id',arguments:goldens.cases[0].arguments})).value===goldens.cases[0].result,'Failed compilation destroyed retained exports');
    106         }
    107         await c.send({type:'compile',source:goldens.cases[0].source});
    108         assert((await c.send({type:'inspect',name:'id',arguments:goldens.cases[0].arguments})).value===goldens.cases[0].result,'Compiler failed to recover');
    109       } finally {c.terminate();}
    110     }
    111     for(const host of ['js','wasm']) {
    112       const c=client();
    113       try {
    114         await c.send(init(host,identity,idHash));
    115         const before=ticks;
    116         assert((await c.send({type:'diverge'})).type==='running','No divergence acknowledgment');
    117         await new Promise(r=>setTimeout(r,100));
    118         assert(ticks>before,'Main thread blocked by evaluator');
    119         c.terminate();
    120         await new Promise(r=>setTimeout(r,100));
    121         assert(c.unsolicited.length===0,'Divergent evaluator returned before Stop');
    122       } finally {c.terminate();}
    123       // A separate fresh Worker must still initialize and execute after Stop.
    124       const fresh=client();try{await fresh.send(init(host,identity,idHash));assert((await fresh.send({type:'text',value:'restart🙂'})).value==='restart🙂','Restart failed');}finally{fresh.terminate();}
    125       controls.push(`${host}: hard termination, responsive main thread and fresh restart`);
    126     }
    127     return {schema:1,userAgent:navigator.userAgent,wasmSha256:await digest(wasm),compilerSha256:goldens.compilerBundleSha256,samples,controls};
    128   } finally {clearInterval(heartbeat);}
    129 }