tricu

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

module-probe.mjs (11540B)


      1 // Production adapters + real page; expected values are independent arithmetic,
      2 // native header contracts, and the CLI cases in native-modules.py.
      3 const assert=(ok,message)=>{if(!ok)throw Error(message);};
      4 const equal=(actual,expected,label)=>assert(JSON.stringify(actual)===JSON.stringify(expected),label+': '+JSON.stringify(actual));
      5 async function rejects(fn,pattern){try{await fn();}catch(e){assert(pattern.test(e.message),'Wrong rejection: '+e.message);return;}throw Error('Expected '+pattern);}
      6 const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms));
      7 async function status(value){for(let i=0;i<4500;i++){if(el('status').textContent===value)return;await delay(20);}throw Error('Timed out '+value+': '+el('status').textContent+' '+el('compile-diagnostic').textContent);}
      8 const input=(id,value)=>{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='haskell-wasm'){
     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 {packageIndex,packageReleases}=(await import(new URL('dist/probe-api.js',base))).packages;
     14  const releases=packageIndex(await(await fetch(new URL('packages.json',base))).json());
     15  const lib=name=>packageReleases(releases,'tricu-lib-'+name)[0];
     16  const math=lib('native-math'),foundation=lib('base'),io=lib('io'),modules=lib('source-module');
     17  const host={before:async()=>{},after:()=>{},write:()=>{},read:async()=>'',running:()=>{}};
     18  const fault=async data=>{await fetch(control+'/fault',{method:'POST',body:JSON.stringify(data)});};
     19  const source=`-- native header; position masking must preserve this line\n!import ${math.manifest} as N\n!export answer\nprivate = 99\nanswer = N.add 40 2`;
     20  const s=await createRuntimeSession(backend,true);
     21  const value=async expression=>(await s.runExpression(expression,'number','pure',host)).value;
     22  try {
     23   const catalogue=await(await fetch(new URL('catalogue.json',base))).json();
     24   const fixtures=await(await fetch('/zig-fixtures.json')).json();
     25   for(const demo of catalogue){
     26    assert(demo.environment==='empty','Every demo is editable without hidden helper injection');
     27    await s.compile(demo.source,demo.environment);
     28    for(const c of fixtures.demos[demo.id].haskellCases){
     29     let terminal='';
     30     const result=await s.run(c.export,c.arguments.map(value=>({type:'tree',value})),'ternary',demo.mode,
     31       {...host,read:async()=>c.input,write:text=>{terminal+=text;}});
     32     equal(result.value,c.result,'Recompiled demo golden '+demo.id);
     33     equal(terminal,c.terminal,'Recompiled terminal '+demo.id);
     34    }
     35   }
     36   equal(await s.compile(source),['answer'],'Only explicit public exports');
     37   equal(await value('answer'),'42','Qualified module application');
     38   equal(await value('N.add answer private'),'141','Imported and private local bindings survive expression runs');
     39   await s.compile(source,'practical');
     40   await rejects(()=>value('isNat? 1'),/unresolved name/); // native headers override legacy demo profiles
     41   for(let i=0;i<10;i++)equal(await value('N.add answer 1'),'43','Repeated retained imported application');
     42   await rejects(()=>s.run('private',[],'number','pure',host),/Missing definition|missing export/);
     43   equal(await s.compile(`!import ${foundation.manifest} *\n!export answer\nanswer = add 20 22`),['answer'],'Open full base has no implicit contract collisions');
     44   equal(await value('add answer 8'),'50','Open library helpers retained');
     45   await s.compile(`!import ${foundation.manifest} *\n!export checkedIdentity\ncheckedIdentity x =@nat? x`);
     46   equal(await value('checkedIdentity 42'),'42','Native annotations resolve explicit contract imports');
     47   equal(await s.compile(`!import ${math.manifest} as A\n!import ${math.manifest} as B\n!export *\nanswer = A.add (B.add 10 11) 21`),['answer'],'Same manifest under two aliases');
     48   equal(await value('answer'),'42','Repeated reference');
     49   await s.compile(`!import ${modules.manifest} as M\n!export answer\nanswer = M.sourceModuleQualified "N" "add"`);
     50   equal((await s.runExpression('answer','string','pure',host)).value,'N.add','Compiler helper library is ordinarily importable');
     51   const goldens=await(await fetch('/module-goldens.json')).json();
     52   for(const golden of goldens.trees){
     53    await s.compile(golden.source);
     54    const tree=(await s.run(golden.name,[],'ternary','pure',host)).value;
     55    equal(tree.length,golden.bytes,'Complete native function tree length');
     56    const hash=[...new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(tree)))].map(n=>n.toString(16).padStart(2,'0')).join('');
     57    equal(hash,golden.ternaryAsciiSha256,'Exact function tree agrees with independent native compiler');
     58   }
     59   const names=await s.compile(`!export * from ${io.manifest}`);
     60   assert(names.includes('io')&&names.includes('readFile')&&names.length>16,'Header-only full re-export');
     61   equal((await s.run('io',[{type:'number',value:'42'}],'tree','pure',host)).type,'tree','Re-export is executable, not source expansion');
     62   await rejects(()=>value('readFile'),/unresolved name/); // reexports do not introduce lexical imports
     63   await s.compile(`!import ${io.manifest} as I\n!export main\nmain = I.io (I.readFile "no-browser-filesystem")`);
     64   await rejects(()=>s.run('main',[],'string','io',host),/Unsupported.*20|unsupported.*20/i);
     65   await s.compile(`!import ${math.manifest} as N\n!export answer\nloop x = loop x\nanswer = N.add 40 2`);
     66   equal(await value('answer'),'42','Unused divergent function is never normalized');
     67   const failures=[
     68    [`!import ${math.manifest}\n!export answer\nanswer = 42`,/hash imports require an alias/],
     69    [`!import ${math.manifest} as N\n!import ${math.manifest} as N\n!export answer\nanswer = 42`,/duplicate imported name/],
     70    [`!import ${math.manifest} *\n!export add\nadd x y = x`,/Compiler diagnostic 46/],
     71    [`!import ${math.manifest} as N\nanswer = 42`,/module requires an export/],
     72    [`!export absent\nanswer = 42`,/undeclared local export/],
     73    [`!export answer\n!export *\nanswer = 42`,/duplicate export/],
     74    [`!export * from ${math.manifest}\n!export * from ${math.manifest}`,/duplicate export/],
     75    [`!import prelude *\n!export answer\nanswer = 42`,/workspace/],
     76    [`!source prelude\n!export answer\nanswer = 42`,/workspace/],
     77    [`!import ${'0'.repeat(64)} as X\n!export answer\nanswer = 42`,/Unknown module manifest/],
     78    [`!import ${math.manifest} as N\n!export answer\nanswer = missing`,/Compiler diagnostic 20 at 3:/],
     79   ];
     80   for(const [text,pattern] of failures){
     81    await rejects(()=>s.compile(text),pattern);
     82    await rejects(()=>value('answer'),/unresolved name/);
     83    await s.compile(source);equal(await value('answer'),'42','Recover after failed module compilation');
     84   }
     85   for(const field of ['moduleMissing','moduleCorrupt']){
     86    await fault({[field]:releases.find(r=>r.manifest===math.manifest).hash});
     87    await rejects(()=>s.compile(source),/HTTP 404|SHA-256 mismatch/);
     88    await rejects(()=>value('answer'),/unresolved name/);
     89    await fault({[field]:null});
     90    await s.compile(source);equal(await value('answer'),'42','Fetch retry');
     91   }
     92   await fault({moduleManifest:math.manifest});
     93   await rejects(()=>s.compile(source),/Module manifest mismatch/);
     94   await fault({moduleManifest:null});
     95   await s.compile(source);
     96   const memory=[];
     97   for(let i=0;i<12;i++){
     98    await s.compile(source);equal(await value('N.add answer 1'),'43','Repeated module replacement');
     99    if(s.diagnostics){s.collect();memory.push(s.diagnostics());}
    100   }
    101   if(memory.length){
    102    equal(memory.at(-1).owned,memory[0].owned,'No retained import handle leak');
    103    assert(Math.max(...memory.slice(6).map(x=>x.linearMemoryBytes))<=Math.max(...memory.slice(0,6).map(x=>x.linearMemoryBytes))+65536,'Module memory plateaus');
    104   }
    105   await s.compile(`!import ${math.manifest} as N\n!export answer\nbrowserExpression0 = 9\nanswer = 42`);
    106   equal(await value('N.add answer browserExpression0'),'51','Temporary expression names avoid private locals');
    107   await s.compile('answer = 7');equal(await value('answer'),'7','Legacy scratch remains supported');
    108   await rejects(()=>value('N.add 1 2'),/unresolved name/);
    109  }finally{s.dispose();await fault({moduleMissing:null,moduleCorrupt:null,moduleManifest:null});}
    110  // Exercise the actual editable page, including Worker termination and rebuilding.
    111  await status('Ready');change('runtime-backend',backend);await status('Ready');
    112  document.querySelector('[data-compiler-example=module]').click();await status('Compiled');
    113  assert(el('source-editor').value.includes(math.manifest),'Module example uses an indexed manifest, not a bundle digest');
    114  input('expression-editor','answer');change('output-type','number');el('run').click();await status('Finished');
    115  equal(el('result').textContent,'42','Module scratch example');
    116  input('source-editor',source);el('compile').click();await status('Compiled');
    117  equal([...el('export').options].map(x=>x.value),['answer'],'Page public selector');
    118  input('expression-editor','N.add answer private');change('output-type','number');el('run').click();await status('Finished');
    119  equal(el('result').textContent,'141','Actual page imported expression');
    120  input('source-editor',`!import ${math.manifest} as N\n!export answer\nloop x = loop x\nanswer = N.add 40 2`);
    121  el('compile').click();await status('Compiled');
    122  input('expression-editor','loop t');el('run').click();await status('Running');await delay(100);el('stop').click();await status('Stopped');
    123  assert(el('run').disabled,'Stop discards imported heap');
    124  el('compile').click();await status('Compiled');input('expression-editor','N.add answer 1');el('run').click();await status('Finished');
    125  equal(el('result').textContent,'43','Stop/recompile reconstructs verified imports');
    126  // Edit the actual parser/lexer examples, not a scratch copy; verify changed
    127  // behavior, then restore the authenticated original and its behavior.
    128  for(const [id,before,after,expression,field,want,original] of [
    129   ['parser','N.inRange c 97 122','N.inRange c 65 90','parseNames "RED,GREEN"','status','ok','error'],
    130   ['lexer','N.inRange character 97 122','N.inRange character 65 90','scanWord "HELLO,world"','matched','HELLO',''],
    131  ]){
    132   change('program-picker','demo:'+id);await status('Ready');
    133   assert(!el('source-editor').hidden&&!el('source-editor').readOnly,'Editable '+id);
    134   const source=el('source-editor').value;assert(source.includes(before),'Known predicate');
    135   input('source-editor',source.replace(before,after));el('compile').click();await status('Compiled');
    136   input('expression-editor',expression);change('output-type','record');el('run').click();await status('Finished');
    137   equal(JSON.parse(el('result').textContent)[field],want,'Edited '+id+' result');
    138   el('restore-source').click();await status('Ready');equal(el('source-editor').value,source,'Restore '+id);
    139   input('expression-editor',expression);change('output-type','record');el('run').click();await status('Finished');
    140   equal(JSON.parse(el('result').textContent)[field],original,'Restored '+id+' behavior');
    141  }
    142  return {userAgent:navigator.userAgent,checks:['all sixteen editable demos match native goldens; parser/lexer edit and restore; native headers; qualified/open imports; full reexports; retained private/import scope; collision/diagnostic controls; transport and manifest faults; no capabilities; repeated replacement/GC; actual page Stop/recompile']};
    143 }