Enforce boolean conditions, binding discipline, depth limit
This commit is contained in:
parent
d3fa70bea8
commit
bb3c015416
4 changed files with 82 additions and 35 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
-- Factorial example with proper precedence
|
-- Factorial example with proper precedence
|
||||||
factorial ≜ λn∈ℕ: (n≤1 ⟹ 1) | (n×factorial(n-1));
|
factorial ≜ λn∈ℕ: (n≤1 ⟹ 1) | (n×factorial(n-1));
|
||||||
result ← factorial(5);
|
result ≜ factorial(5);
|
||||||
✎result;
|
✎result;
|
||||||
99
js/mpl.js
99
js/mpl.js
|
|
@ -73,33 +73,80 @@ const showQ=v=>typeof v==='string'?'"'+v+'"':show(v);
|
||||||
(ruling 7). */
|
(ruling 7). */
|
||||||
const fnIn=v=>!!v&&typeof v==='object'&&(v.closure?true:Array.isArray(v)?v.some(fnIn):false);
|
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;
|
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;
|
let steps=0,depth=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}
|
/* Explicit-stack machine (ruling 10): the work stack lives on the heap, so
|
||||||
|
recursion is bounded by the λ-application depth counter — limit 10000,
|
||||||
|
err_depth — never by the host stack. No keyless host error is reachable.
|
||||||
|
err_steps (500000 node evaluations) is this implementation's resource
|
||||||
|
limit (ruling 11), counted once per frame like the old per-ev-call count.
|
||||||
|
Guard conditions and ∧ ∨ operands must be boolean — err_bool (rulings
|
||||||
|
13, 14; ∧ ∨ short-circuit, so an unevaluated operand raises nothing).
|
||||||
|
≜ binds once per scope (err_redef); ← requires an existing binding
|
||||||
|
(err_unbound) — ruling 16. */
|
||||||
|
function ev(root,sc0){
|
||||||
|
const K=[{n:root,sc:sc0,st:0}];let ret;
|
||||||
|
const push=(n,sc)=>{K.push({n,sc,st:0});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}};
|
||||||
|
const bool=(v,n)=>{if(typeof v!=='boolean')rte('err_bool',n);return v};
|
||||||
|
const apply=f=>{const fn=f.fn,n=f.n;
|
||||||
|
if(fn.ps.length!==f.args.length)rte('err_arity',n);
|
||||||
|
if(++depth>10000)rte('err_depth',n);
|
||||||
|
const inner={vars:new Map(),parent:fn.sc};fn.ps.forEach((pn,ix)=>inner.vars.set(pn,f.args[ix]));
|
||||||
|
f.st=3;push(fn.body,inner)};
|
||||||
|
while(K.length){const f=K[K.length-1],n=f.n,sc=f.sc;
|
||||||
switch(n.k){
|
switch(n.k){
|
||||||
case 'lit':return n.v;
|
case 'lit':ret=n.v;K.pop();break;
|
||||||
case 'id':{const s=lookup(sc,n.v);if(!s)rte('err_undef',n);return s.vars.get(n.v)}
|
case 'id':{const s=lookup(sc,n.v);if(!s)rte('err_undef',n);ret=s.vars.get(n.v);K.pop();break}
|
||||||
case 'seq':{let v=BOT;for(const e of n.es){v=ev(e,sc);if(v===NOMATCH)v=BOT}return v}
|
case 'lam':ret={closure:true,ps:n.ps,body:n.body,sc};K.pop();break;
|
||||||
case 'def':{const v=strip(ev(n.e,sc));sc.vars.set(n.name,v);return v}
|
case 'seq':{if(f.st>0)f.last=strip(ret);
|
||||||
case 'set':{const v=strip(ev(n.e,sc));const s=lookup(sc,n.name)||sc;s.vars.set(n.name,v);return v}
|
if(f.st<n.es.length)push(n.es[f.st++],sc);
|
||||||
case 'alt':{const l=ev(n.l,sc);return l===NOMATCH?ev(n.r,sc):l}
|
else{ret=n.es.length?f.last:BOT;K.pop()}break}
|
||||||
case 'imp':{const c=strip(ev(n.c,sc));return c===true||(isRat(c)&&c.n!==0n)?ev(n.e,sc):NOMATCH}
|
case 'def':{if(f.st===0){if(sc.vars.has(n.name))rte('err_redef',n);f.st=1;push(n.e,sc)}
|
||||||
case 'or':{const l=strip(ev(n.l,sc));return l===true?true:strip(ev(n.r,sc))===true}
|
else{const v=strip(ret);sc.vars.set(n.name,v);ret=v;K.pop()}break}
|
||||||
case 'and':{const l=strip(ev(n.l,sc));return l===true?strip(ev(n.r,sc))===true:false}
|
case 'set':{if(f.st===0){const s=lookup(sc,n.name);if(!s)rte('err_unbound',n);f.s=s;f.st=1;push(n.e,sc)}
|
||||||
case 'cmp':{const a=strip(ev(n.l,sc)),b=strip(ev(n.r,sc));
|
else{const v=strip(ret);f.s.vars.set(n.name,v);ret=v;K.pop()}break}
|
||||||
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}
|
case 'alt':{if(f.st===0){f.st=1;push(n.l,sc)}
|
||||||
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);
|
else if(f.st===1){if(ret===NOMATCH){f.st=2;push(n.r,sc)}else K.pop()}
|
||||||
switch(n.o){case 'lt':return d<0;case 'gt':return d>0;case 'leq':return d<=0;case 'geq':return d>=0}}
|
else K.pop();break}
|
||||||
case 'bin':{const a=strip(ev(n.l,sc)),b=strip(ev(n.r,sc));switch(n.o){
|
case 'imp':{if(f.st===0){f.st=1;push(n.c,sc)}
|
||||||
case 'plus':return(typeof a==='string'||typeof b==='string')?show(a)+show(b):R.add(num(a,n),num(b,n));
|
else if(f.st===1){if(bool(strip(ret),n)){f.st=2;push(n.e,sc)}else{ret=NOMATCH;K.pop()}}
|
||||||
case 'minus':return R.sub(num(a,n),num(b,n));
|
else K.pop();break}
|
||||||
case 'mul':return R.mul(num(a,n),num(b,n));
|
case 'or':case 'and':{const want=n.k==='or';
|
||||||
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)}}}
|
if(f.st===0){f.st=1;push(n.l,sc)}
|
||||||
case 'neg':return R.neg(num(strip(ev(n.e,sc)),n));
|
else if(f.st===1){if(bool(strip(ret),n)===want){ret=want;K.pop()}else{f.st=2;push(n.r,sc)}}
|
||||||
case 'trace':{const v=strip(ev(n.e,sc));print(show(v));return v}
|
else{ret=bool(strip(ret),n);K.pop()}break}
|
||||||
case 'lam':return{closure:true,ps:n.ps,body:n.body,sc};
|
case 'cmp':{if(f.st===0){f.st=1;push(n.l,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))}
|
else if(f.st===1){f.a=strip(ret);f.st=2;push(n.r,sc)}
|
||||||
case 'forall':{const it=strip(ev(n.it,sc));if(!Array.isArray(it))rte('err_iter',n);let v=BOT;for(const x of it){const inner={vars:new Map([[n.v,x]]),parent:sc};v=strip(ev(n.body,inner))}return v}
|
else{const a=f.a,b=strip(ret);
|
||||||
case 'list':return n.es.map(e=>strip(ev(e,sc)))}}
|
if(n.o==='eq'||n.o==='neq'){if(fnIn(a)||fnIn(b))rte('err_fn_eq',n);const eq=dEq(a,b);ret=n.o==='eq'?eq:!eq}
|
||||||
|
else{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);
|
||||||
|
ret=n.o==='lt'?d<0:n.o==='gt'?d>0:n.o==='leq'?d<=0:d>=0}
|
||||||
|
K.pop()}break}
|
||||||
|
case 'bin':{if(f.st===0){f.st=1;push(n.l,sc)}
|
||||||
|
else if(f.st===1){f.a=strip(ret);f.st=2;push(n.r,sc)}
|
||||||
|
else{const a=f.a,b=strip(ret);
|
||||||
|
if(n.o==='plus')ret=(typeof a==='string'||typeof b==='string')?show(a)+show(b):R.add(num(a,n),num(b,n));
|
||||||
|
else if(n.o==='minus')ret=R.sub(num(a,n),num(b,n));
|
||||||
|
else if(n.o==='mul')ret=R.mul(num(a,n),num(b,n));
|
||||||
|
else{const bb=num(b,n);if(bb.n===0n)rte('err_div0',n);const aa=num(a,n);ret=rat(aa.n*bb.d,aa.d*bb.n)}
|
||||||
|
K.pop()}break}
|
||||||
|
case 'neg':{if(f.st===0){f.st=1;push(n.e,sc)}else{ret=R.neg(num(strip(ret),n));K.pop()}break}
|
||||||
|
case 'trace':{if(f.st===0){f.st=1;push(n.e,sc)}else{const v=strip(ret);print(show(v));ret=v;K.pop()}break}
|
||||||
|
case 'call':{if(f.st===0){f.st=1;push(n.f,sc)}
|
||||||
|
else if(f.st===1){const fn=strip(ret);if(!fn||!fn.closure)rte('err_notfn',n);f.fn=fn;f.args=[];
|
||||||
|
if(n.args.length){f.st=2;push(n.args[0],sc)}else apply(f)}
|
||||||
|
else if(f.st===2){f.args.push(strip(ret));
|
||||||
|
if(f.args.length<n.args.length)push(n.args[f.args.length],sc);else apply(f)}
|
||||||
|
else{depth--;ret=strip(ret);K.pop()}break}
|
||||||
|
case 'forall':{if(f.st===0){f.st=1;push(n.it,sc)}
|
||||||
|
else if(f.st===1){const it=strip(ret);if(!Array.isArray(it))rte('err_iter',n);f.it=it;f.i=0;f.st=2;f.last=BOT;
|
||||||
|
if(it.length)push(n.body,{vars:new Map([[n.v,it[f.i++]]]),parent:sc});else{ret=BOT;K.pop()}}
|
||||||
|
else{f.last=strip(ret);
|
||||||
|
if(f.i<f.it.length)push(n.body,{vars:new Map([[n.v,f.it[f.i++]]]),parent:sc});
|
||||||
|
else{ret=f.last;K.pop()}}break}
|
||||||
|
case 'list':{if(f.st===0){f.es=[];f.st=1;if(!n.es.length){ret=[];K.pop();break}push(n.es[0],sc)}
|
||||||
|
else{f.es.push(strip(ret));if(f.es.length<n.es.length)push(n.es[f.es.length],sc);else{ret=f.es;K.pop()}}break}
|
||||||
|
}}
|
||||||
|
return ret}
|
||||||
const out=strip(ev(ast,global));return show(out)}
|
const out=strip(ev(ast,global));return show(out)}
|
||||||
|
|
||||||
if (typeof module !== 'undefined') module.exports = { runMPL, ESCAPES };
|
if (typeof module !== 'undefined') module.exports = { runMPL, ESCAPES };
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// Test fixtures copied verbatim from mpl_codes js/site.js @ 5b50cc0 when
|
// Test fixtures: the programs mpl.codes ships (originally copied verbatim
|
||||||
// the interpreter test suite migrated into this repo. These are the exact
|
// from mpl_codes js/site.js @ 5b50cc0; the entries' third field is the
|
||||||
// programs the site ships; the entries' third field is the site's i18n
|
// site's i18n label key). Stage-3 ruling 16 (← requires an existing
|
||||||
// label key, preserved untouched so nothing about the facts changed in the
|
// binding) required changing first bindings in ex2/ex4 from ← to ≜ — the
|
||||||
// migration.
|
// site adopts the same programs in Stage 3 Phase B.
|
||||||
|
|
||||||
export const SYMBOLS = [
|
export const SYMBOLS = [
|
||||||
['✎','\\trace','sym_trace'],['λ','\\lambda','sym_lambda'],['≜','\\coloneq','sym_def'],
|
['✎','\\trace','sym_trace'],['λ','\\lambda','sym_lambda'],['≜','\\coloneq','sym_def'],
|
||||||
|
|
@ -15,9 +15,9 @@ export const SYMBOLS = [
|
||||||
|
|
||||||
export const EXERCISES = [
|
export const EXERCISES = [
|
||||||
{t:'ex1_t',d:'ex1_d',lvl:'lvl1',code:'✎ "Hello, World!";\n✎ "Jambo!";\n✎ "你好!";\n✎ "مرحبا!";'},
|
{t:'ex1_t',d:'ex1_d',lvl:'lvl1',code:'✎ "Hello, World!";\n✎ "Jambo!";\n✎ "你好!";\n✎ "مرحبا!";'},
|
||||||
{t:'ex2_t',d:'ex2_d',lvl:'lvl1',code:'length ← 5;\nwidth ← 3;\n✎("Area = " + length × width);'},
|
{t:'ex2_t',d:'ex2_d',lvl:'lvl1',code:'length ≜ 5;\nwidth ≜ 3;\n✎("Area = " + length × width);'},
|
||||||
{t:'ex3_t',d:'ex3_d',lvl:'lvl2',code:'fact ≜ λn: (n ≤ 1 ⟹ 1) | (n × fact(n - 1));\n✎("5! = " + fact(5));'},
|
{t:'ex3_t',d:'ex3_d',lvl:'lvl2',code:'fact ≜ λn: (n ≤ 1 ⟹ 1) | (n × fact(n - 1));\n✎("5! = " + fact(5));'},
|
||||||
{t:'ex4_t',d:'ex4_d',lvl:'lvl2',code:'total ← 0;\n∀ n ∈ [1, 2, 3, 4, 5]: total ← total + n × n;\n✎("Σ = " + total);'},
|
{t:'ex4_t',d:'ex4_d',lvl:'lvl2',code:'total ≜ 0;\n∀ n ∈ [1, 2, 3, 4, 5]: total ← total + n × n;\n✎("Σ = " + total);'},
|
||||||
{t:'ex5_t',d:'ex5_d',lvl:'lvl2',code:'even ≜ λn: (n = 0 ⟹ true) | ((n = 1 ⟹ false) | even(n - 2));\n∀ n ∈ [1, 2, 3, 4, 5, 6, 7, 8]: (even(n) ⟹ ✎(n)) | ⊥;'},
|
{t:'ex5_t',d:'ex5_d',lvl:'lvl2',code:'even ≜ λn: (n = 0 ⟹ true) | ((n = 1 ⟹ false) | even(n - 2));\n∀ n ∈ [1, 2, 3, 4, 5, 6, 7, 8]: (even(n) ⟹ ✎(n)) | ⊥;'},
|
||||||
{t:'ex6_t',d:'ex6_d',lvl:'lvl3',code:'twice ≜ λf: λx: f(f(x));\ninc ≜ λn: n + 1;\n✎ twice(inc)(40);'}
|
{t:'ex6_t',d:'ex6_d',lvl:'lvl3',code:'twice ≜ λf: λx: f(f(x));\ninc ≜ λn: n + 1;\n✎ twice(inc)(40);'}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ test('every palette escape lexes identically to its glyph', () => {
|
||||||
const glyphProgram = [
|
const glyphProgram = [
|
||||||
'check ≜ λa, b: ((a ≤ b) ∧ (b ≥ a) ∧ (a ≠ b) ⟹ ✎ "cmp") | ⊥;',
|
'check ≜ λa, b: ((a ≤ b) ∧ (b ≥ a) ∧ (a ≠ b) ⟹ ✎ "cmp") | ⊥;',
|
||||||
'check(1, 2);',
|
'check(1, 2);',
|
||||||
't ← 0;',
|
't ≜ 0;', // ruling 16: first binding is ≜ (← still covered by the ∀ line)
|
||||||
'∀ n ∈ [1, 2, 3]: t ← t + n;',
|
'∀ n ∈ [1, 2, 3]: t ← t + n;',
|
||||||
'✎(t × 2);',
|
'✎(t × 2);',
|
||||||
'✎(t ÷ 2);',
|
'✎(t ÷ 2);',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue