live-ux-probe.mjs (10858B)
1 // Actual page, both production backends. No replacement evaluator or UI implementation. 2 const assert=(value,message)=>{if(!value)throw Error(message);}; 3 const el=id=>document.getElementById(id); 4 const delay=ms=>new Promise(r=>setTimeout(r,ms)); 5 async function wait(fn){const end=Date.now()+90000;while(Date.now()<end){if(fn())return;await delay(20);}throw Error('Timed out: '+el('status').textContent+' / '+el('result').textContent+' / '+el('compile-diagnostic').textContent);} 6 const status=text=>wait(()=>el('status').textContent===text); 7 const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));}; 8 const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));}; 9 export async function run(control,backend='haskell-wasm',alreadyUsed=false){ 10 await status('Ready'); 11 change('runtime-backend',backend);await status('Ready'); 12 assert(!el('use-guided-inputs').checked,'Expression input is the default'); 13 const evaluate=async(expression,value,codec='number')=>{ 14 input('expression-editor',expression);change('output-type',codec);el('run').click();await status('Finished'); 15 assert(el('result').textContent===(typeof value==='string'?value:JSON.stringify(value,null,2)),el('result').textContent); 16 }; 17 const requests=async()=>await(await fetch(control+'/requests')).json(); 18 if(!alreadyUsed)assert(!(await requests()).some(x=>x.endsWith('/data/compiler.dag')),'Loading alone does not fetch the compiler kit'); 19 const fixtures=await(await fetch('/zig-fixtures.json')).json(); 20 const base=new URL('../',document.querySelector('script[type=module][src]').src); 21 const {Graph}=(await import(new URL('dist/probe-api.js',base))).graph; 22 const {view,readList}=(await import(new URL('dist/probe-api.js',base))).codecs; 23 const {printLiteral}=(await import(new URL('dist/probe-api.js',base))).literalInput; 24 const active=new Set([...el('program-picker').options].map(o=>o.value)); 25 for(const [name,demo] of Object.entries(fixtures.demos).filter(([name])=>active.has('demo:'+name))){ 26 change('program-picker','demo:'+name);await status('Ready'); 27 const c=demo.haskellCases[0];change('export',c.export); 28 const asSource=ternary=>{let at=0;const read=()=>{const tag=ternary[at++];return tag==='0'?'t':tag==='1'?`(t ${read()})`:`(t ${read()} ${read()})`;};return read();}; 29 const graph=new Graph(),templates=JSON.parse(el('arguments').value); 30 const typedSource=(tree,template)=>{ 31 if(!template||template.type==='tree')return asSource(graph.toTernary(tree,100000)); 32 if(template.type==='list')return '['+readList(graph,tree).map((child,i)=>typedSource(child,template.value[i])).join(' ')+']'; 33 return printLiteral({type:template.type,value:view(graph,tree,template.type).value}); 34 }; 35 input('expression-editor',[c.export,...c.arguments.map((arg,i)=>typedSource(graph.fromTernary(arg),templates[i]))].join(' ')); 36 const expected=view(graph,graph.fromTernary(c.result),el('output-type').value); 37 assert(!['tree','list'].includes(expected.type),'Full scalar/record golden required'); 38 el('run').click(); 39 if(demo.mode==='io'){await status('Waiting for input');input('line-input',c.input);el('input-form').requestSubmit();} 40 await status('Finished'); 41 assert(el('result').textContent===(typeof expected.value==='string'?expected.value:JSON.stringify(expected.value,null,2)),'Complete expression golden: '+name+' '+el('result').textContent); 42 assert(el('terminal').textContent===c.terminal,'Complete terminal golden: '+name); 43 } 44 change('program-picker','demo:language-reference');await status('Ready'); 45 await evaluate('identity (first 42 7)','42'); 46 await evaluate('let\n local x = identity x\n in local 19','19'); 47 const exports=[...el('export').options].map(x=>x.value).join(','); 48 input('expression-editor','unknownBrowserName');el('run').click();await status('Stopped'); 49 assert(/at expression 1:1.*unresolved name/.test(el('result').textContent),'Expression reports user coordinates'); 50 input('expression-editor','7)\ninjected = 9\nescape = (');el('run').click();await status('Stopped'); 51 assert(/Compiler diagnostic|one expression/.test(el('result').textContent),'Expression cannot inject definitions'); 52 await evaluate('identity 23','23'); 53 assert([...el('export').options].map(x=>x.value).join(',')===exports,'Expressions never publish temporary names'); 54 // No editable source required: bind exact published library exports to trees. 55 assert(el('settings-panel').hidden,'Package loading must not require opening Settings'); 56 change('program-picker','package:tricu-lib-packages-base');await status('Ready'); 57 assert(!el('run').disabled&&el('settings-panel').hidden,'Selecting a package makes it usable without a hidden Load action'); 58 assert(el('result').textContent===''&&el('terminal').textContent==='','Package selection only loads; it never evaluates'); 59 assert(el('source-editor').hidden,'Library has no editable source'); 60 assert([...el('export').options].some(x=>x.value==='append'),'Published list export exists'); 61 await evaluate('append "hello " "λ🙂"','hello λ🙂','string'); 62 await evaluate('showNumber 30','30','string'); 63 // Source revision safety, deterministic late completion, and edit coalescing. 64 change('program-picker','demo:language-reference');await status('Ready'); 65 await evaluate('answer','42'); 66 const NativeWorker=globalThis.Worker,held=[],submitted=[];let hold=true; 67 globalThis.Worker=class extends NativeWorker { 68 postMessage(message,...args){if(message.type==='compile')submitted.push(message.source);super.postMessage(message,...args);} 69 set onmessage(handler){super.onmessage=event=>{if(hold&&['compiled','error'].includes(event.data.type))held.push(()=>handler(event));else handler(event);};} 70 }; 71 const original=el('source-editor').value; 72 input('source-editor',original.replace('first 42 7','first 43 7')); 73 assert(el('run').disabled,'No running stale source'); 74 assert(!el('result-stale').hidden&&el('result').textContent==='42','Keep previous result, marked stale'); 75 await status('Compiling');assert(!el('source-editor').disabled,'Editing continues while compiling'); 76 input('source-editor',original.replace('first 42 7','first 44 7')); 77 input('source-editor',original.replace('first 42 7','first 45 7')); 78 await wait(()=>held.length>0);assert(submitted.length===1,'Do not restart on each keystroke'); 79 hold=false;held.shift()();await status('Compiled'); 80 assert(submitted.length===2&&submitted[1].includes('first 45 7'),'Compile only latest queued revision'); 81 assert(el('result').textContent==='42','Compiling never executes'); 82 await evaluate('answer','45'); 83 hold=true;input('source-editor','answer = missingStaleName'); 84 await wait(()=>held.length>0);input('source-editor','answer = 45'); 85 hold=false;held.shift()();await status('Compiled'); 86 assert(el('compile-diagnostic').hidden,'Stale diagnostics are discarded too'); 87 // A diagnostic cannot leave a preceding definition runnable. 88 input('source-editor','answer = missingLatestName');await status('Compilation failed'); 89 assert(el('run').disabled&&!el('compile-diagnostic').hidden,'Invalid latest source blocks evaluation'); 90 assert(el('result').textContent==='45'&&!el('result-stale').hidden,'Diagnostic preserves prior result'); 91 input('source-editor','answer = 46');await status('Compiled');await evaluate('answer','46'); 92 // Runtime switching rebuilds automatically but cannot carry definitions across. 93 const other=backend==='haskell-wasm'?'zig-wasm':'haskell-wasm'; 94 change('runtime-backend',other);assert(el('run').disabled,'Backend change invalidates runnable revision'); 95 await status('Compiled');await evaluate('answer','46'); 96 change('runtime-backend',backend);await status('Compiled'); 97 // Stop cancels queued idle work; it is not a request to restart compilation. 98 input('source-editor','answer = 47');el('stop').click();await delay(900); 99 assert(el('status').textContent==='Stopped','Stop suppresses idle work'); 100 input('source-editor','answer = 48');await status('Compiled');await evaluate('answer','48'); 101 el('source-editor').dispatchEvent(new CompositionEvent('compositionstart')); 102 input('source-editor','answer = 49');await delay(900); 103 assert(el('status').textContent==='Changes pending','IME input is not compiled mid-composition'); 104 el('source-editor').dispatchEvent(new CompositionEvent('compositionend'));await status('Compiled');await evaluate('answer','49'); 105 input('source-editor','answer = 50');change('program-picker','demo:language-reference');await status('Ready');await delay(900); 106 assert(el('status').textContent==='Ready','Changing package cancels queued source compilation'); 107 // An IO expression is never executed just by editing/compiling it. 108 document.querySelector('[data-compiler-example=input]').click();await status('Compiled'); 109 assert(el('input-form').hidden&&el('terminal').textContent==='','Idle compilation has no host effects'); 110 document.querySelector('[data-compiler-example=input]').click(); 111 assert(el('run').disabled,'Even identical source in a new scratch context must wait for its own revision'); 112 await status('Compiled'); 113 input('expression-editor','io');el('run').click();await status('Waiting for input'); 114 assert(el('source-editor').disabled,'Background edits cannot interrupt an IO session'); 115 assert(el('reset').disabled,'Clear output cannot interrupt an active IO session'); 116 input('line-input','hello λ🙂');el('input-form').requestSubmit();await status('Finished'); 117 assert(el('result').textContent==='hello λ🙂','Expression IO result'); 118 // Invalid guided arguments must not leave a stale downloadable payload. 119 el('use-guided-inputs').checked=true;el('use-guided-inputs').dispatchEvent(new Event('change')); 120 input('arguments','invalid JSON');el('run').click();await status('Stopped'); 121 assert(el('copy-output').disabled&&el('download-output').disabled,'Displayed errors cannot download old output'); 122 input('arguments','[]');el('use-guided-inputs').checked=false;el('use-guided-inputs').dispatchEvent(new Event('change')); 123 // Unused divergent trees must not be normalized merely to build an environment. 124 input('source-editor','id x = x\nbomb = (x : x x) (x : x x)');await status('Compiled'); 125 change('mode','pure');await evaluate('id 51','51'); 126 input('expression-editor','bomb');el('run').click();await status('Running');await delay(150); 127 assert(el('status').textContent==='Running','CPU-bound expression is still running'); 128 el('stop').click();await status('Stopped'); 129 globalThis.Worker=NativeWorker; 130 return {userAgent:navigator.userAgent,checks:[backend+' expressions: published, read-only library, local bindings, diagnostics, IO','idle compilation: stale completion rejection, coalescing, no implicit execution, output retention, Stop, backend rebuild']}; 131 }