tricu

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

browser-worker.mjs (15403B)


      1 import {WASI,File,OpenFile,ConsoleStdout,PreopenDirectory} from './shim/index.js';
      2 import {Session} from './site/io-session.js';
      3 import {constructors,runPrivate} from './site/io-private.js';
      4 import pins,{compilerHash} from './pins.mjs';
      5 import {moduleDependencies} from './site/module-stage.js';
      6 import {fetchBytes} from './site/bundle.js';
      7 import {environmentProfile} from './site/packages.js';
      8 import {frame,authenticatedBundle,loadFields,application,evaluation,snapshotFields,snapshotBundle,exportedTree,environmentFields,emptyEnvironment,dagBytes} from './bridge.mjs';
      9 let instance,booted=false,dead=false,stderr="",storedBundle=null,busy=false,compilerReady=false;
     10 const dataFiles=new Map();
     11 let privateRun=0n,privateSession=null;
     12 const encoder=new TextEncoder(),decoder=new TextDecoder('utf-8',{fatal:true});
     13 function scalarText(s,limit){
     14   if(typeof s!=='string')throw Error('Expected string');
     15   let count=0;
     16   for(const c of s){const n=c.codePointAt(0);if(++count>limit||n>=0xd800&&n<=0xdfff)throw Error('Invalid Unicode or scalar limit');}
     17   return s;
     18 }
     19 async function checkedFile(name,corrupt){
     20   const response=await fetch(new URL('data/'+name,import.meta.url));
     21   if(!response.ok)throw Error('Data fetch failed');
     22   const bytes=new Uint8Array(await response.arrayBuffer());
     23   // Test-only fault injection can corrupt, never replace the trusted pin.
     24   if(corrupt&&bytes.length)bytes[0]^=1;
     25   const hash=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),b=>b.toString(16).padStart(2,'0')).join('');
     26   if(hash!==pins[name])throw Error('Prepared input SHA-256 mismatch');
     27   return new File(bytes,{readonly:true});
     28 }
     29 function call(name,text){
     30   const e=instance.exports;
     31   let ptr=0,status;
     32   try{
     33     if(text===undefined)status=e[name]();
     34     else{
     35       const bytes=typeof text==='string'?encoder.encode(text):text;
     36       if(bytes.length>16777216)throw Error('Input byte limit');
     37       ptr=e.malloc(Math.max(bytes.length,1));if(!ptr)throw Error('Allocation failed');
     38       new Uint8Array(e.memory.buffer,ptr,bytes.length).set(bytes);
     39       status=e[name](ptr,bytes.length);
     40     }
     41   }finally{if(ptr)e.free(ptr);}
     42   const length=e.tricu_response_size();
     43   if(length<0||length>16777216)throw Error('Invalid response length');
     44   ptr=e.malloc(Math.max(length,1));if(!ptr)throw Error('Allocation failed');
     45   let output;
     46   try{
     47     if(e.tricu_response_copy(ptr,length)!==length)throw Error('Response copy failed');
     48     // Refresh the view after every call: memory.grow detaches old views.
     49     output=new Uint8Array(e.memory.buffer,ptr,length).slice();
     50   }finally{e.free(ptr);}
     51   const decoded=decoder.decode(output);
     52   if(status!==0){const error=Error(decoded.replace(/^error\n/,''));error.recoverable=status===1;throw error;}
     53   if(!decoded.startsWith('ok\n'))throw Error('Invalid response tag');
     54   return output.subarray(3).slice().buffer;
     55 }
     56 onmessage=async({data:m})=>{
     57   if(busy){postMessage({id:m.id,ok:false,error:'Worker is busy',fatal:false});return;}
     58   busy=true;
     59   const begin=performance.now();
     60   try{
     61     if(dead)throw Error('Disposed runtime');
     62     let bytes,phases,scopeNames;
     63     const encode=value=>encoder.encode(JSON.stringify(value)).buffer;
     64     const operation=fields=>call('tricu_request',frame(fields));
     65     const safe=async action=>{try{return await action();}catch(e){e.recoverable=true;throw e;}};
     66     if(m.type==='init'){
     67       if(booted)throw Error('Already initialized');booted=true;
     68       const files=m.compiler===false?[]:await Promise.all(Object.keys(pins).map(async name=>[name,await checkedFile(name,m.corrupt===name)]));
     69       for(const [name,file] of files)dataFiles.set(name,file);
     70       const fetched=performance.now();
     71       const response=await fetch(new URL('reactor.wasm',import.meta.url));
     72       if(!response.ok)throw Error('WASM fetch failed');
     73       const module=await WebAssembly.compileStreaming(response);
     74       const compiled=performance.now();
     75       const wasi=new WASI([],[],[new OpenFile(new File([])),ConsoleStdout.lineBuffered(console.log),ConsoleStdout.lineBuffered(s=>{stderr=(stderr+s+"\n").slice(-4096);}),new PreopenDirectory('/data',dataFiles)]);
     76       instance=await WebAssembly.instantiate(module,{wasi_snapshot_preview1:wasi.wasiImport});
     77       wasi.initialize(instance);instance.exports.tricu_rts_init();
     78       const initialized=performance.now();
     79       bytes=call(m.compiler===false?'tricu_initialize_empty':'tricu_initialize');
     80       compilerReady=m.compiler!==false;
     81       phases={data_ms:fetched-begin,wasm_fetch_compile_ms:compiled-fetched,instantiate_rts_ms:initialized-compiled,load_ms:performance.now()-initialized};
     82     }else{
     83       if(!instance)throw Error('Not initialized');
     84       if(privateSession&&!['io-session-turn','io-public-view','io-public-clear'].includes(m.type))await safe(()=>{throw Error('Private IO session is pending');});
     85       if(m.type==='compiler-init'){
     86         if(!compilerReady){
     87           const files=await safe(()=>Promise.all(Object.keys(pins).map(async name=>[name,await checkedFile(name)])));
     88           for(const [name,file] of files)dataFiles.set(name,file);
     89           bytes=call('tricu_initialize');compilerReady=true;
     90         }else bytes=encoder.encode('ready').buffer;
     91       }else if(m.type==='io-session-start'){
     92         const args=await safe(()=>application(m.name,m.arguments));
     93         if(privateRun===0xffffffffffffffffn)throw Error('Run identity exhausted');
     94         const start=JSON.parse(decoder.decode(operation(['io-tree-start',...args])));
     95         const invoke=(name,...fields)=>JSON.parse(decoder.decode(operation(['io-tree',name,...fields.map(String)])));
     96         try{
     97           const tree=constructors({leaf:start.leaf,observe:h=>invoke('observe',h),stem:h=>invoke('stem',h),fork:(a,b)=>invoke('fork',a,b),app:(a,b)=>invoke('app',a,b)});
     98           const run=String(++privateRun),session=new Session(tree,start.action,run,roots=>invoke('retain',...new Set(roots)));
     99           privateSession={session,clear:()=>invoke('clear')};
    100           const report=await session.turn({run,kind:'poll'});bytes=encode(report);
    101           if(report.done){privateSession.clear();privateSession=null;}
    102         }catch(error){privateSession=null;try{invoke('clear');}catch{}throw error;}
    103       }else if(m.type==='io-public-view'){
    104         await safe(()=>{if(!privateSession?.public||m.run!==privateSession.session.run||privateSession.session.core.result?.kind!=='completed')throw Error('No completed IO result');});
    105         bytes=encode(privateSession.view(m.output));
    106       }else if(m.type==='io-public-clear'){
    107         await safe(()=>{if(!privateSession?.public||m.run!==privateSession.session.run||privateSession.started&&!privateSession.session.core.result)throw Error('HostIO cleanup is pending');});
    108         privateSession.clear();privateSession=null;bytes=encode({});
    109       }else if(m.type==='io-session-turn'){
    110         await safe(()=>{if(!privateSession||m.run!==privateSession.session.run)throw Error('Stale session run');if(!['poll','reply','stop'].includes(m.kind))throw Error('Invalid session command');});
    111         privateSession.started=true;
    112         const report=await privateSession.session.turn({run:m.run,kind:m.kind,reply:m.reply});bytes=encode(report);
    113         if(report.done&&!privateSession.public){privateSession.clear();privateSession=null;}
    114       }else if(m.type==='io-core'){
    115         const args=await safe(()=>application(m.name,m.arguments));
    116         if(privateRun===0xffffffffffffffffn)throw Error('Run identity exhausted');
    117         const start=JSON.parse(decoder.decode(operation(['io-tree-start',...args])));
    118         const invoke=(name,...fields)=>JSON.parse(decoder.decode(operation(['io-tree',name,...fields.map(String)])));
    119         try{
    120           const tree=constructors({leaf:start.leaf,observe:h=>invoke('observe',h),stem:h=>invoke('stem',h),fork:(a,b)=>invoke('fork',a,b),app:(a,b)=>invoke('app',a,b)});
    121           bytes=encode(await runPrivate(tree,start.action,++privateRun,roots=>invoke('retain',...new Set(roots))));
    122         }finally{invoke('clear');}
    123       }else if(m.type==='io-header'){
    124         const args=await safe(()=>application(m.name,m.arguments));
    125         bytes=operation(['io-header',...args]);
    126       }else if(m.type==='expression'){
    127         await safe(()=>{scalarText(m.source,16384);if(typeof m.name!=='string'||!/^browserExpression[0-9]+$/.test(m.name)||!['pure','io'].includes(m.mode))throw Error('Invalid expression request');});
    128         bytes=operation(['expression',m.name,m.source,m.mode,m.output]);
    129       }else if(m.type==='module-header'){
    130         const source=await safe(()=>scalarText(m.source,16384));
    131         bytes=operation(['module-header',source]);
    132       }else if(m.type==='compile'){
    133         try {
    134           const source=await safe(()=>{const text=scalarText(m.source,16384);if(!text.trim())throw Error('Enter a source unit or native module');return text;});
    135           const stage=JSON.parse(decoder.decode(operation(['module-stage',source])));
    136           const base=await safe(()=>{const url=new URL(m.base??'../../',import.meta.url);if(url.origin!==location.origin)throw Error('Cross-origin site base');return url;});
    137           if(stage.native){
    138             const dependencies=await safe(()=>moduleDependencies(base,stage,m.localModules));
    139             for(const [ref,bundle] of dependencies){
    140               const fields=await safe(()=>['module-dependency',ref,dagBytes(bundle.nodes),...bundle.exports.flatMap(e=>[e.name,String(e.root)])]);
    141               operation(fields);
    142             }
    143             bytes=operation(['module-compile']);
    144           }else{
    145             let environment=m.environment;
    146             if(m.profileName!==undefined){
    147               environment=await safe(async()=>{
    148                 const raw=JSON.parse(decoder.decode(await fetchBytes(new URL('environments.json',base),32768)));
    149                 const selected=environmentProfile(raw,m.profileName);
    150                 if(selected.compiler!==compilerHash)throw Error('Demo environment/compiler pin mismatch');
    151                 return selected.profile?{bytes:new Uint8Array(await fetchBytes(new URL(`/_arboricx/bundle/hash/${selected.profile.hash}`,base))).buffer,hash:selected.profile.hash,names:selected.profile.names}:null;
    152               });
    153             }
    154             if(environment===undefined)bytes=call('tricu_compile',source);
    155             else{
    156               let fields;
    157               if(environment===null)fields=emptyEnvironment();
    158               else{
    159                 const env=await safe(()=>authenticatedBundle(environment.bytes,environment.hash));
    160                 fields=await safe(()=>environmentFields(env,environment.names));
    161               }
    162               bytes=operation(['compile',source,...fields]);
    163             }
    164           }
    165           scopeNames=JSON.parse(decoder.decode(operation(['module-scope'])));
    166           storedBundle=null;
    167         }catch(error){
    168           if(error.recoverable)operation(['module-abort']);
    169           throw error;
    170         }
    171       }else if(m.type==='load'){
    172         const bundle=await safe(()=>authenticatedBundle(m.bytes,m.hash));
    173         const fields=await safe(()=>loadFields(bundle));
    174         bytes=operation(fields);storedBundle=bundle;
    175       }else if(m.type==='probe-io'){
    176         const fields=await safe(()=>{const f=evaluation(m.name,[],'tree');return ['inspect-definition',f[1],'io-envelope'];});
    177         bytes=operation(fields);
    178       }else if(m.type==='inspect-definition'){
    179         const fields=await safe(()=>{const f=evaluation(m.name,[],m.output);return ['inspect-definition',f[1],f[2]];});
    180         bytes=operation(fields);
    181       }else if(m.type==='evaluate'){
    182         const fields=await safe(()=>evaluation(m.name,m.arguments,m.output));
    183         bytes=operation(fields);
    184       }else if(m.type==='module-snapshot'){
    185         bytes=operation(['module-snapshot']);
    186       }else if(m.type==='preview'||m.type==='snapshot'){
    187         const fields=await safe(()=>snapshotFields(m.name,m.arguments,m.limit??(m.type==='preview'?64:100000),m.type==='snapshot'));
    188         bytes=operation(fields);
    189       }else if(m.type==='export'){
    190         if(storedBundle)bytes=encode(await safe(()=>exportedTree(storedBundle,m.name,m.format)));
    191         else{
    192           const fields=await safe(()=>snapshotFields(m.name,[],m.limit??100000,true));
    193           const snapshot=JSON.parse(decoder.decode(operation(fields)));
    194           bytes=encode(await safe(()=>exportedTree(snapshotBundle(snapshot,m.name),m.name,m.format)));
    195         }
    196       }else if(m.type==='io-definition-start'){
    197         const fields=await safe(()=>{const f=evaluation(m.name,[],'tree');return ['io-definition-start',f[1]];});
    198         bytes=operation(fields);
    199       }else if(m.type==='io-start'){
    200         const args=await safe(()=>application(m.name,m.arguments));
    201         bytes=operation(['io-start',...args]);
    202       }else if(m.type==='stats'){
    203         bytes=encode({linearMemoryBytes:instance.exports.memory.buffer.byteLength});
    204       }else if(m.type==='observe'){
    205         // The private wire uses newline-separated names/ternary arguments.
    206         if(typeof m.name!=='string'||!m.name||m.name.length>128||/[\r\n]/.test(m.name)||!Array.isArray(m.arguments)||m.arguments.length>16||m.arguments.some(x=>typeof x!=='string'||x.length>50000||!/^[012]+$/.test(x))){const e=Error('Invalid observation request');e.recoverable=true;throw e;}
    207         bytes=call('tricu_observe',[m.name,...m.arguments].join('\n'));
    208       }else if(['dump','reset','collect'].includes(m.type)){bytes=call('tricu_'+m.type);if(m.type==='reset')storedBundle=null;}
    209       else{const e=Error('Unknown operation');e.recoverable=true;throw e;}
    210     }
    211     if(['io-start','io-definition-start'].includes(m.type)||m.type==='expression'&&m.mode==='io'){
    212       const selected=JSON.parse(decoder.decode(bytes));
    213       if(selected.kind==='v2'){
    214         if(privateRun===0xffffffffffffffffn)throw Error('Run identity exhausted');
    215         const operation=fields=>call('tricu_request',frame(fields));
    216         const invoke=(name,...fields)=>JSON.parse(decoder.decode(operation(['io-tree',name,...fields.map(String)])));
    217         if(m.selectIO!==true){invoke('clear');const error=Error('Unsupported tricuIO version for legacy worker client');error.recoverable=true;throw error;}
    218         try{
    219           const start=selected.tree,run=String(++privateRun);
    220           const tree=constructors({leaf:start.leaf,observe:h=>invoke('observe',h),stem:h=>invoke('stem',h),fork:(a,b)=>invoke('fork',a,b),app:(a,b)=>invoke('app',a,b)});
    221           const session=new Session(tree,start.action,run,roots=>invoke('retain',...new Set(roots)),{summarize:false,steps:m.ioStep===true?1:128,backend:'haskell-wasm'});
    222           privateSession={session,public:true,started:false,clear:()=>invoke('clear'),view:output=>invoke('view',session.core.result.value,output)};
    223           bytes=encoder.encode(JSON.stringify({kind:'v2',run})).buffer;
    224         }catch(error){privateSession=null;try{invoke('clear');}catch{}throw error;}
    225       }
    226     }
    227     postMessage({id:m.id,ok:true,bytes,milliseconds:performance.now()-begin,linearBytes:instance.exports.memory.buffer.byteLength,phases,scopeNames,compilerHash:['init','compiler-init'].includes(m.type)?(compilerReady?compilerHash:null):undefined},[bytes]);
    228   }catch(error){
    229     const fatal=m.type==='init'||!error.recoverable;
    230     if(fatal)dead=true;
    231     postMessage({id:m.id,ok:false,error:error.message+(stderr?"\n"+stderr:""),fatal});
    232     if(fatal)close();
    233   }finally{busy=false;}
    234 };