El problema fundamental que scriptc aborda es la sobrecarga inherente de los runtimes JavaScript (como V8) en entornos de producción donde el rendimiento, el tamaño del binario y el consumo de memoria son críticos. Tradicionalmente, TypeScript (y JavaScript) se ejecutan en máquinas virtuales pesadas, lo que introduce latencia de arranque y un footprint de memoria significativo, incluso para aplicaciones relativamente simples. scriptc postula que una gran parte del código TypeScript es inherentemente estático y puede ser compilado directamente a código máquina, eliminando la necesidad de un motor de ejecución en tiempo real.

Este enfoque no es nuevo en el ámbito de los lenguajes dinámicos; proyectos como GraalVM o incluso la compilación AOT en lenguajes como C# o Java buscan reducir la dependencia de un JIT o VM en tiempo de ejecución. Sin embargo, scriptc se enfoca específicamente en TypeScript, aprovechando su sistema de tipos para realizar optimizaciones estáticas profundas y ofrecer una experiencia de desarrollo familiar, sin dialectos ni anotaciones especiales, a diferencia de otras soluciones que requieren reescrituras o subconjuntos del lenguaje. La relevancia actual radica en la creciente demanda de servicios serverless, edge computing y microservicios de arranque rápido, donde cada milisegundo de latencia y cada megabyte de memoria importan.

Arquitectura del Sistema

La arquitectura de scriptc se basa en un pipeline de compilación de tres etapas. Primero, el código TypeScript es procesado por una API del compilador TypeScript (tsc) para realizar el análisis sintáctico y semántico, y la verificación de tipos. Esta etapa genera una representación intermedia (IR) tipada. Esta IR es el punto central de la arquitectura, sirviendo como la única interfaz entre el frontend (TypeScript) y los backends de generación de código.

Desde la IR, scriptc puede generar código C como backend de referencia, o directamente código máquina a través de LLVM, que es el generador de código por defecto. El backend de C es crucial para la depuración y la validación, ofreciendo una salida legible y anotada con líneas de código fuente. El runtime de C (paquete packages/runtime) proporciona las primitivas necesarias para el código compilado, incluyendo un sistema de valores con conteo de referencias y recolector de ciclos, fibras stackful para async/await, un event loop basado en kqueue, y una implementación de la pila de red (net, http, https, tls con mbedTLS). Las características se vinculan de forma granular, asegurando que los binarios solo incluyan el código necesario. Para el código que no puede ser compilado estáticamente (ej. any types, dependencias npm), scriptc ofrece un modo --dynamic que embebe un motor JavaScript ligero (quickjs-ng) en el binario, con validación en tiempo de ejecución para los valores que cruzan entre el código estático y dinámico. La interoperabilidad con C (FFI) permite la vinculación directa a llamadas ABI de C y bibliotecas del sistema.

Flujo de Compilación de scriptc

  1. 1 TypeScript Source Código fuente .ts
  2. 2 tsc API Análisis sintáctico, semántico y verificación de tipos
  3. 3 Lowering Transformación a representación intermedia (IR) tipada
  4. 4 IR (Typed Intermediate Rep) Representación interna del programa
  5. 5 C Backend (opcional) Generación de código C legible
  6. 6 LLVM Backend (default) Generación de código máquina optimizado
  7. 7 clang Compilador C/C++ para ensamblar y enlazar
  8. 8 Native Executable Binario auto-contenido
CapaTecnologíaJustificación
compute TypeScript Compiler API Frontend de compilación, análisis sintáctico, semántico y verificación de tipos del código fuente TypeScript.
compute LLVM Backend de generación de código por defecto, responsable de optimizar y generar código máquina de alto rendimiento a partir de la IR.
compute clang Compilador C/C++ utilizado para compilar el código C generado (si se usa el backend C) y para enlazar las partes nativas del ejecutable.
compute quickjs-ng Motor JavaScript embebido para el modo `--dynamic`, ejecutando código que no puede ser compilado estáticamente (ej. dependencias npm, `any` types). ~620KB de tamaño de motor
networking mbedTLS Implementación de TLS vendida para la pila de red (https, tls), utilizada en el runtime de C.
observability AddressSanitizer (ASan) Herramienta de detección de errores de memoria utilizada en el carril de seguridad de memoria para identificar fugas y usos después de la liberación.

Trade-offs

Ganancias
  • ▲▲ Tiempo de arranque
  • ▲▲ Tamaño del binario
  • ▲▲ Consumo de memoria (RSS)
  • Rendimiento en tiempo de ejecución
Costes
  • Compatibilidad total con el ecosistema JS/Node
  • Soporte para características dinámicas de JS
  • Dependencia de herramientas de compilación nativa (clang)
comptime(() => {
  // Código TypeScript que se ejecuta en el compilador
  const config = { port: 8080, host: 'localhost' };
  return JSON.stringify(config);
});
Ejecución de código TypeScript en tiempo de compilación para generar valores literales en el binario.
const config = JSON.parse(jsonString) as Config;
// Si jsonString no coincide con la estructura de Config, lanza un TypeError catchable
Validación en tiempo de ejecución de aserciones de tipo `as` para prevenir corrupción de memoria o errores lógicos.

Fundamentos Teóricos

La idea de compilar lenguajes de alto nivel a código nativo para mejorar el rendimiento y reducir la sobrecarga de runtime tiene raíces profundas en la informática. El concepto de 'lowering' (reducción) de un lenguaje a una representación intermedia más simple antes de la generación de código es un principio fundamental en el diseño de compiladores, como se describe en obras clásicas como 'Compilers: Principles, Techniques, and Tools' de Aho, Lam, Sethi y Ullman (el 'Dragon Book').

La estrategia de scriptc de identificar y compilar estáticamente la mayor parte del código TypeScript, mientras se recurre a un motor dinámico para las partes más complejas o impredecibles, se alinea con el concepto de compilación 'híbrida' o 'just-in-time' (JIT) con optimizaciones AOT (Ahead-Of-Time). Aunque scriptc no es un JIT en el sentido tradicional, su enfoque de tres niveles (estático, dinámico, rechazado) refleja una comprensión de los trade-offs entre la optimización estática y la flexibilidad dinámica. La monomorfización de genéricos, la devirtualización de llamadas a métodos y la gestión de memoria basada en conteo de referencias con recolección de ciclos son técnicas bien establecidas en la investigación de compiladores y runtimes, buscando equilibrar el rendimiento con la complejidad del lenguaje y la seguridad de la memoria. La validación en tiempo de ejecución de tipos en los límites entre código estático y dinámico es una aplicación práctica de los principios de seguridad de tipos y contratos de software.