sábado, 13 de junio de 2015

La generación de resultados y el código fuente del lenguaje m2k2

Introducción

En este post vamos a seguir todos los pasos necesarios para diseñar e implementar un intérprete. Un intérprete es un programa que recibe como entrada un programa en un lenguaje determinado y que produce como salida el resultado de su ejecución. El lenguaje aceptado como entrada por el intérprete es el denominado m2k2, definido formalmente en el primer post.

El diseño del intérprete se estructura en cuatro partes claramente diferenciadas que a su vez configuran la estructura principal de este documento:

  • Análisis léxico
  • Análisis sintáctico
  • Análisis semántico
  • Generación de resultados

En este post se expone la generación de resultados y al final se proporciona el código fuente.

Generación de resultados

El árbol de sintaxis abstracta construido durante la fase de análisis semántico no solo es útil para realizar las comprobaciones semánticas. Este árbol resulta especialmente cómodo para realizar la interpretación. Para ello, cada nodo del AST cuenta con un método interpret que realiza la interpretación de ese nodo y a su vez dispara las interpretaciones oportunas sobre sus nodos hijos. Todo el proceso de interpretación comienza en el programa principal con la llamada:

    AST.interpret()

Obviamente, el AST se interpreta solo cuando la relación de comprobaciones semánticas finaliza con éxito. Se enumera a continuación la interpretación de cada uno de los distintos nodos del AST.

    node_Declaracion.interpret( )

La interpretación de este nodo tiene como efecto lateral la declaración como variables de los lexemas que aparecen en la lista de identificadores.

    node_Asignacion.interpret( )

La interpretación de este nodo calcula el valor que devuelve la interpretación de la expresión y como efecto lateral asigna ese valor al identificador que actúa como receptor de la asignación. En caso de ser necesario, realiza el typecasting.

    node_Expresion.interpret( )

La interpretación de este nodo calcula el valor que devuelve la interpretación de la expresión y imprime ese valor por el stdout.

    node_MasBinario.interpret( )

La interpretación de este nodo devuelve el resultado de sumar la interpretación de sus dos términos.

    node_MenosBinario.interpret( )

La interpretación de este nodo devuelve el resultado de restar la interpretación de sus dos términos.

    node_O.interpret( )

La interpretación de este nodo devuelve el resultado de aplicar el operador O lógico a la interpretación de sus dos términos. La interpretación del nodo se realiza por cortocircuito, lo cual quiere decir que si el primer térmito se evalúa como "true", ya no se interpreta el segundo término. Esto tiene como consecuencia que una expresión como esta:

    >>> 1 | (1/0)
    1

no provoca error de ejecución por división por cero.

    node_Mul.interpret( )

La interpretación de este nodo devuelve el resultado de multiplicar la interpretación de sus dos factores.

    node_Div.interpret( )

La interpretación de este nodo devuelve el resultado de dividir la interpretación de sus dos factores.

    node_Y.interpret( )

La interpretación de este nodo devuelve el resultado de aplicar el operador Y lógico a la interpretación de sus dos factores. La interpretación del nodo se realiza por cortocircuito, lo cual quiere decir que si la interpretación el primer factor devuelve falso como resultado, el segundo factor ya no se evalúa, con lo que una línea como esta,

    >>> 0 & (1/0)
    1

se evalúa sin que se produzca el error de ejecución.

    node_PorCien.interpret( )

La interpretación de este nodo devuelve el resultado de calcular el resto a la interpretación de sus dos factores.

    node_Cmp.interpret( )

La interpretación de este nodo devuelve el resultado de comparar la interpretación de sus dos factores.

    node_Ident.interpret( )

La interpretación de este nodo devuelve el valor que el identificador tiene almacenado en la tabla de símbolos.

    node_NrEnter.interpret( )

La interpretación de este nodo devuelve el valor de la constante entera.

    node_NrReal.interpret( )

La interpretación de este nodo devuelve el valor de la constante real.

    node_MasUnario.interpret( )

La interpretación de este nodo devuelve el resultado de la interpretación del factor, manteniendo su signo.

    node_MenosUnario.interpret( )

La interpretación de este nodo devuelve el resultado de la interpretación del factor, cambiando su signo.

    node_No.interpret( )

La interpretación de este nodo devuelve el resultado de la interpretación del factor con la lógica invertida.

    node_OpTorio.interpret( )

La interpretación de este nodo calcula en primer lugar el resultado de la interpretación de las expresiones 1 y 2 del operatorio. Después de ésto, asigna a la variable muda del operatorio el valor devuelto por la interpretación de la expresión 1 y comienza a iterar sobre la variable muda del operatorio desde el valor devuelto por la interpretación de la expresión 1 mas uno hasta el valor devuelto por la interpretación de la expresión 2, acumulando las interpretaciones parciales del operatorio en una variable temporal res. Al terminar las iteraciones, el operatorio devuelve res como resultado de interpretar el operatorio.

    >>> (+) (i, 1..3, i)
    >>> 6

Al finalizar las iteraciones, la variable muda del operatorio contiene el valor devuelto por la interpretación de la expresión 2 del operatorio si este valor es mayor o igual que el valor devuelto por la interpretación de la expresión 1.

    >>> i
    >>> 3

Si el valor devuelto por la interpretación de la expresión 2 es menor que el valor devuelto por la interpretación de la expresión 1, la variable muda contiene el valor devuelto por la interpretación de la expresión 1.

    >>> (+) (i, 1..0, i)
    >>> 1
    >>> i
    >>> 1

Al igual que ocurre con los nodos relacionados con operaciones lógicas, los operatorios lógicos tambien se interpretan por cortocircuito. En ese caso, al finalizar la interpretación del nodo, la variable muda no contiene el valor máximo del rango sino el valor del rango que ha causado el cortocircuito de la interpretación del operatorio. Por ejemplo:

    >>> (&) (i, -2..2, i)
    >>> 0
    >>> i
    >>> 0

Vemos en el ejemplo anterior como la variable "i" usada como indice del operatorio se ha incrementado desde -2 hasta alcanzar el valor 0. Ese valor 0 ha hecho falsa la expresión (&). Y ahi ha finalizado la evaluación del operatorio, ya no se han evaluado los valores 1 y 2.

Errores de ejecución

La gestión de los errores de ejecución se hace con la misma filosofía con la que se gestionan los errores léxicos, sintácticos y semánticos: en el programa principal se capturan todas las posibles excepciones que se pueden lanzar durante la ejecución de alguna acción dentro del código del intérprete. En concreto se capturan estas tres excepciones:

  1. OverflowError
  2. ZeroDivisionError
  3. ValueError

La excepción "OverflowError" se dispara cuando se desborda un rango al calcular una operación aritmética. Por ejemplo:

    >>> 100000 * 100000
    Execution Error: overflow error

La excepción "ZeroDivisionError" se dispara cuando se produce una división por cero. Por ejemplo:

    >>> 1/(2-2)
    Execution Error: zero division error

La excepción "ValueError" se dispara cuando se utiliza la función atoi o la función atof sobre una cadena de dígitos que excede el rango de representación de los enteros o los reales respectivamente. Por ejemplo:

    >>> 9999999999999999999999
    Execution Error: value error

Con cualquiera de las 3 excepciones anteriores, el programa principal actúa capturando la excepción y mostrando por el stderr el mensaje informativo que corresponda a la excepción capturada, tal como se observa en los ejemplos anteriores. Una vez atendido el error de ejecución, se atiende la siguiente línea del stdin.

El código fuente

Directamente descargable desde github, en este repositorio:

https://github.com/aicastell/m2k2.git

Espero que lo disfrutéis! :)

No hay comentarios:

Visitas:

Seguidores