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.

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>