Posts

De SSA a Código Máquina

De SSA a Código Máquina

En el artículo anterior , exploramos cómo el compiler transforma IR en SSA—una representación donde cada variable se asigna exactamente una vez. Vimos cómo el compiler construye SSA usando Values y Blocks, luego ejecuta más de 30 pases de optimización. Observamos cómo el pase de lowering convierte operaciones genéricas en instrucciones específicas de arquitectura como AMD64ADDQ y ARM64ADD.

Ahora estamos en la recta final. El compiler ha optimizado SSA con operaciones específicas de arquitectura. Lo único que queda es convertir esas operaciones en bytes de código máquina.

El Motor de Ejecución

El Motor de Ejecución

En el artículo anterior , exploramos cómo el planificador de PostgreSQL elige la estrategia óptima de ejecución. El planificador produce un árbol de plan abstracto—nodos como “Sequential Scan,” “Hash Join,” “Sort”—que describen qué hacer. Ahora el motor de ejecución necesita hacer el trabajo real: leer páginas del disco, seguir índices, unir tablas y producir resultados.

El ejecutor usa el modelo de ejecución Volcano—un patrón elegantemente simple donde cada operación implementa la misma interfaz: “dame la siguiente fila.” Una operación sort no se preocupa de si su entrada viene de un escaneo de tabla o de un join—simplemente pide filas y ordena lo que recibe. Este enfoque uniforme permite construir consultas arbitrariamente complejas desde piezas simples y componibles.

La Fase SSA

La Fase SSA

En el artículo anterior , exploramos el IR—el formato de trabajo del compilador donde ocurren la desvirtualización, el inlining y el análisis de escape. El IR optimiza tu código a alto nivel, tomando decisiones inteligentes sobre qué funciones hacer inline y dónde deben vivir las variables, el heap o el stack.

Pero el IR todavía se parece mucho a tu código fuente. Tiene variables que pueden asignarse múltiples veces, flujo de control complejo con bucles y condicionales, y operaciones que se mapean directamente a la sintaxis de Go.

El Query Planner

El Query Planner

En el artículo anterior , exploramos cómo el rewriter de PostgreSQL transforma las consultas—expandiendo vistas, aplicando políticas de seguridad y ejecutando reglas personalizadas. Al final de esa fase, tu consulta ha sido completamente expandida y asegurada, lista para su ejecución.

Pero aquí viene la pregunta del millón: ¿Cómo debe ejecutar PostgreSQL tu consulta realmente?

Déjame mostrarte por qué esto es importante. Considera esta consulta simple:

SELECT c.first_name, c.last_name, r.rental_date
FROM customer c
JOIN rental r ON c.customer_id = r.customer_id
WHERE c.active = 1;

PostgreSQL podría ejecutar esto de docenas de formas diferentes:

El IR

El IR

En los artículos anteriores , hemos explorado cómo el compilador de Go procesa tu código: el scanner lo divide en tokens, el parser construye un Abstract Syntax Tree, el comprobador de tipos lo valida todo, y el formato Unified IR serializa el AST con tipos en una representación binaria compacta.

Ahora llegamos a un punto crítico de transformación. El compilador toma ese Unified IR—ya sea recién serializado desde tu código o cargado desde un archivo de caché—y lo deserializa directamente en nodos IR. Aquí es donde tu código fuente se convierte realmente en el formato de trabajo del compilador.

Reescritura de Queries

Reescritura de Queries

En el artículo anterior , exploramos cómo PostgreSQL transforma texto SQL en un Query Tree validado mediante análisis sintáctico y semántico. Al final de ese viaje, PostgreSQL sabe que tus tablas existen, tus columnas son válidas, tus tipos coinciden y tu query tiene sentido.

Pero antes de que el planificador pueda determinar cómo ejecutar tu query, hay un paso más de transformación crítico: la reescritura de queries.

Déjame mostrarte por qué esto es importante. Cuando escribes esta simple query:

El Formato Unified IR

El Formato Unified IR

En el artículo anterior , exploramos cómo el comprobador de tipos del compilador de Go analiza tu código. Vimos cómo resuelve identificadores, verifica la compatibilidad de tipos y asegura que tu programa sea semánticamente correcto.

Ahora que tenemos un AST completamente verificado en tipos, el siguiente paso lógico sería generar la Representación Intermedia (IR) del compilador—la forma que utiliza para optimización y generación de código. Pero aquí hay algo interesante: el compilador de Go no transforma inmediatamente el AST en IR. En su lugar, toma lo que podría parecer un desvío—serializa el AST verificado en tipos en un formato binario, luego lo deserializa de vuelta a nodos IR.

Parsing y Análisis

Parsing y Análisis

En el artículo anterior , exploramos cómo PostgreSQL establece conexiones y se comunica usando su wire protocol. Una vez que tu conexión está establecida y el proceso backend está listo, finalmente puedes enviar consultas. Pero cuando PostgreSQL recibe tu SQL, es solo una cadena de texto—la base de datos no puede ejecutar texto directamente.

Déjame mostrarte qué sucede cuando PostgreSQL recibe esta consulta:

SELECT name FROM users WHERE id = 42;

PostgreSQL aún no ve esto como un comando. Ve caracteres: S, E, L, E, C, T, y así sucesivamente. El viaje desde este texto crudo hasta algo que PostgreSQL puede ejecutar involucra dos transformaciones principales: parsing (entender la estructura) y análisis semántico (agregar significado).

El Comprobador de Tipos

El Comprobador de Tipos

En los artículos anteriores , exploramos el scanner—que convierte código fuente en tokens—y el parser —que toma esos tokens y construye un Árbol de Sintaxis Abstracta.

En artículos futuros, cubriré la Representación Intermedia (IR)—cómo el compilador transforma el AST en una forma intermedia más low level. Pero antes de que podamos llegar ahí, necesitamos hablar sobre dos pasos intermedios cruciales: verificación de tipos (este artículo) y el Unified IR (que cubriré en un artículo separado pronto).

Conexiones y Comunicación

Conexiones y Comunicación

En el artículo anterior , exploramos el viaje completo que hace una consulta SQL a través de PostgreSQL—desde el parsing hasta la ejecución. Pero antes de que cualquiera de eso pueda suceder, tu aplicación necesita establecer una conexión con la base de datos.

Esto podría parecer un simple apretón de manos, pero en realidad hay un proceso sofisticado sucediendo detrás de escena—involucrando gestión de procesos, autenticación y un protocolo binario para comunicación eficiente.

El Parser

El Parser

En el artículo anterior del blog , exploramos el scanner—el componente que convierte tu código fuente de un flujo de caracteres en un flujo de tokens.

Ahora estamos listos para el siguiente paso: el parser.

Aquí está el desafío que resuelve el parser: ahora mismo, tenemos una lista plana de tokens sin relaciones entre ellos. El scanner nos dio package, main, {, fmt, ., Println… pero no tiene idea de que Println pertenece al paquete fmt, o que toda la declaración fmt.Println("Hello world") vive dentro de la función main.

Visión General

Visión General

¿Alguna vez te preguntaste qué sucede cuando escribes SELECT * FROM users WHERE id = 42; y presionas Enter? Esa consulta simple desencadena un viaje fascinante a través de los internals de PostgreSQL—una serie compleja de operaciones que involucra múltiples procesos, gestión sofisticada de memoria y décadas de investigación en optimización.

Este es el primer artículo de una serie donde exploraremos la ejecución de consultas de PostgreSQL en profundidad. En esta visión general, te guiaré a través del viaje completo desde texto SQL hasta resultados, dándote el mapa de ruta. Luego, en los siguientes artículos, nos sumergiremos profundamente en cada componente—el parser, analizador, rewriter, planificador y ejecutor—explorando los detalles de cómo funciona cada uno.

El Scanner

El Scanner

Esta es parte de una serie donde te guiaré a través del compilador completo de Go, cubriendo cada fase desde código fuente hasta ejecutable. Si alguna vez te preguntaste qué sucede cuando ejecutas go build, estás en el lugar correcto.

Nota: Este artículo está basado en Go 1.25.3. El funcionamiento interno del compilador pueden cambiar en versiones futuras, pero los conceptos centrales probablemente permanecerán iguales.

Voy a usar el ejemplo más simple posible para guiarnos a través del proceso—un programa clásico “hola mundo”:

Bienvenido a Internals for Interns

Bienvenido a Internals for Interns

¡Bienvenido! Estoy emocionado de finalmente lanzar este proyecto—algo en lo que he estado pensando durante casi una década.

De qué se trata esto

Durante más de 10 años, he estado dando charlas en conferencias sobre cómo funcionan las cosas por dentro. Comencé en la comunidad de Python, explorando temas como el modelo de objetos, la recolección de basura y los internals del intérprete CPython. Más tarde, me expandí a otras comunidades—Go, PostgreSQL y más allá—siempre con el mismo objetivo: hacer que los internals complejos sean accesibles.