Sie sind auf Seite 1von 8

Cómo crear un

compilador de
expresiones en
.NET
Alberto Población
Cómo crear un compilador de expresiones en .NET
Nivel: Intermedio-Avanzado
por Alberto Población

En un artículo anterior Alberto nos daba explicaciones acerca de cómo escribir un intérprete
capaz de tomar en tiempo de ejecución una expresión del tipo “5*x-3*(x+1)” y evaluarla para
calcular el resultado.

En aquel momento comentábamos que el primer paso consistía en elaborar un diagrama


sintáctico como el de la figura, y luego escribir código para recorrer las distintas ramas del
gráfico interpretando sobre la marcha las operaciones encontradas:

Una limitación que tiene esa forma de operar –y que ya se comentaba en aquel texto- es que
resulta lenta. Si es necesario evaluar la misma función miles de veces, por ejemplo, para
dibujar una gráfica punto por punto, entonces se repite miles de veces el seguimiento del
diagrama y la interpretación de las operaciones.
Una posible solución consiste en “compilar” la expresión. Se recorre el diagrama, igual que en
el caso de la interpretación, pero cada vez que hay que realizar una de las operaciones de
cálculo, lo que se hace, en lugar de operar sobre la marcha, es generar código ejecutable que
implemente esa operación.

Terminado el diagrama, y generado todo el código, las sucesivas ejecuciones se realizan


mediante llamadas a dicho ejecutable, esta vez a toda velocidad.

Para este fin, vamos a utilizar las clases disponibles en el espacio de nombres
System.Reflection.Emit. Aunque es posible generar un ensamblado dinámico o salvarlo a
disco, para la aplicación concreta que tenemos entre manos es preferible utilizar lo que se
denomina un DynamicMethod, que viene a ser un método generado dinámicamente en
memoria y que podemos ejecutar sobre la marcha. La ventaja del método dinámico es que el
Garbage Collector es capaz de liberarlo cuando ya no se necesite, mientras que si se genera
un ensamblado, no se descarga de memoria mientras no se descargue el dominio de aplicación
completo.

Los pasos necesarios para generar el método dinámico son estos:

1. Definir un delegado que sirva para apuntar a un método del tipo que queremos
generar. En nuestro ejemplo utilizaremos el tipo DelegadoParaEvaluar que sirve
para apuntar a métodos que reciban como argumento un double y devuelvan como
resultado otro double.

2. Crear una instancia de la clase DynamicMethod, pasándole en el constructor el


nombre del método y los Type de los argumentos y el resultado.

3. Llamar al método GetILGenerator del DynamicMethod para obtener una instancia


del generador de código IL (“Intermediate Language”).

4. Llamar al método Emit del generador cuantas veces sea necesario para ir escribiendo
el código de nuestro método dinámico. Esta es la operación que se va realizando
repetidamente cada vez que en el diagrama sintáctico se ve la necesidad de realizar
una operación. Los argumentos de Emit indican cuál es la operación concreta que se
va a realizar.

5. Llamar al método CreateDelegate del DynamicMethod para obtener un delegado,


que el que finalmente se usa para ejecutar el código generado.

En el ejemplo de código que se adjunta como Listado 1 a continuación, hemos reproducido en


su mayor parte el analizador sintáctico que ya introdujimos en el artículo sobre el intérprete,
pero hemos sustituido las rutinas que ejecutaban las operaciones matemáticas
(OperacionSuma, OperacionResta, etc.) por otras equivalentes que generan código
mediante Reflection.Emit.

El Listado 2 muestra la forma de realizar la llamada para evaluar una expresión.


Listado 1
using System.Reflection.Emit;
...
public class Compilador
{
private string expresion;
private int posicion;
private Simbolos ultimoSimbolo;
private double ultimaConstante;

public delegate double DelegadoParaEvaluar(double valor);


public DelegadoParaEvaluar Evaluar;
private ILGenerator il;

public Compilador(string expresion)


{
this.expresion = expresion;
this.posicion = 0;

Type tipoADevolver = typeof(double);


Type[] tiposDeLosParametros = new Type[] { typeof(double) };
DynamicMethod evaluador = new DynamicMethod(
"Evaluador", tipoADevolver, tiposDeLosParametros);
il = evaluador.GetILGenerator();

ultimoSimbolo = ObtenerSiguienteSímbolo();
Expresion();
il.Emit(OpCodes.Ret);

Evaluar = (DelegadoParaEvaluar)evaluador.CreateDelegate(
typeof(DelegadoParaEvaluar));
}

private Simbolos ObtenerSiguienteSímbolo()


{
char c;
do
{
if (posicion >= expresion.Length)
return Simbolos.FinDeLaExpresión;
c = expresion[posicion++];
}
while (c == ' ');
switch (c)
{
case '+': return Simbolos.Suma;
case '-': return Simbolos.Resta;
case '*': return Simbolos.Multiplicación;
case '/': return Simbolos.División;
case '(': return Simbolos.AbrirParéntesis;
case ')': return Simbolos.CerrarParéntesis;
case 'x':
case 'X': return Simbolos.Variable;
}
Regex re = new Regex(@"^\d+([,\.]\d+)?");
string exp = expresion.Substring(posicion - 1);
if (re.IsMatch(exp))
{
Match m = re.Match(exp);
string s = m.Value;
posicion += m.Length - 1;
ultimaConstante = double.Parse(s.Replace(".", ","));
return Simbolos.Constante;
}
throw new Exception(
"Simbolo no reconocido en la posición " + posicion);
}
Continúa...
private void Expresion()
{
Termino();
while (true)
{
switch (ultimoSimbolo)
{
case Simbolos.Suma:
ultimoSimbolo = ObtenerSiguienteSímbolo();
Termino();
OperacionSuma();
break;
case Simbolos.Resta:
ultimoSimbolo = ObtenerSiguienteSímbolo();
Termino();
OperacionResta();
break;
default: return;
}
}
}

private void Termino()


{
Factor();
while (true)
{
switch (ultimoSimbolo)
{
case Simbolos.Multiplicación:
ultimoSimbolo = ObtenerSiguienteSímbolo();
Factor();
OperacionMultiplicacion();
break;
case Simbolos.División:
ultimoSimbolo = ObtenerSiguienteSímbolo();
Factor();
OperacionDivision();
break;
default: return;
}
}
}

private void Factor()


{
if (ultimoSimbolo == Simbolos.AbrirParéntesis)
{
ultimoSimbolo = ObtenerSiguienteSímbolo();
Expresion();
if (ultimoSimbolo != Simbolos.CerrarParéntesis)
throw new Exception("Falta ')'");
ultimoSimbolo = ObtenerSiguienteSímbolo();
}
else if (ultimoSimbolo == Simbolos.Constante)
{
OperacionConstante();
ultimoSimbolo = ObtenerSiguienteSímbolo();
}
else if (ultimoSimbolo == Simbolos.Variable)
{
OperacionVariable();
ultimoSimbolo = ObtenerSiguienteSímbolo();
}
else
throw new Exception("Factor");
} Continúa...
private void OperacionConstante()
{
il.Emit(OpCodes.Ldc_R8, ultimaConstante);
}
private void OperacionVariable()
{
il.Emit(OpCodes.Ldarg_0);
}
private void OperacionSuma()
{
il.Emit(OpCodes.Add);
}
private void OperacionResta()
{
il.Emit(OpCodes.Sub);
}
private void OperacionMultiplicacion()
{
il.Emit(OpCodes.Mul);
}
private void OperacionDivision()
{
il.Emit(OpCodes.Div);
}
}

enum Simbolos
{
Ninguno,
Suma, Resta, Multiplicación, División,
AbrirParéntesis, CerrarParéntesis,
Constante, Variable,
FinDeLaExpresión
}

Listado 2
//Se ejecuta una vez, creando el método dinámico
string expresion = "5*x-3*(x+1) ";
Compilador comp = new Compilador(expresion);

//Se ejecuta cuantas veces sea necesario


double x = ...;
double resultado = comp.Evaluar(x);

Al igual que ya mencionamos cuando hablábamos del intérprete, aplicando procedimientos


similares a los que hemos visto aquí se pueden procesar expresiones tan complejas como
deseemos, pudiendo llegar incluso a construir un compilador para un lenguaje de
programación completo.
Acerca del autor
Alberto Población lleva 27 años desarrollando software. Ha sido reconocido por Microsoft como MVP
(Most Valuable Professional) de C#. Cuenta, entre otras, con las certificaciones MCT, MCSE, MCDBA,
MCITP, MCSD y MCPD en sus tres variantes (Desarrollador Web, Desarrollador Windows y Desarrollador
de Aplicaciones Empresariales). En la actualidad se dedica principalmente a la formación, asesoramiento
y desarrollo de aplicaciones. Es tutor de campusMVP.

Acerca de campusMVP
CampusMVP te ofrece la mejor formación en tecnología Microsoft a través de nuestros cursos online y
nuestros libros especializados, impartidos y escritos por conocidos MVP de Microsoft. Visita nuestra
página y prueba nuestros cursos y libros gratuitamente. www-campusmvp.com

Reconocimiento - NoComercial - CompartirIgual (by-nc-sa):


No se permite un uso comercial de este documento ni de las posibles obras derivadas, la
distribución de las cuales se debe hacer con una licencia igual a la que regula esta obra original. Se
debe citar la fuente.