Posted to tcl by st3ve at Sun Nov 10 06:46:02 GMT 2013view pretty

/* =================================================== -*- C++ -*-
 * tcl.js "A Tcl implementation in Javascript"
 *
 * Released under the same terms as Tcl itself.
 * (BSD license found at <http://www.tcl.tk/software/tcltk/license.html>)
 *
 * Based on Picol by Salvatore Sanfilippo (<http://antirez.com/page/picol>)
 * (c) Stéphane Arnold 2007
 * Richard Suchenwirth 2007, 2013: cleanup, additions
 * vim: syntax=javascript autoindent softtabwidth=4
 */
_step = 0; // set to 1 for debugging
var fs = require('fs');
puts = console.log;

function TclInterp () {
    this.patchlevel = 0.4;
    this.callframe = [{}];
    this.level         = 0;
    this.commands    = {};
    this.procs         = [];
    this.script        = "";
    this.OK    = 0;
    this.RET = 1;
    this.BRK = 2;
    this.CNT = 3;
    this.getVar = function(name) {
        var nm = name.toString();
        if (nm.match("^::env[(]")) nm=nm.substr(2);
        if (nm.match("^env[(]")) {
            var key = nm.substr(4,nm.length-5);
            var val = process.env[key];
        } else if (nm.match("^::")) {
            var val = this.callframe[0][nm.substr(2)]; // global
        } else {
            var val = this.callframe[this.level][name];
        }
        if (val == null) throw 'can\'t read "'+name+'": no such variable';
        return val;
    }
    this.setVar = function(name, val) {
        var nm = name.toString();
        if (nm.match("^::")) {
            this.callframe[0][nm.substr(2)] = val;
        } else {this.callframe[this.level][name] = val;}
        return val;
    }
    this.setVar("argc",    process.argv.length-2);
    this.setVar("argv0", process.argv[1]);
    this.setVar("argv",    process.argv.slice(2));
    this.setVar("errorInfo", "");

    this.incrLevel = function() {
        this.callframe[++this.level] = {};
        return this.level;
    }
    this.decrLevel = function() {
        this.callframe[this.level] = null;
        this.level--;
        if (this.level<0) throw "Exit application";
        this.result = null;
    }
    this.getCommand = function(name) {
        try {
            return this.commands[name];
        } catch (e) {throw "No such command '"+name+"'";}
    }
    this.registerCommand = function(name, func, privdata) {
        if (func == null) throw "No such function: "+name;
        this.commands[name] = new TclCommand(func, privdata);
    }
    this.renameCommand = function (name, newname) {
        this.commands[newname] = this.commands[name];
        if (this.procs[name]) {
            this.procs[name] = null;
            this.procs[newname] = true;
        }
        this.commands[name] = null;
    }
    this.registerSubCommand = function(name, subcmd, func, privdata) {
        if (func == null) throw "No such subcommand: "+ name +" " + subcmd;
        var path = name.split(" ");
        var ens;
        name = path.shift();
        var cmd = this.commands[name];
        if (cmd == null) {
            ens = {};
            ens["subcommands"]    = new TclCommand(Tcl.InfoSubcommands, null);
            this.commands[name] = new TclCommand(Tcl.EnsembleCommand, null, ens);
        }
        ens = this.commands[name].ensemble;
        if (ens == null) throw "Not an ensemble command: '"+name+"'";
        // walks deeply into the subcommands tree
        while (path.length > 0) {
            name = path.shift();
            cmd = ens[name];
            if (cmd == null) {
                cmd = new TclCommand(Tcl.EnsembleCommand, null, {});
                ens[name] = cmd;
                ens = cmd.ensemble;
                ens["subcommands"] = new TclCommand(Tcl.InfoSubcommands, null);
            }
        }
        ens[subcmd] = new TclCommand(func, privdata);
    }
    this.eval = function (code) {
        try {
            return this.eval2(code);
        } catch (e) {
            var msg = code.substr(0,128);
            if(msg.length >= 125) msg += "...";
            puts(e);
            this.setVar("::errorInfo",e+'\n        while executing\n"'+msg+'"');
        }
    }
    this.eval2 = function(code) {
        this.code = this.OK;
        var parser = new TclParser(code);
        var args = [];
        var first = true;
        var text, prevtype, result;
        result = "";
        while (true) {
            prevtype = parser.type;
            try {
                parser.getToken();
            } catch (e) {break;}
            if (parser.type == (parser.EOF)) break;
            text = parser.getText();
            if (parser.type == (parser.VAR)) {
                try {
                    text = this.getVar(text);
                } catch (e) {throw "No such variable '" + text + "'";}
            } else if (parser.type == (parser.CMD)) {
                try {
                    text = this.eval2(text);
                } catch (e) {throw (e + "\nwhile parsing \"" + text + "\"");}
            } else if (parser.type == (parser.ESC)) {
                // escape handling missing!
            } else if (parser.type == (parser.SEP)) {
                prevtype = parser.type;
                continue;
            }
            text = this.objectify(text);
            if (parser.type ==parser.EOL || parser.type == parser.EOF) {
                prevtype = parser.type;
                if (args.length > 0) {
                    try {
                        result = this.call(args);
                    } catch(e) {
                        if(e.toString().match("Cannot call method")) 
                            throw 'invalid command name "'+args[0].toString()+'"';
                        throw e;
                    }
                    if (this.code != this.OK) return this.objectify(result);
                }
                args = [];
                continue;
            }
            if (prevtype == parser.SEP || prevtype == parser.EOL) {
                args.push(text);
            } else {
                args[args.length-1] = args[args.length-1].toString() + text.toString();
            }
        }
        if (args.length > 0) result = this.call(args);
        return this.objectify(result);
    }
    //---------------------------------- Commands in alphabetical order
    this.registerCommand("append", function (interp, args) {
        this.requireMinArgc(args, 2);
        var vname = args[1].toString();
        if (interp.callframe[interp.level][vname] != null) {
            var str = interp.getVar(vname);
        } else var str = "";
        for (var i = 2; i < args.length; i++) str += args[i].toString();
        interp.setVar(vname, str);
        return str;
    });
    this.registerCommand("break", function (interp, args) {
        interp.code = interp.BRK;
        return;
    });
    this.registerCommand("cd", function (interp, args) {
            this.requireArgcRange(args, 1, 2);
            var dir = process.env.HOME;
            if (args.length == 2) dir = args[1].toString();
            process.chdir(dir);
            return;
        });
    this.registerCommand("continue", function (interp, args) {
        interp.code = interp.CNT;
        return;
    });
    this.registerSubCommand("clock", "format", function (interp, args) {
        var now = new Date();
        now.setTime(args[1]);
        return now.toString();
    });
    this.registerSubCommand("clock", "milliseconds", function (interp, args) {
            var t = new Date();
            return t.valueOf();
    });
    this.registerSubCommand("clock", "scan", function (interp, args) {
        return Date.parse(args[1]);
     });
    this.registerSubCommand("clock", "seconds", function (interp, args) {
            return Math.floor((new Date()).valueOf()/1000);
    });
    
    this.registerSubCommand("dict", "create", function (interp, args) {
            if(args.length % 2 == 0) 
                throw 'wrong # args: should be "dict create ?key value ...?';
            return new TclObject(args.slice(1));
    });
    this.registerSubCommand("dict", "get", function (interp, args) {
            if(args.length < 2) 
                throw 'wrong # args: should be "dict get ?key ...?';
            var dict = args[1].toList();
            var key = args[2].toString();
            for (var i=0;i < dict.length;i+=2) {
                if(dict[i].toString() == key) return dict[i+1];
            }
            throw 'key "'+key+'" not known in dictionary';
        });
    this.registerSubCommand("dict", "set", function (interp, args) {
            this.requireExactArgc(args, 4);
            var name = args[1];
            var dict = interp.getVar(name);
            var key     = args[2].toString();
            var val     = args[3].toString();
            var found = false;
            var list = dict.toList();
            for (var i=0;i < list.length;i+=2) {
                if(list[i].toString() == key) {
                    list[i+1] = val;
                    found = true;
                    break;
                }
            }
            if (!found) {
                list.push(interp.objectify(key)); 
                list.push(interp.objectify(val));
            } 
            interp.setVar(name, dict);
            return dict;
        });
    /*
    if(typeof(jQuery) != 'undefined') {
        this.registerCommand("dom", function (interp, args) {
            var selector = args[1].toString();
            var fn = args[2].toString();
            args = args.slice(3);
            for (var i in args) args[i] = args[i].toString();
            var q = $(selector);
            q[fn].apply(q,args);
            return "dom    " + selector;
        });
        }*/
    this.registerCommand("eval",function (interp, args) {
        this.requireMinArgc(args, 2);
        for (var i = 1; i < args.length; i++) args[i] = args[i].toString();
        if (args.length == 2) var code = args[1];
        else                                    var code = args.slice(1).join(" ");
        return interp.eval(code);
    });
    /*
    this.registerCommand("exec",function (interp, args) {
            this.requireMinArgc(args, 2);
            var exec = require('child_process').exec,
                child;
            puts("exec "+args.slice(1).join(" "));
            child = exec(args.slice(1).join(" "),
                        function (error, stdout, stderr) {
                                var res = stdout.toString();
                                //console.log('stdout: ' + stdout.toString());
                                if (error !== null) {
                                    throw('exec error: ' + error);
                                } 
                                return res;
                        });
            return this.execres;
        });
    */
    this.registerCommand("exit",function (interp, args) {
            this.requireMinArgc(args, 1);
            var rc = 0;
            if (args.length == 2) rc = args[1];
            process.exit(rc);
        });
    acos = Math.acos;
    exp    = Math.exp;
    sqrt = Math.sqrt; // "publish" other Math.* functions as needed

    this.registerCommand("expr", function (interp, args) {
            var expression = args.slice(1).join(" ");
            return interp.expr(interp, expression);
    });
    this.expr =    function ($interp, $expression) {
        var $mx = $expression.match(/(\$[A-Za-z0-9_:]+)/g);
        for ($i in $mx)
            eval("var "+$mx[$i]+" = "+$interp.getVar($mx[$i].slice(1)));
        var res = eval($expression);
        if(res == false) res = 0; else if(res == true) res = 1;
        return res;
    };
    this.registerCommand("for", function (interp, args) {
        this.requireExactArgc(args, 5);
        interp.eval(args[1].toString());
        if(interp.code != interp.OK) return;
        var cond = args[2].toString();
        var step = args[3].toString();
        var body = args[4].toString();
        interp.inLoop = true;
        interp.code = interp.OK;
        while (true) {
            test = interp.objectify(interp.expr(interp, cond));
            if (!test.toBoolean()) break;
            interp.eval(body);
            var ic = interp.code; // tested after step command
            interp.eval(step);
            if(ic == interp.BRK) break;
            if(ic == interp.CNT) continue;
        }
        interp.inLoop = false;
        if(interp.code == interp.BRK || interp.code == interp.CNT)
            interp.code = interp.OK;
        return "";
    });
    this.registerCommand("foreach", function (interp, args) {
        this.requireExactArgc(args, 4);
        var list = args[2].toList();
        var body = args[3].toString();
        var res    = "";
        interp.inLoop = true;
        interp.code = interp.OK;
        for(i in list) {
             interp.setVar(args[1],interp.objectify(list[i]));
             interp.eval(body);
             if(interp.code == interp.BRK) break;
             if(interp.code == interp.CNT) continue;
        }
        interp.inLoop = false;
        if(interp.code == interp.BRK || interp.code == interp.CNT)
            interp.code=interp.OK;
        return "";
    });
    /*    this.registerCommand("gets", function (interp, args) {
        this.requireArgcRange(args, 2, 3);
        var reply; // = prompt(args[1],"");
        process.stdin.resume();
        process.stdin.on('data', function(str) {
        reply = str;
            });
        if(args[2] != null) {
            interp.setVar(args[2],interp.objectify(reply));
            return reply.length;
        } else return reply;
        }); */
    this.registerCommand("if", function (interp, args) {
        this.requireMinArgc(args, 3);
        var cond = args[1].toString();
        var test = interp.objectify(interp.expr(interp, cond));
        if (test.toBoolean()) return interp.eval(args[2].toString());
        if (args.length == 3) return;
        for (var i = 3; i < args.length; ) {
            switch (args[i].toString()) {
            case "else":
                this.requireExactArgc(args, i + 2);
                return interp.eval(args[i+1].toString());
            case "elseif":
                this.requireMinArgc(args, i + 3);
                test = interp.objectify(interp.expr(interp, args[i+1].toString()));
                if (test.toBoolean())
                    return interp.eval(args[i+2].toString());
                i += 3;
                break;
            default:
                throw "Expected 'else' or 'elseif', got "+ args[i];
            }
        }
    });
    this.registerCommand("incr", function (interp, args) {
        this.requireArgcRange(args, 2, 3);
        var name = args[1];
        if (args.length == 2) var incr = 1;
        else var incr = interp.objectify(args[2]).toInteger();
        incr += interp.getVar(name).toInteger();
        return interp.setVar(name, new TclObject(incr, "INTEGER"));
    });
    this.registerSubCommand("info", "args", function (interp, args) {
        this.requireExactArgc(args, 2);
        var name = args[1].toString();
        if (!interp.procs[name]) throw '"'+name+'" isn\'t a procedure';
        return interp.getCommand(name).privdata[0];
    });
    this.registerSubCommand("info", "body", function (interp, args) {
        this.requireExactArgc(args, 2);
        var name = args[1].toString();
        if (!interp.procs[name]) throw '"'+name+'" isn\'t a procedure';
        return interp.getCommand(name).privdata[1];
    });
    this.registerSubCommand("info", "commands", function (interp, args) {
        return interp.mkList(interp.commands);
    });
    this.registerSubCommand("info", "globals", function (interp, args) {
        return interp.mkList(interp.callframe[0]);
    });
    this.registerSubCommand("info", "isensemble", function (interp, args) {
        this.requireExactArgc(args, 2);
        var name = args[1].toString();
        return (interp.getCommand(name).ensemble != null);
    });
    this.registerSubCommand("info", "patchlevel", function (interp, args) {
            return interp.patchlevel.toString();
    });
    this.registerSubCommand("info", "na", function (interp, args) {
            return process.execPath;
    });
    this.registerSubCommand("info", "procs", function (interp, args) {
        return interp.mkList(interp.procs);
    });
    this.registerSubCommand("info", "exists", function (interp, args) {
            var name = args[1];
            try {interp.getVar(name); return 1;} catch(e) {return 0;}
    });
    this.registerSubCommand("info", "script", function (interp, args) {
        return interp.script;
    });
    this.registerSubCommand("info", "vars", function (interp, args) {
        return interp.mkList(interp.callframe[interp.level]);
    });
    this.registerCommand("join", function (interp, args) {
            this.requireArgcRange(args, 2, 3);
            var lst = args[1].toList();
            var sep = " ";
            if(args.length == 3) sep = args[2].toString();
            return lst.join(sep);
        });
    this.registerCommand("jseval", function (interp, args) {
        return eval(args[1].toString());
    });
    this.registerCommand("lappend", function (interp, args) {
        this.requireMinArgc(args, 2);
        var vname = args[1].toString();
        if (interp.callframe[interp.level][vname] != null) {
            var list = interp.getVar(vname);
        } else var list = new TclObject([]);
        list.toList();
        for (var i = 2; i < args.length; i++) {
            list.content.push(interp.objectify(args[i]));
        }
        interp.setVar(vname, list);
        return list;
    });
    this.registerCommand("lindex", function (interp, args) {
        this.requireMinArgc(args, 3);
        var list = interp.objectify(args[1]);
        for (var i = 2; i < args.length; i++) {
            try {
                var index = list.listIndex(args[i]);
            } catch (e) {
                if (e == "Index out of bounds") return "";
                throw e;
            }
            list = list.content[index];
        }
        return interp.objectify(list);
    });
    this.registerCommand("list", function (interp, args) {
            args.shift();
            return new TclObject(args);
    });
    this.registerCommand("llength", function (interp, args) {
        this.requireExactArgc(args, 2);
        return args[1].toList().length;
    });
    this.registerCommand("lrange", function (interp, args) {
        this.requireExactArgc(args, 4);
        var list    = interp.objectify(args[1]);
        var start = list.listIndex(args[2]);
        var end     = list.listIndex(args[3])+1;
        try {
            return list.content.slice(start, end);
        } catch (e) {return [];}
    });
    this.registerCommand("lset", function (interp, args) {
        this.requireMinArgc(args, 4);
        var list = interp.getVar(args[1].toString());
        var elt = list;
        for (var i = 2; i < args.length-2; i++) {
            elt.toList();
            elt = interp.objectify(elt.content[elt.listIndex(args[i])]);
        }
        elt.toList();
        i = args.length - 2;
        elt.content[elt.listIndex(args[i])] = interp.objectify(args[i+1]);
        return list;
    });
    this.registerCommand("lsearch", function (interp, args) {
        this.requireExactArgc(args, 3);
        var lst = args[1].toList();
        for(i in lst) if(lst[i] == args[2].toString()) return i;
        return -1;
    });
    this.registerCommand("lsort", function (interp, args) {
        this.requireExactArgc(args, 2);
        return args[1].toList().sort();
    });
    this.registerCommand("pid", function (interp, args) {
            return process.pid;
    });
    this.registerCommand("puts", function (interp, args) {
            this.requireExactArgc(args, 2);
            puts(args[1].toString());
        });
    this.registerCommand("pwd", function (interp, args) {
            return process.cwd();
    });
    this.registerCommand("proc", function (interp, args) {
        this.requireExactArgc(args, 4);
        var name = args[1].toString();
        var argl = interp.parseList(args[2]);
        var body = args[3].toString();
        var priv = [argl, body];
        interp.commands[name] = new TclCommand(Tcl.Proc, priv);
        interp.procs[name] = true;
    });
    this.registerCommand("regexp", function (interp, args) {
        this.requireExactArgc(args, 3);
        var re    = new RegExp(args[1].toString());
        var str = args[2].toString();
        return (str.search(re) > -1? "1":"0");
    });
    this.registerCommand("regsub", function (interp, args) {
        this.requireExactArgc(args, 4);
        var re    = new RegExp(args[1].toString());
        var str = args[2].toString();
        var trg = args[3].toString();
        return (str.replace(re,trg));
    });
    this.registerCommand("rename", function (interp, args) {
        this.requireExactArgc(args, 3);
        interp.renameCommand(args[1], args[2]);
    });
    this.registerCommand("return", function (interp, args) {
        this.requireArgcRange(args, 1, 2);
        var r = args[1];
        interp.code = interp.RET;
        return r;
    });
    this.registerCommand("set", function (interp, args) {
        this.requireArgcRange(args, 2, 3);
        var name = args[1];
        if (args.length == 3) interp.setVar(name, args[2]);
        return interp.getVar(name);
    });
    this.registerCommand("source", function (interp, args) {
        this.requireExactArgc(args, 2);
        interp.script = args[1].toString();
        try {
            var data = fs.readFileSync(interp.script).toString();
        } catch(e) {
            throw 'couldn\' read file "'+interp.script+'": no such file or directory';}
            var res    = interp.eval(data);
            interp.script = "";
            return res;
        });
    this.registerCommand("split", function (interp, args) {
        this.requireArgcRange(args, 2, 3);
        var str = args[1].toString();
        var sep = " ";
        if (args.length == 3) sep = args[2].toString();
        return str.split(sep);
    });
    this.registerSubCommand("string", "equal", function (interp, args) {
        this.requireExactArgc(args, 3);
        return (args[1].toString() == args[2].toString())? "1": "0";
    });
    this.registerSubCommand("string", "index", function (interp, args) {
        this.requireExactArgc(args, 3);
        var s = args[1].toString();
        try {
            return s.charAt(args[1].stringIndex(args[2]));
        } catch (e) {return "";}
    });
    this.registerSubCommand("string", "length", function (interp, args) {
        this.requireExactArgc(args, 2);
        return args[1].toString().length;
    });
    this.registerSubCommand("string", "range", function (interp, args) {
        this.requireExactArgc(args, 4);
        var s = args[1];
        try {
            var b = s.stringIndex(args[2]);
            var e = s.stringIndex(args[3]);
            if (b > e) return "";
            return s.toString().substring(b, e + 1);
        } catch (e) {return "";}
    });
    this.registerSubCommand("string", "tolower", function (interp, args) {
        this.requireExactArgc(args, 2);
        return args[1].toString().toLowerCase();
    });
    this.registerSubCommand("string", "toupper", function (interp, args) {
        this.requireExactArgc(args, 2);
        return args[1].toString().toUpperCase();
    });
    this.registerSubCommand("string", "trim", function (interp, args) {
        this.requireExactArgc(args, 2);
        return args[1].toString().trim();
    });
    function sec_msec () {
        var t = new Date();
        return t.getSeconds()*1000 + t.getMilliseconds();
    }
    this.registerCommand("time", function (interp, args) {
        this.requireArgcRange(args, 2, 3);
        var body = args[1].toString();
        var n        = (args.length == 3)? args[2] : 1;
        var t0     = sec_msec();
        for(var i = 0; i < n; i++) interp.eval(body);
        return (sec_msec()-t0)*1000/n + " microseconds per iteration";
    });
    this.registerCommand("unset", function (interp, args) {
        this.requireMinArgc(args, 2);
        for (var i = 2; i < args.length; i++)
            interp.setVar(args[i], null);
    });
    this.registerCommand("uplevel",function (interp, args) {
        this.requireMinArgc(args, 3);
        var delta = args[1].toInteger();
        interp.level -= delta;
        for (var i = 2; i < args.length; i++) args[i] = args[i].toString();
        if (args.length == 3) var code = args[2];
        else                                    var code = args.slice(2).join(" ");
        var res = interp.eval(code);
        interp.level += delta;
        return res;
    });
    this.registerCommand("while", function (interp, args) {
        this.requireExactArgc(args, 3);
        var cond = args[1].toString();
        var body = args[2].toString();
        var res    = "";
        interp.inLoop = true;
        interp.code = interp.OK;
        while (true) {
            test = interp.objectify(interp.eval(cond));
            if (!test.toBoolean()) break;
            res = interp.eval(body);
            if(interp.code == interp.CNT) continue;
            if(interp.code != interp.OK)    break;
        }
        interp.inLoop = false;
        if(interp.code == interp.BRK || interp.code == interp.CNT)
            interp.code=interp.OK;
        return interp.objectify(res);
    });
    // native cmdname {function(interp, args) {...}}
    this.registerCommand("native", function (interp, args) {
        this.requireExactArgc(args, 3);
        var cmd = args[1].toList();
        var func = eval(args[2].toString());
        //alert("in: "+args[2].toString()+", func: "+ func);
        if (cmd.length == 1) {
            interp.registerCommand(cmd[0].toString(), func);
            return;
        }
        base = cmd[0].toString();
        cmd.shift();
        interp.registerSubCommand(base, cmd.join(" "), eval(args[2].toString()));
        return;
    });
    this.math = function (name, a, b) {
        switch (name) {
        case "+": return a + b;
        case "-": return a - b;
        case "*": return a * b;
        case "/": return a / b;
        case "%": return a % b;
        case "<": return a < b? "1":"0";
        case ">": return a > b? "1":"0";
        case "==": return a == b? "1":"0";
        case "!=": return a != b? "1":"0";
        default: throw "Unknown operator: '"+name+"'";
        }
    }
    var ops = ["+","-","*","/","%","<",">","==","!="];
    for (i in ops)
        this.registerCommand(ops[i],function (interp, args) {
            this.requireExactArgc(args, 3);
            var name = args[0].toString();
            var a = interp.objectify(args[1]);
            var b = interp.objectify(args[2]);
            if (name == '==') 
                return new TclObject(a.toString() == b.toString()?"1":"0","BOOL");
            if (name == '!=') 
                return new TclObject(a.toString() != b.toString()?"1":"0","BOOL");

            var x = a.getNumber();
            var y = b.getNumber();
            if (a.isInteger() && b.isInteger())
                return new TclObject(interp.math(name, x, y),"INTEGER");
            if (a.isReal() && b.isReal())
                return new TclObject(interp.math(name, x, y),"REAL");
            return new TclObject(interp.math(name, args[1].toString(), args[2].toString()));
        });
    this.mkList = function(x) {
        var list = [];
        for (var name in x) {list.push(name);}
        return list;
    }
    this.objectify = function (text) {
        if (text == null) text = "";
        else if (text instanceof TclObject) return text;
        return new TclObject(text);
    }
    this.parseString = function (text) {
        text = text.toString();
        switch (text.charAt(0)+text.substr(text.length-1)) {
        case "{}":
        case "\"\"":
            text = text.substr(1,text.length-2);
            break;
        }
        return this.objectify(text);
    }
    this.parseList = function (text) {
        text = text.toString();
        switch (text.charAt(0)+text.substr(text.length-1)) {
        case "{}":
        case "\"\"":
            text = [text];
            break;
        }
        return this.objectify(text);
    }
    this.call = function(args) {
        if(_step) console.log("this.call "+args);
        var func = this.getCommand(args[0].toString());
        var r = func.call(this,args);
        switch (this.code) {
        case this.OK: case this.RET: return r;
        case this.BRK:
            if (!this.inLoop) throw "Invoked break outside of a loop";
            break;
        case this.CNT:
            if (!this.inLoop) throw "Invoked continue outside of a loop";
            break;
        default: throw "Unknown return code " + this.code;
        }
        return r;
    }
}




var Tcl = {};
Tcl.isReal         = new RegExp("^[+\\-]?[0-9]+\\.[0-9]*([eE][+\\-]?[0-9]+)?$");
Tcl.isDecimal    = new RegExp("^[+\\-]?[1-9][0-9]*$");
Tcl.isHex            = new RegExp("^0x[0-9a-fA-F]+$");
Tcl.isOctal        = new RegExp("^[+\\-]?0[0-7]*$");
Tcl.isHexSeq     = new RegExp("[0-9a-fA-F]*");
Tcl.isOctalSeq = new RegExp("[0-7]*");
Tcl.isList         = new RegExp("[\\{\\} ]");
Tcl.isNested     = new RegExp("^\\{.*\\}$");
Tcl.getVar         = new RegExp("^[a-zA-Z0-9_]+", "g");

Tcl.Proc = function (interp, args) {
    var priv = this.privdata;
    interp.incrLevel();
    var arglist = priv[0].toList();
    var body        = priv[1];
    args.shift();
    for (var i = 0; i < arglist.length; i++) {
        var name = arglist[i].toString();
        if (i >= args.length) {
            if (name == "args") {
                interp.setVar("args", Tcl.empty);
                break;
            }
        }
        if (Tcl.isList.test(name)) {
            name = interp.parseString(name).toList();
            if (name[0] == "args") throw "'args' defaults to the empty string";
            if (i >= args.length)
                interp.setVar(name.shift(), interp.parseString(name.join(" ")));
            else interp.setVar(name[0], interp.objectify(args[i]));
        } else if (name == "args") {
            interp.setVar("args", new TclObject(args.slice(i, args.length)));
            break;
        }
        interp.setVar(name, interp.objectify(args[i]));
    }
    if (name == "args" && i+1 < arglist.length)
        throw "'args' should be the last argument";
    try {
        var r = interp.eval(body);
        interp.code = interp.OK;
        interp.decrLevel();
        return r;
    } catch (e) {
        interp.decrLevel();
        throw "Tcl.Proc exception "+e;
    }
}

/** Manage subcommands */
Tcl.EnsembleCommand = function (interp, args) {
    var sub    = args[1].toString();
    var main = args.shift().toString()+" "+sub;
    args[0] = main;
    var ens = this.ensemble;
    if (ens == null) {
        throw "Not an ensemble command: "+main;
    } else if( ens[sub] == null) {
        throw 'unknown or ambiguous subcommand "'+sub+'": must be '+
        ens["subcommands"].valueOf();
    }
    return ens[sub].call(interp, args);
}



/** Get subcommands of the current ensemble command. */
Tcl.InfoSubcommands = function(interp, args) {
    var r = [];
    for (var i in this.ensemble) r.push(i);
    return interp.objectify(r);
}




function TclObject(text) {
    this.TEXT    = 0;
    this.LIST    = 1;
    this.INTEGER = 2;
    this.REAL    = 3;
    this.BOOL    = 4;
    switch (arguments[0]) {
    case "LIST":
    case "INTEGER":
    case "REAL":
    case "BOOL":
        this.type = this[arguments[0]];
        break;
    default:
        this.type = this.TEXT;
        if (text instanceof Array) this.type = this.LIST;
        else text = text.toString();
        break;
    }
    this.content = text;
    this.stringIndex = function (i) {
        this.toString();
        return this.index(i, this.content.length);
    }
    this.listIndex = function (i) {
        this.toList();
        return this.index(i, this.content.length);
    }
    this.index = function (i, len) {
        var index = i.toString();
        if (index.substring(0,4) == "end-")
            index = len - parseInt(index.substring(4)) - 1;
        else if (index == "end") index = len-1;
        else index = parseInt(index);
        if (isNaN(index)) throw "Bad index "+i;
        if (index < 0 || index >= len) throw "Index out of bounds";
        return index;
    }
    this.isInteger = function () {return (this.type == this.INTEGER);}
    this.isReal        = function () {return (this.type == this.REAL);}
    this.getString = function (list, nested) {
        var res = [];
        for (var i in list) {
            res[i] = list[i].toString();
            if (Tcl.isList.test(res[i]) && !Tcl.isNested.test(res[i]))
                res[i] = "{" + res[i] + "}";
        }
        if (res.length == 1) return res[0];
        return res.join(" ");
    }
    this.toString = function () {
        if (this.type != this.TEXT) {
            if (this.type == this.LIST)
                this.content = this.getString(this.content);
            else this.content = this.content.toString();
            this.type = this.TEXT;
        }
        return this.content;
    }
    this.toList = function () {
        if (this.type != this.LIST) {
            if (this.type != this.TEXT)
                this.content[0] = this.content;
            else {

                var text = this.content;
                if (text.charAt(0) == "{" && text.charAt(text.length-1) == "}")
                    text = text.substring(1, text.length-1);
                if (text == "")
                    return [];

                var parser = new TclParser(text.toString());
                this.content = [];
                for(;;) {
                    parser.parseList();
                    this.content.push(new TclObject(parser.getText()));
                    if (parser.type == parser.EOL || parser.type == parser.ESC)
                        break;
                }
            }

            this.type = this.LIST;
        }
        return this.content;
    }
    this.toInteger = function () {
        if (this.type == this.INTEGER) return this.content;
        this.toString();
        if (this.content.match(Tcl.isHex))
            this.content = parseInt(this.content.substring(2), 16);
        else if (this.content.match(Tcl.isOctal))
            this.content = parseInt(this.content, 8);
        else if (this.content.match(Tcl.isDecimal))
            this.content = parseInt(this.content);
        else throw "Not an integer: '"+this.content+"'";
        if (isNaN(this.content)) throw "Not an integer: '"+this.content+"'";
        this.type = this.INTEGER;
        return this.content;
    }
    this.getFloat = function (text) {
        if (!text.toString().match(Tcl.isReal))
        throw "Not a real: '"+text+"'";
        return parseFloat(text);
    }
    this.toReal = function () {
        if (this.type == this.REAL)
        return this.content;
        this.toString();
        // parseFloat doesn't control all the string, so need to check it
        this.content = this.getFloat(this.content);
        if (isNaN(this.content)) throw "Not a real: '"+this.content+"'";
        this.type = this.REAL;
        return this.content;
    }
    this.getNumber = function () {
        try {
            return this.toInteger();
        } catch (e) {return this.toReal();}
    }
    this.toBoolean = function () {
        if (this.type == this.BOOL) return this.content;
        try {
            this.content = (this.toInteger() != 0);
        } catch (e) {
            var t = this.content;
            if (t instanceof Boolean) return t;
            switch (t.toString().toLowerCase()) {
            case "yes":case "true":case "on":
                this.content = true;
                break;
            case "false":case "off":case "no":
                this.content = false;
                break;
            default:
                throw "Boolean expected, got: '"+this.content+"'";
            }
        }
        this.type = this.BOOL;
        return this.content;
    }
}



function TclCommand(func, privdata) {
    if (func == null) throw "No such function";
    this.func = func;
    this.privdata = privdata;
    this.ensemble = arguments[2];

    this.call = function(interp, args) {
        var r = (this.func)(interp, args);
        r = interp.objectify(r);
        return r;
    }
    this.requireExactArgc = function (args, argc) {
        if (args.length != argc) {
            throw argc + " words expected, got "+args.length;
        }
    }
    this.requireMinArgc = function (args, argc) {
        if (args.length < argc) {
            throw argc + " words expected at least, got "+args.length;
        }
    }
    this.requireArgcRange = function (args, min, max) {
        if (args.length < min || args.length > max) {
            throw min + " to "+max + " words expected, got "+args.length;
        }
    }
}



function TclParser(text) {
    this.OK    = 0;
    this.SEP = 0;
    this.STR = 1;
    this.EOL = 2;
    this.EOF = 3;
    this.ESC = 4;
    this.CMD = 5;
    this.VAR = 6;
    this.text    = text;
    this.start = 0;
    this.end     = 0;
    this.insidequote = false;
    this.index = 0;
    this.len = text.length;
    this.type = this.EOL;
    this.cur = this.text.charAt(0);
    this.getText = function () {
        return this.text.substring(this.start,this.end+1);
    }
    this.parseString = function () {
        var newword = (this.type==this.SEP ||
                                     this.type == this.EOL || this.type == this.STR);
        if (newword && this.cur == "{") return this.parseBrace();
        else if (newword && this.cur == '"') {
            this.insidequote = true;
            this.feedchar();
        }
        this.start = this.index;
        while (true) {
            if (this.len == 0) {
                this.end = this.index-1;
                this.type = this.ESC;
                return this.OK;
            }
            if (this.cur == "\\") {
                if (this.len >= 2) this.feedSequence();
            }
            else if ("$[ \t\n\r;".indexOf(this.cur)>=0) {
                if ("$[".indexOf(this.cur)>=0 || !this.insidequote) {
                    this.end = this.index-1;
                    this.type = this.ESC;
                    return this.OK;
                }
            }
            else if (this.cur == '"' && this.insidequote) {
                this.end    = this.index-1;
                this.type = this.ESC;
                this.feedchar();
                this.insidequote = false;
                return this.OK;
            }
            this.feedchar();
        }
        return this.OK;
    }
    this.parseList = function () {
        level = 0;
        this.start = this.index;
        while (true) {
            if (this.len == 0) {
                this.end = this.index;
                this.type = this.EOL;
                return;
            }
            switch (this.cur) {
            case "\\":
                if (this.len >= 2) this.feedSequence();
                break;
            case " ": case "\t": case "\n": case "\r":
                if (level > 0) break;
                this.end    = this.index - 1;
                this.type = this.SEP;
                this.feedchar();
                return;
            case '{': level++; break;
            case '}': level--; break;
            }
            this.feedchar();
        }
        if (level != 0) throw "Not a list";
        this.end = this.index;
        return;
    }
    this.parseSep = function () {
        this.start = this.index;
        while (" \t\r\n".indexOf(this.cur)>=0) this.feedchar();
        this.end    = this.index - 1;
        this.type = this.SEP;
        return this.OK;
    }
    this.parseEol = function () {
        this.start = this.index;
        while(" \t\n\r;".indexOf(this.cur)>=0) this.feedchar();
        this.end    = this.index - 1;
        this.type = this.EOL;
        return this.OK;
    }
    this.parseCommand = function () {
        var level = 1;
        var blevel = 0;
        this.feedcharstart();
        while (true) {
            if (this.len == 0) break;
            if (this.cur == "[" && blevel == 0)
                level++;
            else if (this.cur == "]" && blevel == 0) {
                level--;
                if (level == 0) break;
            } else if (this.cur == "\\") {
                this.feedSequence();
            } else if (this.cur == "{") {
                blevel++;
            } else if (this.cur == "}") {
                if (blevel != 0) blevel--;
            }
            this.feedchar();
        }
        this.end    = this.index-1;
        this.type = this.CMD;
        if (this.cur == "]") this.feedchar();
        return this.OK;
    }
    this.parseVar = function () {
        this.feedcharstart();
        this.end = this.index
        + this.text.substring(this.index).match(Tcl.getVar).toString().length-1;
        if (this.end == this.index-1) {
            this.end = --this.index;
            this.type = this.STR;
        } else this.type = this.VAR;
        this.setPos(this.end+1);
        return this.OK;
    }
    this.parseBrace = function () {
        var level = 1;
        this.feedcharstart();
        while (true) {
            if (this.len > 1 && this.cur == "\\") {
                this.feedSequence();
            } else if (this.len == 0 || this.cur == "}") {
                level--;
                if (level == 0 || this.len == 0) {
                    this.end = this.index-1;
                    if (this.len > 0) this.feedchar();
                    this.type = this.STR;
                    return this.OK;
                }
            } else if (this.cur == "{") level++;
            this.feedchar();
        }
        return this.OK; // unreached
    }
    this.parseComment = function () {
        while (this.cur != "\n" && this.cur != "\r") this.feedchar();
    }
    this.getToken = function () {
        while (true) {
            if (this.len == 0) {
                if (this.type == this.EOL) this.type = this.EOF;
                if (this.type != this.EOF) this.type = this.EOL;
                return this.OK;
            }
            switch (this.cur) {
                case ' ':
                case '\t':
                if (this.insidequote) return this.parseString();
                return this.parseSep();
                case '\n':
                case '\r':
                case ';':
                if (this.insidequote) return this.parseString();
                return this.parseEol();
                case '[': return this.parseCommand();
                case '$': return this.parseVar();
            }
            if (this.cur == "#" && this.type == this.EOL) {
                this.parseComment();
                continue;
            }
            return this.parseString();
        }
        return this.OK; // unreached
    }
    this.feedSequence = function () {
        if (this.cur != "\\") throw "Invalid escape sequence";
        var cur = this.steal(1);
        var specials = {};
        specials.a = "\a";
        specials.b = "\b";
        specials.f = "\f";
        specials.n = "\n";
        specials.r = "\r";
        specials.t = "\t";
        specials.v = "\v";
        switch (cur) {
        case 'u':
            var hex = this.steal(4);
            if (hex != Tcl.isHexSeq.exec(hex))
                throw "Invalid unicode escape sequence: "+hex;
            cur = String.fromCharCode(parseInt(hex,16));
            break;
        case 'x':
            var hex = this.steal(2);
            if (hex != Tcl.isHexSeq.exec(hex))
                throw "Invalid unicode escape sequence: "+hex;
            cur = String.fromCharCode(parseInt(hex,16));
            break;
        case "a": case "b": case "f": case "n":
        case "r": case "t": case "v":
            cur = specials[cur];
            break;
        default:
            if ("0123456789".indexOf(cur) >= 0) {
                cur = cur + this.steal(2);
                if (cur != Tcl.isOctalSeq.exec(cur))
                    throw "Invalid octal escape sequence: "+cur;
                cur = String.fromCharCode(parseInt(cur, 8));
            }
            break;
        }
        this.text[index] = cur;
        this.feedchar();
    }
    this.steal = function (n) {
        var tail = this.text.substring(this.index+1);
        var word = tail.substr(0, n);
        this.text = this.text.substring(0, this.index-1) + tail.substring(n);
        return word;
    }
    this.feedcharstart = function () {
        this.feedchar();
        this.start = this.index;
    }
    this.setPos = function (index) {
        var d = index-this.index;
        this.index = index;
        this.len -= d;
        this.cur = this.text.charAt(this.index);
    }
    this.feedchar = function () {
        this.index++;
        this.len--;
        if (this.len < 0)
            throw "End of file reached";
        this.cur = this.text.charAt(this.index);
    }

}


//------------------------------------- main Read-Eval-Print loop
var itp = new TclInterp();
var res;
process.argv.slice(2).forEach(function(cmd,index,array) {
    itp.eval(cmd);
});
var readline = require('readline');
var rl = readline.createInterface(process.stdin, process.stdout);
rl.setPrompt('% ');
rl.prompt();
rl.on('line', function(line) {
    try {
        res = itp.eval(line.trim());
    } catch(e) {res = e;}
    if(res != null && res.toString() != "" && res.toString().length) 
        console.log(res.toString());
    rl.prompt();
}).on('close',function() {
    process.exit(0);
});

Comments

Posted by suchenwi at Sun Nov 10 11:17:19 GMT 2013 [text] [code]

Thank you! The test suite passes indeed: % suchenwi@suchenwi-NC10:~$ nodejs tcl053.js "source test_tcljs.tcl" ------------------------ test_tcljs.tcl total 63 tests, passed 63, failed 0