Escribiendo un Intérprete en .NET (Parte 7)

Vuelta al ruedo! Un nuevo post en esta serie. En los anteriores posts, estuve escribiendo un intérprete en .NET, usando TDD (Test-Driven Development). Ya tengo un parser, un lexer, algunas expresiones y solamente un comando. Es hora de agregar un nuevo comando a mi intérprete. El nuevo comando es IfCommand:

Pueden bajarse el código fuente desde Interpreter07.zip.

La clase IfCommand implementa la interfaz ICommand. Fue armada usando TDD: escribiendo los test, haciendo que compilen, primero en rojo, luego en verde, refactor. Mis primeros tests:

        [TestMethod]
        public void CreateIfCommand()
        {
            IExpr ession condition = new ConstantExpr ession(0);
            ICommand thenCommand = new SetCommand("a", new ConstantExpr ession(1));
            ICommand elseCommand = new SetCommand("b", new ConstantExpr ession(2));
            IfCommand command = new IfCommand(condition, thenCommand, elseCommand);
            Assert.AreEqual(condition, command.Condition);
            Assert.AreEqual(thenCommand, command.ThenCommand);
            Assert.AreEqual(elseCommand, command.ElseCommand);
        }
        [TestMethod]
        public void EvaluateIfCommandWithZeroAsCondition()
        {
            IExpr ession condition = new ConstantExpr ession(0);
            ICommand thenCommand = new SetCommand("a", new ConstantExpr ession(1));
            ICommand elseCommand = new SetCommand("b", new ConstantExpr ession(2));
            IfCommand command = new IfCommand(condition, thenCommand, elseCommand);
            BindingEnvironment environment = new BindingEnvironment();
            command.Execute(environment);
            Assert.IsNull(environment.GetValue("a"));
            Assert.AreEqual(2, environment.GetValue("b"));
        }


La implementación de IfCommand:



    public class IfCommand : ICommand
    {
        private IExpr ession condition;
        private ICommand thenCommand;
        private ICommand elseCommand;
        public IfCommand(IExpr ession condition, ICommand thenCommand)
            : this(condition, thenCommand, null)
        {
        }
        public IfCommand(IExpr ession condition, ICommand thenCommand, ICommand elseCommand)
        {
            this.condition = condition;
            this.thenCommand = thenCommand;
            this.elseCommand = elseCommand;
        }
        public IExpr ession Condition { get { return this.condition; } }
        public ICommand ThenCommand { get { return this.thenCommand; } }
        public ICommand ElseCommand { get { return this.elseCommand; } }
        public void Execute(BindingEnvironment environment)
        {
            object result = this.condition.Evaluate(environment);
            bool cond = !IsFalse(result);
            if (cond)
                this.thenCommand.Execute(environment);
            else if (this.elseCommand != null)
                this.elseCommand.Execute(environment);
        }
        private static bool IsFalse(object obj)
        {
            if (obj == null)
                return true;
            if (obj is bool)
                return !(bool)obj;
            if (obj is int)
                return (int)obj == 0;
            if (obj is string)
                return string.IsNullOrEmpty((string)obj);
            if (obj is long)
                return (long)obj == 0;
            if (obj is short)
                return (short)obj == 0;
            if (obj is double)
                return (double)obj == 0;
            if (obj is float)
                return (float)obj == 0;
            return false;
        }
    }


IfCommand evalúa una expresión, que retorne un objeto. Este objeto podría no ser un booleano. Tomé la decisión de evaluar null, 0, string vacío como false (algo parecido a lo que hace PHP). El único lugar donde necesito evaluar un objeto cualquiera como verdadero o false es, ahora, en este método  IfCommand.Execute. Así, esta lógica de evaluación está ahora en un método privado. Planeo refactorearlo, moverlo a otra clase, en cuanto lo necesite desde otros lugares, como cuando implemente el comando WhileCommand y otras expresiones.



Después de escribir IfCommand, necesitaba parsear comandos, no sólo expresiones. No tenía un método .ParseCommand() en la clase Parser. Mis primeros tests (hay más en el código):



        [TestMethod]
        public void ParseAndEvaluateSimpleIfCommand()
        {
            Parser parser = new Parser("if (a) b=1; else b=2;");
            ICommand command = parser.ParseCommand();
            Assert.IsNotNull(command);
            Assert.IsInstanceOfType(command, typeof(IfCommand));
            BindingEnvironment environment = new BindingEnvironment();
            command.Execute(environment);
            Assert.AreEqual(2, environment.GetValue("b"));            
        }


Luego, implementé nuevos métodos en Parser:





Parser.ParseCommand() tienen una implementación ingenua. Solamente dos clases de comandos son soportados: comandos if, y comandos de seteo de variables:



        public ICommand ParseCommand()
        {
            Token token = this.NextToken();
            if (token == null)
                return null;
            if (token.TokenType == TokenType.Name && token.Value.Equals("if"))
                return ParseIfCommand();
            this.PushToken(token);
            return ParseSetCommand();
        }
        private ICommand ParseSetCommand()
        {
            string name = this.ParseName();
            this.ParseToken(TokenType.Operator, "=");
            IExpr ession expr = this.ParseExpr ession();
            this.ParseToken(TokenType.Separator, ";");
            return new SetCommand(name, expr);
        }
        private ICommand ParseIfCommand()
        {
            IExpr ession condition;
            ICommand thencmd;
            ICommand elsecmd;
            this.ParseToken(TokenType.Separator, "(");
            condition = this.ParseExpr ession();
            this.ParseToken(TokenType.Separator, ")");
            thencmd = this.ParseCommand();
            Token token = this.NextToken();
            if (token != null && token.TokenType == TokenType.Name && token.Value.Equals("else")) 
            {
                elsecmd = this.ParseCommand();
                return new IfCommand(condition, thencmd, elsecmd);
            }
            if (token != null)
                this.PushToken(token);
            return new IfCommand(condition, thencmd);
        }


Todos los tests quedaron en verde:





Buen code coverage:





Próximos pasos: agregar más comandos (while, for, etc…), declaraciones de funciones, manejo de números reales, etc.



Nos leemos!



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

This entry was posted in 11699, 1389, 8870. Bookmark the permalink.

One Response to Escribiendo un Intérprete en .NET (Parte 7)

  1. Jorge says:

    Angel,

    Excelente tu blog y estos posts son fantásticos!. Yo también hago a mano interpretes que aplico en mis proyectos, algunos evalúan a demanda (a la shunting-yard) y otros generan el árbol de evaluación. Personalmente, no he visto la clara necesidad de utilizar una herramienta de gramáticas, quisas puedas comentar tu opinión al respecto.

    Felicitaciones, espero leer pronto la parte 8!

    Saludos,
    JM

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>