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 }