¿Por qué LLVM para TypeScript?
Un compilador anticipado (AOT) vive en un régimen distinto al de un JIT. Un JIT compila mientras el usuario espera, así que la latencia de compilación es la restricción. Un compilador AOT como Perry compila una sola vez — en la máquina del desarrollador o en CI — y el binario se ejecuta millones de veces después. Esa asimetría es exactamente donde un optimizador pesado se paga solo.
LLVM aporta dos décadas de trabajo en el middle-end: vectorización de bucles, loop-invariant code motion, global value numbering, sparse conditional constant propagation, inlining agresivo, análisis de alias. El trabajo de Perry es entregarle a esa maquinaria un IR que realmente pueda optimizar — y ahí es donde entra la información de tipos de TypeScript.
La pipeline de lowering
El código fuente se parsea con SWC, y luego se reduce a un IR tipado de alto nivel (HIR) donde ocurren las decisiones interesantes antes de que LLVM llegue siquiera a ver el código:
- Monomorfización. Las funciones y clases genéricas se especializan para cada instanciación concreta, la misma estrategia que usan Rust y C++.
Stack<number>yStack<string>se convierten en dos funciones independientes y completamente tipadas — así que el optimizador trabaja con tipos concretos en lugar de un blob de dispatch genérico, y los genéricos no cuestan nada en runtime. - Static dispatch. Cuando el tipo del receptor se conoce en tiempo de compilación, las llamadas a métodos se compilan como llamadas directas que LLVM puede inlinear, no como búsquedas en tablas hash.
- Acceso directo a campos. Los campos de los objetos se resuelven en índices en tiempo de compilación, así que leer una propiedad es una carga de desplazamiento fijo — no una búsqueda en un diccionario.
NaN-boxing e inline lowerings
Donde los valores son dinámicos, Perry usa NaN-boxing: cada valor es una palabra de 64 bits. Los doubles se almacenan directamente; los objetos, strings, booleans, null y undefined se codifican en los patrones de bits no usados de un NaN silencioso de IEEE 754. Los números tienen coste cero — sin boxing, sin asignación para la aritmética.
El truco está en que las operaciones sobre valores que no son números necesitan secuencias de bits de desempaquetar-operar-reempaquetar. Si esas secuencias viven como llamadas a un runtime compilado por separado, LLVM las ve como cajas negras opacas y no puede optimizar a través de ellas. Por eso Perry emite las operaciones calientes — cargas de propiedades, dispatch de métodos, asignación de objetos — como LLVM IR en línea que el optimizador puede fusionar y simplificar. La asignación de objetos, por ejemplo, se compila como una asignación bump thread-local en línea:
%off_ptr = getelementptr i8, ptr %state, i64 8
%offset = load i64, ptr %off_ptr ; current bump offset
%new_off = add i64 %offset, 96 ; headers + 8 fields
%sz_ptr = getelementptr i8, ptr %state, i64 16
%size = load i64, ptr %sz_ptr ; block capacity
%fits = icmp ule i64 %new_off, %size
br i1 %fits, label %fast, label %slow¿Por qué no Cranelift?
El primer backend de Perry fue Cranelift — el codegen detrás de wasmtime, construido para una compilación rápida y predecible. Fue el punto de partida correcto, y sigue siendo una excelente opción para JITs y runtimes en sandbox. Dos cosas forzaron el cambio:
- El techo del optimizador. Cranelift es deliberadamente un compilador rápido de un solo nivel: «código decente y rápido», que es la decisión correcta para un JIT y la incorrecta para un compilador AOT cuyo argumento de venta es el máximo rendimiento nativo.
- arm64_32. Apple Watch usa una ABI (instrucciones de 64 bits, punteros de 32 bits) que Cranelift no soporta. Para que watchOS existiera como plataforma de destino, LLVM era necesario — y mantener dos backends significaba dos conjuntos de bugs, tests y líneas base de rendimiento.
La migración no fue gratis: la primera release exclusiva de LLVM empeoró algunos benchmarks hasta 70x, porque las operaciones calientes al principio pasaban por llamadas opacas a helpers del runtime. Recuperarse — inline lowerings, el asignador bump de arriba, mejores límites de inlining — llevó al backend más allá de las cifras de Cranelift, y para cuando se estabilizó, Perry superaba a Node.js en todos los benchmarks de su suite, de 1,7x a 24,6x con dos empates (abril de 2026). Vale la pena leer el post-mortem completo: De Cranelift a LLVM.
Profundizando
La página de internos del compilador cubre NaN-boxing, monomorfización y static dispatch con más detalle. En el blog, Optimizarlo todo recorre el trabajo de optimización release por release, y GC generacional, JSON perezoso y benchmarks defendibles explica cómo funciona la metodología de benchmarks (RUNS=11, mediana + p95). Para el panorama completo, empieza por el resumen de compilador nativo de TypeScript.