tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

import-picker-probe.mjs (19808B)


      1 import {evaluateSource,inspect} from './source-probe-helpers.mjs';
      2 import {terminalHost} from './terminal-host.mjs';
      3 const assert=(ok,message)=>{if(!ok)throw Error(message);};
      4 const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms));
      5 const input=(id,value)=>{if(id==='source-editor'){globalThis.__sourceEditor.replaceSource(value,true);return;}el(id).value=value;el(id).dispatchEvent(new Event('input'));};
      6 const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));};
      7 async function wait(fn,label){for(let i=0;i<4500;i++){if(fn())return;await delay(20);}throw Error('Timeout '+label+' / '+el('status').textContent+' / '+el('import-status').textContent);}
      8 const status=value=>wait(()=>el('status').textContent===value,value);
      9 export async function run(control,backend='haskell-wasm'){
     10  const base=new URL('../',document.querySelector('script[type=module][src]').src);
     11  const api=await import(new URL('dist/probe-api.js',base));
     12  const {createRuntimeSession}=api.runtimeSession,{Terminal}=api.ioTerminal;
     13  const catalogue=await(await fetch(new URL('reference/libraries.json',base))).json();
     14  const lib=name=>catalogue.releases.find(r=>r.name==='tricu-lib-'+name&&r.version==='0.12.0');
     15  const math=lib('native-math'),foundation=lib('base');
     16  // Explicit dependencies adapt the user's divisor-first mod helper without
     17  // changing the FizzBuzz/range/map program under test.
     18  const fizzBuzzSource=`!import ${math.manifest} as N
     19 !import ${foundation.manifest} as B
     20 !export main
     21 !export x
     22 !export inside
     23 !export outside
     24 mod divisor value = N.remainder value divisor
     25 if = B.if
     26 gt? = B.gt?
     27 append = B.append
     28 add = B.add
     29 map = B.map
     30 fizzBuzz n = case [(mod 5 n) (mod 3 n)] of
     31   [0 0] = "fizzbuzz"
     32   [0 _] = "buzz"
     33   [_ 0] = "fizz"
     34   _ = n
     35 range a b = if (gt? a b) [] (append [a] (range (add a 1) b))
     36 x = range 1 100
     37 main = map fizzBuzz (range 1 100)
     38 zeros 0 = []
     39 zeros n = t 0 (zeros (N.subtract n 1))
     40 inside = zeros 4096
     41 outside = zeros 4097`;
     42  const expectedFizz=Array.from({length:100},(_,i)=>{const n=i+1,value=n%15===0?'fizzbuzz':n%5===0?'buzz':n%3===0?'fizz':n;return {type:typeof value==='string'?'string':'number',value:String(value)};});
     43  const expectedFizzText='['+expectedFizz.map(item=>item.type==='string'?JSON.stringify(item.value):item.value).join(' ')+']';
     44  // Static references and independently specified examples can be consumed without
     45  // a page session or a JavaScript module catalogue.
     46  const llms=await(await fetch(new URL('llms.txt',location.href))).text();
     47  assert(llms.includes('reference/getting-started.md'),'Agent entrypoint points into this release');
     48  const links=[...llms.matchAll(/\]\(([^)]+)\)/g)];assert(links.length>0,'Agent entrypoint contains reference links');
     49  for(const [,href]of links){
     50   const url=new URL(href);assert(url.origin==='https://arbori.cx','Agent reference uses the canonical absolute origin');
     51   // Serve the production URL's path through this test's local site mount.
     52   const path=url.pathname.replace(/^\//,'');
     53   assert((await fetch(new URL(path,location.href))).ok,'Readable static reference '+href);
     54  }
     55  const examples=await(await fetch(new URL('reference/examples.json',base))).json();
     56  const host={before:async()=>{},after:()=>{},write:()=>{},read:async()=>'',running:()=>{}};
     57  const session=await createRuntimeSession(backend,true);
     58  try{
     59   for(const example of examples){
     60    await session.compile(example.source,'empty');
     61    const terminal=example.mode==='io'?terminalHost(Terminal,null):null;
     62    const result=terminal
     63     ? await session.runDefinitionIO(example.expression,example.output,terminal)
     64     : await session.inspectDefinition(example.expression,example.output);
     65    assert(result.value===example.expected,'Documented result '+example.id+': '+JSON.stringify(result));
     66    assert((terminal?.terminalText??'')===example.terminal,'Documented terminal '+example.id);
     67   }
     68   await session.compile('!export answer\nanswer = private\nprivate = 42','empty');
     69   assert((await session.inspectDefinition('private','number')).value==='42','Compiled private binding can be inspected without an expression');
     70   const tree=await session.inspectDefinition('answer','tricu');assert(tree.file.text.length>0,'Compiled tree source');
     71   let invalid=false;try{await session.inspectDefinition('private','string');}catch{invalid=true;}
     72   assert(invalid,'Invalid interpretation rejected');
     73   assert((await session.inspectDefinition('answer','number')).value==='42','Decoder failure preserves definitions');
     74   await session.compile('!export samples\nsamples = ["hello" 42 1267650600228229401496703205376]','empty');
     75   const samples=await session.inspectDefinition('samples','values');
     76   assert(JSON.stringify(samples.value)===JSON.stringify([{type:'string',value:'hello'},{type:'number',value:'42'},{type:'number',value:'1267650600228229401496703205376'}]),'Bounded decoded scalar list, including exact big naturals');
     77   await session.compile(fizzBuzzSource,'empty');
     78   assert(JSON.stringify((await session.inspectDefinition('main','values')).value)===JSON.stringify(expectedFizz),'Compiled 100-element FizzBuzz list decodes completely');
     79   assert((await session.inspectDefinition('x','values')).value.length===100,'Numeric range has the same decoded-list capacity');
     80   assert((await session.inspectDefinition('inside','values')).value.length===4096,'Decoded-list boundary accepts 4096 entries');
     81   let overLimit=false;try{await session.inspectDefinition('outside','values');}catch(error){overLimit=/List decode limit/.test(String(error));}
     82   assert(overLimit,'4097 entries still fail closed, not a partial list');
     83   assert(JSON.stringify((await session.runExpression('main','values','pure',host)).value)===JSON.stringify(expectedFizz),'Over-limit failure preserves scope for ordinary expression evaluation');
     84   await session.compile('!export answer\nanswer = 42','empty');
     85   const stage=await session.inspectModule(`-- ๐Ÿ•\n!import ${math.manifest} as N\n!export answer\nanswer = 1`);
     86   assert(stage.imports[0][0]===math.manifest&&stage.imports[0][1]==='N','Native header response');
     87   assert((await session.runExpression('answer','number','pure',host)).value==='42','Inspection preserves compiled scope');
     88   let rejected=false;try{await session.inspectModule(`!import ${math.manifest} as\n!export answer\nanswer=1`);}catch{rejected=true;}
     89   assert(rejected,'Malformed header rejected');
     90   assert((await session.runExpression('answer','number','pure',host)).value==='42','Failed inspection preserves compiled scope');
     91  }finally{session.dispose();}
     92  await status('Ready');
     93  const inspectButton=name=>document.querySelector(`[aria-label="Inspect compiled definition ${name}"]`);
     94  inspectButton('greeting').click();await wait(()=>!document.querySelector('.definition-card select').disabled,'published inspection');
     95  const interpretation=value=>{const select=document.querySelector('.definition-card select');select.value=value;select.dispatchEvent(new Event('change'));};
     96  await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','automatic published text interpretation');
     97  assert(document.querySelector('.definition-card select').value==='auto'&&document.querySelector('.definition-card [role=status]').textContent.includes('Auto โ†’ Text'),'Auto identifies its chosen interpretation');
     98  const card=document.querySelector('.definition-card'), displayed=card.querySelector('pre'), controls=card.querySelector('.definition-card-controls');
     99  assert(card.firstElementChild===controls,'Definition heading precedes the result in reading order');
    100  assert(controls.getBoundingClientRect().bottom<=displayed.getBoundingClientRect().top,'Definition controls are above the result');
    101  assert(parseFloat(getComputedStyle(displayed).fontSize)>parseFloat(getComputedStyle(controls.querySelector('select')).fontSize),'Result typography is more prominent than controls');
    102  assert(card.querySelector('[role=status]').textContent==='Auto โ†’ Text','Successful inspection uses concise metadata');
    103  assert(card.querySelector('button').getAttribute('aria-label')==='Close definition inspection','Compact close remains accessible');
    104  inspectButton('greeting').click();assert(document.querySelector('.definition-card').hidden,'Second glyph click closes, not cycles');
    105  inspectButton('greeting').click();await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','reopen Auto');
    106  assert(card.querySelector('.inline-io').hidden&&el('terminal-io').textContent==='','Pure inspection keeps IO controls and terminal inactive');
    107  interpretation('number');await wait(()=>document.querySelector('.definition-card [role=status]').textContent.includes('Cannot interpret'),'invalid view');
    108  interpretation('string');await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','view recovery');
    109  document.querySelector('.definition-card button').click();
    110  document.querySelector('[data-compiler-example=module]').click();await status('Compiled');
    111  const original='-- preserve this comment ๐Ÿ•\n!export answer\nanswer = 42';
    112  input('source-editor','!export answer\nidentity x = x\nanswer = identity private\nprivate = 42');await status('Compiled');
    113  inspectButton('private').click();interpretation('number');await wait(()=>document.querySelector('.definition-card pre').textContent==='42','private inline value');
    114  const cardBounds=document.querySelector('.definition-card').getBoundingClientRect(), editorBounds=el('source-editor').getBoundingClientRect();
    115  assert(cardBounds.top>=editorBounds.top&&cardBounds.bottom<=editorBounds.bottom,'Inline result is anchored inside source viewport');
    116  input('source-editor','!export answer\nidentity x = x\nanswer = identity private\nprivate = 7');
    117  assert(document.querySelector('.definition-card pre').textContent==='','Edit invalidates old inline value immediately');
    118  await wait(()=>document.querySelector('.definition-card pre').textContent==='7','open inspection refreshes after compilation');
    119  assert(document.querySelector('.definition-card select').value==='number','Manual interpretation persists across edits');
    120  assert(!el('expression-editor'),'No parallel expression editor');
    121  input('source-editor','!export answer\nanswer = (');await status('Compilation failed');
    122  assert(document.querySelector('.definition-card pre').textContent==='','Failed compilation cannot display previous tree');
    123  input('source-editor','!export answer\nanswer = 42\nzero = t\nyes = t t\nidentity x = x\nsamples = ["hello" 42]');await status('Compiled');
    124  for(const [name,value,label] of [['zero','0','Natural number'],['yes','true','Boolean'],['samples','["hello" 42]','List ยท text / numbers']]){
    125   inspectButton(name).click();await wait(()=>document.querySelector('.definition-card pre').textContent===value,'Auto '+name);
    126   assert(document.querySelector('.definition-card [role=status]').textContent.includes('Auto โ†’ '+label),'Visible Auto interpretation '+name);
    127   inspectButton(name).click();
    128  }
    129  inspectButton('identity').click();await wait(()=>document.querySelector('.definition-card [role=status]').textContent.includes('Auto โ†’ tricu'),'function falls back to source');inspectButton('identity').click();
    130  assert(await inspect('samples','values')==='["hello" 42]','Explicit decoded-list view');
    131  input('source-editor',fizzBuzzSource);await status('Compiled');
    132  inspectButton('main').click();await wait(()=>document.querySelector('.definition-card pre').textContent===expectedFizzText,'Auto decodes 100-element FizzBuzz');
    133  assert(document.querySelector('.definition-card [role=status]').textContent==='Auto โ†’ List ยท text / numbers','Auto no longer falls back to tree source for FizzBuzz');
    134  interpretation('values');await wait(()=>document.querySelector('.definition-card pre').textContent===expectedFizzText&&document.querySelector('.definition-card [role=status]').textContent==='List ยท text / numbers'&&!document.querySelector('.definition-card select').disabled,'Explicit List also decodes all 100 entries');
    135  inspectButton('main').click();
    136  input('source-editor','!export answer\nanswer = 42\nloop = (x : x x) (x : x x)');await status('Compiled');
    137  inspectButton('loop').click();await wait(()=>el('status').textContent==='Inspecting loop','divergent observation started');
    138  el('stop').click();await wait(()=>!el('add-import').disabled,'inspection Stop');
    139  assert(document.querySelector('.definition-card pre').textContent==='','Stop discards unfinished inspection');
    140  input('source-editor',original);await status('Compiled');
    141  const open=async()=>{el('add-import').click();await wait(()=>el('import-results').querySelector('button'),'library list');assert(el('import-dialog').open,'Modal open');};
    142  const select=async()=>{input('import-search','remainder');await wait(()=>el('import-results').querySelector(`[data-manifest="${math.manifest}"]`),'search export');el('import-results').querySelector(`[data-manifest="${math.manifest}"]`).click();input('import-alias','N');};
    143  await open();await select();
    144  assert(el('import-title').textContent==='native-math','Search reaches native math');
    145  assert(globalThis.__sourceEditor.getSource()===original,'Browsing preserves draft');
    146  assert(el('import-preview').textContent===`!import ${math.manifest} as N`,'Exact full manifest preview: '+el('import-preview').textContent);
    147  assert(el('import-version').value==='0.12.0','Newest version initially selected');
    148  assert(el('import-version').options.length>=2,'History available in details: '+[...el('import-version').options].map(o=>o.value));
    149  input('import-alias','Numbers');change('import-version','0.1.0');await wait(()=>!el('insert-import').disabled,'historical release check');
    150  assert(el('import-version').value==='0.1.0'&&el('import-preview').textContent.endsWith('as Numbers'),'History preserves chosen namespace');
    151  assert(el('import-usage').textContent.includes('multiply')&&el('import-example').textContent.includes('Expected result: 42'),'Calling conventions and tested example available inline');
    152  change('import-version','0.12.0');input('import-alias','N');
    153  await wait(()=>!el('insert-import').disabled,'candidate compiled');
    154  el('insert-import').click();
    155  assert(!el('import-dialog').open,'Insertion closes dialog');
    156  assert(globalThis.__sourceEditor.getSource()===`!import ${math.manifest} as N\n`+original,'Insertion preserves all original source');
    157  assert(globalThis.__sourceEditor.getSelection().from===0&&globalThis.__sourceEditor.getSelection().to===(`!import ${math.manifest} as N\n`).length,'Inserted line selected');
    158  await status('Compiled');
    159  // Import insertion retains one undo transaction across compilation.
    160  globalThis.__sourceEditor.focus();assert(globalThis.__sourceEditor.undo(),'Undo available');
    161  assert(globalThis.__sourceEditor.getSource()===original,'One undo restores exact original source');await status('Compiled');
    162  await open();await select();await wait(()=>!el('insert-import').disabled,'reinsert checked');el('insert-import').click();await status('Compiled');
    163  assert(await evaluateSource('N.add answer 8')==='50','Inserted namespace usable from source');
    164  const inserted=globalThis.__sourceEditor.getSource();
    165  await open();await select();await wait(()=>!el('insert-import').disabled,'existing import');
    166  assert(el('import-status').textContent.includes('Already imported as N'),'Existing namespace shown');el('insert-import').click();
    167  assert(globalThis.__sourceEditor.getSource()===inserted,'Jump does not edit');
    168  // Alias conflict is based on the native header; a failed check leaves the editor intact.
    169  await open();input('import-search','conversions');el('import-results').querySelector('button').click();input('import-alias','N');
    170  await wait(()=>el('import-status').textContent.includes('already used'),'namespace conflict');assert(el('insert-import').disabled,'Conflict blocks insert');el('import-cancel').click();
    171  assert(globalThis.__sourceEditor.getSource()===inserted,'Cancellation preserves source');
    172  input('source-editor','!export answer\nanswer = (');await status('Compilation failed');
    173  await open();await select();await wait(()=>el('import-status').textContent.includes('copy the import'),'malformed source fallback');assert(el('insert-import').disabled,'Malformed source blocks insertion');el('import-cancel').click();
    174  input('source-editor',original);await status('Compiled');
    175  await open();await select();el('import-cancel').click();await delay(400);assert(!el('import-dialog').open,'Check cancelled');assert(globalThis.__sourceEditor.getSource()===original,'Cancellation is an edit-free operation');
    176  // Local filenames are text and their validated interfaces are searchable.
    177  const bytes=await(await fetch('/_arboricx/bundle/hash/'+math.hash)).arrayBuffer();
    178  const files=new DataTransfer();files.items.add(new File([bytes],'<svg onload=bad>.arboricx'));el('local-module-file').files=files.files;el('local-module-file').dispatchEvent(new Event('change'));
    179  await wait(()=>el('local-module-list').children.length===1&&!el('local-module-file').disabled,'local validation');await status('Compiled');
    180  await open();input('import-search','<svg');assert(!el('import-results').querySelector('svg'),'Filename is text');el('import-results').querySelector('button').click();
    181  assert(el('import-version').hidden,'Local file has no invented version');await wait(()=>!el('insert-import').disabled,'local import check');el('insert-import').click();await status('Compiled');
    182  assert(await evaluateSource('Local.add 5 6')==='11','Local library from unified picker');
    183  await open();input('import-search','remainder');
    184  const definition=[...el('import-results').querySelectorAll('button')].find(button=>button.querySelector('strong').textContent.startsWith('remainder ยท'));
    185  assert(definition,'Definition matches have their own results');definition.click();
    186  assert(el('import-definition').textContent.includes('Definition: remainder'),'Direct definition detail');
    187  assert(el('try-library').hidden,'Editable source has no expression-only Try action');
    188  const beforeBrowse=globalThis.__sourceEditor.getSource();el('import-cancel').click();
    189  assert(globalThis.__sourceEditor.getSource()===beforeBrowse,'Definition browsing preserves authored source without generating experiments');
    190  // Source-free library exploration is available and never inserts into a hidden editor.
    191  change('program-picker','hash');input('bundle-hash',math.hash);el('program-hash').dispatchEvent(new Event('submit',{cancelable:true}));await status('Ready');
    192  assert(el('source-editor').hidden&&!el('add-import').hidden,'Explorer available without source');
    193  await open();input('import-search','remainder');[...el('import-results').querySelectorAll('button')].find(button=>button.querySelector('strong').textContent.startsWith('native-math ยท')).click();
    194  assert(el('insert-import').disabled&&!el('try-library').hidden,'Source-free exploration offers try rather than insertion');
    195  el('try-library').click();await status('Ready');
    196  assert(el('program-picker').value==='package:tricu-lib-native-math','Explorer loads selected library');
    197  return {userAgent:navigator.userAgent,checks:['100-element mixed FizzBuzz Auto/explicit list; 4096/4097 decoded-list bounds; automatic text/natural/list/Boolean/source interpretation; sticky manual formats; open/close behavior; inline compiled/public/private definitions; interpretation recovery; edit refresh and failed-compile invalidation; cancellable observation; anchored result card; separate definition search; draft-preserving browsing; source-free explorer; static agent references; complete published interfaces; general documented examples; native read-only header inspection; export search; exact imports; undo; existing import jump; namespace conflicts; malformed source copy fallback; cancellation; local-file discovery'],backend};
    198 }