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 }