Overview
The Expresiones compiler uses the Visitor pattern to traverse the Abstract Syntax Tree (AST) generated by ANTLR. The Visitor.py file contains the semantic analysis and execution logic for the language.
Visitor Architecture
The Visitor Class
The Visitor class extends ExpresionesVisitor and maintains a symbol table:
class Visitor(ExpresionesVisitor):
def __init__(self):
self.tabla_simbolos = {}
Symbol Table
Variables are stored using the Simbolo class:
class Simbolo:
def __init__(self, nombre, tipo, valor=None):
self.nombre = nombre
self.tipo = tipo
self.valor = valor
This tracks:
- nombre: Variable identifier
- tipo: Data type (int, float, bool)
- valor: Current value (None if uninitialized)
Visitor Methods
Each labeled rule in the grammar generates a corresponding visit* method.
Program Entry Point
def visitProg(self, ctx):
print("Iniciando ejecución...")
return self.visitChildren(ctx)
Called when parsing begins. Use this for initialization or setup tasks.
Variable Declarations
def visitInstrDecl(self, ctx):
tipo = ctx.declaracion().TIPO().getText()
nombre = ctx.declaracion().ID().getText()
valor_inicial = None
if ctx.declaracion().ASIGNACION():
valor_inicial = self.visit(ctx.declaracion().expr())
if nombre in self.tabla_simbolos:
print(f"Error Semántico: Variable '{nombre}' ya declarada.")
else:
self.tabla_simbolos[nombre] = Simbolo(nombre, tipo, valor_inicial)
print(f"Declaración: {nombre} ({tipo}) = {valor_inicial}")
return None
Key operations:
- Extract type and variable name from context
- Evaluate initialization expression if present
- Check for duplicate declarations (semantic error)
- Add symbol to table
Variable Assignments
def visitInstrAsig(self, ctx):
nombre = ctx.asignacion().ID().getText()
valor = self.visit(ctx.asignacion().expr())
if nombre in self.tabla_simbolos:
self.tabla_simbolos[nombre].valor = valor
print(f"Asignación: {nombre} = {valor}")
else:
print(f"Error: Variable '{nombre}' no declarada.")
return valor
Validates that variables exist before assignment.
Arithmetic Expressions
def visitAritmetica(self, ctx):
izq = self.visit(ctx.expr(0))
der = self.visit(ctx.expr(1))
op = ctx.getChild(1).getSymbol().type
if op == ExpresionesParser.SUMA: return izq + der
if op == ExpresionesParser.RESTA: return izq - der
if op == ExpresionesParser.MULT: return izq * der
if op == ExpresionesParser.DIV: return izq // der if der != 0 else 0
return 0
Division by zero returns 0 instead of raising an error. Consider adding proper error handling for production use.
Relational Operators
def visitRelacional(self, ctx):
izq = self.visit(ctx.expr(0))
der = self.visit(ctx.expr(1))
op = ctx.op.type
if op == ExpresionesParser.MAYOR: return izq > der
if op == ExpresionesParser.MENOR: return izq < der
if op == ExpresionesParser.IGUAL: return izq == der
if op == ExpresionesParser.DIFERENTE: return izq != der
if op == ExpresionesParser.MAYOR_IGUAL: return izq >= der
if op == ExpresionesParser.MENOR_IGUAL: return izq <= der
return False
Returns boolean values for use in conditional statements.
Logical Operators
def visitLogica(self, ctx):
izq = self.visit(ctx.condicion(0))
der = self.visit(ctx.condicion(1))
op = ctx.getChild(1).getSymbol().type
if op == ExpresionesParser.Y_LOGICO: return bool(izq and der)
if op == ExpresionesParser.O_LOGICO: return bool(izq or der)
return False
def visitNotLogica(self, ctx):
return not self.visit(ctx.condicion())
Implements AND, OR, and NOT operations.
Conditional Statements
def visitInstrIf(self, ctx):
if self.visit(ctx.condicion()):
return self.visit(ctx.bloque(0))
elif ctx.bloque(1):
return self.visit(ctx.bloque(1))
return None
Evaluates condition and executes appropriate block (if or else).
Leaf Nodes
def visitNumero(self, ctx):
val = ctx.NUMERO().getText()
return float(val) if '.' in val else int(val)
def visitVariable(self, ctx):
nombre = ctx.ID().getText()
if nombre in self.tabla_simbolos:
val = self.tabla_simbolos[nombre].valor
return val if val is not None else 0
return 0
Extract values from literals and variable references.
Adding New Language Features
Example: Adding a Print Statement
PRINT : 'print' ;
instrucciones
: ...
| PRINT PAR_IZQ expr PAR_DER PUNTO_COMA #InstrPrint
;
Step 2: Regenerate Parser
antlr4 -Dlanguage=Python3 -visitor Expresiones.g
Step 3: Implement Visitor Method
def visitInstrPrint(self, ctx):
valor = self.visit(ctx.expr())
print(f"Output: {valor}")
return None
program {
int x = 42;
print(x);
}
Example: Adding a While Loop
WHILE : 'while' ;
instrucciones
: ...
| WHILE PAR_IZQ condicion PAR_DER bloque #InstrWhile
;
Step 2: Implement Visitor Method
def visitInstrWhile(self, ctx):
while self.visit(ctx.condicion()):
self.visit(ctx.bloque())
return None
This implementation doesn’t prevent infinite loops. Consider adding a maximum iteration limit for safety.
program {
int i = 0;
while (i < 5) {
i = i + 1;
}
}
Example: Adding String Support
TIPO : 'int' | 'float' | 'bool' | 'string' ;
STRING_LIT : '"' ~['"']* '"' ;
expr: ...
| STRING_LIT #StringLiteral
;
Step 2: Add String Concatenation
expr: expr PLUS expr #Concat
| ...
;
def visitStringLiteral(self, ctx):
# Remove quotes from string
return ctx.STRING_LIT().getText()[1:-1]
def visitConcat(self, ctx):
izq = self.visit(ctx.expr(0))
der = self.visit(ctx.expr(1))
# Handle both string concat and numeric addition
if isinstance(izq, str) or isinstance(der, str):
return str(izq) + str(der)
return izq + der
Modifying Existing Visitor Methods
Adding Type Checking
Enhance visitInstrAsig to validate types:
def visitInstrAsig(self, ctx):
nombre = ctx.asignacion().ID().getText()
valor = self.visit(ctx.asignacion().expr())
if nombre in self.tabla_simbolos:
simbolo = self.tabla_simbolos[nombre]
# Type checking
if simbolo.tipo == 'int' and not isinstance(valor, int):
print(f"Error de tipo: {nombre} es int, pero se asignó {type(valor).__name__}")
return None
simbolo.valor = valor
print(f"Asignación: {nombre} = {valor}")
else:
print(f"Error: Variable '{nombre}' no declarada.")
return valor
Adding Scoped Symbol Tables
Replace the flat dictionary with a stack of scopes:
class Visitor(ExpresionesVisitor):
def __init__(self):
self.scopes = [{}] # Stack of symbol tables
def enter_scope(self):
self.scopes.append({})
def exit_scope(self):
self.scopes.pop()
def lookup(self, name):
# Search from innermost to outermost scope
for scope in reversed(self.scopes):
if name in scope:
return scope[name]
return None
def declare(self, name, symbol):
self.scopes[-1][name] = symbol
Then update block visiting:
def visitInstrIf(self, ctx):
if self.visit(ctx.condicion()):
self.enter_scope()
result = self.visit(ctx.bloque(0))
self.exit_scope()
return result
elif ctx.bloque(1):
self.enter_scope()
result = self.visit(ctx.bloque(1))
self.exit_scope()
return result
return None
Working with Context Objects
Accessing Child Nodes
# Get specific terminal node
ctx.TIPO().getText() # Returns "int", "float", etc.
ctx.ID().getText() # Returns variable name
# Get child expressions (for binary operations)
ctx.expr(0) # Left operand
ctx.expr(1) # Right operand
# Check optional elements
if ctx.ASIGNACION(): # Returns None if not present
# Handle initialization
# Get operator type
op = ctx.getChild(1).getSymbol().type
# Get line and column for error reporting
line = ctx.start.line
column = ctx.start.column
Use context methods to extract information rather than manually parsing text. This is more robust and handles edge cases better.
Best Practices
1. Separate Concerns
Consider splitting the visitor into multiple passes:
- Pass 1: Build symbol table
- Pass 2: Type checking
- Pass 3: Code generation/execution
2. Error Handling
Improve error messages with context:
def error(self, ctx, message):
line = ctx.start.line
col = ctx.start.column
print(f"Error at {line}:{col} - {message}")
3. Return Values
Be consistent about what visitor methods return:
- Expressions: Return computed values
- Statements: Return None or control flow indicators
- Declarations: Return None
4. Document Visitor Methods
def visitInstrDecl(self, ctx):
"""
Handles variable declarations.
Checks for duplicate names and adds to symbol table.
Supports optional initialization.
"""
# Implementation...
Debugging Tips
Print AST Structure
def visit(self, tree):
print(f"Visiting: {tree.__class__.__name__}")
return super().visit(tree)
Trace Execution
def visitInstrAsig(self, ctx):
nombre = ctx.asignacion().ID().getText()
print(f"[TRACE] Assigning to {nombre}")
valor = self.visit(ctx.asignacion().expr())
print(f"[TRACE] Computed value: {valor}")
# ...
Validate Symbol Table State
Add a method to dump the symbol table at any point:
def dump_symbols(self):
print("\n--- SYMBOL TABLE ---")
for name, symbol in self.tabla_simbolos.items():
print(f"{name}: {symbol.tipo} = {symbol.valor}")
Next Steps
- Learn about testing strategies to validate your extensions
- Review the grammar structure to understand syntactic constraints
- Explore advanced ANTLR features like listeners and error recovery