tricu

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

site-session.mjs (11411B)


      1 // Zig's private site adapter. Only cloneable values cross the session boundary;
      2 // native owned handles stay here. Shared helpers validate/authenticate inputs.
      3 import {WasmRuntime} from './runtime.mjs';
      4 export async function create({base,compiler}) {
      5   const {bundle,codec,comp,contracts,packages,expressions,treeResults,modules,moduleBundle}=await import(new URL('dist/zig-helpers.js',base).href);
      6   const bytes=await bundle.fetchBytes(new URL('runtime/zig/arboricx.wasm',base));
      7   const g=await WasmRuntime.create(bytes,{unbounded:true});
      8   // The shared DAG reader's tick is JS accounting, not a native work ceiling.
      9   g.tick=()=>{};
     10   let closed=false,busy=false,definitions=new Map(),library=new Map(),expressionScope=null,compilerHash;
     11   let cancel;const cancelled=new Promise((_,reject)=>{cancel=reject;});cancelled.catch(()=>{});
     12   const alive=()=>{if(closed)throw Error('Disposed Zig session');};
     13   const cleanup=()=>{try{g.dispose();}catch{/* A poisoned instance is discarded, never reused. */}};
     14   async function exclusive(fn) {
     15     alive();if(busy)throw Error('Session already busy');busy=true;
     16     try{return await fn();}
     17     catch(error){if(g.poisoned||error instanceof WebAssembly.RuntimeError)closed=true;throw error;}
     18     finally{busy=false;if(closed)cleanup();}
     19   }
     20   function importBundle(value) {
     21     return g.scope(({keep})=>new Map([...bundle.loadBundle(g,value)].map(([name,h])=>[name,keep(g.retain(h))])));
     22   }
     23   const release=values=>{for(const h of values.values())g.release(h);};
     24   function clearDefinitions(){
     25     release(definitions);definitions=new Map();
     26     if(expressionScope)release(expressionScope);expressionScope=null;
     27   }
     28   const fetchJSON=async(name,limit)=>JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(await bundle.fetchBytes(new URL(name,base),limit)));
     29   function literal(value) {
     30     if(value.type==='string')return g.ofText(value.value);
     31     if(value.type==='list')return codec.list(g,value.value.map(literal));
     32     return codec.makeLiteral(g,value);
     33   }
     34   function view(h,type) {
     35     if(treeResults.isTreeResult(type))return treeResults.treeResult({...g.snapshot(h),aliases:[]},type);
     36     if(type==='string')return {type,value:g.toText(h)};
     37     if(type==='tree')return {type,value:{...g.preview(h),aliases:[]}};
     38     if(type==='list')return {type,value:codec.readList(g,h,32).map(x=>({...g.preview(x,8),aliases:[]}))};
     39     return codec.view(g,h,type);
     40   }
     41   const wait=promise=>Promise.race([promise,cancelled]);
     42   async function io(handle,host) {
     43     const [sentinel,body]=g.pair(handle);
     44     if(g.toText(sentinel,7)!=='tricuIO')throw Error('Missing tricuIO sentinel');
     45     const [version,initial]=g.pair(body);
     46     if(codec.readNatural(g,version)!==1n)throw Error('Unsupported tricuIO version');
     47     const frames=[];let action=initial,output=0;
     48     const names=new Map([[0n,'pure'],[1n,'bind'],[2n,'abortIO'],[10n,'putStr'],[11n,'getLine']]);
     49     for(let step=1;step<=128;step++) {
     50       alive();g.collect();
     51       const [tag,payload]=g.pair(action),code=codec.readNatural(g,tag),name=names.get(code);
     52       if(!name)throw Error(`Unsupported browser IO action ${code}`);
     53       const detail=code===10n?g.toText(payload):'retained value';
     54       await wait(host.before({step,name,detail,depth:frames.length}));alive();
     55       if(code===1n){const [left,continuation]=g.pair(payload);frames.push(continuation);action=left;host.after({step,name,value:'retained value'});continue;}
     56       if(code===2n)throw Error('Execution aborted (retained diagnostic; no payload forcing).');
     57       let value;
     58       if(code===0n)value=payload;
     59       else if(code===10n){output+=Array.from(detail).length;if(output>16384)throw Error('Terminal output limit exceeded');host.write(detail);value=g.leaf;}
     60       else {const line=await wait(host.read());alive();if(/[\r\n]/u.test(line))throw Error('Input must be one line');value=g.ofText(line,256);}
     61       host.after({step,name,value:'retained value'});
     62       if(!frames.length)return value;
     63       host.running();action=g.app(frames.pop(),value);
     64     }
     65     throw Error('IO action limit exceeded');
     66   }
     67   const execute=(name,args,mode,host,reader)=>exclusive(async()=>{
     68     const root=definitions.get(name);if(root===undefined)throw Error('Missing definition');
     69     args=codec.literals(args);
     70     if(!['pure','io'].includes(mode))throw Error('Unsupported execution mode');
     71     try{return await g.scopeAsync(async()=>{
     72       const applied=g.apply(root,args.map(literal));
     73       return reader(mode==='io'?await io(applied,host):applied);
     74     });}finally{if(!g.poisoned)g.collect();}
     75   });
     76   async function ensureCompiler(){
     77     if(compilerHash)return;
     78     const hash=comp.compilerDescriptor(await fetchJSON('compiler.json',4096)).hash;
     79     const next=importBundle(await bundle.fetchBundle(new URL(`/_arboricx/bundle/hash/${hash}`,base),hash,bundle.COMPILER_BUNDLE_LIMIT));
     80     if(!next.has(comp.COMPILER_EXPORT)){release(next);throw Error('Missing stable compiler entrypoint');}
     81     library=next;compilerHash=hash;
     82   }
     83   const session={
     84     get disposed(){return closed;},
     85     load(bytes,hash){return exclusive(async()=>{
     86       bundle.bundleHash(hash);
     87       if(!(bytes instanceof Uint8Array)||bytes.byteLength>bundle.BUNDLE_LIMIT)throw Error('Bundle byte limit/type');
     88       bytes=bytes.slice(); // Authenticate the same immutable snapshot we decode.
     89       if(await bundle.sha256(bytes)!==hash)throw Error('Bundle SHA-256 mismatch');
     90       const next=importBundle(bundle.decodeBundle(bytes));clearDefinitions();definitions=next;g.collect();
     91     });},
     92     inspectModule(source){return exclusive(async()=>{
     93       if(!compilerHash)throw Error('Compiler not initialized');
     94       return g.scope(()=>modules.stageModule(g,library,comp.sourceText(source)));
     95     });},
     96     exportModule(){return exclusive(async()=>{
     97       if(!definitions.size)throw Error('No compiled module');
     98       return g.scope(()=>moduleBundle.moduleSnapshot([...definitions.keys()],{...g.snapshot(codec.list(g,[...definitions.values()])),aliases:[]}));
     99     });},
    100     compile(source,profileName,localModules=[]){return exclusive(async()=>{
    101       clearDefinitions();
    102       if(!compilerHash)throw Error('Compiler not initialized');
    103       try {
    104         source=comp.sourceText(source);
    105         const staged=g.scope(()=>modules.stageModule(g,library,source));
    106         if(staged.native){
    107           const dependencies=await modules.moduleDependencies(base,staged,localModules);alive();
    108           const next=g.scope(({keep})=>{
    109             const loaded=new Map([...dependencies].map(([ref,value])=>[ref,importBundle(value)]));
    110             const imported=new Map();
    111             const qualify=library.get('source_module.sourceModuleQualified');
    112             if(qualify===undefined)throw Error('Missing native module compiler helper');
    113             for(const [ref,alias] of staged.imports){
    114               const values=loaded.get(ref);if(!values)throw Error('Missing module dependency');
    115               for(const [name,h] of values){
    116                 const qualified=g.scope(()=>g.toText(g.apply(qualify,[g.ofText(alias),g.ofText(name)]),4096));
    117                 if(imported.has(qualified))throw Error('duplicate imported name');
    118                 if(imported.size>=4096)throw Error('Module binding limit exceeded');
    119                 imported.set(qualified,h);
    120               }
    121             }
    122             const environment=codec.list(g,[...imported].map(([name,h])=>g.fork(g.ofText(name),h)));
    123             const result=g.apply(library.get(comp.COMPILER_EXPORT),[environment,g.ofText(staged.body,16384)]);
    124             const locals=comp.compiledDefinitions(g,result,true);
    125             const publicValues=modules.selectModuleExports(staged,locals,loaded);
    126             const own=values=>new Map([...values].map(([name,h])=>[name,keep(g.retain(h))]));
    127             return {public:own(publicValues),scope:own(new Map([...imported,...locals]))};
    128           });
    129           definitions=next.public;expressionScope=next.scope;
    130         }else{
    131           let support,selected;
    132           if(profileName!==undefined){
    133             selected=packages.environmentProfile(await fetchJSON('environments.json',32768),profileName);
    134             if(selected.compiler!==compilerHash)throw Error('Demo environment/compiler pin mismatch');
    135             if(selected.profile)support=await bundle.fetchBundle(new URL(`/_arboricx/bundle/hash/${selected.profile.hash}`,base),selected.profile.hash);
    136           }
    137           alive();
    138           definitions=g.scope(({keep})=>{
    139             let environment;
    140             if(profileName===undefined)environment=contracts.contractEnvironment(g,library);
    141             else {
    142               const values=support?importBundle(support):new Map();
    143               environment=codec.list(g,(selected.profile?.names??[]).map(name=>{
    144                 const h=values.get(name);if(h===undefined)throw Error(`Missing demo environment binding ${name}`);
    145                 return g.fork(g.ofText(name),h);
    146               }));
    147             }
    148             const result=g.apply(library.get(comp.COMPILER_EXPORT),[environment,g.ofText(source,16384)]);
    149             const values=comp.compiledDefinitions(g,result);
    150             for(const h of values.values())keep(h);
    151             return values;
    152           });
    153         }
    154         return [...definitions.keys()];
    155       }finally{if(!g.poisoned&&!closed)g.collect();}
    156     });},
    157     run(name,args,output,mode,host){
    158       output=codec.outputType(output);
    159       return execute(name,args,mode,host,h=>view(h,output));
    160     },
    161     runExpression(source,output,mode,host){return exclusive(async()=>{
    162       const unit=expressions.expressionUnit(source,(expressionScope??definitions).keys());
    163       output=codec.outputType(output);
    164       if(!['pure','io'].includes(mode))throw Error('Unsupported execution mode');
    165       await ensureCompiler();
    166       try{return await g.scopeAsync(async()=>{
    167         const environment=codec.list(g,[...(expressionScope??definitions)].map(([name,h])=>g.fork(g.ofText(name),h)));
    168         const values=comp.compiledDefinitions(g,g.apply(library.get(comp.COMPILER_EXPORT),[environment,g.ofText(unit.source,16384)]));
    169         if(values.size!==1||!values.has(unit.name))throw Error('Enter one expression, not a source unit');
    170         const result=values.get(unit.name);
    171         return view(mode==='io'?await io(result,host):result,output);
    172       });}catch(error){throw expressions.expressionDiagnostic(error);}finally{if(!g.poisoned)g.collect();}
    173     });},
    174     // Explicit, bounded full observation for independent embedding conformance;
    175     // never used to preserve definitions or to implement the site's Run button.
    176     inspect(name,args,mode,host){return execute(name,args,mode,host,h=>g.toTernary(h,1000000));},
    177     preview(name,limit=64){return exclusive(()=>g.scope(()=>{
    178       const root=definitions.get(name);if(root===undefined)throw Error('Missing definition');
    179       return {...g.preview(root,limit),aliases:[]};
    180     }));},
    181     collect(){alive();try{g.collect();}catch(error){if(g.poisoned)session.dispose();throw error;}},
    182     diagnostics(){alive();return {...g.stats(),linearMemoryBytes:g.e.memory.buffer.byteLength};},
    183     stats(){alive();return {backend:'zig-wasm',linearMemoryBytes:g.e.memory.buffer.byteLength};},
    184     dispose(){if(closed)return;closed=true;cancel(Error('Disposed Zig session'));if(!busy)cleanup();},
    185   };
    186   try {
    187     if(compiler)await ensureCompiler();
    188     return session;
    189   }catch(error){session.dispose();throw error;}
    190 }