tricu

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

browser-worker.mjs (10700B)


      1 import {WASI,File,OpenFile,ConsoleStdout,PreopenDirectory} from './shim/index.js';
      2 import pins,{compilerHash} from './pins.mjs';
      3 import {moduleDependencies} from './site/module-stage.js';
      4 import {fetchBytes} from './site/bundle.js';
      5 import {environmentProfile} from './site/packages.js';
      6 import {frame,authenticatedBundle,loadFields,application,evaluation,snapshotFields,snapshotBundle,exportedTree,environmentFields,emptyEnvironment,dagBytes} from './bridge.mjs';
      7 let instance,booted=false,dead=false,stderr="",storedBundle=null,busy=false,compilerReady=false;
      8 const dataFiles=new Map();
      9 const encoder=new TextEncoder(),decoder=new TextDecoder('utf-8',{fatal:true});
     10 function scalarText(s,limit){
     11   if(typeof s!=='string')throw Error('Expected string');
     12   let count=0;
     13   for(const c of s){const n=c.codePointAt(0);if(++count>limit||n>=0xd800&&n<=0xdfff)throw Error('Invalid Unicode or scalar limit');}
     14   return s;
     15 }
     16 async function checkedFile(name,corrupt){
     17   const response=await fetch(new URL('data/'+name,import.meta.url));
     18   if(!response.ok)throw Error('Data fetch failed');
     19   const bytes=new Uint8Array(await response.arrayBuffer());
     20   // Test-only fault injection can corrupt, never replace the trusted pin.
     21   if(corrupt&&bytes.length)bytes[0]^=1;
     22   const hash=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),b=>b.toString(16).padStart(2,'0')).join('');
     23   if(hash!==pins[name])throw Error('Prepared input SHA-256 mismatch');
     24   return new File(bytes,{readonly:true});
     25 }
     26 function call(name,text){
     27   const e=instance.exports;
     28   let ptr=0,status;
     29   try{
     30     if(text===undefined)status=e[name]();
     31     else{
     32       const bytes=typeof text==='string'?encoder.encode(text):text;
     33       if(bytes.length>16777216)throw Error('Input byte limit');
     34       ptr=e.malloc(Math.max(bytes.length,1));if(!ptr)throw Error('Allocation failed');
     35       new Uint8Array(e.memory.buffer,ptr,bytes.length).set(bytes);
     36       status=e[name](ptr,bytes.length);
     37     }
     38   }finally{if(ptr)e.free(ptr);}
     39   const length=e.tricu_response_size();
     40   if(length<0||length>16777216)throw Error('Invalid response length');
     41   ptr=e.malloc(Math.max(length,1));if(!ptr)throw Error('Allocation failed');
     42   let output;
     43   try{
     44     if(e.tricu_response_copy(ptr,length)!==length)throw Error('Response copy failed');
     45     // Refresh the view after every call: memory.grow detaches old views.
     46     output=new Uint8Array(e.memory.buffer,ptr,length).slice();
     47   }finally{e.free(ptr);}
     48   const decoded=decoder.decode(output);
     49   if(status!==0){const error=Error(decoded.replace(/^error\n/,''));error.recoverable=status===1;throw error;}
     50   if(!decoded.startsWith('ok\n'))throw Error('Invalid response tag');
     51   return output.subarray(3).slice().buffer;
     52 }
     53 onmessage=async({data:m})=>{
     54   if(busy){postMessage({id:m.id,ok:false,error:'Worker is busy',fatal:false});return;}
     55   busy=true;
     56   const begin=performance.now();
     57   try{
     58     if(dead)throw Error('Disposed runtime');
     59     let bytes,phases,scopeNames;
     60     const encode=value=>encoder.encode(JSON.stringify(value)).buffer;
     61     const operation=fields=>call('tricu_request',frame(fields));
     62     const safe=async action=>{try{return await action();}catch(e){e.recoverable=true;throw e;}};
     63     if(m.type==='init'){
     64       if(booted)throw Error('Already initialized');booted=true;
     65       const files=m.compiler===false?[]:await Promise.all(Object.keys(pins).map(async name=>[name,await checkedFile(name,m.corrupt===name)]));
     66       for(const [name,file] of files)dataFiles.set(name,file);
     67       const fetched=performance.now();
     68       const response=await fetch(new URL('reactor.wasm',import.meta.url));
     69       if(!response.ok)throw Error('WASM fetch failed');
     70       const module=await WebAssembly.compileStreaming(response);
     71       const compiled=performance.now();
     72       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)]);
     73       instance=await WebAssembly.instantiate(module,{wasi_snapshot_preview1:wasi.wasiImport});
     74       wasi.initialize(instance);instance.exports.tricu_rts_init();
     75       const initialized=performance.now();
     76       bytes=call(m.compiler===false?'tricu_initialize_empty':'tricu_initialize');
     77       compilerReady=m.compiler!==false;
     78       phases={data_ms:fetched-begin,wasm_fetch_compile_ms:compiled-fetched,instantiate_rts_ms:initialized-compiled,load_ms:performance.now()-initialized};
     79     }else{
     80       if(!instance)throw Error('Not initialized');
     81       if(m.type==='compiler-init'){
     82         if(!compilerReady){
     83           const files=await safe(()=>Promise.all(Object.keys(pins).map(async name=>[name,await checkedFile(name)])));
     84           for(const [name,file] of files)dataFiles.set(name,file);
     85           bytes=call('tricu_initialize');compilerReady=true;
     86         }else bytes=encoder.encode('ready').buffer;
     87       }else if(m.type==='expression'){
     88         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');});
     89         bytes=operation(['expression',m.name,m.source,m.mode,m.output]);
     90       }else if(m.type==='module-header'){
     91         const source=await safe(()=>scalarText(m.source,16384));
     92         bytes=operation(['module-header',source]);
     93       }else if(m.type==='compile'){
     94         try {
     95           const source=await safe(()=>{const text=scalarText(m.source,16384);if(!text.trim())throw Error('Enter a source unit or native module');return text;});
     96           const stage=JSON.parse(decoder.decode(operation(['module-stage',source])));
     97           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;});
     98           if(stage.native){
     99             const dependencies=await safe(()=>moduleDependencies(base,stage,m.localModules));
    100             for(const [ref,bundle] of dependencies){
    101               const fields=await safe(()=>['module-dependency',ref,dagBytes(bundle.nodes),...bundle.exports.flatMap(e=>[e.name,String(e.root)])]);
    102               operation(fields);
    103             }
    104             bytes=operation(['module-compile']);
    105           }else{
    106             let environment=m.environment;
    107             if(m.profileName!==undefined){
    108               environment=await safe(async()=>{
    109                 const raw=JSON.parse(decoder.decode(await fetchBytes(new URL('environments.json',base),32768)));
    110                 const selected=environmentProfile(raw,m.profileName);
    111                 if(selected.compiler!==compilerHash)throw Error('Demo environment/compiler pin mismatch');
    112                 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;
    113               });
    114             }
    115             if(environment===undefined)bytes=call('tricu_compile',source);
    116             else{
    117               let fields;
    118               if(environment===null)fields=emptyEnvironment();
    119               else{
    120                 const env=await safe(()=>authenticatedBundle(environment.bytes,environment.hash));
    121                 fields=await safe(()=>environmentFields(env,environment.names));
    122               }
    123               bytes=operation(['compile',source,...fields]);
    124             }
    125           }
    126           scopeNames=JSON.parse(decoder.decode(operation(['module-scope'])));
    127           storedBundle=null;
    128         }catch(error){
    129           if(error.recoverable)operation(['module-abort']);
    130           throw error;
    131         }
    132       }else if(m.type==='load'){
    133         const bundle=await safe(()=>authenticatedBundle(m.bytes,m.hash));
    134         const fields=await safe(()=>loadFields(bundle));
    135         bytes=operation(fields);storedBundle=bundle;
    136       }else if(m.type==='evaluate'){
    137         const fields=await safe(()=>evaluation(m.name,m.arguments,m.output));
    138         bytes=operation(fields);
    139       }else if(m.type==='module-snapshot'){
    140         bytes=operation(['module-snapshot']);
    141       }else if(m.type==='preview'||m.type==='snapshot'){
    142         const fields=await safe(()=>snapshotFields(m.name,m.arguments,m.limit??(m.type==='preview'?64:100000),m.type==='snapshot'));
    143         bytes=operation(fields);
    144       }else if(m.type==='export'){
    145         if(storedBundle)bytes=encode(await safe(()=>exportedTree(storedBundle,m.name,m.format)));
    146         else{
    147           const fields=await safe(()=>snapshotFields(m.name,[],m.limit??100000,true));
    148           const snapshot=JSON.parse(decoder.decode(operation(fields)));
    149           bytes=encode(await safe(()=>exportedTree(snapshotBundle(snapshot,m.name),m.name,m.format)));
    150         }
    151       }else if(m.type==='io-start'){
    152         const args=await safe(()=>application(m.name,m.arguments));
    153         bytes=operation(['io-start',...args]);
    154       }else if(m.type==='io-resume'){
    155         await safe(()=>{if(!Number.isSafeInteger(m.token)||m.token<1)throw Error('Invalid IO token');scalarText(m.value??'',256);});
    156         bytes=operation(['io-resume',String(m.token),m.value??'']);
    157       }else if(m.type==='stats'){
    158         bytes=encode({linearMemoryBytes:instance.exports.memory.buffer.byteLength});
    159       }else if(m.type==='io-cancel'){
    160         bytes=operation(['io-cancel']);
    161       }else if(m.type==='io-snapshot'){
    162         bytes=operation(['io-snapshot',String(m.limit??100000)]);
    163       }else if(m.type==='io-view'){
    164         bytes=operation(['io-view',String(m.output)]);
    165       }else if(m.type==='observe'){
    166         // The private wire uses newline-separated names/ternary arguments.
    167         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;}
    168         bytes=call('tricu_observe',[m.name,...m.arguments].join('\n'));
    169       }else if(['dump','reset','collect'].includes(m.type)){bytes=call('tricu_'+m.type);if(m.type==='reset')storedBundle=null;}
    170       else{const e=Error('Unknown operation');e.recoverable=true;throw e;}
    171     }
    172     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]);
    173   }catch(error){
    174     const fatal=m.type==='init'||!error.recoverable;
    175     if(fatal)dead=true;
    176     postMessage({id:m.id,ok:false,error:error.message+(stderr?"\n"+stderr:""),fatal});
    177     if(fatal)close();
    178   }finally{busy=false;}
    179 };