Archive for October, 2008
Reescribiendo el Frontend y el Backend
Posted by Urriellu in Pigmeo Compiler on October 29th, 2008
Tras mucho pelearme con la estructura interna del compilador durante meses, he decidido reescribir gran parte de Pigmeo Compiler.
Hasta ahora, el proceso de compilación seguía más o menos el siguiente esquema:
Se leía el programa del usuario (.exe que contiene CIL bytecodes) utilizando Mono.Cecil (una capa de abstracción sobre System.Reflection), se buscaban todas sus referencias, se creaba un nuevo ejecutable de .NET (.exe) desde cero, al que llamamos bundle, que contenía solamente las partes útiles del programa del usuario junto con las partes útiles de las librerías a las que se hacía referencia (es decir, las partes necesarias por el programa del usuario), después se añadía un CustomAttribute al bundle para indicar la arquitectura para la que estábamos compilando, el bundle era modificado directamente mediante reflexión para ser optimizado, y finalmente el Backend procesaba el código CIL contenido en el bundle buscando “conjuntos de instrucciones” que representasen operaciones conocidas (por ejemplo la instrucción ldc.i4 seguida de stsfld indicaría que se está almacenando un valor constante en una variable estática) para generar finalmente el código en lenguaje ensamblador.
Este proceso había sido diseñado de esta manera, centrándose en trabajar sobre ejecutables de .NET y siendo CIL el código intermedio utilizado internamente por el compilador, para ser simple y corto, sin necesidad de inventar nuevos lenguajes intermedios. No obstante, generar ejecutables de .NET desde cero y modificarlos es mucho más complicado de lo que parece, incluso utilizando librerías como Cecil que facilitan la tarea. Casi haría falta un MBA en .NET y C#, aunque con paciencia también se puede lograr.
Por lo tanto, durante los últimos meses he estado reescribiendo grandes partes de Pigmeo Compiler y, sobre todo, añadiendo capas de abstracción y pasos intermedios.
La aplicación del usuario sigue siendo leída utilizando Cecil, pero ahora existe una capa de abstracción sobre Cecil que se encuentra en el namespace Pigmeo.Internal.Reflection, y permite reflejar ejecutables de .NET (sólo lectura) de manera sencilla e incluye características no soportadas por System.Reflection ni por Mono.Cecil, además de más características especializadas que, aunque no tienen relación directa con .NET, son útiles de cara al proceso de compilación. Por ejemplo, podemos reflejar un archivo y una propiedad nos devuelve una referencia al objeto que representa la librería de dispositivo que ese programa está usando; también podemos saber fácilmente la arquitectura o microcontrolador para los que está diseñada la aplicación, o podemos desensamblar de manera sencilla todo el programa, o clases o métodos individuales (se utiliza especialmente para depurar el compilador).
Tras reflejar el programa del usuario (usando Pigmeo.Internal.Reflection), en lugar de generar el bundle con CIL bytecodes como se hacía antes, ahora se convierte el programa a PIR (Pigmeo Intermediate Representation) que, como su nombre indica, es una representación intermedia. Normalmente esto se hace con lenguajes intermedios, pero realmente no necesitamos un lenguaje, sólo una jerarquía de clases que representen el programa, así que no existe una sintaxis “oficial” para convertir PIR a string, aunque sí que están implementadas todas las funciones para poder mostrar por pantalla el programa una vez convertido a PIR (y, como en el caso de P.I.Reflection, se utiliza sobre todo para depurar el compilador).
PIR está diseñado para ser fácilmente manipulable (y fácil de comprender al mostrarlo en pantalla), de manera que podamos hacer múltiples cambios al programa y, sobre todo, diversas optimizaciones sin romper la lógica original del programa ni complicarnos con Cecil ni System.Reflection.
Una vez convertido el programa a PIR, éste es optimizado de la misma manera para todas las arquitecturas, tras lo cual se envía el PIR optimizado al Backend. El Backend estaba diseñado para “recibir” CIL bytecodes (del bundle) por lo que también ha sido reescrito para poder manejar PIR. En la primera fase del Backend se lee la arquitectura de destino del programa y se envía el PIR al Backend especializado en dicha arquitectura. Allí, el PIR vuelve a ser optimizado y modificado específicamente para la arquitectura de destino, y una vez preparado, ya puede convertirse el PIR a lenguaje ensamblador.