RuScript, Ruby Intérprete en JavaScript (1) El Proyecto

Desde hace unos meses estoy trabajando en mi tiempo libre en:

https://github.com/ajlopez/RuScript

Y escribiendo todo siguiendo el flujo de TDD (Test-Driven Development) (ver la historia de commits)

La idea es tener un intérprete de Ruby escrito en JavaScript, que pueda correr en el browser o en el server (en este caso con Node.js). No estoy implementando un compilador a JavaScript, porque quiero seguir “baby steps”, y porque veo que la semántica de Ruby puede ser bastante distinta de la de JavaScript en algunos casos. La resolución de métodos a invocar, o ver si una variable es de instancia, de clase o local a una función, son temas que difieren entre los dos lenguajes. Así que me he decantado por escribir un intérprete.

Estoy aplicando “dog fooding”, porque el parser está construido basado en:

https://github.com/ajlopez/SimpleGrammar

Que también estoy aplicando en otros proyectos, como RustScript. (Hay implementación de SimpleGrammar en C#, ver GrammGen). Fragmento de las reglas definidas para el parser de RuScript:

// Expression Level 1
get('Expression1').generate('Expression'),
get('Expression1', 'Plus', 'Expression0')
    .generate('Expression1', function (values) { return new AddExpression(values[0], values[2]); }),
get('Expression1', 'Minus', 'Expression0')
    .generate('Expression1', function (values) { return new SubtractExpression(values[0], values[2]); }),

// Expression Level 0
get('Expression0').generate('Expression1'),
get('Expression0', 'Multiply', 'Term')
    .generate('Expression0', function (values) { return new MultiplyExpression(values[0], values[2]); }),
get('Expression0', 'Divide', 'Term')
    .generate('Expression0', function (values) { return new DivideExpression(values[0], values[2]); }),

// Term
get('Term').generate('Expression0'),
get('Term', 'LeftBracket', 'ExpressionList', 'RightBracket')
    .generate('Term', function (values) { return new IndexedExpression(values[0], values[2]); }),
get('Integer').generate('Term'),
get('@@', 'Name').generate('ClassVariable', 
    function (values) { return new ClassVariableExpression(values[1].getName()); }),
get('@', 'Name').generate('InstanceVariable', 
    function (values) { return new InstanceVariableExpression(values[1].getName()); }),
get('Name').generate('Term'),
get('InstanceVariable').generate('Term'),
get('ClassVariable').generate('Term'),
get('String').generate('Term'),
get('Array').generate('Term'),
get('LeftParenthesis', 'Expression', 'RightParenthesis')
    .generate('Expression0', function (values) { return values[1]; }),
get('LeftBracket', 'RightBracket')
    .generate('Array', function (values) { return new ArrayExpression([]); }),
get('LeftBracket', 'ExpressionList', 'RightBracket')
    .generate('Array', function (values) { return new ArrayExpression(values[1]); }),

Por cada elemento que voy descubriendo, voy armando una expresión, que puede evaluarse dado un contexto (un contexto indica cuales son las variables accesibles en determinado momento, y sus valores respectivos). Ejemplo:

function NameExpression(name) {
    this.evaluate = function (context) {
        var value = context.getValue(name);
        
        if (typeof value == 'function')
            return value();
        
        return value;
    };
    
    this.getName = function () { return name; };
    
    this.setValue = function (context, value) {
        context.setLocalValue(name, value);
    }
}

function InstanceVariableExpression(name) {
    this.evaluate = function (context) {
        if (!context.$self.$vars)
            return null;
            
        return context.$self.$vars[name];
    };
    
    this.getName = function () { return name; };
    
    this.setValue = function (context, value) {
        if (!context.$self.$vars)
            context.$self.$vars = { };
            
        context.$self.$vars[name] = value;
    }
}

function ClassVariableExpression(name) {
    this.evaluate = function (context) {
        if (!context.$self.$class.$vars)
            return null;
            
        return context.$self.$class.$vars[name];
    };
    
    this.getName = function () { return name; };
    
    this.setValue = function (context, value) {
        if (!context.$self.$class.$vars)
            context.$self.$class.$vars = { };
            
        context.$self.$class.$vars[name] = value;
    }
}

function ConstantExpression(value) {
    this.evaluate = function () {
        return value;
    };
}

Tengo acceso a JavaScript desde Ruby, por ejemplo, un test:

exports['Evaluate JavaScript global name'] = function (test) {
    global.myglobal = 42;
    var context = contexts.createContext();
    var parser = parsers.createParser("js.myglobal");
    //parser.options({ log: true });
    var expr = parser.parse("Expression");
    var result = expr.value.evaluate(context);
    test.equal(result, 42);
}

El “namespace” js es el que posibilita acceder a las variables definidas en JavaScript y a todo su ecosistema, ya sea en el browser o en el servidor Node.

Espero poder presentar mi avance en la RubyConf 2014 de Argentina.

Próximos pasos: acceder a los “require” de Node.js, ejemplos de sitios web usando Node.js/Express, ejemplos de consola, ejemplos en el browser, etc.

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

This entry was posted in Interprete, JavaScript, Node, NodeJs, Proy, Proyectos Open Source, Ruby, RuScript, Test-Driven Development. Bookmark the permalink.