tricu

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

local-module-probe.mjs (8713B)


      1 import {evaluateSource} from './source-probe-helpers.mjs';
      2 const assert=(ok,message)=>{if(!ok)throw Error(message);};
      3 const equal=(a,b,message)=>assert(JSON.stringify(a)===JSON.stringify(b),message+': '+JSON.stringify(a));
      4 async function rejects(fn,pattern){try{await fn();}catch(e){assert(pattern.test(String(e)),'Wrong rejection: '+e);return;}throw Error('Expected rejection');}
      5 const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms));
      6 async function wait(fn){for(let i=0;i<4500;i++){if(fn())return;await delay(20);}throw Error('Timed out: '+el('status').textContent+' / '+el('compiled-module-status').textContent);}
      7 const status=s=>wait(()=>el('status').textContent===s);
      8 const input=(id,value)=>{if(id==='source-editor'){globalThis.__sourceEditor.replaceSource(value,true);return;}el(id).value=value;el(id).dispatchEvent(new Event('input'));};
      9 const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));};
     10 export async function run(control,backend){
     11  const base=new URL('../',document.querySelector('script[type=module][src]').src);
     12  const {createRuntimeSession}=(await import(new URL('dist/probe-api.js',base))).runtimeSession;
     13  const {validateLocalModule}=(await import(new URL('dist/probe-api.js',base))).localModules;
     14  const {encodeModule}=(await import(new URL('dist/probe-api.js',base))).moduleBundle;
     15  const {decodeBundle}=(await import(new URL('dist/probe-api.js',base))).bundle;
     16  const index=await(await fetch(new URL('packages.json',base))).json();
     17  const math=index.releases.find(r=>r.name==='tricu-lib-native-math'&&r.version==='0.1.0');
     18  const source=`!import ${math.manifest} as N\n!export echo\n!export plus\n!export value\nprivate = 9\necho x = x\nplus x = N.add x private\nvalue = 42\nq x = x x\nhidden = q q`;
     19  const host={before:async()=>{},after:()=>{},read:async()=>'',write:()=>{},running:()=>{}};
     20  const s=await createRuntimeSession(backend,true);let exported;
     21  try{
     22   await s.compile(source);
     23   exported=await validateLocalModule(await s.exportModule());
     24   equal(exported.names,['echo','plus','value'],'Only public exports, no imports/private divergent value');
     25   equal((await s.runExpression('N.add value private','number','pure',host)).value,'51','Export preserves expression scope');
     26   const start=(await(await fetch(control+'/requests')).json()).length;
     27   await s.compile(`!import ${exported.manifest} as L\n!export answer\nanswer = L.plus 33`,undefined,[exported]);
     28   equal((await s.runExpression('L.echo answer','number','pure',host)).value,'42','Local qualified import');
     29   await s.compile(`!import ${exported.manifest} *\n!export answer\nanswer = plus 33`,undefined,[exported]);
     30   equal((await s.run('answer',[],'number','pure',host)).value,'42','Local open import');
     31   await s.compile(`!export * from ${exported.manifest}`,undefined,[exported]);
     32   equal((await validateLocalModule(await s.exportModule())).manifest,exported.manifest,'Full reexport preserves native identity');
     33   const requests=(await(await fetch(control+'/requests')).json()).slice(start);
     34   assert(!requests.some(p=>p.endsWith('/packages.json')||p.startsWith('/_arboricx/')),'Local resolution performs no registry requests');
     35   const corrupt=exported.bytes.slice();corrupt[0]^=1;
     36   await rejects(()=>s.compile(`!import ${exported.manifest} *\n!export value`,undefined,[{...exported,bytes:corrupt}]),/magic/);
     37   await rejects(()=>s.exportModule(),/No compiled module/);
     38   const bindings=['x0 = t t t'];for(let i=1;i<=30;i++)bindings.push(`x${i} = t x${i-1} x${i-1}`);
     39   await s.compile('!export deep\n!export alias\ndeep = (let\n  '+bindings.join('\n  ')+'\n  in x30)\nalias = deep');
     40   const shared=decodeBundle(await s.exportModule());
     41   assert(shared.nodes.length<40,'Export preserves exponential sharing');
     42   equal(shared.exports[0].root,shared.exports[1].root,'Cross-export sharing preserved');
     43   const original=decodeBundle(exported.bytes),names=Array.from({length:24},(_,i)=>'entry'+i);
     44   const wide=await validateLocalModule(encodeModule({...original,exports:names.map(name=>({...original.exports[0],name}))}));
     45   equal(await s.compile(`!export * from ${wide.manifest}`,undefined,[wide]),names,'Local public interface is not capped at 16 definitions');
     46   equal((await validateLocalModule(await s.exportModule())).manifest,wide.manifest,'All 24 reexports survive portable export');
     47 
     48   await s.compile('!export action\naction = t "tricuIO" (t 1 (t 20 "file"))');
     49   const capability=await validateLocalModule(await s.exportModule());
     50   await s.compile(`!import ${capability.manifest} as L\n!export main\nmain = L.action`,undefined,[capability]);
     51   await rejects(()=>s.run('main',[],'string','io',host),/Unsupported|unsupported/);
     52 
     53  }finally{s.dispose();}
     54  await status('Ready');
     55  document.querySelector('[data-compiler-example=module]').click();await status('Compiled');
     56  input('source-editor',source);el('compile').click();await status('Compiled');
     57  const createURL=URL.createObjectURL,click=HTMLAnchorElement.prototype.click;let downloaded,name;
     58  URL.createObjectURL=blob=>{downloaded=blob;return createURL(blob);};HTMLAnchorElement.prototype.click=function(){name=this.download;};
     59  try{
     60   el('export-compiled-module').click();await wait(()=>downloaded&&el('status').textContent==='Compiled');
     61  }finally{URL.createObjectURL=createURL;HTMLAnchorElement.prototype.click=click;}
     62  const file=await validateLocalModule(new Uint8Array(await downloaded.arrayBuffer()));
     63  equal(file.manifest,exported.manifest,'Actual-page download contains compiled public interface');
     64  equal(name,file.hash+'.arboricx','Download named by bundle hash');
     65  const select=bytes=>{const transfer=new DataTransfer();transfer.items.add(new File([bytes],'<svg onload=bad>.arboricx'));el('local-module-file').files=transfer.files;el('local-module-file').dispatchEvent(new Event('change'));};
     66  select(file.bytes);await wait(()=>el('local-module-list').children.length===1&&!el('local-module-file').disabled);
     67  assert(!el('local-module-list').querySelector('svg'),'Filename renders as text');
     68  assert(el('local-module-list').textContent.includes(file.manifest),'Visible manifest import statement');
     69  await status('Compiled');
     70  select(file.bytes);await wait(()=>el('local-module-status').textContent.includes('already available'));
     71  equal(el('local-module-list').children.length,1,'Duplicates are idempotent');
     72  select(new Uint8Array([0]));await wait(()=>el('local-module-status').textContent.includes('Truncated'));
     73  equal(el('local-module-list').children.length,1,'Invalid files preserve collection');
     74  select(file.bytes);el('cancel-local-module').click();await wait(()=>el('local-module-status').textContent.includes('cancelled'));
     75  equal(el('local-module-list').children.length,1,'Cancelling validation preserves collection');
     76  const consumer=`!import ${file.manifest} as Mine\n!export answer\nanswer = Mine.plus 33`;
     77  input('source-editor',consumer);el('compile').click();await status('Compiled');
     78  equal(await evaluateSource('Mine.echo answer'),'42','File selection makes module importable');
     79  // Stop an actually divergent export, then rebuild using page-owned local bytes.
     80  input('source-editor','!export bad\nq x = x x\nbad = q q');el('compile').click();await status('Compiled');
     81  el('export-compiled-module').click();await status('Exporting module');await delay(150);el('stop').click();await status('Stopped');
     82  assert(el('export-compiled-module').disabled,'Stop invalidates exportable draft');
     83  input('source-editor',consumer);el('compile').click();await status('Compiled');
     84  el('stop').click();
     85  el('compile').click();await status('Compiled');equal(await evaluateSource('answer'),'42','Local bytes survive Stop and runtime replacement');
     86  const frame=document.createElement('iframe');frame.src=new URL('index.html?noProbe=1',location.href).href;document.body.append(frame);
     87  await wait(()=>frame.contentDocument?.getElementById('status')?.textContent==='Ready');
     88  equal(frame.contentDocument.getElementById('local-module-list').children.length,0,'A new page has no persisted local modules');frame.remove();
     89  [...el('local-module-list').querySelectorAll('button')].find(b=>b.textContent==='Remove').click();
     90  await status('Compilation failed');equal(el('local-module-list').children.length,0,'Removal clears module');
     91  assert(el('export-compiled-module').disabled,'Removed dependency cannot use a stale compiled scope');
     92  return {userAgent:navigator.userAgent,source,manifest:exported.manifest,bytes:[...exported.bytes],checks:['local file validation; offline local-first imports; public portable export/reexport; shared DAGs; no private divergence; actual export/file import; duplicates/corruption; Stop/runtime retention; page-only lifetime; removal invalidation']};
     93 }