tricu

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

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 }