En este módulo, aprenderemos a construir un compilador simple para un subconjunto del lenguaje ALGOL. Este ejercicio te permitirá comprender los conceptos fundamentales de la compilación, incluyendo el análisis léxico, el análisis sintáctico, la generación de código intermedio y la generación de código final.
Objetivos del Módulo
- Comprender los componentes básicos de un compilador.
- Implementar un analizador léxico (lexer).
- Implementar un analizador sintáctico (parser).
- Generar código intermedio.
- Generar código final ejecutable.
- Componentes Básicos de un Compilador
Un compilador típico se compone de varias fases:
- Análisis Léxico (Lexer): Convierte el código fuente en una secuencia de tokens.
- Análisis Sintáctico (Parser): Convierte la secuencia de tokens en una estructura de datos (generalmente un árbol de sintaxis abstracta, AST).
- Análisis Semántico: Verifica la semántica del programa (por ejemplo, tipos de datos).
- Generación de Código Intermedio: Convierte el AST en una representación intermedia.
- Optimización de Código: Mejora el código intermedio para hacerlo más eficiente.
- Generación de Código Final: Convierte el código intermedio en código máquina o bytecode.
- Implementación del Analizador Léxico (Lexer)
El analizador léxico toma el código fuente y lo divide en tokens. Un token es una secuencia de caracteres que representa una unidad léxica, como una palabra clave, un identificador, un operador, etc.
Ejemplo de Código para el Lexer
import re # Definición de tokens token_specification = [ ('NUMBER', r'\d+(\.\d*)?'), # Números enteros o decimales ('ASSIGN', r':='), # Operador de asignación ('END', r';'), # Fin de la instrucción ('ID', r'[A-Za-z]+'), # Identificadores ('OP', r'[+\-*/]'), # Operadores aritméticos ('NEWLINE', r'\n'), # Nueva línea ('SKIP', r'[ \t]+'), # Espacios y tabulaciones ('MISMATCH', r'.'), # Cualquier otro carácter ] # Compilación de las expresiones regulares token_re = '|'.join('(?P<%s>%s)' % pair for pair in token_specification) get_token = re.compile(token_re).match # Función para generar tokens def tokenize(code): line_num = 1 line_start = 0 mo = get_token(code) while mo is not None: kind = mo.lastgroup value = mo.group(kind) if kind == 'NEWLINE': line_start = mo.end() line_num += 1 elif kind == 'SKIP': pass elif kind == 'MISMATCH': raise RuntimeError(f'{value!r} inesperado en la línea {line_num}') else: yield kind, value mo = get_token(code, mo.end()) if mo is None: raise RuntimeError(f'Error de tokenización en la línea {line_num}') # Ejemplo de uso code = 'x := 10 + 20;' tokens = list(tokenize(code)) print(tokens)
Explicación del Código
- Definición de tokens: Se define una lista de tuplas donde cada tupla contiene el nombre del token y su expresión regular correspondiente.
- Compilación de expresiones regulares: Se combinan todas las expresiones regulares en una sola expresión.
- Función
tokenize
: Esta función toma el código fuente y genera una secuencia de tokens. Utiliza la expresión regular compilada para encontrar coincidencias en el código fuente.
- Implementación del Analizador Sintáctico (Parser)
El analizador sintáctico toma la secuencia de tokens generada por el lexer y construye un árbol de sintaxis abstracta (AST).
Ejemplo de Código para el Parser
class ASTNode: def __init__(self, type, value=None): self.type = type self.value = value self.children = [] def add_child(self, node): self.children.append(node) def parse(tokens): def parse_expression(index): token_type, token_value = tokens[index] if token_type == 'NUMBER': node = ASTNode('NUMBER', token_value) return node, index + 1 elif token_type == 'ID': node = ASTNode('ID', token_value) return node, index + 1 elif token_type == 'OP': node = ASTNode('OP', token_value) return node, index + 1 else: raise SyntaxError(f'Token inesperado: {token_value}') def parse_statement(index): node = ASTNode('STATEMENT') left_node, index = parse_expression(index) node.add_child(left_node) token_type, token_value = tokens[index] if token_type == 'ASSIGN': assign_node = ASTNode('ASSIGN', token_value) node.add_child(assign_node) index += 1 right_node, index = parse_expression(index) node.add_child(right_node) else: raise SyntaxError(f'Token inesperado: {token_value}') return node, index ast, index = parse_statement(0) return ast # Ejemplo de uso tokens = [('ID', 'x'), ('ASSIGN', ':='), ('NUMBER', '10'), ('OP', '+'), ('NUMBER', '20'), ('END', ';')] ast = parse(tokens) print(ast.type, [child.type for child in ast.children])
Explicación del Código
- Clase
ASTNode
: Representa un nodo en el árbol de sintaxis abstracta. Cada nodo tiene un tipo, un valor opcional y una lista de hijos. - Función
parse_expression
: Analiza una expresión y devuelve un nodo del AST y el índice del siguiente token. - Función
parse_statement
: Analiza una sentencia y devuelve un nodo del AST y el índice del siguiente token. - Función
parse
: Inicia el análisis sintáctico y devuelve el AST completo.
- Generación de Código Intermedio
El código intermedio es una representación del programa que es más fácil de optimizar y traducir a código máquina.
Ejemplo de Código para la Generación de Código Intermedio
def generate_intermediate_code(ast): if ast.type == 'STATEMENT': left_code = generate_intermediate_code(ast.children[0]) right_code = generate_intermediate_code(ast.children[2]) return f'{left_code} := {right_code}' elif ast.type == 'NUMBER': return ast.value elif ast.type == 'ID': return ast.value elif ast.type == 'OP': left_code = generate_intermediate_code(ast.children[0]) right_code = generate_intermediate_code(ast.children[1]) return f'{left_code} {ast.value} {right_code}' # Ejemplo de uso intermediate_code = generate_intermediate_code(ast) print(intermediate_code)
Explicación del Código
- Función
generate_intermediate_code
: Toma el AST y genera una representación intermedia del código. La función es recursiva y maneja diferentes tipos de nodos del AST.
- Generación de Código Final
La generación de código final convierte el código intermedio en código máquina o bytecode que puede ser ejecutado por una máquina virtual o un procesador.
Ejemplo de Código para la Generación de Código Final
def generate_final_code(intermediate_code): # Este es un ejemplo simplificado que simplemente devuelve el código intermedio return intermediate_code # Ejemplo de uso final_code = generate_final_code(intermediate_code) print(final_code)
Explicación del Código
- Función
generate_final_code
: En este ejemplo simplificado, la función simplemente devuelve el código intermedio. En un compilador real, esta función convertiría el código intermedio en código máquina o bytecode.
Ejercicio Práctico
Ejercicio
Implementa un compilador simple para un subconjunto del lenguaje ALGOL que soporte asignaciones y operaciones aritméticas básicas. El compilador debe incluir las siguientes fases:
- Análisis Léxico
- Análisis Sintáctico
- Generación de Código Intermedio
- Generación de Código Final
Solución
# Implementación completa del compilador # Análisis Léxico import re token_specification = [ ('NUMBER', r'\d+(\.\d*)?'), ('ASSIGN', r':='), ('END', r';'), ('ID', r'[A-Za-z]+'), ('OP', r'[+\-*/]'), ('NEWLINE', r'\n'), ('SKIP', r'[ \t]+'), ('MISMATCH', r'.'), ] token_re = '|'.join('(?P<%s>%s)' % pair for pair in token_specification) get_token = re.compile(token_re).match def tokenize(code): line_num = 1 line_start = 0 mo = get_token(code) while mo is not None: kind = mo.lastgroup value = mo.group(kind) if kind == 'NEWLINE': line_start = mo.end() line_num += 1 elif kind == 'SKIP': pass elif kind == 'MISMATCH': raise RuntimeError(f'{value!r} inesperado en la línea {line_num}') else: yield kind, value mo = get_token(code, mo.end()) if mo is None: raise RuntimeError(f'Error de tokenización en la línea {line_num}') # Análisis Sintáctico class ASTNode: def __init__(self, type, value=None): self.type = type self.value = value self.children = [] def add_child(self, node): self.children.append(node) def parse(tokens): def parse_expression(index): token_type, token_value = tokens[index] if token_type == 'NUMBER': node = ASTNode('NUMBER', token_value) return node, index + 1 elif token_type == 'ID': node = ASTNode('ID', token_value) return node, index + 1 elif token_type == 'OP': node = ASTNode('OP', token_value) return node, index + 1 else: raise SyntaxError(f'Token inesperado: {token_value}') def parse_statement(index): node = ASTNode('STATEMENT') left_node, index = parse_expression(index) node.add_child(left_node) token_type, token_value = tokens[index] if token_type == 'ASSIGN': assign_node = ASTNode('ASSIGN', token_value) node.add_child(assign_node) index += 1 right_node, index = parse_expression(index) node.add_child(right_node) else: raise SyntaxError(f'Token inesperado: {token_value}') return node, index ast, index = parse_statement(0) return ast # Generación de Código Intermedio def generate_intermediate_code(ast): if ast.type == 'STATEMENT': left_code = generate_intermediate_code(ast.children[0]) right_code = generate_intermediate_code(ast.children[2]) return f'{left_code} := {right_code}' elif ast.type == 'NUMBER': return ast.value elif ast.type == 'ID': return ast.value elif ast.type == 'OP': left_code = generate_intermediate_code(ast.children[0]) right_code = generate_intermediate_code(ast.children[1]) return f'{left_code} {ast.value} {right_code}' # Generación de Código Final def generate_final_code(intermediate_code): return intermediate_code # Ejemplo de uso code = 'x := 10 + 20;' tokens = list(tokenize(code)) ast = parse(tokens) intermediate_code = generate_intermediate_code(ast) final_code = generate_final_code(intermediate_code) print(final_code)
Explicación del Ejercicio
- Análisis Léxico: Se tokeniza el código fuente.
- Análisis Sintáctico: Se construye el AST a partir de los tokens.
- Generación de Código Intermedio: Se genera una representación intermedia del código.
- Generación de Código Final: Se genera el código final a partir del código intermedio.
Conclusión
En este módulo, hemos construido un compilador simple para un subconjunto del lenguaje ALGOL. Hemos cubierto las fases principales de un compilador: análisis léxico, análisis sintáctico, generación de código intermedio y generación de código final. Este ejercicio te proporciona una base sólida para comprender cómo funcionan los compiladores y te prepara para explorar temas más avanzados en la compilación.
En el próximo módulo, aplicaremos estos conocimientos para implementar algoritmos más complejos y explorar estudios de caso y proyectos prácticos.
Curso de Programación en ALGOL
Módulo 1: Introducción a ALGOL
- ¿Qué es ALGOL?
- Historia y Evolución de ALGOL
- Configuración del Entorno ALGOL
- Primer Programa en ALGOL
Módulo 2: Sintaxis y Estructura Básica
- Estructura del Programa ALGOL
- Variables y Tipos de Datos
- Entrada y Salida Básica
- Operadores y Expresiones
Módulo 3: Estructuras de Control
Módulo 4: Funciones y Procedimientos
- Definición de Funciones
- Parámetros de Función y Valores de Retorno
- Funciones Recursivas
- Procedimientos en ALGOL
Módulo 5: Estructuras de Datos
Módulo 6: Temas Avanzados
Módulo 7: Aplicaciones Prácticas
- Métodos Numéricos
- Implementación de Algoritmos
- Construcción de un Compilador Simple
- Estudios de Caso y Proyectos