Cómo crear un compilador de expresiones en .NET

8

Click here to load reader

description

Con este artículo nuestro tutor Alberto Población termina la serie sobre cómo construir nuestros propios intérpretes y compiladores de expresiones. En esta ocasión, para finalizar, nos explica cómo crear nuestro propio compilador de código en C#. ¿Te atreves a crear tu propio lenguaje?

Transcript of Cómo crear un compilador de expresiones en .NET

Page 1: Cómo crear un compilador de expresiones en .NET

Cómo crear un compilador de expresiones en .NET Alberto Población

Page 2: Cómo crear un compilador de expresiones en .NET

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.

Page 3: Cómo crear un compilador de expresiones en .NET

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.

Page 4: Cómo crear un compilador de expresiones en .NET

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...

Page 5: Cómo crear un compilador de expresiones en .NET

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...

Page 6: Cómo crear un compilador de expresiones en .NET

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.

Page 7: Cómo crear un compilador de expresiones en .NET

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.

Page 8: Cómo crear un compilador de expresiones en .NET