mrscheme
Version:
Scheme didactic subset interpreter in JavaScript.
590 lines (502 loc) • 17.9 kB
JavaScript
import { M$ } from './message101.mjs';
import { SchemeNumber } from "./schemeNumber101.mjs";
import { BoolValue, VectorValue, StringValue, NilValue, PairValue,
IntegerValue, NumberValue, RealValue, RationalValue, UnitValue }
from './values.mjs';
function BuildPairFromList(evaluator, lexenv, list) {
var pair = new NilValue();
for(var i=list.elems.length-1;i>=0;i--) {
var qarg = list.elems[i];
if(qarg.type=='qexpr') {
qarg = EvalQuoted(evaluator,lexenv,qarg.expr);
if(qarg.type=="evalError") {
return qarg;
}
} else {
qarg = EvalQuoted(evaluator,lexenv,qarg);
if(qarg.type=="evalError") {
return qarg;
}
}
pair = new PairValue(qarg,pair);
}
return pair;
}
function EvalError(msg,expr) {
this.type = "evalError";
this.expr = expr;
this.startPos = expr.startPos;
this.endPos = expr.endPos;
this.message = msg;
this.expr = expr;
this.toString = function() {
var str = "Eval error: frm (line "+expr.startPos.lpos+", col "+expr.startPos.cpos+") to (line "+expr.endPos.lpos+", col "+expr.endPos.cpos+')\n';
str = str + " ==> " + this.message + "\n";
return str;
}
this.toHTML = function() {
return '<span class="error"><strong>'+M$("Eval Error").toString()+'</strong>: '+this.message+'</span>';
}
}
function FunEnv() {
this.funs = new Array();
this.fetch = function(name) {
return this.funs["___"+name];
}
this.register = function(name,fun) {
if (this.funs["___"+name]==null) {
this.funs["___"+name] = fun;
return true;
} else {
return false;
}
}
this.unregister = function(name) {
var fun = this.fetch(name);
if(fun==null) {
throw "Cannot unregister function '"+name+"' (please report)";
}
this.funs["___"+name] = null;
}
}
function UserFunction(expr,lexenv) {
this.type = "function";
this.expr = expr;
this.name = expr.fname;
this.params = expr.params;
this.arity = this.params.length;
this.callable = true;
this.innerDefs = expr.innerDefs;
this.body = expr.body;
this.defEnv = lexenv;
this.call = function(evaluator,lexenv,callExpr,args) {
// remark : lexenv is of course unused with lexical scoping (the args have been calculated in lexenv)
// check the arity
if(args.length < this.arity) {
return new EvalError(M$("Not enough arguments in call (given $0, needs $1)",new Array(""+args.length,""+this.arity)),callExpr);
} else if(args.length > this.arity) {
return new EvalError(M$("Too many arguments in call (given $0, needs $1)",new Array(""+args.length,""+this.arity)),callExpr);
}
// first dig the definitional lex. env
var callEnv = this.defEnv.dig();
// then bind the parameters
for(var i=0;i<this.params.length;i++) {
var ok = callEnv.bind(this.params[i], args[i]);
if(!ok) {
return new EvalError(M$("Parameter '$0' already defined",this.params[i]), this.expr);
}
}
// now must evaluate the inner functions and also bind them in the call env
for(var i=0;i<this.innerDefs.length;i++) {
var result = EvalDefine(evaluator,callEnv,this.innerDefs[i]);
if(result.type!="function") {
return result;
}
var ok = callEnv.bind(result.name,result);
if(!ok) {
return new EvalError(M$("Inner function '$0' already defined",result.name),this.innerDefs[i]);
}
}
// ok to evaluate the body
return evaluator.evalExpr(callEnv,this.body);
}
this.toString = function() {
return this.name;
}
this.toHTML = function() {
return '<span class="value">function:'+this.name+'<span class="tooltip">type <strong>Function</strong></span></span>';
}
}
function LexEnv(parent) {
this.parent = parent;
this.vars = new Array();
this.bind = function(varname,value) {
if (this.vars["___"+varname]==null) {
this.vars["___"+varname] = value;
return true;
} else {
return false;
}
}
this.fetch = function(varname) {
var result = this.vars["___"+varname];
if(result!=null) {
return result;
}
if(this.parent==null) {
return null;
} else {
return this.parent.fetch(varname);
}
}
this.dig = function() {
var nenv = new LexEnv(this);
return nenv;
}
this.bury = function() {
return parent;
}
this.toString = function() {
var str = "[";
var i = 0;
for(var key in this.vars) {
str = str + key + "->" + this.vars[key];
i++;
if(i<this.vars.length-1) {
str = str+",";
}
}
str = str + "]";
if(parent!=null) {
str = str + parent;
}
return str;
}
}
export function Evaluator(primsEnv) {
if(primsEnv==null) {
throw new "PrimsEnv undefined (please report)";
}
this.primsEnv = primsEnv; // environment of primitives
this.funEnv = new FunEnv(); // environment of functions
this.nbTestsPassed = 0;
this.lookup = function(lexenv,symbol) {
// 1) search in lexenv
var entry = lexenv.fetch(symbol.value);
if(entry!=null) {
return entry;
}
// 2) search in funEnv
entry = this.funEnv.fetch(symbol.value);
if(entry!=null) {
return entry;
}
// 3) search in primsEnv
entry = this.primsEnv.fetch(symbol.value);
if(entry!=null) {
return entry;
}
return new EvalError(M$("Unknown variable '$0'",symbol.value),symbol);
}
this.eval = function(expr, traceMode) {
var lexenv = new LexEnv();
// install constants
lexenv.bind("pi", new RealValue(SchemeNumber.fn["string->number"](""+Math.PI)));
// then run evaluator
if(traceMode) {
this.evalTraceExpr = this.evalExpr;
this.evalExpr = this.traceExpr;
}
var result = this.evalExpr(lexenv,expr);
if(traceMode) {
this.evalExpr = this.evalTraceExpr;
this.evalTraceExpr = null;
}
return result;
}
this.traceExpr = function(lexenv,expr) {
this.evalDepth++;
// 1. show the eval step
if (this.traceMode == "all") {
var msg = "";
for(var i=0;i<this.evalDepth-1;i++) {
msg += "·";
}
msg += "| ";
msg += expr.toString();
if(expr.type=="symbol") {
var entry = lexenv.fetch(expr.value);
if(entry!=null) {
msg += " <== " + entry.toString();
}
}
this.traceln("<pre class='traceExpr'>" + msg +"</pre>");
}
// 2. perform the step
var result = this.evalTraceExpr(lexenv,expr);
this.evalDepth--;
// XXX: show something at the end ?
return result;
}
this.evalExpr = function(lexenv,expr) {
if(expr.type=="Number") {
return new NumberValue(expr.value);
} else if(expr.type=="bool") {
return new BoolValue(expr.value);
} else if(expr.type=="string") {
return new StringValue(expr.value);
} else if(expr.type=="nil") {
return new NilValue();
} else if(expr.type=='qlist') { // quoted list
return BuildPairFromList(this,lexenv,expr);
} else if(expr.type=='qexpr') { // quoted expression
return EvalQuoted(this,lexenv,expr.expr);
} else if(expr.type=="symbol") {
return this.lookup(lexenv,expr);
} else if(expr.type=="application") {
return EvalApplication(this,lexenv,expr);
} else if(expr.type=="if") {
return EvalIf(this,lexenv,expr);
} else if(expr.type=="and") {
return EvalAnd(this,lexenv,expr);
} else if(expr.type=="or") {
return EvalOr(this,lexenv,expr);
} else if(expr.type=="test") {
return EvalTest(this,lexenv,expr);
} else if(expr.type=="define") {
var fun = EvalDefine(this,lexenv,expr);
if(fun.type!="function") {
return new EvalError("Define is not a function (please report)",expr);
}
var ok = this.funEnv.register(fun.name,fun);
if(!ok) {
return new EvalError(M$("Function '$0' already defined",fun.name),expr);
}
return new UnitValue();
} else if(expr.type=="let") {
return EvalLet(this,lexenv,expr);
} else if(expr.type=="let*") {
return EvalLetStar(this,lexenv,expr);
} else if(expr.type=="cond") {
return EvalCond(this,lexenv,expr);
} else {
return new EvalError(M$("Cannot (yet) evaluate expression of type: $0",expr.type),expr);
}
}
}
function EvalApplication(evaluator,lexenv,expr) {
var args = new Array();
// evaluate arguments (beware of recursion here)
for(var i=1;i<expr.size();i++) {
var arg = evaluator.evalExpr(lexenv,expr.get(i));
if (arg.type=="evalError") {
return arg;
}
args.push(arg);
}
// evaluation functional argument
var fun = evaluator.evalExpr(lexenv,expr.get(0));
if (fun.type=="evalError") {
return fun;
} else if(!fun.callable) {
return new EvalError(M$("Functional argument is not callable"),expr);
}
// trace
var showStep = false;
if (evaluator.traceMode == "calls" && fun.type != "primitive") {
showStep = true;
}
if(showStep) {
var msg = "";
for(var i=0;i<evaluator.evalDepth-1;i++) {
msg += "·";
}
msg += "> ";
msg += "(" + expr.get(0).toString();
for(var j=0;j<args.length;j++) {
msg += " ";
msg += args[j].toString();
}
msg += ")";
evaluator.traceln("<pre class='traceExpr'>" + msg+"</pre>");
}
var result = fun.call(evaluator,lexenv,expr,args);
if(showStep) {
var msg = "";
for(var i=0;i<evaluator.evalDepth-1;i++) {
msg += "·";
}
msg += "< ";
if(result.type=="evalError") {
msg += "{ERROR}";
} else {
msg += result.toString();
}
evaluator.traceln("<pre class='traceExpr'>" + msg+"</pre>");
}
return result;
}
function EvalIf(evaluator,lexenv,expr) {
var condition = evaluator.evalExpr(lexenv,expr.condition);
if(condition.type=="evalError") {
return condition;
}
if(condition.type=="bool" && !condition.value) { // same meaning as in javascript
return evaluator.evalExpr(lexenv,expr.elseClause);
} else { // anything except #f is ok
return evaluator.evalExpr(lexenv,expr.thenClause);
}
}
function EvalDefine(evaluator,lexenv,expr) {
return new UserFunction(expr,lexenv);
}
function EvalAnd(evaluator,lexenv,expr) {
var result = new BoolValue(true);
for(var i=1;i<expr.size();i++) {
result = evaluator.evalExpr(lexenv,expr.get(i));
//alert(""+expr.get(i)+" = " + result);
if(result.type=="evalError") {
return result;
}
if(result.type=="bool" && !result.value) {
return new BoolValue(false);
}
}
return result;
}
function EvalOr(evaluator,lexenv,expr) {
var result = new BoolValue(false);
for(var i=1;i<expr.size();i++) {
var result = evaluator.evalExpr(lexenv,expr.get(i));
if(result.type=="evalError") {
return result;
}
if(result.type!="bool" || result.value) {
return result;
}
}
return result;
}
function EvalLet(evaluator,lexenv,expr) {
var newEnv = lexenv.dig(); // allows to shadow variables
var vals = new Array();
for(var i=0;i<expr.bindings.length;i++) {
var bexpr = expr.bindings[i].expr;
var result = evaluator.evalExpr(lexenv,bexpr);
if(result.type=="evalError") {
return result;
}
vals.push(result);
}
for(var i=0;i<expr.bindings.length;i++) {
var name = expr.bindings[i].name;
if(!newEnv.bind(name,vals[i])) {
return new EvalError(M$("Dupplicate variable '$0' un $1-expression", new Array(name,expr.type)), expr);
}
}
var result = null;
for(var i=0;i<expr.body.length;i++) {
result = evaluator.evalExpr(newEnv, expr.body[i]);
if(result.type=="evalError") {
return result;
}
}
return result;
}
function EvalLetStar(evaluator,lexenv,expr) {
var newEnv = lexenv.dig(); // allows to shadow variables
for(var i=0;i<expr.bindings.length;i++) {
var bexpr = expr.bindings[i].expr;
var result = evaluator.evalExpr(newEnv,bexpr);
if(result.type=="evalError") {
return result;
}
var name = expr.bindings[i].name;
if(!newEnv.bind(name,result)) {
return new EvalError(M$("Dupplicate variable '$0' un $1-expression", new Array(name,expr.type)), expr);
}
}
var result = null;
for(var i=0;i<expr.body.length;i++) {
result = evaluator.evalExpr(newEnv, expr.body[i]);
if(result.type=="evalError") {
return result;
}
}
return result;
}
function EvalCond(evaluator,lexenv,expr) {
for(var i=0;i<expr.clauses.length-1;i++) {
var clause = expr.clauses[i];
var condval = evaluator.evalExpr(lexenv, clause.condition);
if(condval.type=="evalError") {
return condval;
}
if(condval.type!="bool" || condval.value!=false) {
var result = null;
for(var j=0;j<clause.body.length;j++) {
result = evaluator.evalExpr(lexenv, clause.body[j]);
if(result.type=="evalError") {
return result;
}
}
return result;
}
}
var elseClause = expr.clauses[expr.clauses.length-1];
var result = null;
for(var j=0;j<elseClause.body.length;j++) {
result = evaluator.evalExpr(lexenv, elseClause.body[j]);
if(result.type=="evalError") {
return result;
}
}
return result;
}
function EvalTest(evaluator,lexenv,expr) {
//QNC check that the checked function is indeed a function
// see parser101/parser101.js
var f = evaluator.evalExpr(lexenv, expr.testFun);
if ( f.type === 'evalError' ) {
return f;
}
if ( ! f.callable ) {
return new EvalError(M$("First parameter of verifier is not a function: $0", f.toString()),expr.testFun);
}
for(var index=0; index<expr.testCases.length;index++) {
var testCase = expr.testCases[index];
// evaluate the expected value (quoted)
var expected = evaluator.evalExpr(lexenv, testCase.expectedValue);
if(expected.type=="evalError") {
throw "Cannot evaluate expected value: "+expected.toString()+" (please report)"; // XXX: should not happen !
}
// evaluate test expression
var result = evaluator.evalExpr(lexenv, testCase.testExpr);
if(expected.type=="symbol" && (expected.value=="ERROR" || expected.value=="ERREUR")) {
if(result.type!="evalError") {
return new EvalError(M$("Test case failed: expected error but obtained value = $0",result.toString()), testCase.testExpr);
}
} else {
// XXX: an error should not happen now
if(result.type=="evalError") {
return new EvalError(M$("Test case failed: expected value = $0 but obtained error = $1",new Array(expected.toString(),result.message.toString())), testCase.testExpr);
}
// compare
var compare = result.equal(expected);
// QNC
if( !compare && !compareApprox(result, expected) ) {
return new EvalError(M$("Test case failed: expected value = $0 but computed value = $1",new Array(expected.toString(),result.toString())), testCase.testExpr);
}
}
evaluator.nbTestsPassed++;
}
return new UnitValue();
}
// QNC
var epsilon = 0.001;
function compareApprox(a, b) {
var aa = SchemeNumber.fn["number?"](a.value);
var bb = SchemeNumber.fn["number?"](b.value);
if ( aa && bb ) {
var bminus = SchemeNumber.fn["-"](b.value, epsilon);
if ( SchemeNumber.fn["<"](bminus, a.value) ) {
var bplus = SchemeNumber.fn["+"](b.value, epsilon);
var result = SchemeNumber.fn["<"](a.value, bplus);
return result;
} else {
return false;
}
} else {
// At least one argument is not a number:
return false;
}
}
function EvalQuoted(evaluator,lexenv,expr) {
if(expr.type=='qlist') {
return BuildPairFromList(evaluator,lexenv,expr);
} else if(expr.type=='symbol') {
return new SymbolValue(expr.value);
} else { // auto-quoted value
return evaluator.evalExpr(lexenv,expr);
}
}