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 }