tricu

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

local-module-probe.mjs (8884B)


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