tricu

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

io-core.test.mjs (7918B)


      1 import test from 'node:test';
      2 import assert from 'node:assert/strict';
      3 import {Graph} from '../dist/graph.js';
      4 import {natural,text,readNatural,readList,readText} from '../dist/codecs.js';
      5 import {Core,Wire} from '../dist/io-core.js';
      6 function fixture(){
      7  const g=new Graph(),tree={leaf:g.leaf,observe:g.observe.bind(g),fork:g.fork.bind(g),stem:g.stem.bind(g),app:g.app.bind(g),natural:n=>natural(g,n),text:s=>text(g,s)};
      8  const w=new Wire(tree,1n),n=x=>natural(g,BigInt(x)),act=(code,p=g.leaf)=>g.fork(n(code),p),k=x=>g.fork(g.leaf,x);
      9  const bind=(left,right)=>act(1,g.fork(left,k(right)));
     10  const spawn=(child,sup=true,keep=true)=>act(5,g.fork(n(sup?1:0),g.fork(n(keep?1:0),g.fork(g.leaf,k(child)))));
     11  const request=(args=g.leaf)=>act(10,g.fork(text(g,'future'),g.fork(n(1),g.fork(text(g,'read'),args))));
     12  const provider={request:e=>()=>e.wire.result(false,e.wire.problem('unsupported','future/read')),release:()=>assert.fail('borrowed backend close')};
     13  const core=(action,p=provider,limit)=>new Core(tree,1n,action,[null,null,null],p,limit);
     14  return {g,tree,w,n,act,k,bind,spawn,request,provider,core};
     15 }
     16 async function drain(e,turn=()=>{}){
     17  for(let i=0;i<20000&&!e.result;i++){
     18   const progress=e.advance();turn();
     19   if(!progress){assert(e.work.counts.pending,'interpreter stalled');await e.wait();}
     20  }
     21  assert(e.result,'interpreter did not settle');
     22  assert.deepEqual(e.lifecycle.tasks.counts,{scopes:0,tasks:0,hosts:0});
     23  assert.deepEqual(e.lifecycle.resources.counts,{resources:0,admissions:0,leases:0});
     24  assert.deepEqual(e.work.counts,{pending:0,replies:0});return e.result;
     25 }
     26 function result(f,value){const [flag,tail]=f.w.pair(value);return {ok:f.g.observe(flag).node.kind==='stem',value:f.w.pair(tail)[0]};}
     27 
     28 test('real tree bind/scope/current-scope execute, with roots collected between turns',async()=>{
     29  const f=fixture(),e=f.core(f.bind(f.act(0,f.n(1)),f.act(3,f.k(f.act(4)))));
     30  const out=await drain(e,()=>f.g.collect([...e.roots()]));assert.equal(out.kind,'completed');
     31  const parts=readList(f.g,out.value,3);assert.equal(readText(f.g,parts[0]),'io.v2.scope');assert.equal(readNatural(f.g,parts[2]),1n);
     32 });
     33 test('opaque terminal diagnostics never demand payloads or abandoned continuations',async()=>{
     34  for(const tag of [2,9]){
     35   const f=fixture(),poison=f.g.app(f.g.leaf,f.g.leaf),observe=f.tree.observe;
     36   f.tree.observe=h=>{if(h===poison)throw Error('forced opaque diagnostic');return observe(h);};
     37   const e=f.core(f.act(1,f.g.fork(f.act(tag,poison),poison))),out=await drain(e);
     38   assert.equal(out.kind,tag===2?'failed':'cancelled');assert.equal(out.value,poison);assert.equal(e.fault,undefined);
     39  }
     40 });
     41 test('spawn/join encode the native outcome contract and consume tickets once',async()=>{
     42  const f=fixture(),join=f.act(6,f.w.marker('io.v2.join',1));
     43  const e=f.core(f.bind(f.spawn(f.act(0,f.n(42))),join)),out=await drain(e),joined=result(f,out.value);
     44  assert(joined.ok);const parts=readList(f.g,joined.value,2);assert.equal(readText(f.g,parts[0]),'completed');assert.equal(readNatural(f.g,parts[1]),42n);
     45  const other=fixture(),again=other.act(6,other.w.marker('io.v2.join',1));
     46  const repeated=await drain(other.core(other.bind(other.spawn(other.act(0)),other.bind(again,again))));
     47  const stale=result(other,repeated.value);assert(!stale.ok);assert.equal(readText(other.g,readList(other.g,stale.value,4)[0]),'stale-task');
     48 });
     49 test('supervised failure is data; uncontained failure defeats parent success',async()=>{
     50  for(const supervised of [true,false]){
     51   const f=fixture(),out=await drain(f.core(f.bind(f.spawn(f.act(2,f.n(17)),supervised),f.act(6,f.w.marker('io.v2.join',1)))));
     52   assert.equal(out.kind,supervised?'completed':'failed');
     53   if(supervised){const joined=result(f,out.value);assert(joined.ok);assert.equal(readText(f.g,readList(f.g,joined.value,3)[0]),'failed');}
     54   else assert.equal(readNatural(f.g,out.value),17n);
     55  }
     56 });
     57 test('unsupported facilities stay ordinary Results without argument demand',async()=>{
     58  const f=fixture(),poison=f.g.app(f.g.leaf,f.g.leaf),observe=f.tree.observe;f.tree.observe=h=>{if(h===poison)throw Error('demanded arguments');return observe(h);};
     59  const e=f.core(f.request(poison)),out=await drain(e,()=>f.g.collect([...e.roots()]));assert.equal(out.kind,'completed');assert.equal(result(f,out.value).ok,false);
     60 });
     61 test('tree cancellation drains pending provider before join and suppresses abandoned continuation',async()=>{
     62  const f=fixture(),args=f.n(123);let acknowledge,aborts=0,argumentLive=false;
     63  const gate=new Promise(resolve=>{acknowledge=resolve;});
     64  const p={...f.provider,request:async(e,_scope,request,signal)=>{
     65   await new Promise(resolve=>signal.addEventListener('abort',()=>{aborts++;resolve();},{once:true}));
     66   await gate;argumentLive=readNatural(f.g,request.arguments)===123n;return ()=>{throw Error('cancelled reply encoder ran');};
     67  }};
     68  const cancel=f.act(8,f.g.fork(f.w.marker('io.v2.task',1),f.n(7)));
     69  const e=f.core(f.bind(f.spawn(f.request(args)),f.bind(cancel,f.act(6,f.w.marker('io.v2.join',1)))),p);
     70  for(let i=0;i<40;i++){const progress=e.advance();f.g.collect([...e.roots()]);if(!progress)break;}
     71  assert.equal(aborts,1);assert.equal(e.result,null);assert.equal(e.work.counts.pending,1);
     72  acknowledge();await e.wait();const out=await drain(e,()=>f.g.collect([...e.roots()]));assert(argumentLive);
     73  const joined=result(f,out.value),parts=readList(f.g,joined.value,3);assert.equal(readText(f.g,parts[0]),'cancelled');assert.equal(readNatural(f.g,parts[1]),7n);
     74 });
     75 test('malformed action is terminal, malformed operation is an ordinary Result',async()=>{
     76  const f=fixture(),terminal=await drain(f.core(f.act(1,f.g.leaf)));assert.equal(terminal.kind,'failed');
     77  const ordinary=await drain(f.core(f.act(6,f.g.leaf)));assert.equal(ordinary.kind,'completed');assert(!result(f,ordinary.value).ok);
     78 });
     79 test('evaluator fault after lexical admission drains admitted scopes without retrying evaluation',async()=>{
     80  const f=fixture(),failure=new Error('injected evaluator failure');let applications=0;
     81  f.tree.app=()=>{applications++;throw failure;};const e=f.core(f.act(3,f.k(f.act(0))));
     82  assert.equal((await drain(e)).kind,'failed');assert.equal(e.fault,failure);assert.equal(applications,1);
     83 });
     84 test('action bound stops the entire run and cleans up',async()=>{
     85  const f=fixture(),e=f.core(f.bind(f.act(0),f.act(0)),f.provider,1);
     86  assert.equal((await drain(e)).kind,'failed');assert.equal(e.fault,undefined);
     87 });
     88 
     89 test('evaluator errors cannot masquerade as protocol or ordinary-operation errors',async()=>{
     90  for(const message of ['invalid computation shape','invalid-argument']){
     91   const f=fixture(),fault=new Error(message),action=f.act(6,f.g.leaf),observe=f.tree.observe;
     92   f.tree.observe=h=>{if(h===(message==='invalid-argument'?f.g.leaf:action))throw fault;return observe(h);};
     93   const e=f.core(action);assert.equal((await drain(e)).kind,'failed');assert.equal(e.fault,fault);assert(e.hasFault);
     94  }
     95 });
     96 test('undefined provider rejection is retained as an embedding fault, not successful undefined data',async()=>{
     97  const f=fixture(),e=f.core(f.request(),{...f.provider,request:()=>Promise.reject(undefined)});
     98  assert.equal((await drain(e)).kind,'failed');assert(e.hasFault);assert.equal(e.fault,undefined);
     99 });
    100 
    101 test('lexical physical cleanup failure defeats success once, with uncertain release progress',async()=>{
    102  const f=fixture();let closed=0;
    103  const e=f.core(f.act(3,f.k(f.request())),{
    104   request:(engine,scope)=>{
    105    const r=engine.lifecycle.resources;r.publish(r.beginAcquire(scope),'test',null);
    106    return ()=>engine.wire.result(true,engine.tree.leaf);
    107   },release:()=>{closed++;throw Error('uncertain close');}
    108  });
    109  const out=await drain(e);assert.equal(out.kind,'failed');assert.equal(closed,1);assert.equal(out.cleanup.items.length,0);
    110  const problem=readList(f.g,out.value,4);assert.equal(readText(f.g,problem[0]),'cleanup-failed');assert.equal(readText(f.g,problem[3]),'unknown');
    111 });