wandres.dev
NIVEL DIOS: SÍNTESIS · el lenguaje completo

Leer el código fuente de Swift: navegar la stdlib y aprender de quien la escribió

El repositorio de Swift es público, enorme y perfectamente navegable si sabes su geografía. Dónde vive cada cosa, qué trucos verás que tú no puedes usar, cómo bajar de Swift a SIL para ver lo que el compilador ve, y el método de arqueología que convierte cualquier línea rara en una decisión documentada.

⏱ 26 min

Llega un momento en el que la documentación deja de responder tus preguntas. Quieres saber por qué Array.append amortiza como amortiza, si Set usa sondeo lineal o encadenamiento, qué hace exactamente withUnsafeTemporaryAllocation cuando el tamaño no se conoce en compilación, o por qué ese error de aislamiento aparece solo cuando el tipo es genérico. Todas esas respuestas existen, están escritas, y están a un git clone de distancia. El proyecto Swift es de los pocos lenguajes industriales cuyo compilador, biblioteca estándar, propuestas de diseño, discusiones de revisión y suites de pruebas son públicos y están vinculados entre sí. Leerlo no es una actividad de mantenedores: es la técnica de aprendizaje de mayor rendimiento que te queda, porque cada respuesta que encuentras viene acompañada del razonamiento que la produjo.

🎯 Al terminar esta lección sabrás
  • Orientarte en el repositorio: dónde está la stdlib, dónde el compilador, dónde las pruebas y dónde el diseño.
  • Reconocer los mecanismos internos que verás en el código y saber cuáles no debes copiar a tu proyecto.
  • Bajar de Swift a SIL y a ensamblador para responder preguntas de rendimiento con evidencia y no con intuición.
  • Aplicar el método de arqueología que conecta una línea de código con su propuesta, su revisión y su prueba.

La geografía del repositorio

El monorepo principal se llama swiftlang/swift y tiene cuatro barrios que importan. El primero es stdlib/public/core, donde vive la biblioteca estándar escrita en Swift: Array.swift, Dictionary.swift, StringGraphemeBreaking.swift, Optional.swift, Sequence.swift. Es el mejor sitio por donde empezar porque el lenguaje es el que ya sabes. Al lado, stdlib/public/Concurrency contiene el runtime de actores y tareas, mitad Swift y mitad C++, y stdlib/public/runtime guarda el corazón en C++: conteo de referencias, metadatos de tipos, casting dinámico.

El segundo barrio es lib, el compilador propiamente dicho, y su orden interno es el orden del pipeline: Parse produce el árbol sintáctico, Sema hace la comprobación de tipos y la resolución de sobrecargas, SILGen baja a la representación intermedia de Swift, SILOptimizer la transforma, e IRGen produce LLVM IR. Los encabezados correspondientes viven en include/swift. El tercero es test, con decenas de miles de pruebas escritas con la sintaxis de FileCheck: cuando quieras saber qué diagnóstico exacto produce una situación, el test es más rápido que el código. Y el cuarto es la documentación de diseño, repartida entre docs en el repo del compilador y el repositorio aparte swiftlang/swift-evolution, donde cada propuesta aceptada explica motivación, diseño alternativo y detalles de compatibilidad.

flowchart LR
A[Codigo Swift] --> P[Parse: arbol sintactico]
P --> S[Sema: tipos y sobrecargas]
S --> G[SILGen: SIL bruto]
G --> O[SILOptimizer: SIL canonico]
O --> I[IRGen: LLVM IR]
I --> L[LLVM: codigo maquina]
style G fill:#89b4fa,color:#11111b
style O fill:#f9e2af,color:#11111b

Alrededor del monorepo orbitan repositorios que te interesarán más pronto que tarde: swift-syntax para macros, swift-foundation con la reescritura de Foundation en Swift, swift-collections, swift-algorithms, swift-async-algorithms, swift-testing y sourcekit-lsp, que es el servidor que alimenta tu Neovim. Son más pequeños, más legibles y con un umbral de contribución mucho más bajo.

Navegarlo desde el editor no requiere compilar el compilador, y ese es el malentendido que frena a más gente. Para leer basta con búsqueda y un índice.

git clone --depth 1 https://github.com/swiftlang/swift.git
cd swift

# Donde se define algo, sin indice ni servidor de lenguaje
rg --type swift 'func append' stdlib/public/core/

# Que diagnostico exacto produce una situacion: el test lo dice
rg -n 'error: .*actor-isolated' test/Concurrency/ | head

# Que atributos internos usa un archivo, para saber cuanto vas a sufrir
rg -o '@_[a-zA-Z]+' stdlib/public/core/Array.swift | sort -u

La tercera orden es la que conviene ejecutar antes de leer un archivo nuevo: te dice de antemano cuántos contratos internos vas a encontrarte y por tanto si ese fichero es un buen punto de entrada o un pantano. Optional.swift sale casi limpio y se lee de un tirón; Array.swift sale cargado y hay que leerlo con paciencia.

Los trucos que verás y que no debes copiar

En cuanto abras Array.swift verás construcciones que no aparecen en ningún libro. Conviene saber qué son para no confundirte y, sobre todo, para no imitarlas.

🔱

Atributos con guion bajo

@_transparent, @_effects(readnone), @_alwaysEmitIntoClient, @_silgen_name. Son contratos internos entre stdlib y compilador, sin garantía de estabilidad. Verlos es útil; usarlos es firmar que tu código se romperá.

🧬

El modulo Builtin

Builtin.Int64, Builtin.RawPointer, Builtin.isUnique. La capa por debajo del lenguaje, que solo puede importarse con una bandera de frontend. Es donde Int deja de ser un struct y se vuelve un registro.

⚙️

Archivos gyb

Ficheros .swift.gyb con plantillas en Python que generan variantes por tipo. Así se producen las decenas de sobrecargas numéricas sin escribirlas a mano. El archivo generado es el que compila.

🔁

Accesores de corrutina

_read y _modify con la palabra yield. Permiten prestar almacenamiento en vez de copiarlo. Funcionan, pero su forma pública aún no está decidida.

La regla práctica es simple: todo identificador que empiece por guion bajo es una conversación privada entre la stdlib y el compilador. Puedes escucharla para entender el sistema, y debes asumir que cualquier versión puede cambiarla sin aviso ni nota de migración.

Un fragmento típico deja ver los cuatro mecanismos a la vez, y merece la pena descifrarlo entero antes de seguir.

@frozen
public struct MiBuffer<Element> {
    @usableFromInline internal var _almacen: _Almacen

    @inlinable
    public subscript(i: Int) -> Element {
        @inline(__always) get { _almacen[i] }
        _modify { yield &_almacen[i] }        // presta, no copia
    }

    @_transparent
    public var isEmpty: Bool { count == 0 }   // se expande antes de optimizar
}

@frozen congela la disposición para que el cliente pueda asignarlo en la pila. @usableFromInline permite que el cuerpo publicado hable de algo interno sin hacerlo público. _modify con yield cede el almacenamiento en lugar de devolver una copia. Y @_transparent obliga a expandir la función tan temprano en el pipeline que el optimizador nunca llega a ver la llamada. Tú puedes usar los dos primeros en tu propia biblioteca; los dos últimos no, y esa asimetría es exactamente la frontera entre ser cliente del lenguaje y ser parte de su implementación.

Bajar un nivel: ver lo que el compilador ve

Leer el fuente responde el qué. Para responder el por qué es rápido hace falta ver la representación intermedia. SIL es la capa donde Swift todavía conoce sus propios conceptos —retain, release, existenciales, witness tables— antes de disolverse en LLVM, y es donde se hacen visibles las decisiones que importan.

# SIL recien generado, antes de optimizar: aqui se ve lo que escribiste
swiftc -emit-silgen -Onone Prueba.swift | swift demangle | less

# SIL optimizado: aqui se ve lo que queda de verdad
swiftc -emit-sil -O Prueba.swift | swift demangle | less

# Un nivel mas abajo
swiftc -emit-ir -O Prueba.swift        # LLVM IR
swiftc -emit-assembly -O Prueba.swift  # ensamblador

Con esas cuatro órdenes puedes contestar preguntas que de otro modo son religión. ¿Se eliminó el retain que temías? Busca strong_retain en el SIL optimizado. ¿Se especializó tu genérico o quedó despacho por witness table? Busca el nombre desmangado de la función especializada frente a witness_method. ¿Tu copy-on-write dispara una copia en el bucle caliente? Busca la llamada a isUnique y mira si el optimizador la sacó del bucle. ¿El existencial se está guardando en un buffer heap? El SIL lo dice con alloc_box y init_existential_addr.

⚠️
Compara siempre dos versiones, nunca leas una sola

El SIL de un programa es ilegible en absoluto y elocuente en relativo. La técnica es escribir dos variantes mínimas que difieran en una decisión, generar el SIL de ambas y comparar con diff. Así se responde en dos minutos lo que en un foro tarda dos semanas.

Un recorrido completo, de la pregunta a la respuesta

Vale la pena ver el método entero aplicado a una pregunta concreta, porque la secuencia importa más que cualquiera de sus pasos. La pregunta: ¿por qué mi función genérica va rápido con Int y lenta con un protocolo, si el código es el mismo?

Paso uno, reducir a un caso mínimo. Dos funciones que difieran únicamente en cómo abstraen.

protocol Puntuable { var puntos: Int { get } }
struct Jugada: Puntuable { let puntos: Int }

func totalGenerico<T: Puntuable>(_ xs: [T]) -> Int {
    xs.reduce(0) { $0 + $1.puntos }
}

func totalExistencial(_ xs: [any Puntuable]) -> Int {
    xs.reduce(0) { $0 + $1.puntos }
}

Paso dos, mirar el SIL optimizado de la primera y buscar el nombre de la función especializada; si el optimizador hizo su trabajo verás una copia de totalGenerico con Jugada ya sustituido y el acceso a puntos convertido en una lectura directa de un campo. Paso tres, mirar el SIL de la segunda y buscar open_existential_addr y witness_method: cada elemento pasa por una indirección a través de la tabla de testigos y por una llamada indirecta que ningún inliner puede eliminar, porque el tipo concreto no se conoce en ese punto.

Paso cuatro, ir al fuente para entender por qué existe esa diferencia y no es un fallo. En lib/SILOptimizer está el paso de especialización genérica, y su condición de aplicabilidad explica el criterio: hace falta que el tipo concreto sea visible en el punto de llamada. Paso cinco, cerrar el círculo con la propuesta de evolución que introdujo some y any, donde se argumenta exactamente esta asimetría y por qué se decidió hacerla visible en la sintaxis en lugar de dejarla como un detalle de rendimiento invisible.

ℹ️
El resultado que buscas no es el dato

Al terminar sabrás por qué una versión es más rápida, pero lo que de verdad te llevas es el criterio: la especialización necesita el tipo concreto en el sitio de la llamada. Ese criterio predice el rendimiento de cien situaciones que aún no has visto. La medición concreta solo predice una.

Arqueologia de codigo: como convertir una linea rara en una decision documentada

La habilidad que de verdad separa a quien lee el fuente de quien lo mira es la arqueologia, y tiene un procedimiento fijo que funciona en cualquier repositorio serio pero que en Swift funciona excepcionalmente bien porque la cadena de evidencia esta completa. Paso uno: localiza la linea exacta y ejecuta git log -L sobre ese rango, que reconstruye la historia de esas lineas concretas aunque el archivo se haya movido o renombrado; si buscas cuando aparecio o desaparecio un identificador, git log -S con la cadena hace el trabajo por ti sobre todo el arbol. Paso dos: del commit resultante saca el numero de pull request, porque la politica del proyecto es que casi nada entra sin revision; en esa PR encontraras la discusion tecnica, las objeciones que se plantearon y con frecuencia mediciones concretas. Paso tres: la PR casi siempre cita una propuesta de evolucion con la forma SE- seguida de cuatro digitos; ve al repositorio swift-evolution, lee la propuesta completa y, sobre todo, lee la seccion de alternativas consideradas, que es la parte que ningun tutorial reproduce y donde estan las razones reales. Paso cuatro, el que casi nadie da: busca en los foros oficiales el hilo de revision de esa propuesta, porque cada propuesta pasa por un periodo publico donde usuarios y miembros del core team discuten el diseno, y el mensaje final del gestor de revision resume por que se acepto, que se cambio respecto al borrador y que preocupaciones quedaron abiertas. Paso cinco: vuelve al monorepo y busca en test los ficheros que la PR anadio; una suite de pruebas es la especificacion ejecutable del comportamiento y suele contener los casos limite que el documento no menciona. Al terminar ese ciclo tienes algo que no se puede obtener de ninguna otra forma: no solo sabes que hace el codigo, sabes que alternativas se descartaron, con que argumento y a costa de que. Y aqui esta el rendimiento oculto del metodo, que es de segundo orden: al repetirlo veinte o treinta veces empiezas a internalizar los criterios de decision del equipo, y a partir de ese punto puedes predecir el diseno de una feature que todavia no has leido, o anticipar por que una idea tuya seria rechazada. Eso es exactamente lo que significa aprender de quien escribio el lenguaje, y no se consigue leyendo mas documentacion sino leyendo la conversacion que produjo la documentacion.

📝
Lo esencial

stdlib/public/core para la biblioteca, lib en orden de pipeline para el compilador, test para los diagnósticos exactos y swift-evolution para el razonamiento. Todo lo que empieza por guion bajo es privado. SIL responde las preguntas de rendimiento y la arqueología responde las de diseño.

⚔️ Tu primera excavación
  1. Clona el monorepo y abre stdlib/public/core/Array.swift. Localiza la lógica de crecimiento de capacidad y explica con qué factor crece y por qué eso da coste amortizado constante.
  2. Escribe dos versiones de una función genérica, una restringida a un protocolo y otra sobre un existencial any. Genera el SIL optimizado de ambas y describe la diferencia exacta.
  3. Elige una línea de la stdlib que no entiendas y aplica los cinco pasos de la arqueología hasta llegar al hilo de revisión. Resume la alternativa descartada más interesante.
  4. Busca un archivo .swift.gyb y describe qué genera y por qué no se escribió a mano.
  5. Configura sourcekit-lsp en tu editor apuntando al propio repositorio de Swift y comprueba si el salto a definición funciona dentro de la stdlib. Documenta qué falla y por qué.