Introducción
Un compilador típico realiza los siguientes pasos:
- Análisis: el texto fuente se convierte en un árbol de sintaxis abstracta (AST).
- Resolución de referencias a otros módulos (C pospone este paso hasta el enlace).
- Validación semántica: eliminar declaraciones sintácticamente correctas que no tienen sentido, por ejemplo, código inalcanzable o declaraciones duplicadas.
- Transformaciones equivalentes y optimización de alto nivel: el AST se transforma para representar una computación más eficiente con la misma semántica. Esto incluye, por ejemplo, el cálculo temprano de subexpresiones comunes y expresiones constantes, eliminando asignaciones locales excesivas (ver también SSA ), etc.
- Generación de código: el AST se transforma en código lineal de bajo nivel, con saltos, asignación de registros y similares. Algunas llamadas de función pueden estar en línea en esta etapa, algunos bucles desenrollados, etc.
- Optimización de mirilla: el código de bajo nivel se escanea en busca de ineficiencias locales simples que se eliminan.
La mayoría de los compiladores modernos (por ejemplo, gcc y clang) repiten los últimos dos pasos una vez más. Utilizan un lenguaje intermedio de bajo nivel pero independiente de la plataforma para la generación inicial de código. Luego, ese lenguaje se convierte en código específico de la plataforma (x86, ARM, etc.) haciendo aproximadamente lo mismo de una manera optimizada para la plataforma. Esto incluye, por ejemplo, el uso de instrucciones vectoriales cuando sea posible, la reordenación de instrucciones para aumentar la eficiencia de predicción de ramales, etc.
Después de eso, el código objeto está listo para vincular. La mayoría de los compiladores de código nativo saben cómo llamar a un enlazador para producir un ejecutable, pero no es un paso de compilación per se. En lenguajes como Java y C #, la vinculación puede ser totalmente dinámica, realizada por la máquina virtual en el momento de la carga.
Recuerda lo básico
- Hazlo funcionar
- Hazlo hermoso
- Hazlo eficiente
Esta secuencia clásica se aplica a todo el desarrollo de software, pero conlleva repetición.
Concéntrese en el primer paso de la secuencia. Crea la cosa más simple que podría funcionar.
¡Lee los libros!
Lea el Libro del Dragón de Aho y Ullman. Esto es clásico y todavía es bastante aplicable hoy.
El diseño moderno del compilador también es alabado.
Si estas cosas son demasiado difíciles para usted en este momento, lea primero algunas introducciones sobre el análisis; Por lo general, las bibliotecas de análisis incluyen introducciones y ejemplos.
Asegúrese de sentirse cómodo trabajando con gráficos, especialmente árboles. Estas cosas son las cosas de las que están hechos los programas en el nivel lógico.
Define bien tu idioma
Use la notación que desee, pero asegúrese de tener una descripción completa y coherente de su idioma. Esto incluye tanto la sintaxis como la semántica.
Ya es hora de escribir fragmentos de código en su nuevo idioma como casos de prueba para el compilador futuro.
Usa tu idioma favorito
Está totalmente bien escribir un compilador en Python o Ruby o cualquier idioma que sea fácil para usted. Use algoritmos simples que entienda bien. La primera versión no tiene que ser rápida, eficiente o completa. Solo necesita ser lo suficientemente correcto y fácil de modificar.
También está bien escribir diferentes etapas de un compilador en diferentes idiomas, si es necesario.
Prepárate para escribir muchas pruebas
Todo su idioma debe estar cubierto por casos de prueba; efectivamente será definido por ellos. Conozca bien su marco de prueba preferido. Escribe exámenes desde el primer día. Concéntrese en las pruebas 'positivas' que aceptan el código correcto, en lugar de la detección de código incorrecto.
Ejecute todas las pruebas regularmente. Arregle las pruebas rotas antes de continuar. Sería una pena terminar con un lenguaje mal definido que no puede aceptar un código válido.
Crea un buen analizador
Los generadores de analizadores son muchos . Elige lo que quieras. También puede escribir su propio analizador de cero, pero sólo vale la pena si la sintaxis de la lengua es muerto simple.
El analizador debe detectar e informar errores de sintaxis. Escriba muchos casos de prueba, tanto positivos como negativos; Reutilice el código que escribió al definir el idioma.
La salida de su analizador es un árbol de sintaxis abstracta.
Si su idioma tiene módulos, la salida del analizador puede ser la representación más simple del 'código objeto' que genera. Hay muchas formas simples de volcar un árbol en un archivo y volver a cargarlo rápidamente.
Crear un validador semántico
Lo más probable es que su lenguaje permita construcciones sintácticamente correctas que pueden no tener sentido en ciertos contextos. Un ejemplo es una declaración duplicada de la misma variable o pasar un parámetro de un tipo incorrecto. El validador detectará tales errores mirando el árbol.
El validador también resolverá las referencias a otros módulos escritos en su idioma, cargará estos otros módulos y los usará en el proceso de validación. Por ejemplo, este paso se asegurará de que el número de parámetros pasados a una función desde otro módulo sea correcto.
Nuevamente, escriba y ejecute muchos casos de prueba. Los casos triviales son tan indispensables en la resolución de problemas como inteligentes y complejos.
Generar codigo
Usa las técnicas más simples que conoces. A menudo está bien traducir directamente una construcción de lenguaje (como una if
declaración) a una plantilla de código ligeramente parametrizada, a diferencia de una plantilla HTML.
Nuevamente, ignore la eficiencia y concéntrese en lo correcto.
Apunte a una VM de bajo nivel independiente de la plataforma
Supongo que ignoras las cosas de bajo nivel a menos que estés muy interesado en los detalles específicos del hardware. Estos detalles son sangrientos y complejos.
Sus opciones:
- LLVM: permite la generación eficiente de código de máquina, generalmente para x86 y ARM.
- CLR: objetivos .NET, principalmente basados en x86 / Windows; tiene un buen JIT.
- JVM: apunta al mundo Java, bastante multiplataforma, tiene un buen JIT.
Ignorar optimización
La optimización es difícil. Casi siempre la optimización es prematura. Generar código ineficiente pero correcto. Implemente todo el lenguaje antes de intentar optimizar el código resultante.
Por supuesto, las optimizaciones triviales están bien para introducir. Pero evite cualquier astucia y cosas peludas antes de que su compilador sea estable.
¿Y qué?
Si todo esto no es demasiado intimidante para usted, ¡proceda! Para un lenguaje simple, cada uno de los pasos puede ser más simple de lo que piensas.
Ver un 'Hola mundo' de un programa que creó su compilador podría valer la pena.