wandres.dev
PHD: INTERNALS Y EL FUTURO · MIR, el compilador, el futuro

Dentro de rustc: ver los IR y contribuir al compilador

El compilador no es una caja cerrada: es un conjunto de crates que puedes leer, un binario que puedes interrogar y un proyecto abierto al que puedes contribuir. Recorremos la estructura de rustc por dentro, las banderas -Z y --emit que hacen visible cada representación intermedia con tus propios ojos, y el camino real que va de leer la rustc-dev-guide a abrir un PR, y de un RFC a cambiar el lenguaje que usas cada día.

⏱ 20 min

Durante tres lecciones has mirado dentro de rustc como quien lee un diagrama. Esta lección te da las llaves: rustc es software libre, un conjunto de crates que puedes clonar, leer y modificar, y un binario que —con las banderas adecuadas— te muestra cada una de sus representaciones internas sobre tu propio código. Aprenderás a hacer visibles el AST, el HIR y el MIR con tus ojos, a orientarte en la arquitectura del compilador, y a recorrer el camino que separa a quien usa Rust de quien lo construye. Porque el nivel dios no es solo entender la máquina: es saber que puedes meter la mano dentro.

🎯 Al terminar esta lección sabrás
  • Reconocer la estructura de rustc como conjunto de crates y el sistema de bootstrap.
  • Usar las banderas -Z y --emit para inspeccionar el AST, el HIR y el MIR.
  • Conocer el flujo real de una contribución: de la rustc-dev-guide a un pull request.
  • Entender cómo se cambia el lenguaje mismo mediante el proceso de RFC.

La estructura del compilador

rustc no es un monolito: es una constelación de crates, cada uno responsable de una etapa del pipeline que ya conoces. Los nombres son casi un mapa de las lecciones anteriores: rustc_lexer y rustc_parse para el frente, rustc_ast y rustc_hir para los árboles, rustc_hir_typeck para el chequeo de tipos, rustc_borrowck para los préstamos, rustc_mir_build y rustc_mir_transform para el MIR, rustc_monomorphize y rustc_codegen_llvm para la salida. En el centro, rustc_middle alberga el TyCtxt y el sistema de consultas que lo cose todo, y rustc_driver orquesta la ejecución.

Esa modularidad tiene una consecuencia que quizá ya has disfrutado sin saberlo: los mismos crates que forman rustc se reutilizan fuera de él. clippy no es un analizador aparte, sino un consumidor de las estructuras internas del compilador; herramientas como rust-analyzer reaprovechan o reimplementan partes del frontend para darte diagnósticos mientras escribes. El compilador no es una fortaleza cerrada, es una biblioteca de piezas que el ecosistema comparte. Y para dimensionar la empresa: rustc supera el medio millón de líneas repartidas entre esos crates, así que nadie lo entiende entero de golpe; se aprende una región cada vez, siguiendo el hilo de una consulta concreta.

Compilar el compilador tiene su propio nombre y su propia sutileza: el bootstrap. Como rustc está escrito en Rust, para compilarlo hace falta… un rustc previo. El proceso arranca de un compilador ya construido (stage 0), con él compila una primera versión (stage 1), y con esa vuelve a compilarse (stage 2) para asegurar que el resultado es consistente. La herramienta que gobierna todo esto se invoca con ./x.py, y es tu punto de entrada para construir y probar el compilador localmente.

# Construir el compilador desde el arbol de rust-lang/rust
./x.py build

# Compilar y ejecutar la bateria de tests de una etapa
./x.py test
ℹ️
La rustc-dev-guide es el mapa oficial

No tienes que reconstruir este mapa de memoria. El proyecto mantiene la Rust Compiler Development Guide, la rustc-dev-guide, que documenta cada crate, cada fase y cada convención del compilador. Es el equivalente interno del Book: el texto que todo aspirante a contribuidor lee primero. Cuando un crate te resulte opaco, casi siempre hay un capítulo que lo explica.

Ver los IR con tus propios ojos

No necesitas clonar rustc para ver sus tripas: el binario que ya tienes instalado sabe emitir cada representación intermedia. La mayoría de estas vistas viven tras banderas inestables -Z, así que requieren la cadena nightly —de ahí el +nightly en los comandos—.

# El AST tal como lo produce el parser
rustc +nightly -Zunpretty=ast-tree ejemplo.rs

# El HIR desazucarado: veras el for convertido en loop y match
rustc +nightly -Zunpretty=hir ejemplo.rs

# El MIR: bloques basicos, locales y terminadores
rustc +nightly -Zunpretty=mir ejemplo.rs

# Emitir el MIR a fichero, o el LLVM IR, o el ensamblador
rustc +nightly --emit=mir ejemplo.rs
rustc --emit=llvm-ir ejemplo.rs
rustc --emit=asm ejemplo.rs

Dentro de un proyecto con Cargo, el subcomando rustc deja pasar estas banderas al compilador para un crate concreto, sin que tengas que invocar rustc a mano:

# Ver el MIR de tu crate a traves de Cargo
cargo +nightly rustc -- -Zunpretty=mir

Estas vistas convierten las tres lecciones anteriores en algo tangible. Escribe un for, emite el HIR, y verás con tus ojos el loop con match sobre IntoIterator del que hablábamos. Escribe una función con una rama, emite el MIR, y ahí estarán los bb0, bb1, el switchInt. Deja de creer en el pipeline: míralo.

Hay banderas para casi cada pregunta que puedas hacerte sobre el interior. Una especialmente útil revela cómo dispone el compilador tus tipos en memoria —el tamaño y el relleno de cada campo, el efecto de reordenar los campos de una struct—:

# Ver el tamano y la disposicion en memoria de cada tipo
RUSTFLAGS="-Zprint-type-sizes" cargo +nightly build

Detrás de cada una de estas banderas hay una decisión de diseño del compilador que ahora puedes auditar en lugar de suponer.

⚠️
Las banderas -Z son inestables por diseño

Todo lo que vive tras -Z es territorio inestable: existe para el desarrollo del compilador y la experimentación, no ofrece garantías de compatibilidad y puede cambiar o desaparecer entre versiones de nightly. Es justo lo que quieres para mirar dentro y aprender, y justo lo que no debes incrustar en la construcción de producción de un proyecto serio. La frontera entre inspeccionar y depender de un detalle interno es la misma que separa la curiosidad del acoplamiento frágil.

💡
El Playground también los muestra

Si no quieres tocar la terminal, el Rust Playground trae, bajo el menú de opciones, botones para mostrar el MIR, el LLVM IR y el ASM del fragmento que tengas en pantalla. Y Compiler Explorer —Godbolt— hace lo mismo con múltiples versiones del compilador en paralelo. Son la forma más rápida de convertir una pregunta —“¿esto se optimiza a una sola instrucción?”— en una respuesta que puedes leer.

El camino para contribuir

Que el compilador sea abierto no es un dato decorativo: significa que la distancia entre usarlo y mejorarlo es menor de lo que parece. El camino está trillado y documentado.

Empieza por la rustc-dev-guide y por clonar rust-lang/rust. El proyecto etiqueta los issues aptos para empezar como E-easy y good-first-issue: tareas acotadas, a menudo un mensaje de error que mejorar o un caso que cubrir, elegidas precisamente para que alguien nuevo aprenda el flujo sin ahogarse. La comunicación del equipo del compilador ocurre en su Zulip, donde puedes preguntar por un issue antes de lanzarte.

El flujo de un cambio tiene su propia coreografía. Abres un pull request; un revisor se asigna con la orden r?; cuando aprueba, el cambio no se fusiona a mano sino a través de bors, el bot de la cola de integración, que reejecuta la batería completa de tests sobre cada candidato antes de dejarlo entrar. Esa disciplina —nadie fusiona nada que no pase el CI entero— es lo que mantiene sano un proyecto del que dependen millones de personas.

Lo que más sorprende a quien se asoma por primera vez es lo acogedor del proceso. El compilador arrastra fama de inexpugnable, pero el equipo invierte deliberadamente en bajar la barrera: issues etiquetados para principiantes, mentores asignados, una guía que explica no solo el qué sino el cómo, y una cultura donde preguntar en el Zulip es la norma y no una molestia. Nadie nace sabiendo navegar medio millón de líneas de compilador; se aprende contribuyendo, y el proyecto está montado para que ese aprendizaje sea posible.

flowchart TB
guide[Leer la rustc-dev-guide] --> issue[Elegir un issue E-easy o good-first-issue]
issue --> zulip[Preguntar en el Zulip del equipo]
zulip --> pr[Abrir un pull request]
pr --> rev[Un revisor se asigna con r question]
rev --> bors[bors reejecuta el CI completo]
bors --> merge[El cambio se fusiona]
style guide fill:#89b4fa,color:#11111b
style pr fill:#cba6f7,color:#11111b
style bors fill:#fab387,color:#11111b
style merge fill:#a6e3a1,color:#11111b

Cómo se cambia el lenguaje

Arreglar un error del compilador es una cosa; cambiar el lenguaje es otra, y tiene su propio proceso, deliberadamente lento. Nadie añade sintaxis nueva a Rust en un pull request suelto. El vehículo es el RFC (Request for Comments): un documento en rust-lang/rfcs que describe el cambio propuesto, su motivación, sus alternativas y sus inconvenientes, y que la comunidad y los equipos correspondientes discuten en abierto hasta alcanzar consenso.

Aprobado un RFC, el cambio no aterriza en estable de golpe. Primero se implementa tras una feature gate —el #![feature(...)] que has visto exigir tantas veces en nightly—, se le abre un tracking issue donde se refina con uso real, y solo tras un periodo de comentarios final y un informe de estabilización se activa por defecto para todo el mundo. Es el mismo camino que recorrieron los NLL, los GATs y async fn en traits: de una idea en un documento a una línea de tu código diario, pasando por años de nightly.

No todo cambio necesita un RFC completo. Los ajustes internos del compilador que no alteran el lenguaje visible siguen una vía más ligera, la de las Major Change Proposals (MCP), pensada para decisiones de implementación que el equipo debe acordar pero que no cambian cómo escribes Rust. La distinción es exacta y sana: alterar el lenguaje exige el escrutinio público del RFC; alterar las tripas que lo implementan basta con el acuerdo del equipo responsable. Dos procesos para dos clases de cambio, cada uno con el peso que le corresponde.

El compilador es abierto, y por tanto el lenguaje es tuyo también

Interioriza lo que significa que todo esto sea visible y modificable. En la mayoría de las tecnologías que usas, la herramienta es un oráculo: hace lo que hace, y cuando falla o te limita, solo puedes rodearla. rustc es lo contrario. Cada mensaje de error que lees lo escribió una persona en un fichero que puedes abrir. Cada bandera -Z es una ventana que alguien decidió tallar para que mires dentro. Cada característica del lenguaje que usas —el ?, los async fn en traits, los const generics— existe porque alguien redactó un RFC, lo defendió en público, lo implementó tras una feature gate y lo acompañó durante años de nightly hasta estabilizarlo. Y aquí está el salto de mentalidad que corona el track: no hay una casta de “los que hacen Rust” separada de “los que usan Rust”. Es el mismo continuo. El issue good-first-issue que arreglas hoy, el mensaje de error que haces más claro, el RFC que propones porque chocaste con un límite real: todo eso es Rust haciéndose. El lenguaje no es una tabla de la ley bajada de una montaña; es un artefacto vivo, construido en abierto por gente que un día fue exactamente donde tú estás ahora, resolviendo un ejercicio de una guía. La frontera entre consumir la herramienta y forjarla no es un muro: es una puerta, y acabas de aprender dónde está el picaporte. Cruzarla o no es, a partir de aquí, solo una decisión tuya.

📝
Lo esencial de contribuir

rustc es un conjunto de crates —rustc_parse, rustc_hir_typeck, rustc_borrowck, rustc_codegen_llvm, con rustc_middle en el centro— que se construyen por bootstrap con ./x.py. Puedes inspeccionar cada IR sobre tu código con -Zunpretty=hir, -Zunpretty=mir o --emit=mir y --emit=llvm-ir, o desde el Playground. Contribuir sigue un camino documentado en la rustc-dev-guide: un issue good-first-issue, un pull request, un revisor con r? y la cola bors. Y el lenguaje se cambia por RFC, feature gate en nightly y estabilización tras un periodo de comentarios.

⚔️ Interroga y contribuye
  1. Toma un for de tres líneas, ejecuta rustc +nightly -Zunpretty=hir y localiza el loop con match sobre IntoIterator del descenso al HIR.
  2. Emite el MIR de una función con un if con --emit=mir e identifica los bloques básicos y el switchInt.
  3. Empareja tres crates de rustc con la etapa del pipeline de la que son responsables y explica qué produce cada uno.
  4. Describe, en orden, los pasos de un pull request al compilador desde elegir el issue hasta la fusión por bors.
  5. Elige una característica de Rust que uses a diario y traza su historia probable: RFC, feature gate en nightly, estabilización. Explica por qué el proceso es deliberadamente lento.