Análisis semántico
"Un programa sintácticamente correcto puede ser completamente absurdo. Sumar una cadena de texto con un número no tiene sentido aunque la gramática lo permita."
Qué vas a aprender en este capítulo
El análisis semántico verifica que el programa tenga significado — no solo estructura. Después del parser tenemos el AST; ahora debemos verificar que las variables estén declaradas antes de usarlas, que los tipos sean compatibles, y que las funciones existan. Construimos la tabla de símbolos y anotamos el AST.
4.1 Tabla de símbolos
💡 Intuición
La tabla de símbolos es como el registro civil del compilador: lleva un registro de todo lo que existe — variables, funciones, tipos — y en qué ámbito (scope) viven.
Cuando el compilador ve precio <- precio * 1.13, necesita saber: ¿precio fue declarada? ¿Qué tipo tiene? ¿Puedo multiplicarla por 1.13?
📐 Fundamento
Tabla de símbolos con scoping léxico:
from typing import Optional, Dict, Any
class Simbolo:
def __init__(self, nombre: str, tipo: str, categoria: str = 'variable'):
self.nombre = nombre
self.tipo = tipo # 'entero', 'decimal', 'string', 'funcion'
self.categoria = categoria
self.offset = 0 # para generación de código
class Scope:
"""Un ámbito léxico con su propia tabla de símbolos."""
def __init__(self, nombre: str, padre: Optional['Scope'] = None):
self.nombre = nombre
self.padre = padre
self.simbolos: Dict[str, Simbolo] = {}
def definir(self, simbolo: Simbolo):
if simbolo.nombre in self.simbolos:
raise SemanticError(f"'{simbolo.nombre}' ya está declarado en este ámbito")
self.simbolos[simbolo.nombre] = simbolo
def resolver(self, nombre: str) -> Optional[Simbolo]:
"""Buscar el símbolo en este scope y en los ancestros."""
if nombre in self.simbolos:
return self.simbolos[nombre]
if self.padre:
return self.padre.resolver(nombre)
return None
class TablaSímbolos:
def __init__(self):
self.scope_global = Scope('global')
self.scope_actual = self.scope_global
# Pre-cargar funciones built-in
self.scope_global.definir(Simbolo('imprimir', 'void', 'funcion'))
self.scope_global.definir(Simbolo('leer', 'string', 'funcion'))
self.scope_global.definir(Simbolo('calcular_total', 'decimal', 'funcion'))
def entrar_scope(self, nombre: str):
nuevo = Scope(nombre, padre=self.scope_actual)
self.scope_actual = nuevo
def salir_scope(self):
self.scope_actual = self.scope_actual.padre
def definir(self, nombre: str, tipo: str, categoria: str = 'variable'):
self.scope_actual.definir(Simbolo(nombre, tipo, categoria))
def resolver(self, nombre: str) -> Simbolo:
sim = self.scope_actual.resolver(nombre)
if sim is None:
raise SemanticError(f"'{nombre}' no ha sido declarado")
return sim
class SemanticError(Exception):
pass
Ejemplo de scoping:
programa ejemplo
variables:
x: entero ← scope global del programa
inicio
x <- 10
si x > 5 entonces
# Aquí 'x' del scope global es visible
imprimir(x) # OK
fin_si
fin
4.2 El patrón Visitor
💡 Intuición
Necesitamos recorrer el AST para hacer múltiples operaciones: verificar tipos, generar código, calcular el tamaño del stack... Podríamos agregar un método por operación a cada nodo, pero eso viola el Principio de Responsabilidad Única.
El patrón Visitor separa los algoritmos de la estructura de datos: el AST sabe cómo "aceptar" un visitor; el visitor sabe qué hacer con cada tipo de nodo.
📐 Fundamento
from abc import ABC, abstractmethod
class Visitor(ABC):
"""Interface que todos los visitors deben implementar."""
@abstractmethod
def visitar_programa(self, nodo: Programa): ...
@abstractmethod
def visitar_asignacion(self, nodo: Asignacion): ...
@abstractmethod
def visitar_si_stmt(self, nodo: SiStmt): ...
@abstractmethod
def visitar_imprimir(self, nodo: ImprimirStmt): ...
@abstractmethod
def visitar_binop(self, nodo: BinOp): ...
@abstractmethod
def visitar_numero(self, nodo: Numero): ...
@abstractmethod
def visitar_identificador(self, nodo: Identificador): ...
# Agregar método accept() a cada nodo del AST:
# (modificando las dataclasses del capítulo anterior)
class Programa:
def accept(self, visitor): return visitor.visitar_programa(self)
class Asignacion:
def accept(self, visitor): return visitor.visitar_asignacion(self)
# etc.
Verificador de tipos — un Visitor concreto:
class VerificadorTipos(Visitor):
TIPOS_NUMERICOS = {'entero', 'decimal'}
def __init__(self):
self.tabla = TablaSímbolos()
def visitar_programa(self, nodo: Programa):
# Registrar todas las declaraciones primero
for decl in nodo.declaraciones:
self.tabla.definir(decl.nombre, decl.tipo)
# Verificar el cuerpo
for stmt in nodo.cuerpo:
stmt.accept(self)
def visitar_asignacion(self, nodo: Asignacion):
tipo_destino = self.tabla.resolver(nodo.destino).tipo
tipo_valor = nodo.valor.accept(self)
if not self._tipos_compatibles(tipo_destino, tipo_valor):
raise SemanticError(
f"Tipo incompatible: no se puede asignar '{tipo_valor}' "
f"a '{nodo.destino}' (tipo: '{tipo_destino}')"
)
def visitar_binop(self, nodo: BinOp) -> str:
tipo_izq = nodo.izq.accept(self)
tipo_der = nodo.der.accept(self)
if nodo.op in ('+', '-', '*', '/'):
if tipo_izq == 'string' and nodo.op == '+':
return 'string' # concatenación
if tipo_izq in self.TIPOS_NUMERICOS and tipo_der in self.TIPOS_NUMERICOS:
# Si alguno es decimal, el resultado es decimal
return 'decimal' if 'decimal' in (tipo_izq, tipo_der) else 'entero'
raise SemanticError(
f"Operador '{nodo.op}' no aplicable a tipos '{tipo_izq}' y '{tipo_der}'"
)
if nodo.op in ('==', '!=', '<', '>', '<=', '>='):
if tipo_izq != tipo_der:
raise SemanticError(
f"No se puede comparar '{tipo_izq}' con '{tipo_der}'"
)
return 'booleano'
def visitar_numero(self, nodo: Numero) -> str:
return 'entero' if isinstance(nodo.valor, int) else 'decimal'
def visitar_identificador(self, nodo: Identificador) -> str:
return self.tabla.resolver(nodo.nombre).tipo
def visitar_si_stmt(self, nodo: SiStmt):
tipo_cond = nodo.condicion.accept(self)
if tipo_cond != 'booleano':
raise SemanticError(f"La condición del 'si' debe ser booleana, no '{tipo_cond}'")
for stmt in nodo.entonces:
stmt.accept(self)
if nodo.sino:
for stmt in nodo.sino:
stmt.accept(self)
def visitar_imprimir(self, nodo: ImprimirStmt):
nodo.expresion.accept(self) # solo verificar que la expr sea válida
def _tipos_compatibles(self, destino: str, fuente: str) -> bool:
if destino == fuente:
return True
if destino == 'decimal' and fuente == 'entero':
return True # widening automático
return False
Probar el verificador:
# Código PupusaScript con error de tipos
codigo_error = '''
programa prueba
variables:
precio: decimal
nombre: string
inicio
nombre <- precio + 5 # ERROR: no se puede asignar decimal a string
fin
'''
tokens = tokenizar(codigo_error)
ast = parser.parse(tokens)
verificador = VerificadorTipos()
try:
ast.accept(verificador)
print("Programa válido")
except SemanticError as e:
print(f"Error semántico: {e}")
# → Error semántico: Tipo incompatible: no se puede asignar 'decimal' a 'nombre' (tipo: 'string')
4.3 Verificación estática vs dinámica
📐 Fundamento
Verificación estática: en tiempo de compilación, antes de ejecutar. Lenguajes: Java, C, Haskell, Rust.
// Java — error en tiempo de compilación
String s = 5; // ERROR: incompatible types: int cannot be converted to String
Verificación dinámica: en tiempo de ejecución. Lenguajes: Python, JavaScript, Ruby.
# Python — error en tiempo de ejecución
s = "hola" + 5 # TypeError: can only concatenate str (not "int") to str
Tradeoffs:
| Estática | Dinámica | |
|---|---|---|
| ¿Cuándo se detectan errores? | Antes de ejecutar | Al ejecutar |
| ¿Velocidad de desarrollo? | Más lento (tipos explícitos) | Más rápido |
| ¿Seguridad de tipos? | Alta | Baja |
| ¿Rendimiento en tiempo de ejecución? | Mayor | Menor (checks en runtime) |
| Ejemplos | Java, C++, Rust, Go | Python, JS, Ruby, PHP |
PupusaScript usa verificación estática — los tipos se declaran explícitamente y se verifican antes de ejecutar.
Inferencia de tipos: El compilador deduce el tipo sin que el programador lo declare. Haskell, ML, Rust, y TypeScript tienen inferencia de tipos.
// Rust — el compilador infiere que x es i32
let x = 42; // no necesitamos escribir: let x: i32 = 42;
4.4 Ejercicios
✏️ Ejercicio 4.1 — Identificar errores semánticos
Para cada fragmento de PupusaScript, indicá si hay error semántico, cuál es, y cómo lo detectaría el verificador:
a. resultado <- x + y donde x es entero y y no fue declarada.
b. si "activo" entonces ... fin_si (la condición es un string, no booleano).
c. precio <- precio + descuento donde precio es decimal y descuento es entero.
Solución
a. Error: variable no declarada. tabla.resolver("y") devuelve None → SemanticError: 'y' no ha sido declarado.
b. Error: tipo de condición incorrecto. visitar_si_stmt verifica que tipo_cond == 'booleano', pero "activo" es string → SemanticError: La condición del 'si' debe ser booleana, no 'string'.
c. Sin error. decimal + entero → visitar_binop retorna 'decimal' (regla de widening). precio es decimal y se asigna decimal → compatible.
✏️ Ejercicio 4.2 — Extender el verificador
Extendé el VerificadorTipos para soportar el bucle mientras:
mientras x > 0 hacer
x <- x - 1
fin_mientras
Escribí el método visitar_mientras_stmt indicando qué verifica.
Solución
def visitar_mientras_stmt(self, nodo: MientrasStmt):
# 1. Verificar que la condición sea booleana
tipo_cond = nodo.condicion.accept(self)
if tipo_cond != 'booleano':
raise SemanticError(
f"La condición del 'mientras' debe ser booleana, no '{tipo_cond}'"
)
# 2. Verificar el cuerpo del bucle
# Opcional: entrar a un nuevo scope (si mientras tuviera variables locales)
# self.tabla.entrar_scope('mientras')
for stmt in nodo.cuerpo:
stmt.accept(self)
# self.tabla.salir_scope()
# También agregar al AST:
@dataclass
class MientrasStmt:
condicion: Expr
cuerpo: List[Sentencia]
def accept(self, visitor):
return visitor.visitar_mientras_stmt(self)
4.5 Para profundizar
- Dragon Book, cap. 6 — análisis semántico y verificación de tipos.
- Gamma et al., Design Patterns — Patrón Visitor (cap. 5).
- Siguiente: Generación y optimización de código.
Definiciones nuevas: análisis semántico, tabla de símbolos, scope, símbolo, scoping léxico, patrón Visitor, verificación de tipos, verificación estática, verificación dinámica, inferencia de tipos, widening (conversión implícita), AST anotado.