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