¿Cómo hacer un intérprete de ecuaciones matemáticas en QuickBASIC? Parte 5

avatar

¿Cómo hacer un intérprete de ecuaciones matemáticas en QuickBASIC? Parte 5


Imagen generada usando Bing Image Creator


Para ver la cuarta parte de este artículo puedes usar este enlace: Parte 4


Introducción

En esta parte del texto del tutorial vamos a ver la sección más importante de la implementación del parser de ecuaciones matemáticas a la hora de obtener un resultado con su uso.

En la parte anterior terminamos desarrollando las funciones básicas de reconocimiento, componentes del analizador lexicográfico, y usadas por éste para detectar los distintos símbolos o caracteres válidos de acuerdo a como están definidos por la gramática de una expresión matemática. En esta parte implementaremos el resto del intérprete, enlazando lo hecho hasta ahora con un analizador sintáctico básico mas lo suficientemente sofisticado para detectar errores sintácticos en la ecuación suministrada, y desarrollaremos las rutinas semánticas encargadas de interpretar la ecuación matemática y obtener un resultado.

En otras palabras, vamos a encargarnos de los pasos 3 y 4 expuestos en la tabla mostrada en la introducción de la parte 4 del texto, y con esto vamos a terminar de escribir el código para nuestro intérprete usando el lenguaje de programación QuickBASIC.

El código fuente del parser también estará disponible para descarga puesto debido a su extensión no vamos a describir cada detalle del mismo.


El analizador sintáctico

Tal como sucede con el analizador lexicográfico, el analizador sintáctico se basa en lo expresado en la notación de la gramática de una expresión matemática para concluir si la ecuación a ser procesada está correctamente escrita, si bien en nuestro caso el análisis lexicográfico, el análisis de sintaxis, y la obtención de resultados estará bastante integrada.

La tarea fundamental de un analizador sintáctico se resume nada más en decidir si cada lexema obtenido a través de las subrutinas y funciones del analizador lexicográfico es sucedido por el o los lexemas esperados en cada caso, garantizando con esto que la ecuación esté escrita del modo esperado para ser considerada correcta según la gramática, lo cual es conocido como crear un árbol de sintaxis.

Pero como se ha mencionado, nuestro analizador sintáctico también incluirá la parte semántica y el análisis lexicográfico antes expuesto integrados, y según el lexema recibido con cada llamada para obtener un lexema, se llevará a cabo la función indicada para su interpretación inmediata sin revisar primero la sintaxis de toda la ecuación suministrada, dado para nosotros no tiene caso hacerlo así y crear todo un árbol de sintaxis para luego volver a procesar la ecuación en un momento posterior para poder interpretarla.

Es necesario decir también que como el analizador lexicográfico desarrollado era bastante básico, y se limitaba a devolver cada token o lexema sin otra información relacionada con éste, ahora necesitaremos ampliar un poco sus funciones para que nos resulte más útil, además de hacerle los otros cambios para garantizar su integración con las otras partes del intérprete, lo cual se comentó en la parte anterior del tutorial que íbamos a hacer más adelante.

En particular, nos sería útil poder reconocer si un identificador devuelto por el analizador lexicográfico es una variable o una llamada a una función (como deben recordar no vamos a usar constantes), y para esto se pueden utilizar funciones como las expuestas en la siguiente tabla:

Nombre

Propósito

IsFunction% (Token As String)

Comprueba "Token" y devuelve true si es una función intrínseca.

IsVariable%(Token As String)

Comprueba "Token" y devuelve true si es una variable declarada.

Las funciones IsFunction e IsVariable hacen su trabajo buscando en las respectivas tablas para comprobar si el nombre contenido en Token concuerda con el nombre de una función registrada o de una variable previamente declarada, por tanto, también deberán ser creadas las mencionadas tablas (arreglos) de funciones y variables, así como una subrutina para registrar en la tabla de las variables los nombres de las nuevas variables creadas y una función para obtener su valor actual a partir de su nombre.

En este último caso la tabla de las variables sería como la memoria disponible de nuestro intérprete.

En resumen, todo eso lo podrán ver si revisan el código fuente, y como pueden notar, son piezas relativamente sencillas del intérprete de ecuaciones.

En este momento vamos a proceder con la implementación del analizador sintáctico (y las otras partes necesarias para ponerlo a funcionar), y se darán cuenta de cómo, tal como pasó cuando se hizo el analizador lexicográfico, su estructura es un reflejo más o menos exacto de la gramática de los elementos de una expresión matemática.


La función GetExpression para procesar una expresión

La gramática de una expresión matemática indica como esta se expresa como:

<expression> ::= <term> [<add_op> <term>]*

Por tanto, vamos a necesitar una funcion GetExpression donde se recree lo denotado por la notación listada (como en nuestro intérprete está todo integrado esta función intentará calcular una expresión y devolver un resultado además de comprobar la sintaxis).

El código en QuickBASIC correspondiente a la gramática de una expresión es:

Function GetExpression#
  Dim Result As Double
  If IsAddOp(Character) Then
    Position = Position - 1
    Character = "0"
  End If
  Result = GetTerm
  While IsAddOp(Character)
    Select Case Character
      Case "+"
        Match "+"
        Result = Result + GetTerm
      Case "-"
        Match "-"
        Result = Result – GetTerm
    End Select
  Wend
  GetExpression = Result
End Function

En la implementación anterior, como pueden ver, hemos agregado código para procesar una expresión con signo, lo cual no estaba expresado en la gramática donde sólo se consideraba un factor con signo.

La gramática representada en este código QuickBASIC sería más bien (donde <unary_op> denota un signo "+" o un signo "-"):

<expression> ::= <unary_op> <term> [<add_op> <term>]*

Pero esto es cuestión de comodidad, dado una expresión es un término, y un término es un factor, y un factor puede ser un factor con signo de acuerdo con la gramática; y así no es tan relevante dónde se tenga presente que pueden ser válidos los signos delante de dichos elementos de una ecuación.

Por lo demás, la función en QuickBasic es un reflejo de la gramática de la expresión matemática.

En esta función, la subrutina Match es la encargada de comprobar si la sintaxis es la correcta, dado con ella detectamos si detrás del un token está el token o lexema esperado; la subrutina Match se podría llamar también Check por su función, y en adición se encarga de reportar un error si no se encuentra lo esperado según lo revela la gramática.

Todo lo demás es más bien parte de las rutinas semánticas y realizan el cálculo interpretando la ecuación suministrada.


La función GetTerm para procesar un término

En el código de la función GetExpression podemos ver como se llama a un función GetTerm puesto según la gramática una expresión matemática está compuesta por un término opcionalmente seguido por un símbolo "+" o "-" (<add_op>) y otro término.

La gramática nos dice que un término en una expresión matemática se denota como:

<term> ::= <factor> [<mul_op> <factor>]*

Por tanto la función en QuickBASIC será otra vez un reflejo de la gramática:

Function GetTerm#
  Dim Result As Double
  Result = GetFactor
  While IsMulOp(Character)
    Select Case Character
      Case "*"
        Match "*"
        Result = Result * GetFactor
      Case "/"
        Match "/"
        Result = Result / GetFactor
    End Select
  Wend
  GetTerm = Result
End Function

Todo lo explicado para la función GetExpression también aplica para la función GetTerm.


La función GetFactor para procesar un factor

En este caso, como en los anteriores, se repite lo mismo, si bien un factor es un poco más complicado y la función en QuickBASIC será un poco más extensa en dependencia de hasta donde implementemos el intérprete de ecuaciones.

En particular, esta función interpretará un factor, como su nombre lo indica, y podría crecer mucho más si incluimos en el parser más funciones intrínsecas u otras características distintivas.

La notación de la gramática de un factor dice:

<factor> ::= <number> | (<expression>) | <identifier> | <function> | <signed_factor>

Por su parte, el código en QuickBASIC es:

Function GetFactor#
  Dim Token As String
  Dim Parameter1 As Double
  Dim Parameter2 As Double
  Dim Result As Double
  SkipWhite
  If IsDigit(Character) Then
    Result = GetNumber
  ElseIf Character = "(" Then
    Match "("
    Result = GetExpression
    Match ")"
  ElseIf IsAlpha(Character) Then
    Token = GetIdentifier
    If IsFunction(Token) Then
      Match "("
      Select Case Token
        Case "SQRT"
          Result = SQR(GetExpression)
        Case "SQR"
          Result = GetExpression ^ 2
        Case "POW"
          Parameter1 = GetExpression
          Match ","
          Parameter2 = GetExpression
          Result = Exp(Parameter2 * Log(Parameter1))
        Case Else
          ‘Función no definida en la tabla de funciones
          Error 255
      End Select
      Match ")"
    ElseIf IsVariable(Token) Then
      Result = GetVariable(Token)
      If Character = "=" Then
        Match "="
        Result = GetExpression
        SetVariable Token, Result
      End If
    ElseIf IsAddOp(Character) Then
      Result = GetExpresion
    Else
      'Número, expresión, o identificador (función o variable) esperados
      Error 255
    End If
  End If
  GetFactor = Result
End Function

En este caso, una vez más, el código QuickBASIC es un reflejo de la gramática, salvo por la parte del factor con signo, que como recordarán fue tratado en la función GetExpression para simplificar un poco la función GetFactor, puesto como pueden notar y se comentó es un poco más extensa, y como también se mencionó, no importa mucho dónde se tenga presente ese detalle del signo de un elemento de la ecuación.

En esta función podemos ver mejor la naturaleza recursiva de una expresión matemática, lo cual se había comentado cuando se expuso la notación de su gramática.

En la grámatica del factor también se mostraron por separado indentifier y function, aun cuando en su momento se mencionó que un identificador puede ser un nombre de variable o un nombre de función, el cual es un identificador terminado con paréntesis para los parámetros, pero en el código de programa todo esto se tiene en cuenta y luego de obtener un identificador se procede a reconocer si es una variable o una llamada a función por medio de las antes comentadas funciones IsVariable e IsFunction.

El intérprete de ecuaciones dará prioridad a los nombres de función de modo que no se puede usar un nombre de variable igual a un nombre de una función intrínseca o el intérprete lo tomará como una función y reportará un error cuando la sintaxis no sea la correcta para esta.

Por el momento nada más se implementaron las funciones SQRT (para la raíz cuadrada), SQR (para elevar al cuadrado), y POW (para elevar a otra potencia cualquiera), no obstante, es sencillo agregar más funciones incluyendo más código en la sección correspondiente de la función GetFactor.

En este caso, si se agrega más código para otra función intrínseca, debemos recordar agregar también el nombre de la función en la tabla de funciones del intérprete, para permitir a la función IsFunction reconocerla como una función intrínseca del sistema.

Por supuesto, todavía faltaría definir una cierta cantidad de otras subrutinas y funciones, sin embargo, el resto de las subrutinas o funciones no las describo en este texto porque son mucho más simples y resulta irrelevante hacerlo.

En muchas de esas funciones se sigue la misma lógica, y son un reflejo de la gramática correspondiente de los distintos elementos componentes de una expresión matemática, si bien como comprenderán, el núcleo del intérprete de ecuaciones está compuesto por las funciones GetExpression, GetTerm, y GetFactor, con las cuales comprobamos la sintaxis de la expresión y la interpretamos para obtener los resultados.

El código presentado nos muestra también cómo se ha integrado el analizador lexicográfico, el analizador sintáctico (basado en lo fundamental en la función Match), y las rutinas semánticas encargadas de interpretar la ecuación matemática suministrada por un usuario.

En la próxima parte de este texto vamos a escribir un poco más de código para usar todo esto para interpretar una serie de ecuaciones matemáticas.


Código fuente

Los interesados pueden descargar el fuente de la implementación del intérprete de ecuaciones matemáticas básico por medio de este enlace: 🔗 ExpInp.zip

Nota: El código del programa no ha utilizado códigos de error diferenciados dado esto se está haciendo sólo con fines educativos, todos los errores lanzados desde dentro de las distintas subrutinas y funciones tienen un código común número 255.


¿Qué te ha parecido esta quinta parte?

Tú participación aportando ideas, dando sugerencias, o expresando tu crítica constructiva a través de tus comentarios, es importante y fundamental para permitirnos crecer y crear contenidos con cada vez más calidad.

También es importante tu voto si crees vale la pena concederlo.

¡Nos vemos en la siguiente parte de este artículo tutorial!



0
0
0.000
0 comments