¿Qué pasa entre larva build y un programa en ejecución? Larva es una plataforma de compiladores. Se encarga de todo lo que un programa necesita, salvo el lenguaje en el que está escrito: un modelo de programa tipado, la reducción de ese modelo a una IR de bajo nivel, un bytecode y una máquina virtual, back ends nativos, un runtime con varios recolectores de basura, archivos de proyecto, un gestor de paquetes y bindings nativos. Este artículo sigue a un programa pequeño a lo largo de ese camino, una etapa a la vez. Cada etapa tiene un capítulo en el libro de internos, y los enlaces te llevan ahí.
El camino de un vistazo
Puedes ver pasar las etapas con --timings:
larva build --timings -o hello.exe hello.ant
hello.exe (llvm back end, O3, entry hello::Hello.$Module.Main)
timings: System.Core (compiled library) 83 ms, frontend 1536 ms, HIR link 54 ms, lowering 619 ms, IR checks 226 ms, IR link 93 ms, native (llvm) 1551 ms; total 4173 ms
Cada nombre es un paso: el frontend del paquete de lenguaje convierte el código fuente en HIR, el enlace de HIR (HIR link) lo une con System.Core y otras bibliotecas, la reducción (lowering) convierte HIR en Larva IR, las comprobaciones de IR (IR checks) la verifican, el enlace de IR (IR link) produce un único programa y un back end genera el código. Todo esto ocurre en memoria, dentro del proceso larva. larva build no escribe archivos intermedios. (Estos tiempos vienen de una copia de trabajo de larva. El larva instalado está compilado de antemano y es varias veces más rápido en cada fase.) El diagrama del flujo muestra el panorama completo.
Este es el programa que vamos a seguir:
import System;
into namespace Pipeline
{
public interface IShape
{
Area() : f64;
}
public class Square : IShape
{
f64 side;
public:
new(f64 side) { this.side = side; }
Area() : f64 => side * side;
}
Scale(f64 factor) : func<f64> : f64 => x => x * factor;
Main()
{
IShape shape = new Square(3);
func<f64> : f64 twice = Scale(2);
Console.WriteLine(twice(shape.Area()));
}
}
Tiene una interfaz, una clase, una clausura y una llamada a través de una interfaz. Cada una de estas cosas se decide en un punto concreto del camino.
1. El frontend es un paquete de lenguaje
larva no conoce la sintaxis de Antril ni la de ningún otro lenguaje. Un lenguaje es un paquete de lenguaje: una biblioteca nativa que responde peticiones JSON a través de dos funciones de C, larva_language_call y larva_language_free. La petición compile recibe archivos fuente y devuelve un ensamblado HIR junto con los diagnósticos. Otras peticiones vuelven a escribir HIR como código fuente (source), alimentan larva types, show y search (catalog) y responden al editor (lsp.*).
El frontend resuelve nombres, sobrecargas, tipos e inferencia, y nada más. Cómo se despacha una llamada virtual o cómo se construye una clausura no es decisión suya. Antes de cargar un paquete, larva comprueba cada archivo contra los hashes de su manifiesto y verifica la firma. Las plantillas de proyecto también están escritas en HIR, así que larva new puede escribir la misma plantilla en cualquier lenguaje instalado.
Lee: Paquetes de lenguaje, en especial la ABI y Las plantillas son HIR.
2. HIR, la verdad del programa
Larva HIR es el modelo que escribe cada frontend. Está por encima de la IR: todavía tiene clases, interfaces, llamadas virtuales, lambdas, try, patrones y genéricos, para que Larva pueda reducirlos con todo el contexto. Está por debajo del código fuente: los nombres están resueltos, cada expresión tiene un tipo, las sobrecargas están elegidas y los operadores de los tipos de usuario son llamadas a sus métodos. Puedes ver un tipo como HIR:
larva show --lang hir Pipeline.Square pipeline.ant
public class Pipeline.Square : System.Any, Pipeline.IShape
{
field side: f64
constructor Square(side: f64)
method Area() -> f64
}
La separación entre HIR e IR es la decisión central del diseño de Larva. Antes, el compilador de Antril reducía cada característica directamente a la IR de bajo nivel, y las bibliotecas se referenciaban volviendo a analizar el texto de sus declaraciones. Ahora todos los frontends, herramientas y bibliotecas hablan HIR. El enlazador de HIR une el programa, System.Core, otras bibliotecas y los bindings en un único ensamblado cerrado.
Lee: Larva HIR, desde Por qué existe HIR hasta Enlazar ensamblados HIR.
3. .lva: bibliotecas como metadatos
HIR tiene una forma binaria, .lva. Es lo que devuelve un frontend, lo que escribe larva lib y lo que lleva un paquete de Hive. Un .dll de biblioteca Larva, incluido System.Core.dll, son esos mismos bytes envueltos en una imagen PE mínima, en una sección llamada .lva. El formato tiene un objetivo principal: referenciar una biblioteca debería costar solo sus metadatos. Los cuerpos de los métodos están en su propio bloque al final y solo se decodifican cuando algo los pide. Las cadenas y los tipos se guardan una sola vez en tablas compartidas. Escribir un ensamblado y volver a leerlo da exactamente el mismo HIR.
Lee: El formato .lva y sus objetivos de diseño.
4. El catálogo y las vistas de lenguaje
Todas las herramientas que muestran código a las personas necesitan lo mismo: tipos por espacio de nombres, miembros (también los heredados), búsqueda y una forma de escribir un tipo en el lenguaje de quien lee. Larva.Hir.Services ofrece todo esto una sola vez, sobre HIR. Un lenguaje aporta una vista, que escribe HIR como su propio código fuente, y un análisis de documentos. A cambio obtiene larva types, larva show, larva search, autocompletado, información al pasar el cursor e ir a la definición dentro de las bibliotecas. Por eso larva show Pipeline.Square vuelve a imprimir la clase como Antril, descompilada desde HIR.
Lee: El catálogo y las vistas de lenguaje.
5. La reducción: donde las clases dejan de existir
La reducción (lowering) convierte el HIR enlazado en Larva IR, y está escrita una sola vez, en Larva, para todos los lenguajes. Las clases se convierten en estructuras con una vtable. Square.Area obtiene una ranura, y la llamada a través de IShape pasa por una tabla de interfaz, con ocho entradas fijas en cada vtable para una búsqueda rápida. Los genéricos siguen siendo genéricos en la IR, con requisitos sobre sus parámetros, y solo se resuelven después del enlace. Los back ends nativos los especializan, y la máquina virtual puede compartir un mismo cuerpo entre distintos argumentos de tipo.
La clausura que devuelve Scale se convierte en un objeto común: un Func1 que apunta a una función creada para la lambda, más un objeto de entorno que guarda el factor capturado. Los iteradores y las funciones async también se convierten en objetos. Sus marcos se convierten en máquinas de estados en una sola pasada sobre la IR, la pasada de corrutinas. try/catch se convierte en aristas de desenrollado, y las importaciones en llamadas nativas. La regla es reducir una característica a IR común siempre que eso alcance, para que todas las comprobaciones y todos los back ends la obtengan gratis.
Lee: Reducir HIR a IR (consulta Despacho virtual y Compartido o especializado), y luego Reducir las características del lenguaje para las clausuras y la pasada de corrutinas.
6. Larva IR, donde la memoria es explícita
Larva IR es pequeña y de bajo nivel a propósito: estructuras, handles gc, lugares, bloques básicos y terminadores. Todo lo nativo se alcanza a través de una tabla fija de intrínsecos, la misma en todos los back ends y en la máquina virtual. Como la IR es tan pequeña, cada comprobación y cada back end trabajan sobre un solo lenguaje simple, sin importar de qué lenguaje vino el programa.
Aquí es también donde la memoria se vuelve visible: encabezados de objeto, handles, préstamos. Aquí se ejecutan las comprobaciones de IR: el verificador de tipos, el verificador de regiones para los bloques region { } en modo arena, los puntos seguros y los datos sobre límites.
Lee: Larva IR y sus análisis.
7. El verificador de préstamos
Las referencias &T y &mut T de Antril se verifican aquí, sobre la IR y no sobre el código fuente. Eso da un único verificador para todos los lenguajes, y verifica exactamente el código que se va a ejecutar. Para cada función demuestra la inicialización, los movimientos, la mutabilidad, los préstamos en conflicto y los escapes, con dos reglas más para los objetos a los que pueden llegar muchos handles. Las funciones se verifican de forma independiente y en paralelo; las genéricas, una sola vez antes de instanciarse. El modo viene de borrowcheck: (off, warn o enforce). El servidor de lenguaje ejecuta la misma verificación mientras escribes.
Lee: El verificador de préstamos, empezando por Qué demuestra.
8. Objetos y enlace
Larva enlaza a nivel de IR, sobre todo el programa, antes de que exista código máquina. El enlazador resuelve símbolos, comparte el código de enlace único, asigna los dominios de GC y elige el envoltorio del punto de entrada. Después, el monomorfizador concreta los genéricos, empezando por las raíces. Las bibliotecas se distribuyen como HIR, no como IR. Los archivos .lvo (un módulo verificado) y .lvm (un programa enlazado) existen para herramientas como larva link, larva dis y larva native, pero larva build nunca los escribe.
Lee: Objetos, módulos y enlace.
9. Cuatro back ends
El programa enlazado va a uno de estos:
- La IL y la máquina virtual. Larva IL es un bytecode de pila compacto, codificado en unidades de 4 bits y ejecutado por un intérprete. No es el camino rápido. Es la referencia: no necesita ninguna cadena de herramientas, y los demás back ends se comparan con ella. Consulta La IL y la máquina virtual.
- LLVM, el predeterminado y el que optimiza. Larva llama a LLVM dentro del mismo proceso a través de su API de C, con las bibliotecas que se distribuyen junto a
larva. Es el único back end que apunta a Linux, macOS y WebAssembly, este último mediante--target wasm-llvm. Consulta El back end de LLVM. - x86-64, el compilador base propio de Larva para Windows. No tiene asignador de registros ni optimizador: cada variable local vive en una ranura de la pila. Eso lo hace rápido de compilar y le da a cada valor una dirección en cada llamada, que es lo que necesita un recolector para encontrar sus raíces. Consulta El back end x86-64.
- WebAssembly, ya sea a través de LLVM o con el back end propio de Larva. Ambos escriben
app.wasm,app.jsyapp.html. Hoy solo el camino de LLVM es confiable. Consulta WebAssembly.
Para un programa nativo, el enlazador del sistema (MSVC en Windows) enlaza el archivo objeto con el runtime y produce un .exe o un .dll. Consulta Ejecutables y bibliotecas nativos.
10. El runtime
El código compilado llama a un runtime pequeño para reservar memoria, recolectar, lanzar excepciones, imprimir, usar archivos y sockets, ejecutar hilos y hacer aritmética con tipos numéricos que el procesador no tiene. Hay cuatro implementaciones de la misma interfaz: larva_rt.c para programas nativos, NativeRuntime dentro del propio proceso de larva, larva_rt.js para WebAssembly y la máquina virtual. Comparten una misma disposición de objetos y una misma tabla de intrínsecos, así que un programa produce la misma salida dondequiera que se ejecute. En Windows, el runtime de C se compila una vez por máquina, se guarda en caché y se enlaza sin la biblioteca de C. Un hola mundo solo importa kernel32.dll y ntdll.dll.
Lee: El runtime nativo, desde Cuatro runtimes, una interfaz hasta El runtime sin biblioteca de C.
11. La recolección de basura y los dominios
Cada modo gc: es un conjunto de capacidades. Los back ends nunca preguntan qué modo se está usando. Preguntan si una capacidad está presente (trazado, movimiento, barreras, conteo de referencias, regiones, liberación manual), y cada capacidad activa un tipo de código generado. El recolector por defecto es generacional. Los objetos nuevos se reservan desplazando un puntero en una guardería de bloques de 64 KB, los que sobreviven se copian fuera en paralelo y todo lo que apunta una pila queda fijado en lugar de moverse.
Los dominios de GC son la forma en que un programa mezcla modos. Un programa con trazado puede llamar a una biblioteca con conteo de referencias, y cada lado mantiene sus propias reglas. El dominio no forma parte del tipo: un byte del encabezado de cada objeto dice a qué dominio pertenece. Solo las funciones estáticas, no genéricas, sobre números, booleanos y punteros cruzan entre dominios. El texto y los bytes cruzan como copias, a través de System.Exchange.
Lee: Recolección de basura y Dominios de GC.
12. Alrededor del compilador
Las mismas bibliotecas hacen funcionar el resto de la cadena de herramientas:
- Proyectos. Los archivos
.queenlos lee un analizador que nunca lanza excepciones: cada problema se convierte en un diagnósticoQN. La configuración se combina en capas: la línea de comandos tiene prioridad sobre el chunkBUILDdel proyecto, que a su vez tiene prioridad sobrelarva config.larva runcompara las fechas de modificación para decidir si vuelve a compilar. Consulta Proyectos y compilaciones. - Hive. Los paquetes son archivos zip firmados, verificados archivo por archivo, y las compilaciones los resuelven desde la caché sin usar la red. Consulta Internos de Hive.
- Bindings.
larva bindsepara lo que exporta una biblioteca nativa de cómo se declaran sus funciones (una descripción.bindo un encabezado de C). Ambas cosas se juntan en un ensamblado HIR común de métodos[Import], con llamadas directas a C y sin capa de conversión de datos. Consulta Internos de los bindings. - El servidor de lenguaje. Un solo
larva lspatiende a todos los lenguajes instalados. larva se encarga del protocolo, los proyectos, los archivos descompilados y el verificador de préstamos. El paquete de lenguaje responde todo lo que necesita su sintaxis. Consulta El servidor de lenguaje. - Los instaladores. larva se compila de antemano para cada sistema. System.Core se compila una sola vez, cuando se genera el instalador, así que una instalación no incluye código fuente. Consulta Los instaladores.
Lee el libro
Este artículo solo repasa cada etapa por encima. El libro describe el código tal como es, y cada página dice en qué se queda corto respecto al diseño. Las principales carencias están reunidas en Lo que todavía no está implementado. El libro llega hasta el fondo, con listados reales de HIR, IR, IL, LLVM IR y código máquina. Empieza por Arquitectura y sigue los capítulos en orden. Las preguntas sobre el diseño, o las ideas para un lenguaje propio sobre Larva, son bienvenidas en el foro.