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