git @ Cat's Eye Technologies Pixley / master impl / pixley.js / src / pixley.js
master

Tree @master (Download .tar.gz)

pixley.js @masterraw · history · blame

/*
 * The functions and object in this file implement the semantics of Pixley.
 * (almost.)
 */

var ErrorHandler = function() {
    this.error = function(x) {
        console.error(x);
    };
};
var errorHandler = new ErrorHandler();

/*
 * Our lexical scanner.  This is a straight copy of the yoob.Scanner object
 * from yoob.js 0.3, pasted in here for convenience.
 */
var Scanner = function() {
    this.text = undefined;
    this.token = undefined;
    this.type = undefined;
    this.error = undefined;
    this.table = undefined;
    this.whitespacePattern = "^[ \\t\\n\\r]*";

    this.init = function(table) {
        this.table = table;
    };

    this.reset = function(text) {
        this.text = text;
        this.token = undefined;
        this.type = undefined;
        this.error = undefined;
        this.scan();
    };

    this.scanPattern = function(pattern, type) {
        var re = new RegExp(pattern);
        var match = re.exec(this.text);
        if (match === null) return false;
        this.type = type;
        this.token = match[1];
        this.text = this.text.substr(match[0].length);
        return true;
    };

    this.scan = function() {
        this.scanPattern(this.whitespacePattern, "whitespace");
        if (this.text.length === 0) {
            this.token = null;
            this.type = "EOF";
            return;
        }
        for (var i = 0; i < this.table.length; i++) {
            var type = this.table[i][0];
            var pattern = this.table[i][1];
            if (this.scanPattern(pattern, type)) return;
        }
        if (this.scanPattern("^([\\s\\S])", "unknown character")) return;
        // should never get here
    };
    
    this.expect = function(token) {
        if (this.token === token) {
            this.scan();
        } else {
            this.error = "expected '" + token + "' but found '" + this.token + "'";
        }
    };

    this.on = function(token) {
        return this.token === token;
    };

    this.onType = function(type) {
        return this.type === type;
    };

    this.checkType = function(type) {
        if (this.type !== type) {
            this.error = "expected " + type + " but found " + this.type + " (" + this.token + ")"
        }
    };

    this.expectType = function(type) {
        this.checkType(type);
        this.scan();
    };

    this.consume = function(token) {
        if (this.on(token)) {
            this.scan();
            return true;
        } else {
            return false;
        }
    };
};

/*
 * S-expressions, apropos to Pixley.
 */
var Atom = function(text) {
    this.text = text;

    this.toString = function() {
        return this.text;
    };
};

var Cons = function(head, tail) {
    this.head = head;
    this.tail = tail;

    this.toString = function() {
        return depict(this);
    };
};

var cloneSexp = function(sexp) {
    if (sexp instanceof Atom) {
        return new Atom(sexp.text);
    } else if (sexp instanceof Cons) {
        return new Cons(cloneSexp(sexp.head), cloneSexp(sexp.tail));
    } else {
        return sexp;
    }
};

var depict = function(sexp) {
    var s = '';
    if (sexp instanceof Cons) {
        s += '(';
        s += depict(sexp.head);
        while (sexp.tail instanceof Cons) {
            s += ' ';
            s += depict(sexp.tail.head);
            sexp = sexp.tail;
        }
        s += ')';
        return s;
    } else if (sexp instanceof Atom) {
        return sexp.text;
    } else if (sexp === undefined) {
        return 'undefined';
    } else if (sexp === null) {
        return '()';
    } else {
        return '???' + sexp.toString();
    }
};

/*
 * A simple S-expression parser, mostly a copy of yoob.js 0.3's
 * yoob.SexpParser, but modified to work with our S-expressions
 * instead of yoob.Trees.
 */
var SexpParser = function() {
    this.scanner = undefined;
    
    this.init = function(text) {
        this.scanner = new Scanner();
        this.scanner.init([
            ['paren',  "^(\\(|\\))"],
            ['atom',   "^([a-zA-Z\\?\\*\\-][a-zA-Z0-9\\?\\*\\-]*)"]
        ]);
        this.scanner.reset(text);
    };

    /*
     * SExp ::= Atom | "(" {SExpr} ")".
     */
    this.parse = function(text) {
        if (this.scanner.onType('atom')) {
            var x = this.scanner.token;
            this.scanner.scan();
            return new Atom(x);
        } else if (this.scanner.consume('(')) {
            if (this.scanner.consume(')') || this.scanner.onType('EOF')) {
              return null;
            }

            var top = new Cons(null, null);
            top.head = this.parse();
            var cell = top;
            while (!this.scanner.consume(')') && !this.scanner.onType('EOF')) {
                cell.tail = new Cons(null, null);
                cell = cell.tail;
                cell.head = this.parse();
            }
            return top;
        } else {
            /* TODO: register some kind of error */
            this.scanner.scan();
        }
    };
};

/********************
 * Pixley Semantics *
 ********************/

var listP = function(sexp) {
    if (sexp === null) return true;
    if (!(sexp instanceof Cons)) return false;
    return listP(sexp.tail);
};

var equalP = function(a, b) {
    if (a === null && b == null) return true;
    if (a instanceof Atom && b instanceof Atom && a.text === b.text) {
        return true;
    }
    if (a instanceof Cons && b instanceof Cons) {
        return equalP(a.head, b.head) && equalP(a.tail, b.tail);
    }
    return false;
};

var depictEnv = function(env) {
    var s = '{\n';
    for (var key in env) {
        s += '  ' + key + ': ' + depict(env[key]) + '\n';
    }
    s += '}';
    return s;
};

var cloneEnv = function(env) {
    var newEnv = {};
    for (var key in env) {
        newEnv[key] = env[key];
    }
    return newEnv;
};

var bind = function(identifier, value, env) {
    // console.log('let ' + depict(identifier) + ' = ' + depict(value));
    var newEnv = cloneEnv(env);
    newEnv[identifier] = value;
    return newEnv;
};

var bindAll = function(bindings, env) {
    while (bindings !== null) {
        var binding = bindings.head;
        var value = evalPixley(binding.tail.head, env);
        env = bind(binding.head, value, env);
        bindings = bindings.tail;
    }
    return env;
};

var evalList = function(sexp, env) {
    var args = [];
    while (sexp !== null) {
        if (!sexp instanceof Cons) {
            errorHandler.error('assertion failed: not a Cons');
            return [];
        }
        args.push(evalPixley(sexp.head, env));
        sexp = sexp.tail;
    }
    return args;
};

var extendEnv = function(env, formals, actuals) {
    var sexp = formals;
    var newEnv = cloneEnv(env);
    for (var i = 0; i < actuals.length; i++) {
        var value = actuals[i];
        var identifier = sexp.head.text;
        // console.log(identifier + ' = ' + depict(value));
        newEnv[identifier] = value;
        sexp = sexp.tail;
    }
    return newEnv;
};

var evalPixley = function(ast, env) {
    if (ast === null) {
        return null;
    } else if (ast instanceof Cons) {
        var head = ast.head;
        var fn;
        if (head instanceof Atom) {
            if (head.text === 'quote') {
                return ast.tail.head;
            } else if (head.text === 'car') {
                return evalPixley(ast.tail.head, env).head;
            } else if (head.text === 'cdr') {
                return evalPixley(ast.tail.head, env).tail;
            } else if (head.text === 'cons') {
                var a = evalPixley(ast.tail.head, env);
                var b = evalPixley(ast.tail.tail.head, env);
                return new Cons(a, b);
            } else if (head.text === 'list?') {
                var a = evalPixley(ast.tail.head, env);
                return new Atom(listP(a) ? '#t' : '#f');
            } else if (head.text === 'equal?') {
                var a = evalPixley(ast.tail.head, env);
                var b = evalPixley(ast.tail.tail.head, env);
                return new Atom(equalP(a, b) ? '#t' : '#f');
            } else if (head.text === 'let*') {
                var bindings = ast.tail.head;
                var body = ast.tail.tail.head;
                var newEnv = bindAll(bindings, env);
                // alert(depictEnv(newEnv));
                return evalPixley(body, newEnv);
            } else if (head.text === 'cond') {
                var branch = ast.tail;
                while (branch !== null) {
                    var b = branch.head;
                    var test = b.head;
                    if (test instanceof Atom &&
                        test.text === 'else') {
                        return evalPixley(b.tail.head, env);
                    } else {
                        var result = evalPixley(test, env);
                        if (result instanceof Atom &&
                            result.text === '#t') {
                            return evalPixley(b.tail.head, env);
                        }
                        branch = branch.tail;
                    }
                }
                errorHandler.error('no else in cond');
                return head;
            } else if (head.text === 'lambda') {
                var formals = ast.tail.head;
                var body = ast.tail.tail.head;
                var f = function(args) {
                    return evalPixley(body, extendEnv(env, formals, args));
                };
                return f;
            } else {
                fn = evalPixley(ast.head, env);
            }
        } else {
            fn = evalPixley(ast.head, env);
        }
        return fn(evalList(ast.tail, env));
    } else if (ast instanceof Atom) {
        if (env[ast.text] === undefined) {
            errorHandler.error('Unbound identifier: ' + ast.text);
        }
        return env[ast.text];
    } else {
        errorHandler.error('not a Cons or Atom: ' + depict(ast));
    }
};

var runPixley = function(text) {
    var p = new SexpParser();
    p.init(text);
    var ast = p.parse();
    if (ast) {
        return evalPixley(ast, {});
    } else {
        return undefined;
    }
};