This repository has been archived on 2026-03-14. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Tiger/README.md
Juan Carlos Pujol Mainegra 5a8ddf6978 First public commit!
2017-08-18 03:07:43 -05:00

503 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
TL;DR
=====
- **YATC** (Yet Another Tiger Compiler) is a **Tiger** compiler that generates
**.Net framework** managed binaries.
- YATC was made in 2014 as part of an undergrad Computer Science project by
**Damian Valdés Santiago** and **Juan Carlos Pujol Mainegra**.
- **Tiger** is a relatively simple and imperative language that features
nested functions, records, arrays, integral variables and strings.
- Tiger was defined by **Andrew Appel** in 1998 for *Modern Compiler
Implementation in Java* (Cambridge University Press, 1998)
- This report will explain the architecture and implementation of a Tiger
compiler using **ANTLR 3.3.4** for the grammar definition, **C\# 5.0** in .Net
Framework 4.5 for the construction of the Abstract Syntax Tree (AST) and the
Dynamic Language Runtime (DLR) for the generation of the CIL/MSIL (Common
Intermediary Language/Microsoft Intermediary Language) code.
- **YATC** se presenta *tal cual* y no es mantenido de ninguna forma por sus
autores.
Resumen
-------
- **YATC** (Yet Another Tiger Compiler) es un compilador de **Tiger** que
genera binarios gestionados para **.Net framework**.
- YATC fue realizado en 2014 como parte de un proyecto de pregrado de Ciencias
de la Computación por **Damian Valdés Santiago** y **Juan Carlos Pujol
Mainegra**.
- **Tiger** es un lenguaje relativamente sencillo e imperativo con
funciones anidadas, records, arrays y variables enteras y de tipo cadena
*string*.
- Tiger fue definido por **Andrew Appel** en 1998 para *Modern Compiler
Implementation in Java* (Cambridge University Press, 1998)
- En este informe se explicará la arquitectura e implementación de un
compilador para el lenguaje Tiger usando **ANTLR 3.3.4** para la definición de
la gramática, **C\# 5.0** en .Net Framework 4.5 para la construcción del
*Árbol de Sintaxis Abstracta* (AST) y *Dynamic Language Runtime* (DLR) para la
generación de código CIL/MSIL (Common Intermediary Language/Microsoft
Intermediary Language).
- **YATC** se presenta *tal cual* y no es mantenido de ninguna forma por sus
autores.
License
=======
Yet Another Tiger Compiler (YATC)
Copyright (C) 2014 Damian Valdés Santiago, Juan Carlos Pujol Mainegra
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Sobre el proceso de compilación
===============================
YATC se inscribe en el clásico proceso para compilar un programa,
dividiendo el mismo en cuatro fases en relación de dependencia:
- **Análisis Lexicográfico**: identifica subsecuencias de caracteres
significativas separando el flujo de entrada en estructuras lógicas
llamadas *tokens*. Tokens podrían ser identificadores, palabras
claves, operaciones, etc. del lenguaje que se desea implementar. La
componente de software que realiza esta operación se llama *lexer*.
En resumen, el lexer transforma una cadena de caracteres en una
secuencia de tokens.
- **Análisis sintáctico**: garantiza que la secuencia de tokens tenga
un sentido según las especificaciones del lenguaje, construyendo un
árbol de derivación (DT), que es una representación de las
secuencias de derivaciones de la gramática, para obtener un programa
en Tiger. La componente de software que realiza esta operación se
llama *parser* En resumen, el parser recibe como entrada una
secuencia de tokens y emite un AST como resultado del proceso.
- **Análisis o chequeo semántico**: verifica que en el lenguaje se
tengan sentencias con sentido obteniéndose un AST. En resumen, se
transforma de un DT a un AST.
- **Generación de código**: el AST se convierte en un árbol de
sintaxis concreta y se genera código para las instrucciones del
programa en cuestión.
####
Cuando se compila un programa en Tiger se intenta transformar el mismo
en una secuencia de tokens válida para Tiger, de lo contrario se produce
un error sintáctico. Luego, se construye el AST correspondiente según la
jerarquía de clases implementadas y el patrón *TreeAdaptor* de ANTLR que
conecta la fase anterior con la actual (chequeo semántico). Se verifica
que no existan problemas y se pasa a la generación de código. De lo
contrario se reporta error semántico y se detiene el proceso de
compilación.
Herramientas para la implementación
===================================
En la implementación de YATC se utilizaron los siguientes softwares y
lenguajes:
- ANTLR, ANTLWorks y JRE para la definición de la gramática de Tiger
(entiéndase reglas del lexer y del parser), su prueba y proceso de
*debug* para la detección de fallas.
- Lenguaje .NET C\# en Microsoft Visual Studio 2012 para la jerarquía
de nodos del AST, la realización del chequeo semántico y la
generación de código a través de DLR cuya elección se justificará
más adelante.
- PeVerify suministrado por el SDK de .Net para la corrección del
ensamblado.
- TigerTester 1.2 para la realización de pruebas.
Análisis lexicográfico y sintáctico
===================================
Comentarios anidados
--------------------
Para lograr el anidamiento de los comentarios, primero se crearon las
reglas del lexer que se muestran a continuación continuación:
```antlr
fragment BEGIN_COMMENT : '/*';
fragment END_COMMENT : '*/' ;
COMMENTARY : BEGIN_COMMENT ( options {greedy=false;} : COMMENTARY | . )* 
       END_COMMENT {$channel = Hidden;};
```
Esta regla es engañosa: al probar un código con comentarios anidados
como el que se muestra no se produce error (lo cual es correcto), sin
embargo al crear un código con comentarios anidados y desbalanceados, no
se produce tampoco error y esto sí es incorrecto desde el punto de vista
sintáctico.
```antlr
/* Comment1 /* Comment2 */ Comment3 */
```
El comportamiento por defecto de ANTLR al intentar realizar *match* a
una secuencia es consumirla entera. En ocasiones (como la que se
discute) esto no se desea. Este propósito se logra con *options
{greedy=false;}*. El operador *wildcard* ($\cdot$) representa a
cualquier caracter y por tanto, aunque se coloque en la regla `options
{greedy=false;} : COMMENTARY | $\cdot$`, ANTLR no es capaz de
diferenciar que el `COMMENTARY` no debe estar incluido en lo reconocido
por el operador *wildcard*.
Luego de notar estos errores se redefinen las reglas, obligando a que se
consuman caracteres hasta encontrar un bloque de comentarios seguido de
cualquier secuencia de caracteres.
```antlr
COMMENTARY : BEGIN_COMMENT ( options {greedy=false;} : . )* 
       (COMMENTARY ( options {greedy=false;} : . )* )* 
       END_COMMENT {$channel = Hidden;}
```
Los programa vacíos también son programas
-----------------------------------------
Un programa en Tiger es una expresión. Un programa que no tenga código
escrito también debe reconocerse y no debe presentar ningún tipo de
error. Esto se logra permitiendo que la regla principal, que produce una
expresión, permita que esta no esté a través del operador el operador
$?$.
```antlr
public a_program : expr ? EOF;
```
`a = b = c` es ilegal
---------------------
Sea la siguiente regla del parser:
```antlr
relational_expr : arith_expr ((EQ | NOTEQ | GT | LT | GTEQ | LTEQ ) arith_expr)?;
```
El operador `?` al final de la regla le da la no asociatividad requerida
a los operadores de comparación (notar que en `arith_expr` y
`term_expr` se usa `*` para anidar estas expresiones en una lista). Con
esta regla no se permite `a = b = c`, pero si `a = (b = c)` como está
especificado.
Ayudando al chequeo semántico del for
-------------------------------------
Según las especificaciones del lenguaje Tiger, en un for solo se pueden
evaluar condiciones que retornen valor, por ello se exige en la regla
correspondiente que la primera y segunda expresiones sean
`disjunction_expr`.
```antlr
for_expr : FORKEY ID ASSIGN disjunction_expr TOKEY disjunction_expr DOKEY expr;
```
Reescritura de reglas
---------------------
La mayoría de las reglas del parser se reescriben creando nodos
“ficticios” en la sección tokens que se usarán para generar nodos
homólogos del AST a través del TreeAdaptor. Un ejemplo representativo es
la creación de un token para el operador de negación (menos unario).
Esto permite además eliminar del DT del programa, tokens innecesarios
para la próxima fase, como los paréntesis, las palabras claves, las
comas, los punto y coma, etc.
Análisis semántico
==================
Jerarquía de nodos del AST
--------------------------
Para que el TreeAdaptor funcione correctamente, se crean las clases
correspondientes a los nodos que desean comprobarse semánticamente. Cada
nodo tiene, además de sus propiedades estructurales, dos métodos
```csharp
public abstract void CheckSemantics(TigerScope scope, Report report);
internal abstract void GenerateCode(ModuleBuilder moduleBuilder);
```
El primero (fundamental en esta fase) chequea la semántica del nodo en
cuestión, siguiendo un patrón de gramática atributada: un nodo está bien
si sus hijos lo están y la información brindada por sus hijos o hermanos
en la jerarquía no produce error en su funcionamiento. Estas
informaciones se modelan a través de las clases herederas de
`TigerInfo`, que dejan información conectada para la generación de
código, p.e *bindings* de variables, información relativa a la funciones
(nombre, tipo y cantidad de parámetros), etc.
Notar que el nodo `UnknownNode` se usa para homologar tokens que, si
bien no introducen error en el chequeo semántico, ayudan en la
comprensión y organización del AST. Ejemplos de token que se mapean como
UnknownNode son `FIELDS_INST`, `PARAM_DECL`, `FUN_TYPE_WRAPPER`,
`DECL_BLOCK` y `ARGS_FIELDS`.
Scope
-----
Al usar una variable, un `type` o una función, ¿cómo se sabe si fueron
declaradas antes? Esta información se obtiene de la clase `TigerScope`
que almacena nombres, con sus respectivos objetos, al que se refieren
(variables, *types* o funciones) como una tabla de símbolos. Además,
esta clase permite, mediante un árbol de scopes, anidar funciones y
expresiones `let`, de forma que solo pueda tenerse acceso a las
variables o funciones que están declaradas en ese ámbito o en el ámbito
del nodo padre. Esta clase además se encarga del binding de las
variables. Esta estructura permite que, en caso de que en un scope
interno se cree un `type` o variable de igual nombre que un `type` o
variable de un scope ancestro del actual, entonces se reescriba el valor
antiguo del `type` o variable por el nuevo y se lanzará una advertencia.
Nil
---
En el chequeo semántico de la expresión `if cond_expr then then_expr
else else_expr` se realizan las siguientes comprobaciones. Se realiza
el chequeo semántico de `cond_expr` y luego el de `then_expr`. Si
ambas expresiones están correctas se comprueba que la `cond_expr` tenga
algún valor (no sea `void`) y luego se comprueba que este valor sea
entero. De lo contrario se lanzan errores. En el caso que exista parte
`else` el chequeo es más arduo. Primero se chequea que el nodo
`else_expr` esté correcto. Luego se verifica que el tipo de
`else_expr` sea asignable, o sea, `int`, `string`, `record`, `array`.
De lo contrario, se reporta error. En este punto es donde se chequean
los casos críticos del `nil` expuestos en las especificaciones de Tiger.
Secuencia de declaraciones de tipo
----------------------------------
La construcción y verificación de las secuencias de declaraciones de
tipos dentro de un `let` se realiza mediante tres pasadas a cada uno de
los nodos hijos de cada una de ellas, dígase a los `TypeDeclNode` que
están contenidos dentro de cada `TypeDeclSeqNode`, encargado de agrupar
las mismas. Esto se consigue mediante la regla del parser
```antlr
decl : type_decl+ | var_decl | fun_decl+;
```
Esta regla está contenida dentro de un iterador + **EBNF** (*Extended
Backus-Naur Form*), leyéndose finalmente
```antlr
let_expr : LETKEY decl+ INKEY expr_seq ENDKEY;
```
Aunque esta última pareja constituya una ambigüedad, se usa a propósito
para realizar dicha agrupación, de forma análoga a la selección de a qué
`if` pertenece un determinado `else`, esto es, al de mayor anidamiento.
Esto facilita sobremanera cualquier operación posterior que se realice
sobre los conjuntos de declaraciones, ya sean de tipos o de funciones.
Las variables se tratan como conjuntos *singleton*.
Dada una declaración de tipo
```antlr
type_decl : TYPEKEY ID EQ type;
```
se le dice parte izquierda o cabeza al token ID, que corresponde al
nombre del tipo que se quiere declarar. La parte derecha corresponde a
la regla `type`, que puede ser `type_id`, `array_decl` o
`record_decl`.
Los pasos son los siguientes:
1. Se agregan las cabezas de los tipos, es decir, la parte izquierda de
la declaración de cada uno de ellos, con el sólo propósito de comprobar
que no exista una redeclaración dentro de una secuencia, o de forma
local y global, agregándolo a una estructura que contiene cada
`TypeDeclNode` como llave del nombre del tipo. Este paso incluye
detectar renombramientos de los nombres de los tipos primitivos como
tipos de usuario, esto es, no cambiar a `int` o `string`, por ejemplo.
En este paso se llama al `CheckHeader(TigerScope scope, Report report,
string name)` de cada hijo.
2. Para detectar eventuales ciclos es necesario construir un grafo
dirigido $G \langle V,E \rangle$ con
$$V = \{ \text{nodos de declaración de tipos} \}$$
$$E = \{ (u,v) | ParteIzquierda(u) \and BaseType(u, v) \in { Alias V Array } \and ParteDerecha(v), u, v \in V \}$$
Dicha construcción asegura que las aristas se establezcan sólo entre
nodos de declaraciones de tipos que estén conectados como parte
izquierda y derecha del *statement*, que sea de base `Array` o `Alias`, es
decir, un tipo cuyo tipo, valga la redundancia, sea alguno de los
anteriores. Estos se agregan a una estructura
`Dictionary<TypeDeclNode, Node<TypeDeclNode>>`
donde `Node<T>` es una representación de nodos en un
grafo, con soporte para realizar operaciones de orden topológico además
de detectar ciclos y su composición. Esta estructura permite conectar
los `TypeDeclNode` en el grafo con aquellos otros de la secuencia que se
requiera, y evitar repetir nodos en el grafo que correspondan a una
misma declaración. Esto podría suceder dado que un mismo tipo puede
estar varias veces en parte derecha, y a lo sumo una vez en parte
izquierda si se ha llegado a este paso (el anterior elimina que esté más
de una vez). De esta forma no se tenderá una arista entre un tipo
`aliasInt` que sea alias de `int`, dado que la parte derecha no estará
en primer lugar en la secuencia, y por tanto no puede referenciar hacia
adelante, de forma que no hay peligro de recursión cíclica.
3. Al grafo construido se le realiza un DFS (*Depth-First Search*) con
marcado de nodos, en los que se detecta una arista back-edge, o de
retroceso, si se en la navegación se llega a un nodo marcado como gris
\[ver *Intro To Algorithms*, Cormen et al., alrededor de la 614\]. La prueba
del Teorema 22.12 acompaña el resultado. Si existe un ciclo se detecta
sus nodos componentes y se imprime. De no existir, se recorre el orden
topológico de los nodos resultantes, cuyo valor contenido son las
declaraciones de nodos, y se llama a `CheckSemantics` de cada uno en
dicho orden. La generación de código también se realiza por dicho orden.
Esto evita tener que realizar múltiples búsquedas si para construir
finalmente un tipo, aunque se conozca que está correcto.
####
El último paso, detectar un ciclo, y después navegar por el orden
topológico dado, devuelto en forma de lista enlazada, es vital para la
realización de las operaciones de construcción de tipos, que se realizan
de forma transparente para los hijos, los cuales no requieren búsquedas
adicionales para la determinación completa de los mismos, como se había
dicho. Para ellos se habilita que los nodos que así lo requieran no
contengan una referencia directa al tipo, dado que en el momento de
declaración y comprobación semántica, aunque este sea correcto, puede no
estar determinado, sencillamente porque es el tipo de un record. Uno de
estos casos se analiza a continuación.
####
Supongamos que deseamos comprobar y construir los tipos del siguiente
programa:
```antlr
let
type a = b
  type b = { x: c }
  type c = a
in
end
```
El programa está correcto, dado que los tipos `a`, `b` y `c` están bien
determinados por un ciclo que pasa por al menos un tipo record (`b`). El
tipo básico de `a` es el mismo que el de `c`, que es alias. El orden
topológico podrá arrojar una sola forma de navegarlo para la
construcción. Sea esta $b \rightarrow a \rightarrow c$. Al comprobar
semánticamente a `b` el tipo no está bien determinado, dado que tiene un
campo `x` de un tipo desconocido `c`, pero se conoce que la declaración
esta correcta, dado que `c` existe y está correcto, por lo que `b` está
bien determinado. Su definición, no obstante, no es completa. Por lo
tanto se almacena una referencia a la referencia a la definición del
mismo, de forma que esta pueda se alterada, es decir se puede completar,
y todos los nodos que dependían de esta queden bien.
####
Finalmente los tipos alias no se analiza semánticamente, es decir, son
indistinguibles las definiciones originales de `a`, `b` y `c`, a efectos
del resto del programa.
Generación de código
====================
La generación de código es el proceso mediante el cual un compilador
convierte la información proveniente de las fases anteriores en código
que puede ser ejecutado por una computadora. La ejecución del código
generado la realiza, en este compilador, el *Common Language Runtime*
(CLR) de la plataforma .NET. El CLR usa una estructura basada en un
modelo de pila para modelar la ejecución de código, así como un lenguaje
ensamblador propio llamado *Intermediate Language* (IL). Nuestro
compilador utiliza una componente de la biblioteca de clase de la
plataforma .NET llamada `System.Reflection.Emit` que permite crear un
ejecutable en .NET y agregarle código según se desee; y para generar
código IL se usa *Dynamic Language Runtime* (DLR).
¿Por qué usar DLR?
------------------
El DLR proporciona servicios para implementar lenguajes dinámicos sobre
el CLR. DLR facilita el desarrollo de lenguajes para ejecutar programas
sobre .NET Framework. Los lenguajes por el tipo del objeto que los
representa en tiempo de ejecución, que se especifica en tiempo de diseño
en un lenguaje estáticamnete tipado como C\#.
DLR permite a los programadores implementar lexers, parsers,
analizadores semánticos y generadores de código y la generación de
código se realiza de forma declarativa mediante árboles de expresiones
lo que hace más fácil esta etapa.
Funciones recursivas
--------------------
En la generación de código de las secuencias de funciones se realizan
tres etapas.
1. Se recorren todos las definiciones (`headers`) de las funciones en
la secuencia y se introducen en una lista de `ParameterExpression`
que después se le pasará a la segunda fase.
2. Por cada llamado a la propia función (recursión) o a otra, se crea
una `Expression` ficticia en el que luego se sustituirá el código
del llamado a la función.
3. Se recorren las declaraciones de funciones en la secuencia y se le
asigna a la `Expression` ficticia el código del llamado a la función
correspondiente.
Para más información
====================
- Edwards, Stephen - *Tiger Language Reference Manual*, Columbia
University.
- Appel, Andrew and Ginsburg, Maia - *Modern Compiler Implementation
in C*, Press Syndicate of the University of Cambridge, 1998.
- Parr, Terrence - *The Definitive ANTLR Reference*, The Pragmatic
Programmers, May 2007.
- Parr, Terrence - *The Definitive ANTLR 4 Reference*, The Pragmatic
Programmers, Jan 2013.
- Parr, Terrence - *Language Implementation Patterns*, The Pragmatic
Programmers, 2010.
- Wu, Chaur - *Pro DLR in .NET 4*, Apress, 2010.
- Chiles, Bill - *Expression Trees v2 Spec* (DRAFT).
- Cormen, Thomas *et al.* - *Introduction to algorithms*, 3rd ed,
Massachusetts Institute of Technology, 2009.
- [https://docs.microsoft.com/](https://docs.microsoft.com/ "MSDN")