Análisis Semántico: Type Checker y Borrow Checker
Archivo: src/semantics.rs
El análisis semántico es la fase del compilador responsable de garantizar que el programa tenga coherencia lógica antes de su ejecución. Forja implementa dos subsistemas principales en este archivo: un Type Checker para validación de tipos e inferencia de tipos estáticos, y un Borrow Checker para la verificación de reglas de pertenencia (ownership) y ciclo de vida de variables.
Estructuras del Ámbito Semántico
enum EstadoVariable
Representa el ciclo de vida de una variable en tiempo de compilación:
Viva: Variable declarada y disponible para lectura/escritura.Movida: Su ownership fue transferido a otra variable. Intentar usarla resultará en un error de compilación.Prestada(usize): La variable tiene préstamos de referencias inmutables activos. Durante este tiempo, la variable no puede ser modificada ni movida.
struct InfoVariable
Almacena la metadata de cada variable:
mutable: Si fue declarada conmut.estado: SuEstadoVariableactual.tipo: Su tipo asignado o inferido en tiempo de compilación.
struct TablaSimbolos
Estructura de ámbito con soporte de scopes jerárquicos (anidados) representados como un vector de mapas hash (ambitos: Vec<HashMap<String, InfoVariable>>).
new() -> Self: Constructor de la tabla de ámbitos. Inicializa el ámbito global.entrar_ambito(&mut self): Inserta un nuevo mapa hash al vector, representando el ingreso a un bloque (como el cuerpo de unsiomientras).salir_ambito(&mut self): Elimina el último mapa del vector al salir del bloque actual, liberando las variables locales declaradas dentro de él.declarar(&mut self, nombre: &str, mutable: bool, linea: usize, columna: usize, tipo: Option<Tipo>) -> Result<(), ErrorForja>: Registra una variable en el ámbito más cercano. Si ya existe en ese mismo nivel, retorna unErrorSemanticode duplicado.obtener(&self, nombre: &str) -> Option<&InfoVariable>yobtener_mut: Recorren los ámbitos en orden inverso (del interno al global) buscando el identificador.mover_variable(&mut self, nombre: &str, linea: usize, columna: usize) -> Result<(), ErrorForja>: Cambia el estado de una variable aMovidasi no es de tipoCopy. Si ya había sido movida, emite unErrorDePropiedadeducativo.escribir_variable(&mut self, nombre: &str, ... ) -> Result<(), ErrorForja>: Verifica que la variable sea mutable (mutable == true) y que no tenga préstamos activos (EstadoVariable::Prestada) antes de permitir su asignación.
BorrowChecker
Definición de BorrowChecker
pub struct BorrowChecker {
tabla: TablaSimbolos,
errores: Vec<ErrorForja>,
variantes_enum: HashMap<String, Vec<String>>,
} Desglose de Funciones de BorrowChecker
pub fn analizar(&mut self, programa: &Programa) -> Result<(), Vec<ErrorForja>>
- ¿Por qué?: Validar que todo el programa cumpla con el modelo de ownership y préstamos.
- ¿Cómo?: Recorre secuencialmente todas las declaraciones del AST. Al final, si el vector
erroresno está vacío, retorna el set completo de fallos detectados.
fn es_copy(tipo: &Option<Tipo>) -> bool
- ¿Por qué?: Distinguir tipos que se copian implícitamente de tipos que transfieren ownership (Move).
- ¿Cómo?: Los tipos primitivos como
Entero,Decimal,BooleanoyNuloretornantrue. Tipos complejos y estructurados comoTexto,Clase,Arreglos,ResultadosyCanalesretornanfalse.
fn analizar_declaracion(&mut self, decl: &Declaracion)
- ¿Por qué?: Evaluar el impacto de una sentencia en el estado de las variables.
- ¿Cómo?:
- En declaraciones de variable (
variable x = y): Siyes un identificador no-copy, invoca amover_variable()sobrey. - En asignaciones (
x = y): Llama aescribir_variable()para validar mutabilidad dex. - En bloques condicionales o bucles: Entra y sale de ámbitos adecuadamente para descartar variables locales.
- En declaraciones de variable (
fn analizar_expresion(&mut self, expr: &Expresion) -> Option<Tipo>
- ¿Por qué?: Rastrear préstamos o accesos a variables inyectados dentro de cálculos y expresiones complejas.
- ¿Cómo?: Analiza de forma recursiva expresiones binarias, unarias o accesos a índices. Al detectar referencias (
&mut xo&x), actualiza el estado en la tabla de símbolos aPrestada.
TypeChecker
Definición de TypeChecker
pub struct TypeChecker {
tabla: TablaSimbolos,
errores: Vec<ErrorForja>,
funciones: HashMap<String, (Vec<Option<Tipo>>, Option<Tipo>, Vec<ParametroTipo>)>,
tipos: HashMap<String, Tipo>,
traits: HashMap<String, Vec<FirmaMetodo>>,
variantes_enum: HashMap<String, Vec<String>>,
} Desglose de Funciones de TypeChecker
pub fn analizar(&mut self, programa: &Programa) -> Result<(), Vec<ErrorForja>>
- ¿Por qué?: Asegurar la compatibilidad estática de tipos en el AST completo mediante inferencia.
- ¿Cómo?: Ejecuta un pipeline semántico estructurado en tres pasadas:
- Pasada 0: Recolecta definiciones globales de
Traits medianterecolectar_traits(). - Pasada 1: Recolecta firmas globales de
Funciones medianterecolectar_funciones(). - Pasada 2: Infiere y verifica tipos declaración por declaración usando
analizar_declaraciones().
- Pasada 0: Recolecta definiciones globales de
fn analizar_declaracion(&mut self, decl: &Declaracion)
- ¿Por qué?: Validar tipos en declaraciones individuales de variables, funciones, condicionales y bucles.
- ¿Cómo?:
- En variables: Compara el tipo anotado explícitamente con el tipo inferido de la expresión a la derecha. Si difieren, lanza un
ErrorDeTipo. - En condicionales y bucles: Exige estrictamente que la expresión condicional resuelva al tipo
Tipo::Booleano.
- En variables: Compara el tipo anotado explícitamente con el tipo inferido de la expresión a la derecha. Si difieren, lanza un
fn analizar_expresion(&mut self, expr: &Expresion) -> Tipo
- ¿Por qué?: Computar el tipo resultante de expresiones complejas.
- ¿Cómo?:
- Literales (enteros, flotantes, texto, booleanos) devuelven de inmediato sus tipos correspondientes (
Tipo::Entero, etc.). - Operaciones binarias (aritmetica, comparaciones): Valida que ambos lados sean del mismo tipo compatible (ej: no sumar texto con entero).
- Llamadas a función: Valida la cantidad y los tipos de los argumentos pasados frente a la firma registrada. Resuelve también la instanciación de tipos genéricos.
- Literales (enteros, flotantes, texto, booleanos) devuelven de inmediato sus tipos correspondientes (
- ¿Para qué?: Garantizar la seguridad estática de tipos en operaciones complejas antes de transpilar o ejecutar en VM.