From d3fa70bea8bf1396757a59a738ecda858ca6f801 Mon Sep 17 00:00:00 2001 From: developtheweb Date: Thu, 9 Jul 2026 23:29:01 -0400 Subject: [PATCH] Implement exact rational numbers --- js/mpl.js | 51 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/js/mpl.js b/js/mpl.js index d5c55ea..e7d9ad2 100644 --- a/js/mpl.js +++ b/js/mpl.js @@ -1,6 +1,18 @@ 'use strict'; -/* ================= MPL M0 interpreter (10 tests in js/test/) ================= */ +/* ============ MPL M0 interpreter (ratified semantics: conformance/JUDGMENT_CALLS.md) ============ */ const BOT=Symbol('⊥'),NOMATCH=Symbol('nomatch'); +/* Numbers are exact rationals (ruling 4): BigInt numerator/denominator, + gcd-reduced, denominator > 0, constructed once at creation. */ +const gcd=(a,b)=>{a=a<0n?-a:a;b=b<0n?-b:b;while(b){const t=a%b;a=b;b=t}return a}; +const rat=(n,d=1n)=>{if(d<0n){n=-n;d=-d}const g=gcd(n,d)||1n;return{q:true,n:n/g,d:d/g}}; +const isRat=v=>!!v&&typeof v==='object'&&v.q===true; +const R={add:(a,b)=>rat(a.n*b.d+b.n*a.d,a.d*b.d),sub:(a,b)=>rat(a.n*b.d-b.n*a.d,a.d*b.d), + mul:(a,b)=>rat(a.n*b.n,a.d*b.d),neg:a=>rat(-a.n,a.d), + cmp:(a,b)=>{const l=a.n*b.d,r=b.n*a.d;return lr?1:0},eq:(a,b)=>a.n===b.n&&a.d===b.d}; +/* String ordering is Unicode code-point order (ruling 8). No locale, ever. */ +const cmpStr=(a,b)=>{const A=[...a],B=[...b],m=Math.min(A.length,B.length); + for(let i=0;itoks.push({t,v,line:l,col:c});const err=(key,l,c)=>{const e=new Error(key);e.key=key;e.line=l;e.col=c;throw e}; while(i{for(let k=0;k{for(let s=sc;s;s=s.parent)if(s.vars.has(n))return s;return null}; const rte=(key,node)=>{const e=new Error(key);e.key=key;e.line=node.line;e.col=node.col;throw e}; -const num=(v,node)=>{if(typeof v!=='number')rte('err_num',node);return v}; -const show=v=>v===BOT?'⊥':typeof v==='string'?v:Array.isArray(v)?'['+v.map(showQ).join(', ')+']':typeof v==='object'&&v&&v.closure?'λ':typeof v==='boolean'?(v?'true':'false'):String(v); +const num=(v,node)=>{if(!isRat(v))rte('err_num',node);return v}; +const show=v=>v===BOT?'⊥':typeof v==='string'?v:Array.isArray(v)?'['+v.map(showQ).join(', ')+']':isRat(v)?(v.d===1n?String(v.n):v.n+'/'+v.d):typeof v==='object'&&v&&v.closure?'λ':typeof v==='boolean'?(v?'true':'false'):String(v); const showQ=v=>typeof v==='string'?'"'+v+'"':show(v); +/* Equality is structural on data; any function involved raises err_fn_eq + (ruling 7). */ +const fnIn=v=>!!v&&typeof v==='object'&&(v.closure?true:Array.isArray(v)?v.some(fnIn):false); +const dEq=(a,b)=>isRat(a)&&isRat(b)?R.eq(a,b):Array.isArray(a)&&Array.isArray(b)?a.length===b.length&&a.every((x,ix)=>dEq(x,b[ix])):a===b; let steps=0;const strip=v=>v===NOMATCH?BOT:v; function ev(n,sc){if(++steps>500000){const e=new Error('err_steps');e.key='err_steps';e.line=n.line||1;e.col=n.col||1;throw e} switch(n.k){ @@ -66,12 +82,19 @@ case 'seq':{let v=BOT;for(const e of n.es){v=ev(e,sc);if(v===NOMATCH)v=BOT}retur case 'def':{const v=strip(ev(n.e,sc));sc.vars.set(n.name,v);return v} case 'set':{const v=strip(ev(n.e,sc));const s=lookup(sc,n.name)||sc;s.vars.set(n.name,v);return v} case 'alt':{const l=ev(n.l,sc);return l===NOMATCH?ev(n.r,sc):l} -case 'imp':{const c=strip(ev(n.c,sc));return c===true||(typeof c==='number'&&c!==0)?ev(n.e,sc):NOMATCH} +case 'imp':{const c=strip(ev(n.c,sc));return c===true||(isRat(c)&&c.n!==0n)?ev(n.e,sc):NOMATCH} case 'or':{const l=strip(ev(n.l,sc));return l===true?true:strip(ev(n.r,sc))===true} case 'and':{const l=strip(ev(n.l,sc));return l===true?strip(ev(n.r,sc))===true:false} -case 'cmp':{const a=strip(ev(n.l,sc)),b=strip(ev(n.r,sc));const eq=JSON.stringify(a)===JSON.stringify(b);switch(n.o){case 'eq':return eq;case 'neq':return!eq;case 'lt':return ab;case 'leq':return a<=b;case 'geq':return a>=b}} -case 'bin':{const a=strip(ev(n.l,sc)),b=strip(ev(n.r,sc));switch(n.o){case 'plus':return(typeof a==='string'||typeof b==='string')?show(a)+show(b):num(a,n)+num(b,n);case 'minus':return num(a,n)-num(b,n);case 'mul':return num(a,n)*num(b,n);case 'divi':{const d=num(b,n);if(d===0)rte('err_div0',n);return num(a,n)/d}}} -case 'neg':return -num(strip(ev(n.e,sc)),n); +case 'cmp':{const a=strip(ev(n.l,sc)),b=strip(ev(n.r,sc)); + if(n.o==='eq'||n.o==='neq'){if(fnIn(a)||fnIn(b))rte('err_fn_eq',n);const eq=dEq(a,b);return n.o==='eq'?eq:!eq} + let d;if(isRat(a)&&isRat(b))d=R.cmp(a,b);else if(typeof a==='string'&&typeof b==='string')d=cmpStr(a,b);else rte('err_compare',n); + switch(n.o){case 'lt':return d<0;case 'gt':return d>0;case 'leq':return d<=0;case 'geq':return d>=0}} +case 'bin':{const a=strip(ev(n.l,sc)),b=strip(ev(n.r,sc));switch(n.o){ + case 'plus':return(typeof a==='string'||typeof b==='string')?show(a)+show(b):R.add(num(a,n),num(b,n)); + case 'minus':return R.sub(num(a,n),num(b,n)); + case 'mul':return R.mul(num(a,n),num(b,n)); + case 'divi':{const bb=num(b,n);if(bb.n===0n)rte('err_div0',n);const aa=num(a,n);return rat(aa.n*bb.d,aa.d*bb.n)}}} +case 'neg':return R.neg(num(strip(ev(n.e,sc)),n)); case 'trace':{const v=strip(ev(n.e,sc));print(show(v));return v} case 'lam':return{closure:true,ps:n.ps,body:n.body,sc}; case 'call':{const f=strip(ev(n.f,sc));if(!f||!f.closure)rte('err_notfn',n);if(f.ps.length!==n.args.length)rte('err_arity',n);const inner={vars:new Map(),parent:f.sc};f.ps.forEach((pn,ix)=>inner.vars.set(pn,strip(ev(n.args[ix],sc))));return strip(ev(f.body,inner))}