io-v2-public-probe.mjs (9803B)
1 import {wait,status,change,inspect,el,pause,input} from './source-probe-helpers.mjs'; 2 export async function run(_control,backend){ 3 const assert=(ok,message)=>{if(!ok)throw Error(message);},checks=[]; 4 const base=new URL('../',document.querySelector('script[type=module][src]').src); 5 const {runtimeSession:{createRuntimeSession},ioV2TerminalView:{terminalView}}=await import(new URL('dist/probe-api.js',base)); 6 const fixture=await(await fetch('/io-v2-source.json')).json(),s=await createRuntimeSession(backend,false); 7 const legacy={before:async()=>{},after(){},read(){throw Error('legacy read invoked');},write(){throw Error('legacy write invoked');},running(){}}; 8 const parent=document.createElement('div');document.body.append(parent); 9 let widget,send,closed,steps=0,submitted=false,guardTask; 10 function host(mode='normal'){ 11 parent.replaceChildren();closed=false;submitted=false; 12 return {...legacy,before:async()=>{steps++;},v2:{stepping:mode==='step', 13 open(run,command){send=command;widget=terminalView(parent,run,reply=>command({run,kind:'reply',reply}),()=>command({run,kind:'stop'}));}, 14 event(event){widget.handle(event);if(event.kind==='read'&&mode==='stop'){ 15 guardTask=(async()=>{try{ 16 for(const operation of [()=>s.inspectDefinition('main','tree'),()=>s.compile('changed = 9','empty'),()=>s.runExpression('42','number','pure',legacy)]){ 17 let blocked=false;try{await operation();}catch(error){blocked=/pending|busy/.test(error.message);}assert(blocked,'unrelated work entered active v2 run'); 18 } 19 }finally{send({run:event.run,kind:'stop'});}})(); 20 } 21 if(event.kind==='read'&&mode==='input'){ 22 const form=parent.querySelector('form');if(form&&!form.hidden){if(!submitted){submitted=true;form.querySelector('input').value='位馃榾';form.requestSubmit();}else form.querySelector('button[type=button]').click();} 23 } 24 }, 25 close(_run,settled){assert(settled,'unacknowledged runtime loss');widget.finish();closed=true;}, 26 }}; 27 } 28 try{ 29 await s.load(new Uint8Array(fixture.bytes),fixture.hash); 30 assert(await s.probeIO('main'),'v2 inert recognition');assert(!await s.probeIO('direct'),'bare computation is not an envelope'); 31 for(const name of ['main','publicHandled','publicCancellation','publicBinary']){ 32 const out=await s.runDefinitionIO(name,'number',host());assert(out.value==='42'&&closed,'public compiled outcome '+name); 33 const bytes=widget.snapshot().output.bytes,expected=name==='publicBinary'?[92,120,102,102,255,240,159]:[...new TextEncoder().encode(name==='publicCancellation'?'ready':'位馃榾e\u0301\r\n')]; 34 assert(JSON.stringify(bytes)===JSON.stringify(expected),'public exact output '+name); 35 } 36 assert((await s.runDefinitionIO('publicRoundTrip','number',host('input'))).value==='42','line and EOF outcome'); 37 assert(JSON.stringify(widget.snapshot().output.bytes)===JSON.stringify([...new TextEncoder().encode('位馃榾\n')]),'line/EOF byte preservation'); 38 checks.push('compiled direct/handled/cancellation/binary programs through public RuntimeSession; explicit line/EOF'); 39 const cases=[['string',{type:'string',value:'位馃榾\0'},'位馃榾\0'],['number',{type:'number',value:'184467440737095516160'},'184467440737095516160'],['bool',{type:'bool',value:true},true],['bytes',{type:'bytes',value:[0,255,240]},[0,255,240]]]; 40 for(const [type,literal,expected]of cases){const out=await s.run('returnValue',[literal],type,'io',host());assert(JSON.stringify(out.value)===JSON.stringify(expected),'full codec '+type);} 41 for(const type of ['tree','list','tricu','triangle','dag','ternary']){ 42 const literal=type==='list'?{type:'list',value:[{type:'number',value:'42'}]}:{type:'tree',value:'200'}; 43 const observed=await s.run('returnValue',[literal],type,'io',host());assert(observed.type===type&&observed.value!==null,'full graph codec '+type); 44 if(['tricu','triangle','dag','ternary'].includes(type)){const expected=await s.runExpression('t t t',type,'pure',legacy);// DAG binders reflect physical node numbering, not semantic identities. 45 const alpha=text=>{const names=new Map();return text.replace(/:[fn][0-9]+/g,name=>{if(!names.has(name))names.set(name,':v'+names.size);return names.get(name);});}; 46 assert((type==='dag'?alpha(expected.value)===alpha(observed.value):expected.value===observed.value),'complete graph result differs from independent pure result '+type);} 47 } 48 const values=await s.run('returnValue',[{type:'list',value:[{type:'number',value:'42'},{type:'string',value:'hello'}]}],'values','io',host()); 49 assert(JSON.stringify(values.value)===JSON.stringify([{type:'number',value:'42'},{type:'string',value:'hello'}]),'values codec'); 50 checks.push('all public codecs including >64-bit number, full trees and decoded values'); 51 let rejected=false;try{await s.runDefinitionIO('publicRoundTrip','number',host('stop'));}catch(error){rejected=/cancelled/.test(error.message);}assert(rejected&&closed,'public structured cancellation');await guardTask; 52 let badCodec=false;try{await s.run('returnValue',[{type:'tree',value:'10'}],'number','io',host());}catch{badCodec=true;}assert(badCodec&&closed,'codec failure after cleanup'); 53 assert((await s.runExpression('returnValue "expression result"','string','io',host())).value==='expression result','IO expression result'); 54 assert((await s.run('returnValue',[{type:'number',value:'42'}],'number','io',host('step'))).value==='42'&&steps>0,'public scheduler stepping'); 55 checks.push('structured Stop drains read, definitions remain usable; expression and stepping paths'); 56 }finally{s.dispose();parent.remove();} 57 const doomed=await createRuntimeSession(backend,false);let abandoned=false; 58 try{ 59 await doomed.load(new Uint8Array(fixture.bytes),fixture.hash); 60 let rejected=false;try{await doomed.runDefinitionIO('publicRoundTrip','number',{...legacy,v2:{open(){},event(event){if(event.kind==='read')doomed.dispose();},close(_run,settled){abandoned=!settled;}}});} 61 catch(error){rejected=/disposed|terminated/i.test(error.message);}assert(rejected&&abandoned,'emergency dispose must reject without claiming cleanup'); 62 }finally{doomed.dispose();} 63 checks.push('emergency runtime disposal rejects idle read and explicitly does not acknowledge cleanup'); 64 // Real mounted editor + outer worker + nested native worker, not a mock host. 65 await status('Ready');if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');} 66 const index=await(await fetch(new URL('packages.json',base))).json(); 67 const io=index.releases.find(r=>r.title==='lib/io-v2.tri'&&r.version==='0.4.0');assert(io,'authenticated complete library'); 68 const source=`!import ${io.manifest} as IO\n!export answer\n!export binary\n!export waiting\n!export caps\n!export classification\n!export divergentValue\ndivergentValue = IO.io (IO.pure ((x : x x) (x : x x)))\nbackendOf rows = case rows of\n [["backend" value] | _] = value\n [_ | rest] = backendOf rest\n _ = "missing"\ncaps = IO.io (IO.require IO.capabilities (rows : IO.pure (backendOf rows)))\nloop = (x : x x) (x : x x)\nclassification = IO.io (IO.require (IO.inspect (IO.request "file" 1 "open" loop)) IO.pure)\nanswer = 42\nbinary = IO.io (do IO.require\n writer <- IO.stdout\n IO.writeAll writer [92 120 102 102 255 240 159]\n IO.pure "returned text")\nwaiting = IO.io (do IO.require\n reader <- IO.stdin\n IO.readSome reader 4\n IO.pure 42)`; 69 document.querySelector('[data-compiler-example=identity]').click();input('source-editor',source);await status('Compiled'); 70 await inspect('caps');el('io-return-format').value='string';el('run-io').click();await status('Finished');assert(document.querySelector('.definition-card').querySelector('pre').textContent===backend,'actual native capabilities'); 71 await inspect('classification');el('io-return-format').value='values';el('run-io').click();await status('Finished');assert(document.querySelector('.definition-card').querySelector('pre').textContent.includes('unsupported'),'inert inspection leaves divergent arguments opaque'); 72 await inspect('binary');el('io-return-format').value='string';el('run-io').click();await status('Finished'); 73 assert(el('terminal-v2').querySelector('[data-hex=output]').textContent==='5c 78 66 66 ff f0 9f','actual page lossless output'); 74 assert(document.querySelector('.definition-card').querySelector('pre').textContent==='returned text','actual page full returned text'); 75 await inspect('waiting');el('io-return-format').value='number';el('run-io').click();await wait(()=>el('terminal-v2').querySelector('form input'),'v2 input'); 76 const old=el('terminal-v2').querySelector('form');el('stop').click();assert(!el('stop').hidden,'Stop waits rather than disposing worker'); 77 await status('Stopped');assert(!el('terminal-v2').querySelector('form'),'input physically retired'); 78 old.dispatchEvent(new Event('submit',{cancelable:true}));assert((await inspect('answer','number'))==='42','compiled state survives structured Stop'); 79 // Stop also bypasses a suspended scheduler turn. 80 await inspect('waiting');el('step-mode').checked=true;el('run-io').click();await status('Paused before action');el('stop').click();await status('Stopped');el('step-mode').checked=false; 81 assert((await inspect('answer','number'))==='42','compiled state survives paused Stop'); 82 await inspect('divergentValue');el('io-return-format').value='number';el('run-io').click();await status('IO settled; interpreting returned value');el('stop').click();await status('Stopped'); 83 el('compile').click();await status('Compiled');assert((await inspect('answer','number'))==='42','hard Stop after acknowledged IO settlement can rebuild'); 84 await pause(10);checks.push('actual page exact bytes/full return, pending-input and paused-step Stop, stale DOM isolation and reuse without recompilation'); 85 return {backend,checks}; 86 }