La evolución de los lenguajes de programación que se ejecutan en máquinas virtuales existentes, como la BEAM de Erlang o los runtimes de JavaScript, a menudo implica un trade-off entre la reutilización de la infraestructura del ecosistema y la optimización del rendimiento del compilador. Gleam, un lenguaje con tipado estático para la BEAM y JavaScript, aborda este desafío en su versión 1.19.0 mediante una estrategia de compilación intermedia. Al pasar de generar código fuente Erlang a generar 'Erlang abstract forms', Gleam busca mejorar significativamente los tiempos de compilación y la calidad de la información de depuración, sin incurrir en la complejidad y el alto costo de mantener un generador de bytecode BEAM directo.
Este enfoque resuena con el principio de 'compilación a IR' (Intermediate Representation), una técnica fundamental en la ingeniería de compiladores que permite desacoplar las fases de frontend (análisis léxico, sintáctico, semántico) y backend (generación de código, optimización) de un compilador. Al generar una IR bien definida, Gleam puede aprovechar las optimizaciones y la estabilidad del compilador de Erlang existente, mientras mantiene el control sobre la semántica y las características de su propio lenguaje. La decisión de apuntar a 'abstract forms' en lugar de bytecode BEAM directo es un ejemplo pragmático de cómo los proyectos con recursos limitados pueden lograr mejoras sustanciales en el rendimiento y la experiencia del desarrollador, priorizando la sostenibilidad y la interoperabilidad con ecosistemas maduros.
Arquitectura del Sistema
El cambio central en Gleam v1.19.0 es la reescritura del generador de código para Erlang. Anteriormente, Gleam generaba código fuente Erlang (.erl), que luego era procesado por el compilador de Erlang. Ahora, el compilador de Gleam genera directamente 'Erlang abstract forms', una representación intermedia en formato de árbol anotado que el compilador de Erlang utiliza internamente después de sus fases de tokenización y parsing. Estas 'abstract forms' tienen una codificación binaria usando el 'Erlang external term format', lo que permite cargarlas directamente en la BEAM, omitiendo la primera mitad del compilador de Erlang.
Esta arquitectura de compilación intermedia ofrece varios beneficios. Primero, reduce los tiempos de construcción al evitar la sobrecarga de parsing del código fuente Erlang. Segundo, mejora la precisión de los metadatos de ubicación (líneas de código) en los stacktraces y reportes de crash de la BEAM, ya que ahora apuntan directamente al código fuente de Gleam, no al Erlang generado. Tercero, la decisión de no generar bytecode BEAM directamente se basa en la naturaleza mutable de este último; el bytecode BEAM evoluciona con cada nueva versión de la máquina virtual, lo que requeriría un esfuerzo constante de mantenimiento y sincronización con los desarrolladores de Erlang, un costo insostenible para un proyecto comunitario. En cambio, las 'abstract forms' son una interfaz más estable. Para JavaScript, se han implementado optimizaciones en la generación de código para expresiones 'case' (pattern matching), que ahora producen código JavaScript más plano con menos ramas y variables intermedias, y para literales de lista cortos, que se construyen directamente usando funciones 'prepend' en lugar de convertir un array JavaScript a una lista inmutable de Gleam.
Flujo de Compilación de Gleam a Erlang (Nuevo)
- 1 Código Fuente Gleam El desarrollador escribe código en Gleam.
- 2 Compilador Gleam Procesa el código fuente Gleam.
- 3 Generador de Código Erlang Genera 'Erlang abstract forms' (IR binaria).
- 4 Cargador BEAM Carga directamente las 'abstract forms' en la Erlang VM.
- 5 Erlang VM (BEAM) Ejecuta el código compilado.
| Capa | Tecnología | Justificación |
|---|---|---|
| compute | Gleam Compiler | Componente principal que traduce el código fuente Gleam a formatos ejecutables para Erlang/BEAM y JavaScript. |
| compute | Erlang Abstract Forms | Representación intermedia binaria para el compilador de Erlang, usada como target de compilación para Gleam. vs Erlang Source Code (.erl), BEAM Bytecode Directo |
| compute | Erlang/OTP (BEAM) | Máquina virtual de Erlang, entorno de ejecución para el código Gleam compilado a Erlang. |
| compute | JavaScript Runtimes | Entorno de ejecución para el código Gleam compilado a JavaScript. |
Trade-offs
Ganancias
- ▲ Rendimiento de compilación (Erlang)
- ▲ Precisión de metadatos de depuración (Erlang)
- △ Calidad del código generado (JavaScript)
- ▲ Sostenibilidad del proyecto
Costes
- △ Potencial de optimización máxima (vs. bytecode BEAM directo)
- △ Complejidad de la cadena de herramientas
pub fn go(x) {
case x {
Wibble(1, 2) -> 1
_ -> 2
}
}export function go(x) {
if (isWibble(x) && x[0] === 1 && x[1] === 2) {
return 1;
} else {
return 2;
}
}let numbers = [1, 2, 3]const numbers = prepend(1, prepend(2, prepend(3, empty)))Fundamentos Teóricos
La estrategia de compilación de Gleam, al generar una representación intermedia ('Erlang abstract forms') en lugar de código fuente o bytecode directo, se alinea con los principios fundamentales de la ingeniería de compiladores. Este enfoque fue popularizado por sistemas como el compilador GCC (GNU Compiler Collection), que utiliza múltiples IRs (como GIMPLE y RTL) para permitir optimizaciones independientes del lenguaje fuente y del target. La idea de una representación intermedia para facilitar la portabilidad y la optimización se remonta a trabajos seminales en compiladores, como los de Aho, Sethi y Ullman en 'Compilers: Principles, Techniques, and Tools' (1986), donde se discute la importancia de las IRs para la modularidad y la eficiencia del proceso de compilación.
La decisión de no generar bytecode BEAM directamente, debido a su naturaleza cambiante, refleja el desafío de la 'estabilidad de la ABI' (Application Binary Interface) en entornos de ejecución dinámicos. Mantener la compatibilidad con una ABI en evolución constante es un problema conocido en el desarrollo de sistemas operativos y runtimes, y la elección de Gleam de depender de una IR más estable ('abstract forms') es una solución pragmática para gestionar esta complejidad. Esto también se relaciona con el concepto de 'target-independent optimization', donde las optimizaciones se aplican a la IR antes de la generación de código específico de la máquina, un principio clave para la eficiencia de los compiladores modernos.