Mi primer proyecto utilizando PLY con Python 3

Se desarrollará un intérprete que recibe como entrada varias expresiones aritméticas y presentando como salida el resultado de dichas expresiones.

Las tecnologías a utilizar son:

  • PLY: Generador de analizadores léxicos y sintácticos.
  • Python 3: Es un lenguaje de programación interpretado de alto nivel.
  • Visual Studio Code: Es un editor de código ligero pero poderoso. Existen complementos para trabajar con este lenguaje.

El proyecto completo lo pueden descargar del siguiente enlace

PLY

PLY es una implementación en Python de lex y yacc, herramientas populares para la construcción de compiladores.

La principal tarea de un analizador léxico es leer los caracteres de entrada del programa fuente, agruparlos en lexemas y producir como salida una secuencia de tokens.

  • Un token es un par que consiste en un nombre de token y un valor de atributo opcional.
  • Un lexema es una secuencia de caracteres en el programa fuente, que coinciden con el patrón para un token y que el analizador léxico identifica como una instancia de este token.
  • Un patrón es una descripción de la forma que pueden tomar los lexemas de un token.

El analizador sintáctico obtiene una cadena de tokens del analizador léxico y verifica que dicha cadena pueda generarse con la gramática para el lenguaje fuente. Una gramática proporciona una especificación precisa y fácil de entender de un lenguaje de programación.

En PLY se definen los patrones de los diferentes tokens que se desean reconocer, esto se hace a través de expresiones regulares. Mientras que las producciones y acciones para formar la gramática se definen a través de funciones.

Pre-requisitos

Instalamos PLY

Para hacer uso de PLY en nuestro proyecto no hacemos instalación como tal, lo que necesitamos es descargar el archivo ply-3.11.tar.gz (versión 3.11 al momento de escribir este tutorial) de la página oficial de PLY y lo que hacemos es copiar el fólder “ply” a nuestro proyecto.

Crear nuestro proyecto

Primero crearemos un nuevo fólder, en este caso lo llamaremos PROYECTOPLY. Luego lo abrimos en nuestro editor de texto, en este caso usaremos Visual Studio Code. Finalmente procedemos a crear un nuevo archivo llamado gramatica.py donde escribiremos nuestro compilador.

Los directorios “pycache“, al igual que los archivos “parser.out” y “parsetab.py” son generados por Python los cuales pueden ser excluidos en nuestro controlador de versiones. En este caso, los agregamos a nuestro .gitignore.

1
2
3
parser.out
parsetab.py
**/__pycache__/**

El directorio “ply” es el que descargamos y utilizaremos para construir nuestro compilador.

Código Fuente para el analizador léxico y sintáctico

En el archivo gramatica.py tenemos la construcción de nuestro compilador.

Explicación del código fuente para el analizador léxico

Lo primero que debemos hacer es definir el listado de tokens que vamos a reconocer ya asignarlo a la variable tokens

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tokens  = (
'REVALUAR',
'PARIZQ',
'PARDER',
'CORIZQ',
'CORDER',
'MAS',
'MENOS',
'POR',
'DIVIDIDO',
'DECIMAL',
'ENTERO',
'PTCOMA'
)

Luego escribimos los patrones para los tokens que definimos. Existen dos formas de definir las reglas de nuestros tokens.

La primera, es con expresiones regulares, agregamos el prefijo “t_” al token que queremos definir y luego le especificamos la expresión regular, para esto se hace uso del módulo re de Python.

1
2
3
4
5
6
7
8
9
10
11
# Tokens
t_REVALUAR = r'Evaluar'
t_PARIZQ = r'\('
t_PARDER = r'\)'
t_CORIZQ = r'\['
t_CORDER = r'\]'
t_MAS = r'\+'
t_MENOS = r'-'
t_POR = r'\*'
t_DIVIDIDO = r'/'
t_PTCOMA = r';'

La otra forma es a través de funciones, esto nos sirve para manipular el valor del token que procesamos. Por ejemplo para los valores numéricos los retornamos con el tipo apropiado, hacer validaciones, etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def t_DECIMAL(t):
r'\d+\.\d+'
try:
t.value = float(t.value)
except ValueError:
print("Floaat value too large %d", t.value)
t.value = 0
return t

def t_ENTERO(t):
r'\d+'
try:
t.value = int(t.value)
except ValueError:
print("Integer value too large %d", t.value)
t.value = 0
return t

Es importante definir también los caracteres que se van a ignorar.

1
2
# Caracteres ignorados
t_ignore = " \t"

Las funciones también llevan el prefijo “t_” antes del nombre del token que queremos procesar. La función recibe un parámetro, “t” en nuestro ejemplo, este contiene el valor del token. Retornamos el valor ya procesado que deseamos, o no retornar nada si lo que deseamos es ignorar el token (por ejemplo: comentarios, contadores, etc.).

1
2
3
4
5
6
7
def t_newline(t):
r'\n+'
t.lexer.lineno += t.value.count("\n")

def t_error(t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1)

Finalmente construimos el analizador léxico haciendo uso de las librerías de PLY

1
2
3
# Construyendo el analizador léxico
import ply.lex as lex
lexer = lex.lex()

Explicación del código fuente para el analizador sintáctico

Otra de las ventajas de Python es que en el mismo archivo podemos definir nuestro análisis sintáctico haciendo uso de los tokens previamente definidos en la sección del analizador léxico.

Primeramente definimos la asociatividad y precedencia de los operadores, ya que la gramática escrita es ambigua, es necesario definir una precedencia para que el analizador no entre en conflicto al analizar, en este caso la precedencia es la misma que la de los operadores aritméticos, la precedencia más baja la tienen la suma y la resta, luego están la multiplicación y la división que tienen una precedencia más alta y por último está el signo menos de las expresiones negativas que tendría la precedencia más alta.

1
2
3
4
5
6
# Asociación de operadores y precedencia
precedence = (
('left','MAS','MENOS'),
('left','POR','DIVIDIDO'),
('right','UMENOS'),
)

Ahora procedemos a escribir nuestras producciones, aquí vemos otra de las ventajas de Python, las acciones semánticas de nuestras producciones se hacen en forma de funciones. Las características de estas funciones son:

  • El nombre inicia con el prefijo “_p”. El complemento del nombre queda a nuestra discreción
  • Tiene un único parámetro “t” el cual es una tupla, en cada posición tiene el valor de los terminales y no terminales de la producción.
  • Haciendo uso del docstring de las funciones de Python especificamos las producciones que serán procesadas por la función.
  • En el cuerpo de la función definimos la funcionalidad que deseamos

Por ejemplo:

1
2
3
4
5
6
def p_expresion_evaluar(t):
'expresion : expresion MAS expresion'
# ^ ^ ^ ^
# t[0] t[1] t[2] t[3]

t[0] = t[1] + t[3]

Sintetizamos en p[0] (expresion) el valor del resultado de sumar loo valores de p[1] (expresion) y p[3].

A continuación el código completo de nuestras producciones:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Definición de la gramática
def p_instrucciones_lista(t):
'''instrucciones : instruccion instrucciones
| instruccion '''

def p_instrucciones_evaluar(t):
'instruccion : REVALUAR CORIZQ expresion CORDER PTCOMA'
print('El valor de la expresión es: ' + str(t[3]))

def p_expresion_binaria(t):
'''expresion : expresion MAS expresion
| expresion MENOS expresion
| expresion POR expresion
| expresion DIVIDIDO expresion'''
if t[2] == '+' : t[0] = t[1] + t[3]
elif t[2] == '-': t[0] = t[1] - t[3]
elif t[2] == '*': t[0] = t[1] * t[3]
elif t[2] == '/': t[0] = t[1] / t[3]

def p_expresion_unaria(t):
'expresion : MENOS expresion %prec UMENOS'
t[0] = -t[2]

def p_expresion_agrupacion(t):
'expresion : PARIZQ expresion PARDER'
t[0] = t[2]

def p_expresion_number(t):
'''expresion : ENTERO
| DECIMAL'''
t[0] = t[1]

def p_error(t):
print("Error sintáctico en '%s'" % t.value)

Por último, podemos manejar también las producciones de error para el manejo de errores sintácticos.

Ahora construimos el analizador sintáctico,la funcionalidad para leer el archivo y enviarle su contenido a nuestro compilador.

1
2
3
4
5
6
7
import ply.yacc as yacc
parser = yacc.yacc()

f = open("./entrada.txt", "r")
input = f.read()
print(input)
parser.parse(input)

Creando un archivo de entrada para nuestro analizador

Creamos un nuevo archivo de texto utilizando nuestro editor llamado entrada.txt. El contenido de este archivo es el siguiente:

1
2
3
4
5
Evaluar[1+1];
Evaluar[1+1*2];
Evaluar[-(1+1*6/3-5+7)];
Evaluar[-(1+1*6/3-5+1*-2)];
Evaluar[-(1.6+1.45)];

Ejecución

Para ejecutar este script corremos el siguiente comando:

1
$  python3 .\gramatica.py

Como podemos ver, obtenemos la salida esperada.

Acerca del autor:

Este tutorial fue elaborado por el Auxiliar de Cátedra Rainman Sián, como contribución al curso de Organización de Lenguajes y Compiladores 2 de la Universidad de San Carlos de Guatemala.

Fuentes consultadas:

Compiladores, principios, técnicas y herramientas. Aho, Lam, Sethi y Ullman. Segunda Edición.

Mi primer proyecto utilizando JavaCC

Se desarrollará un intérprete que recibe como entrada varias expresiones aritméticas y presenta como salida el resultado de dichas expresiones.

Las tecnologías para utilizar son:

  • JavaCC: Generador de analizadores léxicos y sintácticos.
  • Windows 10: Sistema operativo.
  • Netbeans 8.2: IDE (entorno de desarrollo integrado)
  • Java 8: Lenguaje de programación.

El proyecto completo del ejemplo puede descargarse del siguiente enlace:

JavaCC

Java Compiler Compiler es un generador de analizadores para utilizar en java. Este generador es una herramienta que lee la especificación gramatical y la convierte en un programa de java que puede reconocer coincidencias con la gramática. Además del generador de analizadores en sí, JavaCC proporciona otras capacidades estándar relacionadas con la generación de analizadores, como la construcción de árboles (a través de una herramienta llamada JJTree incluida con JavaCC), acciones y depuración. Todo lo que se necesita para ejecutar un analizador JavaCC, una vez generado, es Java Runtime Environment (JRE).

Características

  • JavaCC utiliza un analizador descendente lo que permite el uso de gramáticas más generales.
  • Por defecto JavaCC genera un analizador LL(1), aunque JavaCC ofrece capacidades de anticipación sintáctica para resolver ambigüedades.
  • JavaCC permite la utilización de BNF Extendido, o lo que vendría siendo utilizar expresiones regulares tanto en la parte léxica como gramatical.
  • JavaCC permite la utilización de estados para manejar de mejor forma las expresiones regulares.
  • Para más información visitar la página oficial de JavaCC.

Prerrequisitos

Para este este ejemplo necesitamos las siguientes herramientas

Agregar jdk a las variables de entorno

Debemos asegurarnos de que la carpeta bin del JDK haya sido agregada a nuestra variable de entorno Path, para ello vamos a la configuración de dicha variable de entorno

  • Clic derecho en Este equipo
  • Propiedades
  • Configuración avanzada del sistema
  • Variables de entorno
  • Variable Path
  • Editar

y si no existe agregamos la ruta a la carpeta bin del JDK, que en mi caso es:

1
C:\Program Files\Java\jdk1.8.0_211\bin

Descarga e instalación de JavaCC

Nos dirigimos a la página oficial de JavaCC, al ingresar hacemos clic sobre el botón de Download[Version].zip

Una vez descargado el archivo, lo extraemos y podremos ver el siguiente contenido:

El archivo que nos interesa es javacc.jar que se encuentra en la carpeta bootstrap:

Por conveniencia, vamos a trasladar el archivo .jar a la carpeta C:/javacc, sin embargo, podría guardarse en otra ubicación.

Mas adelante le daremos uso a nuestro archivo javacc.jar.

Crear el proyecto utilizando NetBeans

Como mencionamos vamos a utilizar NetBeans, sin embargo podría usarse cualquier otro IDE. Vamos a mostrar la creación del proyecto y su estructura.

  • Seleccionamos la opción de nuevo proyecto.
  • Ahora seleccionamos el tipo de proyecto, en este caso Java Application y damos clic en siguiente.
  • Por último, agregamos el nombre del proyecto y finalizamos.
  • Vemos el resultado de la creación del proyecto.
  • A continuación, creamos un nuevo paquete llamado Analizador, produciendo el siguiente resultado.
  • Dentro de este paquete vamos a crear un nuevo archivo llamado Gramatica.jj, este archivo contendrá la gramática para reconocer el lenguaje que vamos a realizar.

  • Para facilitar la compilación de la gramática vamos a crear un archivo compilarGramatica.bat, con el siguiente contenido, siempre en el paquete Analizador.

1
2
java -cp C:\javacc\javacc.jar javacc Gramatica.jj
pause

Lo que indican estas sentencias:

  • Agregar el archivo jar al classpath mediante el argumento -cp (classpath) – -cp C:\javacc\javacc.jar (o la ubicación de nuestro archivo javacc.jar)
  • Ejecutar java – java
  • Pasar el main del archivo jar – javacc
  • Pasar la gramática a compilar – Gramatica.jj
  • Evitar que se cierre la ventana de comando para ver el resultado – pause
  • Nota: utilizamos el argumento classpath para indicarle a java donde debe buscar los paquetes y clases a ejecutar, mas información en el siguiente link.

Construcción del lenguaje en JavaCC

Luego de esta introducción vamos a construir una programa que reconozca un lenguaje compuesto por una lista de instrucciones Evaluar que reciben una expresión aritmética para ser evaluada, por ejemplo:

1
Evaluar [3*4-2*9]

Explicación de la estructura del archivo Gramatica.jj

  • Sección de opciones: Esta sección es opcional, el área de opciones permite especificar algunas directrices que ayuden a JavaCC a generar analizadores léxico-sintácticos más eficientes y adaptados a las necesidades concretas del desarrollador. Existen muchas, si quieres conocerlas mejor puedes verificar la página 132 del libro Compiladores, de Sergio Gálvez Rojas Y Miguel Ángel Mora Mata. En este caso particular utilizamos solamente dos:

    • Ignore_Case = true, para no hacer distinción entre mayúsculas y minúsculas.
    • Static = false, para que los métodos que genere la compilación no sean estáticos.

      1
      2
      3
      4
      options {
      IGNORE_CASE = true;
      STATIC = false;
      }
  • Clausulas PARSER_BEGIN – PARSER_END: Sirven para indicarle a JavaCC el nombre de nuestra clase principal, así como para englobar tanto a esta como a cualquier otra que se quiera incluir de apoyo. En este ejemplo no definimos ningun método main, solo una clase llamada gramática para nuestro parser, por supuesto que esta clase gramática es la que debemos utilizar para invocar a nuestro parser, y el main lo incluimos fuera de este para tener un código más claro.

    1
    2
    3
    4
    5
    6
    PARSER_BEGIN(Gramatica)
    /** Analizador de expresiones aritmeticas sencillas. */
    package Analizador;
    public class Gramatica {
    }
    PARSER_END(Gramatica)
  • Sección para definición léxica: Esta sección contendrá los tokens permitidos por nuestro lenguaje, contiene distintas clausulas, pero las que utilizamos son:

    • Token: Constituyen los tokens que nuestro analizador va a reconocer, generalmente aquí se incluyen todos los terminales de nuestro lenguaje, aunque también se pueden utilizar tokens en la definición sintáctica sin haberlos definido en esta sección.
    • Skip: En esta sección se incluyen los tokens que se van a ignorar durante el análisis, por ejemplo, los espacios o saltos de línea.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      /** Lexico */
      SKIP : {
      " "
      | "\t"
      | "\r"
      | "\n"
      }

      TOKEN : {
      <NUMERO: (["0"-"9"])+>
      | <DECIMAL: (["0"-"9"])+"."(["0"-"9"])+>
      | <EVALUAR: "Evaluar">
      | <PCOMA: ";">
      | <PARENI: "(">
      | <PAREND: ")">
      | <CORI: "[">
      | <CORD: "]">
      | <MAS: "+">
      | <MENOS: "-">
      | <POR: "*">
      | <DIV: "/">
      }
      /** Fin Lexico */
  • Sección para definición sintáctica: Aquí vamos a definir las producciones para nuestro analizador, estas están definidas como funciones. A continuación explicamos la estructura:

    • Como buena práctica es recomendable agregar en un comentario la producción en formato BNF para que sea más fácil entender la producción actual, ya que las reglas sintácticas en JavaCC pueden ser un poco confusas.
    • La definición de un método incluye:

      1
      2
      3
      <TIPO> <NOMBRE> () : 
      {Sección para código de java, generalmente para declaraciones}
      {Producciones, estas pueden incluir notación de expresiones regulares}
    • Si quisiéramos invocar a otra producción, agregamos su llamada a método y para obtener su valor lo hacemos de la siguiente manera

      1
      2
      3
      4
      5
      6
      /** Instruccion -> evaluar [ Expresion ]; */
      void Instruccion() :
      {double e;}
      {
      <EVALUAR> <CORI> e=Expresion() <CORD> <PCOMA> {System.out.println("El valor de la expresion es: "+e);}
      }
    • En tal caso necesitásemos obtener el valor de un terminal, debemos utilizar el atributo image, ya que cada terminal es un objeto de tipo Token, para obtenerlo hacemos lo siguiente

      1
      2
      3
      4
      5
      6
      7
      8
      9
      double Primitivo() :
      {double e;}
      {
      <NUMERO> {return Double.parseDouble(token.image);}
      |
      <DECIMAL> {return Double.parseDouble(token.image);}
      |
      <PARENI> e=Expresion() <PAREND> {return e;}
      }
    • Algo a tomar en cuenta es que, podemos declarar variables de tipo Token y asignarlas al terminal, esto es por si tuviéramos varios terminales en una misma producción y así sepamos diferenciar cada uno.

Compilación de la gramática

Una vez finalizado nuestro archivo Gramatica.jj, vamos a compilar este para generar los archivos necesarios para su ejecución, vamos a utilizar el archivo compilarGramatica.bat creado al inicio. Al ejecutar el archivo veremos lo siguiente:

Como resultado de esto, en nuestro paquete analizador se crearon los siguientes archivos

  • Gramatica.java: Este archivo contiene las funciones de cada no terminal de la sección sintáctica
  • GramaticaConstanst.java: Esta interfaz contiene las constantes de tipo entero que identifican a cada token de nuestro lenguaje y son asignadas a las variables kind.
  • GramaticaTokenManager.java: Se encarga de reconocer los tokens durante el análisis léxico.
  • ParseException.java: Se utiliza para lanzar los errores durante el análisis sintáctico.
  • TokenMgrError.java: Se encarga de manejar los errores léxicos.
  • Token.java: Representa cada token definido en nuestra sección léxica.

Clase Principal

Por último, vamos a invocar a nuestro parser en el método main, para utilizar nuestro parser basta con crear la clase Gramatica y pasar por parámetro nuestro archivo de entrada, luego de crear la instancia invocamos al método inicial que en nuestro caso sería el método analizar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package proyectojavacc;

import Analizador.Gramatica;
import Analizador.ParseException;
import Analizador.TokenMgrError;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author Pavel
*/
public class ProyectoJavaCC {

/**
* @param args the command line arguments
*/
public static void main(String[] args) {
try {
Gramatica parser = new Gramatica(new BufferedReader(new FileReader("./entrada.txt")));
parser.Analizar();
} catch (ParseException e) {
System.err.println(e.getMessage());
} catch (FileNotFoundException e) {
Logger.getLogger(ProyectoJavaCC.class.getName()).log(Level.SEVERE, "Error al intentar leer el archivo.", e);
} catch(TokenMgrError e){
System.err.println(e.getMessage());
}
}

}

Ejecución del archivo de entrada

El archivo que vamos a utilizar debe encontrarse dentro de la carpeta de nuestro proyecto.

Y su contenido es el siguiente:

1
2
3
4
5
Evaluar[1+1];
Evaluar[1+1*2];
Evaluar[-(1+1*6/3-5+7)];
Evaluar[-(1+1*6/3-5+1*-2)];
Evaluar[-(1+1)];

Ejecutamos nuestro programa y vemos la siguiente salida:

Acerca del autor:

Este tutorial fue elaborado por el Auxiliar de Cátedra Pavel Vásquez, como contribución al curso de Organización de Lenguajes y Compiladores 2 de la Universidad de San Carlos de Guatemala.

Fuentes consultadas

Intérprete sencillo utilizando Irony y C#

En este tutorial se desarrolla un intérprete sencillo que permite ejecutar un archivo de entrada que contiene sentencias tales como declaración de variables, sentencias de control, impresiones en consola, etc. El lenguaje de programación fue diseñado especialmente para este ejemplo. El proyecto cuenta con comentarios que explican su funcionamiento.

Las tecnologías a utilizar son:

  • Irony: Generador de analizadores léxicos y sintácticos que retorna un AST (Abstract Syntax Tree).
  • Visual Studio 2017: Entorno de desarrollo integrado utilizado para programar en C#.
  • Windows 10: Sistema Operativo.
  • Irony.dll: DLL que permite la integración de Irony con C#.

El proyecto completo de este ejemplo puede descargarse del siguiente enlace:

Si desean una pequeña introducción a Irony pueden revisar el post:

En el que se explica paso a paso como utilizar esta herramienta.

El lenguaje de entrada

Dentro de la carpeta del proyecto podremos acceder a /input y allí encontraremos un archivo de entrada llamado “entrada.txt”, en él se muestran ejemplos de todas las funciones del lenguaje diseñado para esta aplicación, al leerlo se puede tener una idea clara de las funciones con las que el lenguaje cuenta, este archivo contiene lo siguiente:

  • Comentarios simples, es decir de una sola línea (//)
  • Comentarios múltiples, es decir de más de una línea (/ /)
  • Concatenación de cadenas, mediante el operador &
  • Función Imprimir: Recibe como parámetro una cadena e imprime su valor en consola.
  • Declaración de variables: Únicamente se acepta definición de variables de tipo numero incluyendo enteros y decimales.
  • Asignación de variables: A cualquier variable se le puede asignar cualquier expresión que tenga como resultado un número.
  • Instrucción Mientras: Tiene el comportamiento clásico del ciclo while, ejecuta el ciclo mientras la expresión booleana que recibe sea verdadera. Esta instrucción soporta anidamiento.
  • Instrucción If e If-Else: Tiene el comportamiento clásico de las sentencias de selección If e If-Else, evalúa la expresión booleana y ejecuta el bloque de instrucciones en If si es verdadera. En caso contrario y si existe un bloque Else se ejecuta este bloque de instrucciones. Estas instrucciones también soportan anidamiento.
  • Expresiones aritméticas: Se soportan las expresiones aritméticas binarias: suma, resta, multiplicación y división. También la expresión unaria: negación. Adicionalmente se soporta expresiones agrupadas en paréntesis. Se maneja la precedencia habitual de las expresiones aritméticas.
  • Expresiones booleanas: Comparan dos expresiones que tengan como resultado un número y soportan unicamente los operados mayor que y menor que (<, >).

El resultado de la ejecución
Al ejecutar la entrada mostrada en nuestro ejemplo, esta fue la salida obtenida:

Tabla de símbolos
La tabla de símbolos es una parte importante en el proceso de ejecución del código, es en esta estructura de datos en donde guardamos información de las variables como su tipo, identificador y valor. En esta estructura podemos agregar variables, modificar los valores de las variables existentes, así como obtener sus valores.

Entornos

El manejo de entornos es sumamente importante ya que deberíamos de crear un nuevo entorno por cada alcance, de manera que los entornos superiores no tengan acceso a las variables declaradas en entornos inferiores pero los entornos inferiores puedan acceder tanto a sus variables como a las de los entornos superiores, esto funciona de manera muy similar a una pila, ya que el ultimo entorno creado debería ser el primero en ser eliminado.

En este ejemplo, esto se logra mediante el creando una tabla local para cada sentencia ejecutada que posea un ámbito propio, como el If, While, etc. Luego de crear la tabla local se agregan todos los símbolos de la tabla del ámbioto padre y se utiliza esta tabla local como tabla principal, al terminar de ejecutar la sentencia esta tabla local desaparece, pues fue declarada dentro de la sentencia que se ejecuta.

Árbol de análisis abstracto AST

Un árbol de sintaxis abstracta (AST) es una representación simplificada de la estructura sintáctica del código fuente. A nivel de programación un AST es una estructura de datos que se genera durante el proceso de análisis sintáctico.
En el código de Irony lo vamos armando por medio de listas de instrucciones, donde cada sentencia es una instrucción y en el bloque contenido en esta sentencia tendríamos otra lista de instrucciones, armando así un árbol en donde cada nodo es un objeto que implementa la interfaz instrucción y puede contener múltiples hijos que serían otros objetos que implementan la interfaz instrucción, que serían otras instrucciones.

El código de nuestro proyecto está organizado en dos paquetes:

  • analizador: que contiene los archivos de Irony.
  • arbol: que contiene todas las clases que forman parte del AST, que se utiliza como estructura primaria en la aplicación.

Teniendo únicamente una clase afuera que seria la clase principal de la aplicación Program.cs.

Acerca del autor:

Este tutorial fue elaborado por el Auxiliar de Cátedra Julio Arango, como contribución al curso de Organización de Lenguajes y Compiladores 2 de la Universidad de San Carlos de Guatemala.

Fuentes consultadas:

  • Compiladores, principios, técnicas y herramientas. Aho, Lam, Sethi y Ullman. Segunda Edición.
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×