48. Grafos en compiladores y análisis de dependencias

Los compiladores utilizan árboles y grafos para representar sintaxis, flujo de control, llamadas y dependencias. Estas estructuras permiten validar, optimizar y ordenar el trabajo de construcción.

48.1 Introducción

Un compilador transforma código fuente mediante varias representaciones intermedias. Cada una expone relaciones distintas: jerarquía sintáctica, posibles saltos, uso de valores o llamadas entre funciones.

Los mismos conceptos sirven para analizar dependencias entre módulos y automatizar construcciones incrementales.

48.2 Árbol de sintaxis abstracta

El AST representa la estructura gramatical relevante y omite detalles como separadores o paréntesis redundantes.

x = a + b * 2 → Asignación(x, Suma(a, Multiplicación(b, 2)))

Es un árbol cuando cada nodo tiene un único padre; referencias posteriores pueden convertir la representación completa en un grafo.

48.3 Recorridos del AST

Un recorrido en preorden resulta útil para inspección; el postorden procesa primero operandos y luego operaciones.

Los visitantes separan las operaciones —validar tipos, generar código o formatear— de las clases de nodos sintácticos.

48.4 Tabla de símbolos y referencias

La tabla de símbolos registra declaraciones, ámbitos, tipos y ubicaciones. Cada uso de un nombre se conecta conceptualmente con su declaración.

Estas relaciones permiten detectar nombres inexistentes, usos antes de inicializar y capturas entre ámbitos.

48.5 Grafo de flujo de control

Un CFG divide una función en bloques básicos. Las aristas representan los saltos posibles durante la ejecución.

bloque básico = secuencia sin saltos internos
arista = transferencia posible de control

Condicionales crean bifurcaciones y bucles crean ciclos.

48.6 Laboratorio de dependencias

Ejecuta el orden topológico una tarea por vez. Agrega la dependencia codegen → parser para formar un ciclo y observar cómo queda vacía la cola de tareas disponibles.

Tareas disponibles

Orden producido

Acción actual

Lexer y documentación no tienen requisitos pendientes.
DAG válido.
Tareas procesadas0 / 8
Dependencias restantes8
Paralelismo inicial2
ResultadoOrdenando

48.7 Dominadores

Un bloque d domina a n si todo camino desde la entrada hasta n pasa por d. El dominador inmediato organiza los bloques en un árbol.

Los dominadores son fundamentales para detectar bucles, ubicar cálculos y construir la forma SSA.

48.8 Análisis de flujo de datos

Cada bloque transforma información que circula por el CFG. El análisis repite ecuaciones hasta alcanzar un punto fijo.

IN[b] = combinación de OUT de predecesores
OUT[b] = transferenciab(IN[b])

Así se calculan variables vivas, definiciones alcanzables y expresiones disponibles.

48.9 Forma SSA

En Static Single Assignment cada variable se define una sola vez. Las funciones φ combinan valores procedentes de caminos de control diferentes.

SSA facilita propagación de constantes, eliminación de código muerto y otras optimizaciones.

48.10 Grafo de llamadas

Los vértices son funciones y una arista f → g indica que f puede llamar a g.

Permite analizar alcanzabilidad, recursión, propagación interprocedimental e impacto de cambios. Las llamadas dinámicas pueden obligar a sobreaproximar destinos.

48.11 Grafo de dependencias

Si B necesita que A esté disponible primero, se crea A → B. La dirección debe mantenerse consistente.

prerrequisito → tarea dependiente

Cuando el grafo es acíclico dirigido, un orden topológico proporciona una secuencia válida.

48.12 Orden topológico con Kahn

function ordenTopologico(vertices, aristas) {
  const entrada = calcularGradosEntrada(vertices, aristas);
  const disponibles = vertices.filter(v => entrada[v] === 0);
  const orden = [];

  while (disponibles.length) {
    const u = disponibles.shift();
    orden.push(u);
    for (const v of dependientes[u]) {
      entrada[v]--;
      if (entrada[v] === 0) disponibles.push(v);
    }
  }
  return orden.length === vertices.length ? orden : null;
}

48.13 Dependencias circulares

Un ciclo significa que ninguna tarea del ciclo puede comenzar antes que las demás. Kahn lo detecta cuando la cola queda vacía sin procesar todos los vértices.

DFS también detecta una arista hacia un vértice gris de la pila activa.

48.14 Componentes fuertemente conexas

En un grafo dirigido, cada componente fuertemente conexa con más de un vértice identifica un grupo de dependencias mutuamente circulares.

Contraer las componentes produce un DAG que resume el orden posible entre grupos.

48.15 Compilación incremental

Cuando cambia un archivo, el sistema recorre aristas hacia sus dependientes transitivos y reconstruye solo lo afectado.

archivo modificado → cierre de dependientes → tareas a reconstruir

Hashes de contenido evitan recompilar cuando la salida relevante no cambió.

48.16 Compilación paralela

Todas las tareas con grado de entrada cero pueden ejecutarse en paralelo. Al finalizar una, se liberan nuevos trabajos.

El camino crítico del DAG establece un límite inferior para el tiempo total, incluso con recursos ilimitados.

48.17 Dependencias de paquetes

Los gestores de paquetes resuelven versiones, dependencias transitivas y conflictos. El grafo puede contener varias versiones posibles antes de elegir una solución.

Una dependencia circular entre paquetes no siempre es ilegal en todos los entornos, pero complica instalación, inicialización y mantenimiento.

48.18 Código inalcanzable

En el CFG, un bloque no alcanzable desde la entrada nunca se ejecuta. En el grafo de llamadas, una función no alcanzable desde puntos de entrada puede ser candidata a eliminación.

El análisis conservador debe considerar reflexión, callbacks y llamadas indirectas antes de borrar código.

48.19 Errores comunes y puntos clave

  • Invertir la dirección de las dependencias.
  • Suponer que cualquier grafo admite orden topológico.
  • No informar qué ciclo causa el bloqueo.
  • Recompilar todo ante cada cambio.
  • Confundir AST, CFG y grafo de llamadas.
  • Ignorar llamadas indirectas.
  • Los DAG permiten orden y paralelismo; las SCC localizan ciclos.

48.20 Conclusión

Los compiladores utilizan distintas estructuras porque cada una responde preguntas diferentes. Árboles describen sintaxis; grafos capturan control, datos, llamadas y dependencias.

En el próximo tema revisaremos bibliotecas y herramientas para representar, analizar y visualizar grafos.