tricu

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

site-session.mjs (11732B)


      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,ioV2Private,ioV2Profile,ioV2Session,ioV2Driver}=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 nextRun=0n;const lifetime=new AbortController();
     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   async function io(handle,host,reader=x=>x) {
     42     const tree=ioV2Private.constructors({leaf:g.leaf,observe:g.observe.bind(g),stem:g.stem.bind(g),fork:g.fork.bind(g),app:g.app.bind(g)});
     43     const selected=ioV2Profile.envelope(tree,handle);
     44       if(!host.v2)throw Error('IO version 2 requires a structured terminal host');
     45       host.selected?.(2);
     46       if(nextRun===0xffffffffffffffffn)throw Error('Run identity exhausted');
     47       const run=String(++nextRun), interpreter=new ioV2Session.Session(tree,selected.action,run,roots=>g.sweepScope(roots),
     48         {summarize:false,steps:host.v2.stepping===true?1:128,backend:'zig-wasm'});
     49       return ioV2Driver.drive({run,signal:lifetime.signal,discard:()=>session.dispose(),turn:command=>{alive();return interpreter.turn(command);},view:async()=>{alive();return reader(interpreter.core.result.value);}},host);
     50   }
     51   const execute=(name,args,mode,host,reader)=>exclusive(async()=>{
     52     const root=definitions.get(name);if(root===undefined)throw Error('Missing definition');
     53     args=codec.literals(args);
     54     if(!['pure','io'].includes(mode))throw Error('Unsupported execution mode');
     55     try{return await g.scopeAsync(async()=>{
     56       const applied=g.apply(root,args.map(literal));
     57       return mode==='io'?await io(applied,host,reader):reader(applied);
     58     });}finally{if(!g.poisoned)g.collect();}
     59   });
     60   async function ensureCompiler(){
     61     if(compilerHash)return;
     62     const hash=comp.compilerDescriptor(await fetchJSON('compiler.json',4096)).hash;
     63     const next=importBundle(await bundle.fetchBundle(new URL(`/_arboricx/bundle/hash/${hash}`,base),hash,bundle.COMPILER_BUNDLE_LIMIT));
     64     if(!next.has(comp.COMPILER_EXPORT)){release(next);throw Error('Missing stable compiler entrypoint');}
     65     library=next;compilerHash=hash;
     66   }
     67   const session={
     68     get disposed(){return closed;},
     69     load(bytes,hash){return exclusive(async()=>{
     70       bundle.bundleHash(hash);
     71       if(!(bytes instanceof Uint8Array)||bytes.byteLength>bundle.BUNDLE_LIMIT)throw Error('Bundle byte limit/type');
     72       bytes=bytes.slice(); // Authenticate the same immutable snapshot we decode.
     73       if(await bundle.sha256(bytes)!==hash)throw Error('Bundle SHA-256 mismatch');
     74       const next=importBundle(bundle.decodeBundle(bytes));clearDefinitions();definitions=next;g.collect();
     75     });},
     76     inspectModule(source){return exclusive(async()=>{
     77       if(!compilerHash)throw Error('Compiler not initialized');
     78       return g.scope(()=>modules.stageModule(g,library,comp.sourceText(source)));
     79     });},
     80     exportModule(){return exclusive(async()=>{
     81       if(!definitions.size)throw Error('No compiled module');
     82       return g.scope(()=>moduleBundle.moduleSnapshot([...definitions.keys()],{...g.snapshot(codec.list(g,[...definitions.values()])),aliases:[]}));
     83     });},
     84     compile(source,profileName,localModules=[]){return exclusive(async()=>{
     85       clearDefinitions();
     86       if(!compilerHash)throw Error('Compiler not initialized');
     87       try {
     88         source=comp.sourceText(source);
     89         const staged=g.scope(()=>modules.stageModule(g,library,source));
     90         if(staged.native){
     91           const dependencies=await modules.moduleDependencies(base,staged,localModules);alive();
     92           const next=g.scope(({keep})=>{
     93             const loaded=new Map([...dependencies].map(([ref,value])=>[ref,importBundle(value)]));
     94             const imported=new Map();
     95             const qualify=library.get('source_module.sourceModuleQualified');
     96             if(qualify===undefined)throw Error('Missing native module compiler helper');
     97             for(const [ref,alias] of staged.imports){
     98               const values=loaded.get(ref);if(!values)throw Error('Missing module dependency');
     99               for(const [name,h] of values){
    100                 const qualified=g.scope(()=>g.toText(g.apply(qualify,[g.ofText(alias),g.ofText(name)]),4096));
    101                 if(imported.has(qualified))throw Error('duplicate imported name');
    102                 if(imported.size>=4096)throw Error('Module binding limit exceeded');
    103                 imported.set(qualified,h);
    104               }
    105             }
    106             const environment=codec.list(g,[...imported].map(([name,h])=>g.fork(g.ofText(name),h)));
    107             const result=g.apply(library.get(comp.COMPILER_EXPORT),[environment,g.ofText(staged.body,16384)]);
    108             const locals=comp.compiledDefinitions(g,result,true);
    109             const publicValues=modules.selectModuleExports(staged,locals,loaded);
    110             const own=values=>new Map([...values].map(([name,h])=>[name,keep(g.retain(h))]));
    111             return {public:own(publicValues),scope:own(new Map([...imported,...locals]))};
    112           });
    113           definitions=next.public;expressionScope=next.scope;
    114         }else{
    115           let support,selected;
    116           if(profileName!==undefined){
    117             selected=packages.environmentProfile(await fetchJSON('environments.json',32768),profileName);
    118             if(selected.compiler!==compilerHash)throw Error('Demo environment/compiler pin mismatch');
    119             if(selected.profile)support=await bundle.fetchBundle(new URL(`/_arboricx/bundle/hash/${selected.profile.hash}`,base),selected.profile.hash);
    120           }
    121           alive();
    122           definitions=g.scope(({keep})=>{
    123             let environment;
    124             if(profileName===undefined)environment=contracts.contractEnvironment(g,library);
    125             else {
    126               const values=support?importBundle(support):new Map();
    127               environment=codec.list(g,(selected.profile?.names??[]).map(name=>{
    128                 const h=values.get(name);if(h===undefined)throw Error(`Missing demo environment binding ${name}`);
    129                 return g.fork(g.ofText(name),h);
    130               }));
    131             }
    132             const result=g.apply(library.get(comp.COMPILER_EXPORT),[environment,g.ofText(source,16384)]);
    133             const values=comp.compiledDefinitions(g,result);
    134             for(const h of values.values())keep(h);
    135             return values;
    136           });
    137         }
    138         return [...definitions.keys()];
    139       }finally{if(!g.poisoned&&!closed)g.collect();}
    140     });},
    141     runDefinitionIO(name,output,host){return exclusive(async()=>{
    142       const root=(expressionScope??definitions).get(name);if(root===undefined)throw Error('Missing definition');
    143       output=codec.outputType(output);
    144       try{return await g.scopeAsync(async()=>io(root,host,h=>view(h,output)));}finally{if(!g.poisoned)g.collect();}
    145     });},
    146     probeIO(name){return exclusive(async()=>{
    147       const root=(expressionScope??definitions).get(name);if(root===undefined)throw Error('Missing definition');
    148       try{return g.scope(()=>{
    149         try{ioV2Profile.envelope({observe:g.observe.bind(g)},root);return true;
    150         }catch(error){if(g.poisoned||error instanceof WebAssembly.RuntimeError)throw error;return false;}
    151       });}finally{if(!g.poisoned)g.collect();}
    152     });},
    153     inspectDefinition(name,output){return exclusive(async()=>{
    154       const root=(expressionScope??definitions).get(name);if(root===undefined)throw Error('Missing definition');
    155       output=codec.outputType(output);
    156       try{return g.scope(()=>view(root,output));}finally{if(!g.poisoned)g.collect();}
    157     });},
    158     run(name,args,output,mode,host){
    159       output=codec.outputType(output);
    160       return execute(name,args,mode,host,h=>view(h,output));
    161     },
    162     runExpression(source,output,mode,host){return exclusive(async()=>{
    163       const unit=expressions.expressionUnit(source,(expressionScope??definitions).keys());
    164       output=codec.outputType(output);
    165       if(!['pure','io'].includes(mode))throw Error('Unsupported execution mode');
    166       await ensureCompiler();
    167       try{return await g.scopeAsync(async()=>{
    168         const environment=codec.list(g,[...(expressionScope??definitions)].map(([name,h])=>g.fork(g.ofText(name),h)));
    169         const values=comp.compiledDefinitions(g,g.apply(library.get(comp.COMPILER_EXPORT),[environment,g.ofText(unit.source,16384)]));
    170         if(values.size!==1||!values.has(unit.name))throw Error('Enter one expression, not a source unit');
    171         const result=values.get(unit.name);
    172         return mode==='io'?await io(result,host,h=>view(h,output)):view(result,output);
    173       });}catch(error){throw expressions.expressionDiagnostic(error);}finally{if(!g.poisoned)g.collect();}
    174     });},
    175     // Explicit, bounded full observation for independent embedding conformance;
    176     // never used to preserve definitions or to implement the site's Run button.
    177     inspect(name,args,mode,host){return execute(name,args,mode,host,h=>g.toTernary(h,1000000));},
    178     preview(name,limit=64){return exclusive(()=>g.scope(()=>{
    179       const root=definitions.get(name);if(root===undefined)throw Error('Missing definition');
    180       return {...g.preview(root,limit),aliases:[]};
    181     }));},
    182     collect(){alive();try{g.collect();}catch(error){if(g.poisoned)session.dispose();throw error;}},
    183     diagnostics(){alive();return {...g.stats(),linearMemoryBytes:g.e.memory.buffer.byteLength};},
    184     stats(){alive();return {backend:'zig-wasm',linearMemoryBytes:g.e.memory.buffer.byteLength};},
    185     dispose(){if(closed)return;closed=true;lifetime.abort(Error('Disposed Zig session'));if(!busy)cleanup();},
    186   };
    187   try {
    188     if(compiler)await ensureCompiler();
    189     return session;
    190   }catch(error){session.dispose();throw error;}
    191 }