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

Published on Author lopezLeave a comment

Anterior Post
Primer Post de la Serie

Esta vez, quiero agregar una pieza que falta: un composite command. El intérprete necesita una manera de ejecutar una lista de comandos, en cualquier lugar donde haya un comando: en un if/then, en el if/else, en el while, etc… Primero, escribí un test (esta version de abajo no es la versión inicial, es la final actual):

        [TestMethod]
        public void CreateAndEvaluateCompositeCommand()
        {
            BindingEnvironment environment = new BindingEnvironment();
            environment.SetValue("a", 0);
            ICommand add1 = this.MakeAddCommand("a", "a", 1);
            ICommand add2 = this.MakeAddCommand("a", "a", 2);
            CompositeCommand cmd = new CompositeCommand(new ICommand[] { add1, add2 });
            Assert.IsNotNull(cmd.Commands);
            Assert.AreEqual(2, cmd.Commands.Count);
            Assert.AreEqual(add1, cmd.Commands.First());
            Assert.AreEqual(add2, cmd.Commands.Skip(1).First());
            cmd.Execute(environment);
            Assert.AreEqual(3, environment.GetValue("a"));
        }
        private ICommand MakeAddCommand(string target, string source, int number)
        {
            IExpression add = new BinaryArithmeticExpression(new VariableExpression(source), new ConstantExpression(number), ArithmeticOperator.Add);
            ICommand set = new SetCommand(target, add);
            return set;
        }

Hecho esto, agregué un nuevo ICommand, el CompositeCommand, refinado hasta que pase el test:

Necesitaba ahora soportar el parsing de un comando compuesto. Decidí usar la sintaxis de C# y Javascript: el comando compuesto estaría delimitado por { y }. Me dí cuenta que no tenía soporte de esos caracteres en el Lexir, así que escribí el test:

        [TestMethod]
        public void ProcessCurlyBrackets()
        {
            Lexer lexer = new Lexer("{}");
            Token token = lexer.NextToken();
            Assert.IsNotNull(token);
            Assert.AreEqual(TokenType.Separator, token.TokenType);
            Assert.AreEqual("{", token.Value);
            token = lexer.NextToken();
            Assert.IsNotNull(token);
            Assert.AreEqual(TokenType.Separator, token.TokenType);
            Assert.AreEqual("}", token.Value);
            Assert.IsNull(lexer.NextToken());
        }

Dió en rojo. Modifiqué el código de Lexer, agregando los nuevos separadores:

private static string[] separators = new string[] { ";", "(", ")", "{", "}" };

Próximo paso: agregar soporte de comandos compuestos en el Parser. Como es usual, un test para ejercitar al Parser:

        [TestMethod]
        public void ParseCompositeCommand()
        {
            Parser parser = new Parser("{ a = a+1; a = a+2; }");
            ICommand command = parser.ParseCommand();
            Assert.IsNotNull(command);
            Assert.IsInstanceOfType(command, typeof(CompositeCommand));
            CompositeCommand ccmd = (CompositeCommand)command;
            Assert.IsNotNull(ccmd.Commands);
            Assert.AreEqual(2, ccmd.Commands.Count);
            Assert.IsInstanceOfType(ccmd.Commands.First(), typeof(SetCommand));
            Assert.IsInstanceOfType(ccmd.Commands.Skip(1).First(), typeof(SetCommand));
        }

Entonces, agregué el código en Parser.ParseCommand (listado parcial):

    if (token.TokenType == TokenType.Separator && token.Value.Equals("{"))
        return this.ParseCompositeCommand();

El nuevo método:

    private ICommand ParseCompositeCommand()
    {
        IList<ICommand> commands = new List<ICommand>();
        Token token = this.NextToken();
        while (token != null && !(token.TokenType == TokenType.Separator 
             && token.Value.Equals("}")))
        {
            this.PushToken(token);
            commands.Add(this.ParseCommand());
            token = this.NextToken();
        }
        if (token == null)
            throw new ParserException("Expected '}'");
        return new CompositeCommand(commands);
    }

Test de un comando no cerrado:

        [TestMethod]
        [ExpectedException(typeof(ParserException))]
        public void RaiseIfCompositeCommandNotClosed()
        {
            Parser parser = new Parser("{ a = a+1; a = a+2; ");
            parser.ParseCommand();
        }

Y la evaluación de comandos compuestos simples, ahora en comandos if y while (no hizo falta cambiar la implementación de esos comandos: ellos pueden procesar cualquier ICommand, y nuestro nuevo CompositeCommand implementa esa interface):

        [TestMethod]
        public void ParseAndEvaluateSimpleIfWithCompositeCommand()
        {
            Parser parser = new Parser("if (1) { a = a+1; a = a+2; }");
            ICommand command = parser.ParseCommand();
            Assert.IsNotNull(command);
            Assert.IsInstanceOfType(command, typeof(IfCommand));
            IfCommand ifcmd = (IfCommand)command;
            Assert.IsInstanceOfType(ifcmd.ThenCommand, typeof(CompositeCommand));
            BindingEnvironment environment = new BindingEnvironment();
            environment.SetValue("a", 2);
            command.Execute(environment);
            Assert.AreEqual(5, environment.GetValue("a"));
        }
        [TestMethod]
        public void ParseAndEvaluateSimpleWhileWithCompositeCommand()
        {
            Parser parser = new Parser("while (a) { a = a-1; b=b+1; }");
            ICommand command = parser.ParseCommand();
            Assert.IsNotNull(command);
            Assert.IsInstanceOfType(command, typeof(WhileCommand));
            WhileCommand whilecmd = (WhileCommand)command;
            Assert.IsInstanceOfType(whilecmd.Command, typeof(CompositeCommand));
            BindingEnvironment environment = new BindingEnvironment();
            environment.SetValue("a", 2);
            environment.SetValue("b", 0);
            command.Execute(environment);
            Assert.AreEqual(0, environment.GetValue("a"));
            Assert.AreEqual(2, environment.GetValue("b"));
        }

El código mostrado es la versión ACTUAL. Durante el desarrollo, y usando TDD, escribí el código de a pasos cortos,  y también apliqué refactoring. Pero la lección es: usar los tests para guiar nuestro desarrollo.

Todos los tests en verde:

Buen code coverage:

Pueden bajar la versión actual desde from InterpreterStep09.zip. Si quieren ver todos los steps, sigo enviando mi código a trunk/Interpreter en mi Google Code Project AjCodeKatas.

Próximos pasos: comando foreach, comando for, declaración de funciones, etc…

Nos leemos!

Angel “Java” Lopez

http://www.ajlopez.com

http://twitter.com/ajlopez

Leave a Reply

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