Calc/tl_bracket_parser.py

404 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Bracket Parser - INTEGRADO con el sistema de auto-descubrimiento de tipos
"""
import ast
import re
from typing import Tuple, Optional, Set, Dict
# Importar sistema de tipos
try:
from type_registry import get_registered_bracket_classes
TYPE_REGISTRY_AVAILABLE = True
except ImportError:
TYPE_REGISTRY_AVAILABLE = False
print("⚠️ Sistema de tipos no disponible, usando clases hardcodeadas")
class BracketParser:
"""Parser que convierte sintaxis con corchetes y detecta ecuaciones contextualmente"""
# Clases de fallback mínimas si falla el registro dinámico
FALLBACK_BRACKET_CLASSES = {'Hex', 'Bin'}
# Operadores de comparación que pueden formar ecuaciones
EQUATION_OPERATORS = {'==', '!=', '<', '<=', '>', '>=', '='}
def __init__(self, use_type_registry: bool = True):
self.debug = False
self.use_type_registry = use_type_registry and TYPE_REGISTRY_AVAILABLE
# Inicializar clases de corchetes dinámicamente
self._update_bracket_classes()
def _get_dynamic_bracket_classes(self) -> Set[str]:
"""
Obtiene las clases de corchetes dinámicamente del registro de tipos
NUEVA FUNCIÓN que reemplaza el hardcoding de DEFAULT_BRACKET_CLASSES
"""
if not self.use_type_registry:
return self.FALLBACK_BRACKET_CLASSES.copy()
try:
# Obtener TODAS las clases registradas que soportan corchetes
registered_classes = get_registered_bracket_classes()
if self.debug:
print(f"🔧 Clases dinámicas obtenidas del registro: {registered_classes}")
# Si no hay clases registradas, usar fallback
if not registered_classes:
if self.debug:
print("⚠️ No se encontraron clases registradas, usando fallback")
return self.FALLBACK_BRACKET_CLASSES.copy()
return registered_classes
except Exception as e:
if self.debug:
print(f"⚠️ Error obteniendo clases dinámicas: {e}")
return self.FALLBACK_BRACKET_CLASSES.copy()
def _update_bracket_classes(self):
"""
Actualiza las clases que soportan sintaxis con corchetes dinámicamente
MODIFICADO: Ya no usa DEFAULT_BRACKET_CLASSES hardcodeadas
"""
# Obtener clases dinámicamente del registro de tipos
self.BRACKET_CLASSES = self._get_dynamic_bracket_classes()
if self.debug:
print(f"🔧 Bracket classes actualizadas dinámicamente: {self.BRACKET_CLASSES}")
def reload_bracket_classes(self):
"""Recarga las clases de corchetes desde el registro dinámicamente"""
if self.debug:
print("🔄 Recargando bracket classes dinámicamente...")
self._update_bracket_classes()
def add_bracket_class(self, class_name: str):
"""Añade una clase que soporta sintaxis con corchetes"""
self.BRACKET_CLASSES.add(class_name)
if self.debug:
print(f" Añadida bracket class: {class_name}")
def remove_bracket_class(self, class_name: str):
"""Remueve una clase de la sintaxis con corchetes"""
self.BRACKET_CLASSES.discard(class_name)
if self.debug:
print(f" Removida bracket class: {class_name}")
def get_bracket_classes(self) -> Set[str]:
"""Retorna el set actual de clases con sintaxis de corchetes"""
return self.BRACKET_CLASSES.copy()
def has_bracket_class(self, class_name: str) -> bool:
"""Verifica si una clase está registrada para sintaxis con corchetes"""
return class_name in self.BRACKET_CLASSES
def parse_line(self, code_line: str) -> Tuple[str, str]:
"""
Parsea una línea de código aplicando todas las transformaciones
Returns:
(transformed_code, parse_info): Código transformado e información de parsing
"""
original_line = code_line.strip()
if not original_line or original_line.startswith('#'):
return code_line, "comment"
try:
# 1. Detectar y transformar atajo solve
transformed_line, has_solve_shortcut = self._transform_solve_shortcut(original_line)
if has_solve_shortcut:
return transformed_line, "solve_shortcut"
# 2. Detectar asignaciones de variables
if self._is_assignment(original_line):
return self._transform_assignment(original_line), "assignment"
# 3. Detectar ecuaciones standalone
if self._is_standalone_equation(original_line):
return f'_add_equation("{original_line}")', "equation"
# 4. Transformar sintaxis con corchetes
transformed_line = self._transform_brackets(original_line)
# 5. Si no hay transformaciones, devolver original
if transformed_line == original_line:
return original_line, "expression"
else:
return transformed_line, "bracket_transform"
except Exception as e:
if self.debug:
print(f"Error parsing line '{original_line}': {e}")
return code_line, "parse_error"
def _transform_solve_shortcut(self, line: str) -> Tuple[str, bool]:
"""
Transforma 'variable=?' en '_solve_variable_in_system("variable")'
en lugar de 'solve(variable)' para resolver usando las ecuaciones del sistema
Returns:
(transformed_line, was_transformed)
"""
# Pattern: variable_name = ?
pattern = r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\?$'
match = re.match(pattern, line.strip())
if match:
var_name = match.group(1)
# CAMBIO: usar función personalizada que resuelve usando el sistema de ecuaciones
return f'_solve_variable_in_system("{var_name}")', True
return line, False
def _is_assignment(self, line: str) -> bool:
"""
Detecta si una línea es una asignación de variable
MEJORADO: Considera si contiene símbolos desconocidos que sugieren ecuación
"""
try:
# Pattern: variable = expresión (que no sea ecuación)
if '=' in line and not any(op in line for op in ['==', '!=', '<=', '>=']):
# Verificar que sea una asignación válida de Python
parts = line.split('=', 1)
if len(parts) == 2:
var_part = parts[0].strip()
expr_part = parts[1].strip()
# Verificar que la parte izquierda sea un identificador válido
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_part):
# NUEVA LÓGICA: Detectar símbolos desconocidos en la expresión
# Si hay símbolos desconocidos, puede ser mejor tratarlo como ecuación
symbols_in_expr = re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', expr_part)
# Funciones y constantes conocidas de SymPy/matemáticas
known_functions = {
# Funciones trigonométricas
'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
'sinh', 'cosh', 'tanh',
# Funciones exponenciales y logarítmicas
'exp', 'log', 'ln', 'sqrt',
# Constantes
'pi', 'e', 'I', 'oo',
# Funciones especiales
'abs', 'sign', 'floor', 'ceiling', 'factorial',
# Álgebra
'diff', 'integrate', 'limit', 'series', 'solve',
'simplify', 'expand', 'factor', 'collect',
'cancel', 'apart', 'together',
# Matrices
'Matrix', 'det', 'inv',
# Funciones de Python comunes
'int', 'float', 'str', 'len', 'max', 'min', 'sum'
}
# Encontrar símbolos que no son funciones conocidas
unknown_symbols = [s for s in symbols_in_expr if s not in known_functions]
if unknown_symbols:
if self.debug:
print(f"🔍 '{line}' contiene símbolos desconocidos: {unknown_symbols}")
print(f"🔍 Considerando como ECUACIÓN en lugar de asignación")
# Si hay símbolos desconocidos, tratarlo como ecuación
return False # No es asignación, será detectado como ecuación
else:
# Solo funciones/constantes conocidas: asignación normal
return True
return False
except:
return False
def _transform_assignment(self, line: str) -> str:
"""Transforma asignación a llamada de función especial"""
parts = line.split('=', 1)
var_name = parts[0].strip()
expression = parts[1].strip()
# Transformar corchetes en la expresión
expression = self._transform_brackets(expression)
return f'_assign_variable("{var_name}", {expression})'
def _is_standalone_equation(self, line: str) -> bool:
"""
Determina si una línea es una ecuación standalone usando análisis AST
SIMPLIFICADO: Evita recursión y es más directo
"""
try:
# Primero verificar si contiene '=' simple
if '=' not in line or any(op in line for op in ['==', '!=', '<=', '>=']):
return False
# Si no es una asignación (según la lógica ya evaluada en parse_line)
# y tiene '=', entonces es una ecuación
try:
tree = ast.parse(line.strip())
if not tree.body:
return False
node = tree.body[0]
# Si es una asignación Python válida, NO es ecuación standalone
if isinstance(node, ast.Assign):
# Pero si contiene símbolos desconocidos, SÍ es ecuación
# Verificar si la expresión contiene símbolos desconocidos
if len(node.targets) == 1:
# Obtener la expresión del lado derecho
try:
expr_code = ast.get_source_segment(line, node.value)
if expr_code:
symbols_in_expr = re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', expr_code)
known_functions = {
'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
'sinh', 'cosh', 'tanh', 'exp', 'log', 'ln', 'sqrt',
'pi', 'e', 'I', 'oo', 'abs', 'sign', 'floor', 'ceiling',
'factorial', 'diff', 'integrate', 'limit', 'series',
'solve', 'simplify', 'expand', 'factor', 'collect',
'cancel', 'apart', 'together', 'Matrix', 'det', 'inv',
'int', 'float', 'str', 'len', 'max', 'min', 'sum'
}
unknown_symbols = [s for s in symbols_in_expr if s not in known_functions]
if unknown_symbols:
if self.debug:
print(f"🔍 '{line}' es asignación pero con símbolos desconocidos: {unknown_symbols} → ECUACIÓN")
return True
except:
# Si falla get_source_segment, usar método alternativo
parts = line.split('=', 1)
if len(parts) == 2:
expr_part = parts[1].strip()
symbols_in_expr = re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', expr_part)
known_functions = {
'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
'sinh', 'cosh', 'tanh', 'exp', 'log', 'ln', 'sqrt',
'pi', 'e', 'I', 'oo', 'abs', 'sign', 'floor', 'ceiling',
'factorial', 'diff', 'integrate', 'limit', 'series',
'solve', 'simplify', 'expand', 'factor', 'collect',
'cancel', 'apart', 'together', 'Matrix', 'det', 'inv',
'int', 'float', 'str', 'len', 'max', 'min', 'sum'
}
unknown_symbols = [s for s in symbols_in_expr if s not in known_functions]
if unknown_symbols:
if self.debug:
print(f"🔍 '{line}' es asignación pero con símbolos desconocidos: {unknown_symbols} → ECUACIÓN")
return True
return False
# Si es una expresión (no asignación), verificar comparaciones
if isinstance(node, ast.Expr):
if isinstance(node.value, ast.Compare):
for op in node.value.ops:
if isinstance(op, (ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE)):
return True
return False
except SyntaxError:
# Error de sintaxis: probablemente ecuación mal formada como "x + 2 = 5"
if self.debug:
print(f"🔍 '{line}' error AST → Tratando como ECUACIÓN")
return True
except Exception as e:
if self.debug:
print(f"🚨 Error en _is_standalone_equation: {e}")
return False
def _transform_brackets(self, line: str) -> str:
"""
Transforma sintaxis Class[args] → Class("args") y maneja métodos
VERSIÓN DINÁMICA que usa las clases registradas
"""
# Crear pattern dinámicamente basado en clases registradas
if not self.BRACKET_CLASSES:
return line # No hay clases registradas
# Pattern principal: ClassName[contenido] usando clases dinámicas
bracket_classes_pattern = '|'.join(re.escape(cls) for cls in self.BRACKET_CLASSES)
pattern = rf'(\b(?:{bracket_classes_pattern})\b)\[([^\]]*)\]'
if self.debug:
print(f"🔍 Usando pattern: {pattern}")
print(f"🔧 Clases registradas: {self.BRACKET_CLASSES}")
def replace_match(match):
class_name = match.group(1)
args_content = match.group(2).strip()
if not args_content:
# Caso: Class[] → Class()
return f'{class_name}()'
else:
# Split arguments by semicolon if present
# Each argument will be individually quoted.
# Example: Class[arg1; arg2] -> Class("arg1", "arg2")
# Example: Class[arg1] -> Class("arg1")
args_list = [arg.strip() for arg in args_content.split(';')]
processed_args = []
for arg_val in args_list:
# Escape backslashes first, then double quotes for string literals
escaped_arg = arg_val.replace('\\', '\\\\').replace('"', '\\"')
processed_args.append(f'"{escaped_arg}"')
return f'{class_name}({", ".join(processed_args)})'
# Aplicar transformación repetidamente hasta que no haya más cambios
transformed = line
while True:
new_transformed = re.sub(pattern, replace_match, transformed)
if new_transformed == transformed:
break
transformed = new_transformed
# Transformar corchetes vacíos en métodos: .método[] → .método()
method_pattern = r'\.([a-zA-Z_][a-zA-Z0-9_]*)\[\]'
transformed = re.sub(method_pattern, r'.\1()', transformed)
return transformed
class EquationDetector:
"""Detector específico para ecuaciones con análisis AST avanzado"""
@staticmethod
def is_equation_in_context(code: str, context: str = "standalone") -> bool:
"""
Determina si el código contiene una ecuación considerando el contexto
Args:
code: Código a analizar
context: Contexto ("standalone", "function_arg", "assignment")
"""
try:
tree = ast.parse(code.strip())
for node in ast.walk(tree):
if isinstance(node, ast.Compare):
# Verificar operadores de ecuación
for op in node.ops:
if isinstance(op, (ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE)):
if context == "standalone":
# En contexto standalone, es una ecuación
return True
elif context == "function_arg":
# En argumentos de función, generalmente NO es ecuación
return False
# Verificar '=' simple (no comparación)
if '=' in code and context == "standalone":
# Verificar que no sea asignación Python válida
try:
ast.parse(code.strip())
return False # Es sintaxis Python válida
except SyntaxError:
return True # Puede ser ecuación mal formada para Python
return False
except Exception:
return False