tricu

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

module-probe.mjs (12833B)


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