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