tricu

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

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 }