Escribiendo un intérprete en .NET (Parte 4)

Published on Author lopezLeave a comment

En este post, agrego un analizador léxico, un lexer, para procesar texto y separar el código en tokens, las “palabras” de nuestra entrada.

La nueva solución:

Pueden bajar el código de InterpreterStep04.zip. Hice refactor de la versión anterior: ahora la class library Interpreter tiene tres directorios y namespaces: Commands, Expressions, Compiler.

El nuevo código es:

El TokenType es una enumeración:

    public enum TokenType
    {
        Name,
        String,
        Integer,
        Operator,
        Separator
    }

Token representa una “palabra” de nuestro lenguaje:

    public class Token
    {
        public TokenType TokenType { get; private set;  }
        public object Value { get; private set;  }
        public Token(TokenType type, object value)
        {
            this.TokenType = type;
            this.Value = value;
        }
    }

Value contiene el token detectado: su valor entero si era un entero, o su texto, si era un nombre o separador u operador.

Lexer está encargado de detectar el próximo token desde un TextReader o desde un string:

       public Lexer(TextReader reader)
        {
            this.reader = reader;
        }
        public Lexer(string text)
            : this(new StringReader(text))
        {
        }
        public Token NextToken()
        {
            int ch;
            for (ch = this.NextChar(); ch != -1 && char.IsWhiteSpace((char)ch); ch = this.NextChar())
                ;
            if (ch == -1)
                return null;
//...
        }

Lexer fue escrito en “baby-steps” (pasos de bebé), siguiendo la evolución de los test (recuerden, estoy usando TDD). Escribí un tests, y modifiqué el código para que pasara a verde, y así. Tests como:

        [TestMethod]
        public void ProcessNameWithWhitespaces()
        {
            Lexer lexer = new Lexer("  one  ");
            Token token = lexer.NextToken();
            Assert.IsNotNull(token);
            Assert.AreEqual(TokenType.Name, token.TokenType);
            Assert.AreEqual("one", token.Value);
            Assert.IsNull(lexer.NextToken());
        }
        [TestMethod]
        public void ProcessTwoNames()
        {
            Lexer lexer = new Lexer("one two");
            Token token = lexer.NextToken();
            Assert.IsNotNull(token);
            Assert.AreEqual(TokenType.Name, token.TokenType);
            Assert.AreEqual("one", token.Value);
            token = lexer.NextToken();
            Assert.IsNotNull(token);
            Assert.AreEqual(TokenType.Name, token.TokenType);
            Assert.AreEqual("two", token.Value);
            Assert.IsNull(lexer.NextToken());
        }
        [TestMethod]
        public void ProcessNameAndSeparator()
        {
            Lexer lexer = new Lexer("one;");
            Token token = lexer.NextToken();
            Assert.IsNotNull(token);
            Assert.AreEqual(TokenType.Name, token.TokenType);
            Assert.AreEqual("one", token.Value);
            token = lexer.NextToken();
            Assert.IsNotNull(token);
            Assert.AreEqual(TokenType.Separator, token.TokenType);
            Assert.AreEqual(";", token.Value);
            Assert.IsNull(lexer.NextToken());
        }

La versión actual (en este paso) del lexer solo reconoce nombres, algunos separadores como “;” y algún operador como “=”. No procesa strings delimitados, números reales u otros separadores y operadores. Mi idea es ir agregando más capacidades a medida que escriba más tests.

Todos los tests de la solución en verde:

Buen code coverage:

Próximos pasos: escribir un parser que reconozca expresiones, luego que procese comandos, y agregar comandos como if y for, así como definición de funciones. Esto es un intérprete, pero en una nueve serie de posts, podría tratar de compilarlo a código .NET nativo, usando Reflection.Emit or CodeDom.

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 *