tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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 }