8
¿Cómo crear un intérprete de expresiones en .NET? Alberto Población

Cómo crear un intérprete de expresiones en .NET

Embed Size (px)

DESCRIPTION

Este artículo da indicaciones generales sobre cómo construir nuestro propio intérprete o compilador de código, que nos permita trabajar con expresiones propias e incluso, por qué no, crear nuestro propio lenguaje.Como prueba de concepto se incluye un ejemplo completo de un sencillo intérprete de expresiones matemáticas.

Citation preview

Page 1: Cómo crear un intérprete de expresiones en .NET

¿Cómo crear un intérprete de expresiones en .NET? Alberto Población

Page 2: Cómo crear un intérprete de expresiones en .NET

¿Cómo crear un intérprete de expresiones en .NET? Nivel: Intermedio-Avanzado por Alberto Población

En un artículo anterior dábamos respuesta a las preguntas de este tipo:

Deseo introducir en un TextBox una exresión tal como “5*x-3*(x+1)” y que mi programa calcule el resultado al pasarle el valor de x.

Y comentábamos que una forma de resolverlo, entre las distintas opciones disponibles, consistía en escribir un intérprete o compilador basado en un analizador sintáctico hecho por nosotros mismos. En este artículo vamos a dar unas indicaciones generales acerca de cómo se escribe un programa de este tipo para interpretar expresiones sencillas.

El primer paso consiste en elaborar un diagrama sintáctico que sirva para representar las expresiones que queremos evaluar. La Figura 1 representa un diagrama muy sencillo, que define la sintaxis de las fórmulas matemáticas que aceptan números paréntesis, las cuatro operaciones matemáticas, y la variable “x”.

Figura 1.- Diagrama sintáctico sencillo.

Page 3: Cómo crear un intérprete de expresiones en .NET

En este diagrama, se han encerrado en un círculo los elementos “finales”, que se leen “tal cual” en la expresión tecleada. Normalmente se escribe un fragmento de programa que en inglés recibe el nombre de parser (“analizador”), que va devolviendo cada uno de los símbolos que componen la expresión. En el listado 1 que se acompaña más abajo, esto está implementado en la subrutina ObtenerSiguienteSímbolo.

Las cajas rectangulares sirven para enlazar entre sí las distintas partes del diagrama, y normalmente se traduce cada una de ellas en un método de nuestro programa (en el listado 1 llevan el mismo nombre que figura escrito dentro de cada caja).

Las flechas indican la estructura del lenguaje (qué elemento puede venir detrás de qué otro), y a la hora de programar se convierten en una secuencia de instrucciones llamando a otro método (cuando las flechas tropiezan con una caja) o llamando al parser para que obtenga el siguiente símbolo (cuando tropiezan con un círculo).

Cuando una flecha se bifurca en dos, el programa fuente debe contener instrucciones para leer el siguiente símbolo, y una instrucción condicional (if o switch) para decidir por cuál de los dos caminos hay que seguir, examinando símbolo obtenido. Por supuesto, no todos los diagramas sintácticos cumplen el requisito de que en cada bifurcación se pueda decidir el camino a seguir examinando únicamente el símbolo siguiente. En los casos en que esto sí que se cumple (como el diagrama de la figura 1), podremos construir un compilador de “un solo paso sin marcha atrás”, que es el tipo más simple y el único que vamos a ver aquí.

Finalmente, nuestro código fuente contendrá las instrucciones necesarias para hacer la operación indicada por el diagrama (suma, resta, multiplicación, etc.) cada vez que pase por uno de los puntos en que se ha reunido la información necesaria. Si la operación se realiza inmediatamente al pasar por ese punto, tenemos un intérprete; si lo que se hace es generar código ejecutable que más tarde al ser ejecutado realice la operación deseada, entonces lo que tenemos es un compilador.

El programa del listado 1 contiene un intérprete de este tipo, realizado conforme con el esquema de la figura 1. Por razones de claridad y simplicidad, no contiene apenas nada de código para el control de errores, que lógicamente habría que añadir en un programa destinado a ser puesto en producción.

Este diagrama es sumamente sencillo, y ha sido fácil dibujarlo en un pequeño gráfico. Cuando la sintaxis se complica más, y el diagrama empieza a crecer, se suele recurrir a otros tipos de notación más formales, tales como BNF (Bakus-Naur Form). Aunque resulta menos claro a simple vista, se trata de un metalenguaje que define la sintaxis exactamente igual que nuestro gráfico, y sirve para escribir nuestro intérprete o compilador de la misma manera.

Page 4: Cómo crear un intérprete de expresiones en .NET

Listado 1:

public class Intérprete

{

private string expresion;

private int posicion;

private Simbolo últimoSimbolo;

private double últimaConstante;

private Stack<double> pila;

private double valorDeX;

public Intérprete(string expresion)

{

this.expresion = expresion;

}

public double Evaluar(double x)

{

this.valorDeX = x;

this.posicion = 0;

this.pila = new Stack<double>();

últimoSimbolo = ObtenerSiguienteSímbolo();

Expresion();

if (pila.Count<1)

throw new Exception("Error al leer de la pila");

return pila.Pop();

}

private Simbolo ObtenerSiguienteSímbolo()

{

char c;

do

{

if (posicion>=expresion.Length)

return Simbolo.FinDeLaExpresión;

c = expresion[posicion++];

}

while (c==' '); //Despreciamos los espacios en blanco

switch (c)

{

case 'x': case 'X': return Simbolo.Variable;

case '(': return Simbolo.AbrirParéntesis;

case ')': return Simbolo.CerrarParéntesis;

case '+': return Simbolo.Suma;

case '-': return Simbolo.Resta;

case '*': return Simbolo.Multiplicación;

case '/': return Simbolo.División;

}

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;

últimaConstante =

double.Parse(s.Replace(".",","));

return Simbolo.Constante;

}

throw new Exception(

"Simbolo no reconocido en la posición " + posicion);

}

Continúa...

Page 5: Cómo crear un intérprete de expresiones en .NET

private void Expresion()

{

Termino();

while (true)

{

switch (últimoSimbolo)

{

case Simbolo.Suma:

últimoSimbolo =

ObtenerSiguienteSímbolo();

Termino();

OperacionSuma();

break;

case Simbolo.Resta:

últimoSimbolo =

ObtenerSiguienteSímbolo();

Termino();

OperacionResta();

break;

default: return;

}

}

}

private void Termino()

{

Factor();

while (true)

{

switch (últimoSimbolo)

{

case Simbolo.Multiplicación:

últimoSimbolo =

ObtenerSiguienteSímbolo();

Factor();

OperacionMultiplicacion();

break;

case Simbolo.División:

últimoSimbolo =

ObtenerSiguienteSímbolo();

Factor();

OperacionDivision();

break;

default: return;

}

}

}

private void Factor()

{

if (últimoSimbolo==Simbolo.AbrirParéntesis)

{

últimoSimbolo = ObtenerSiguienteSímbolo();

Expresion();

if (últimoSimbolo!=Simbolo.CerrarParéntesis)

throw new Exception("Falta ')'");

últimoSimbolo = ObtenerSiguienteSímbolo();

}

else if (últimoSimbolo==Simbolo.Constante)

{

OperacionConstante();

últimoSimbolo = ObtenerSiguienteSímbolo();

}

Continúa...

Page 6: Cómo crear un intérprete de expresiones en .NET

else if (últimoSimbolo==Simbolo.Variable)

{

OperacionVariable();

últimoSimbolo = ObtenerSiguienteSímbolo();

}

else

throw new Exception("Factor");

}

private void OperacionConstante()

{

pila.Push(últimaConstante);

}

private void OperacionVariable()

{

pila.Push(valorDeX);

}

private void OperacionSuma()

{

pila.Push(pila.Pop()+pila.Pop());

}

private void OperacionResta()

{

double op2 = pila.Pop();

double op1 = pila.Pop();

pila.Push(op1-op2);

}

private void OperacionMultiplicacion()

{

pila.Push(pila.Pop()*pila.Pop());

}

private void OperacionDivision()

{

double op2 = pila.Pop();

double op1 = pila.Pop();

pila.Push(op1/op2);

}

}

enum Simbolo

{

Ninguno,

Suma, Resta, Multiplicación, División,

AbrirParéntesis, CerrarParéntesis,

Constante, Variable,

FinDeLaExpresión

}

Este listado contiene una clase Intérprete, que recibe la expresión a interpretar a través de su constructor. Para evaluar la expresión se llama al método Evaluar, que recibe el valor para la variable x que hemos previsto admitir en nuestras expresiones. Mediante este mecanismo, se puede ir llamando repetidamente al intérprete para que evalúe la expresión con distintos valores de x (por ejemplo, para dibujar una gráfica).

Aplicando procedimientos similares a los que hemos visto aquí, se pueden procesar expresiones tan complejas como deseemos, pudiendo llegar incluso a crear un lenguaje de programación completo.

Page 7: Cómo crear un intérprete 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 intérprete de expresiones en .NET