Optimizador de AST (Optimización en Compilación)
Archivo: src/optimizer.rs
El optimizador de Forja opera directamente sobre el Árbol de Sintaxis Abstracta (AST) después de superar las fases de validación semántica. Su función es reestructurar el árbol eliminando redundancias en tiempo de compilación para reducir la cantidad de instrucciones generadas, lo que a su vez disminuye la carga de trabajo en runtime para la máquina virtual y los backends nativos.
El archivo implementa dos fases de optimización independientes:
- Constant Folding (Reducción de Constantes): Evalúa expresiones de valores constantes directas en tiempo de compilación.
- Dead Code Elimination (Eliminación de Código Muerto): Descarta declaraciones de variables y funciones globales que nunca son referenciadas o llamadas en el resto del programa.
1. Reducción de Constantes (Optimizer)
Estructura Optimizer
pub struct Optimizer {
pub cambios_realizados: usize,
} Lleva la cuenta de la cantidad de simplificaciones de nodos del AST ejecutadas con éxito.
Desglose de Funciones
pub fn new() -> Self
- ¿Por qué?: Constructor para inicializar el contador de optimizaciones.
pub fn optimizar(&mut self, programa: &Programa) -> Programa
- ¿Por qué?: Punto de entrada para optimizar el árbol completo.
- ¿Cómo?: Recorre recursivamente todas las declaraciones de la raíz
Programadevolviendo un nuevoProgramaoptimizado.
fn optimizar_declaracion(&mut self, decl: &Declaracion) -> Declaracion
- ¿Por qué?: Propagar la optimización de constantes al interior de las declaraciones.
- ¿Cómo?: Evalúa recursivamente expresiones en asignaciones, condicionales (
si), bucles y declaraciones de variables.
fn optimizar_expresion(&mut self, expr: &Expresion) -> Expresion
- ¿Por qué?: El núcleo del reductor. Simplifica expresiones compuestas de constantes y literales en un único literal directo.
- ¿Cómo?:
- Binarias (ej:
2 + 3): Optimiza el lado izquierdo y el derecho. Si ambos resuelven a literales, evalúa la operación en compilación (ej: reemplaza el nodo de suma binaria porLiteralNumero(5)) e incrementacambios_realizados. - Unarias (ej:
!falso): Optimiza la expresión interna y, si es constante, realiza la negación directa (ej: reemplaza porLiteralBooleano(verdadero)). - Agrupación: Si lo que hay dentro de los paréntesis
(x)resuelve a un literal plano, descarta el nodo de agrupación (ej:(5)pasa a ser5).
- Binarias (ej:
fn evaluar_binaria(&self, a: &ValorConstante, op: &Operador, b: &ValorConstante) -> Option<ValorConstante>
- ¿Por qué?: Realizar el cálculo aritmético o lógico correspondiente en Rust.
- ¿Cómo?: Mapea el operador aritmético o relacional (
+,-,*,/,==,>, etc.) y evalúa los tipos de datos en un match. Maneja de manera segura la división para no causar pánicos en tiempo de compilación por divisiones entre cero (retornaNonesi el divisor es 0).
2. Eliminación de Código Muerto (DeadCodeEliminator)
Estructura DeadCodeEliminator
pub struct DeadCodeEliminator {
pub eliminados: usize,
variables_usadas: HashSet<String>,
funciones_llamadas: HashSet<String>,
} variables_usadasyfunciones_llamadas: Sets de acumulación de identificadores activos.
Desglose de Funciones
pub fn eliminar(&mut self, programa: &Programa) -> Programa
- ¿Por qué?: Analizar y limpiar el AST de funciones o variables globales declaradas pero nunca llamadas/usadas.
- ¿Cómo?:
- Pasada 1 (Recolección): Llama a
recolectar_usos()para recorrer recursivamente todo el AST y poblar los sets de identificadores referenciados en expresiones. - Pasada 2 (Filtro): Filtra la colección de declaraciones globales conservando únicamente aquellas que están presentes en los sets de usos o que no son candidatas a remoción (como la función obligatoria
main).
- Pasada 1 (Recolección): Llama a
fn recolectar_en_expresion(&mut self, expr: &Expresion)
- ¿Por qué?: Rastrear referencias lógicas dentro de las expresiones.
- ¿Cómo?: Registra identificadores de variables en
variables_usadasal detectarExpresion::Identificador, y añade nombres de funciones afunciones_llamadasal encontrarExpresion::LlamadaFuncion.
fn es_muerto(&self, decl: &Declaracion) -> bool
- ¿Por qué?: Identificar si un nodo califica para ser removido del AST.
- ¿Cómo?: Evalúa si una función global no es llamada y no es la función
main, o si una variable global nunca es leída ni escrita en ninguna parte del programa analizado.